Skip to content

Commit 62717f0

Browse files
committed
feat(spec)!: reject unknown keys on flow and permission authoring schemas (#4001 Tier-A)
Zod's default .strip silently discarded undeclared keys on authorable schemas — the instance kept parsing, so a mis-spelled or wrong-layer key shipped as metadata that quietly ignored the author's config (#3405, #1535). This lands the #4001 Tier-A slice inside the v17 breaking window: - security/permission.zod.ts: PermissionSetSchema, ObjectPermissionSchema, FieldPermissionSchema, AdminScopeSchema are .strict() with fixable errors (semantic aliases, wrong-layer pointers, tombstones for the retired contextVariables / isProfile). EffectiveObjectPermissionSchema explicitly .strip()s back — response shapes stay wire-tolerant. - automation/flow.zod.ts: FlowSchema, FlowNodeSchema, FlowEdgeSchema, FlowVariableSchema are .strict(); a node's config record stays open (the executor's configSchema, #4027/#4040, and the ADR-0087 conversion layer own it). - shared/suggestions.zod.ts: new strictUnknownKeyError factory generalizing the #3746 hand-rolled map; ui/action.zod.ts re-homes onto it with byte-identical messages. - PermissionSetSchema gains the declared `description` key — the strict gate's first catch: the built-in default sets author it and the Setup projection reads it, but the schema could not represent it, so it was silently stripped at every parse (ADR-0078 §3 inverse drift). - docs/audits/2026-07-unknown-key-strictness-ledger.md: the authorable / wire / open triage (#4001 step 1) with the ratchet's next targets. Verified zero breakage: spec 6960 tests + tsc clean; lint / metadata / platform-objects / metadata-core / metadata-protocol / sdui-parser / service-automation / plugin-security / core / rest / objectql / runtime suites green; showcase / crm / todo `objectstack validate` clean; all 12 spec check gates pass; no doc teaches a now-rejected key. Refs #4001 (Tier-A slice: ledger + first ratchet step; the long tail stays verify-then-enforce per the ledger). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147tNF4Snk7Ry1KGt4a5PY4
1 parent b3a2318 commit 62717f0

14 files changed

Lines changed: 964 additions & 41 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
feat(spec)!: reject unknown keys on the flow and permission authoring schemas (#4001 Tier-A)
6+
7+
Zod's default `.strip` silently discarded any key these schemas did not
8+
declare — the instance kept parsing, so a mis-spelled or wrong-layer key
9+
shipped as metadata that quietly ignored the author's config (#3405's
10+
action-param `reference`, #1535's object-level `workflows`). #3746 tightened
11+
one schema; this extends the same treatment to the two highest-risk
12+
authorable surfaces, per the ADR-0054 ratchet and the
13+
`docs/audits/2026-07-unknown-key-strictness-ledger.md` triage:
14+
15+
- **`security/permission.zod.ts`**`PermissionSetSchema`,
16+
`ObjectPermissionSchema`, `FieldPermissionSchema`, `AdminScopeSchema` are
17+
now `.strict()`. A silently dropped key on the capability container meant
18+
the author believed a grant or restriction was in place that the runtime
19+
never saw. `EffectiveObjectPermissionSchema` (response-side) explicitly
20+
`.strip()`s back and stays wire-tolerant.
21+
- **`automation/flow.zod.ts`**`FlowSchema`, `FlowNodeSchema`,
22+
`FlowEdgeSchema`, `FlowVariableSchema` are now `.strict()`. A node's
23+
`config` record stays **open**: it is per-node-type, owned by the
24+
registered executor's `configSchema` (#4027/#4040) and the ADR-0087
25+
conversion layer.
26+
- **`shared/suggestions.zod.ts`** — new `strictUnknownKeyError` factory (the
27+
#3746 hand-rolled map, generalized): every rejection names the offending
28+
key(s) and, where recognisable, the canonical spelling or a retired-key
29+
tombstone. `ui/action.zod.ts` re-homes onto it with byte-identical messages.
30+
- **`PermissionSetSchema.description` is now a declared key.** The strict
31+
gate's first catch: the built-in default sets always authored it and the
32+
Setup projection reads it, but the schema could not represent it, so it was
33+
silently stripped at every parse (ADR-0078 §3 inverse drift).
34+
35+
**Migration.** Any key these schemas now reject was previously stripped and
36+
therefore had **no runtime effect** — removing or renaming it never changes
37+
the behavior of a working app; validation simply stops lying about it. The
38+
error message carries the fix; the FROM → TO mappings baked into it include:
39+
40+
- Permission set: `objectPermissions``objects`, `fieldPermissions`/`fls``fields`,
41+
`tabs``tabPermissions`, `rls`/`policies``rowLevelSecurity`;
42+
`read`/`edit`/`export`/…→`allowRead`/`allowEdit`/`allowExport`/…;
43+
`readable`/`editable` vocabulary for FLS (`hidden` → declare `readable: false`).
44+
Retired keys carry tombstones: `contextVariables` (ADR-0105 D11 — use a
45+
registered `rlsMembership` resolver or an inline literal), `isProfile`
46+
(ADR-0090 D2 — use `isDefault`).
47+
- Flow: `steps``nodes`, `connections`/`transitions`/`links``edges`,
48+
`trigger`/`triggerType``type`, `title``label`; edge `from`/`to``source`/`target`,
49+
`guard`/`when`/`expression``condition`; a top-level `object`/`objectName`/`schedule`
50+
belongs on the START node's `config` (`{ objectName, triggerType, condition,
51+
schedule }`), not on the flow.

content/docs/references/security/permission.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ const result = AdminScope.parse(data);
127127
| :--- | :--- | :--- | :--- |
128128
| **name** | `string` || Permission set unique name (lowercase snake_case) |
129129
| **label** | `string` | optional | Display label |
130+
| **description** | `string` | optional | Human-readable description shown in Setup (persisted as sys_permission_set.description) |
130131
| **packageId** | `string` | optional | [ADR-0086 D3] Owning package id for a package-shipped set (absent = env-authored) |
131132
| **managedBy** | `Enum<'package' \| 'platform' \| 'user'>` | optional | [ADR-0086 D3] Record provenance: package (upgrade-owned metadata) vs platform/user (env config) |
132133
| **isDefault** | `boolean` || [ADR-0090 D5] App baseline for the everyone position: app-level sets are auto-bound at boot (guarded, idempotent); package-level sets become install-time suggestions an admin confirms |

content/docs/releases/v17.mdx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,42 @@ model), while `url` platform-wide means an HTTP endpoint to call (`http` node,
304304
webhooks). The singular `input` on `map` / `subflow` / `connector_action` is
305305
those nodes' own canonical key and is untouched.
306306

307+
### Flow and permission schemas reject unknown keys (#4001)
308+
309+
Zod's default is `.strip`: a key a schema does not declare is silently
310+
discarded and the instance keeps parsing. On an authorable surface that is the
311+
worst failure mode — the author (human or AI) gets a success envelope and
312+
ships metadata that quietly ignores their config; that is exactly how a
313+
correctly intended action-param `reference: 'sys_user'` degraded into a
314+
paste-a-UUID text box (#3405). 17.0 extends #3746's strictness from that one
315+
schema to the two highest-risk authorable surfaces, per the triage in
316+
`docs/audits/2026-07-unknown-key-strictness-ledger.md`:
317+
318+
- **Permission sets**`PermissionSetSchema`, `ObjectPermissionSchema`,
319+
`FieldPermissionSchema`, `AdminScopeSchema`. A silently dropped key on the
320+
capability container meant the author believed a grant or restriction was in
321+
place that the runtime never saw. The response-side
322+
`EffectiveObjectPermissionSchema` stays wire-tolerant.
323+
- **Flows**`FlowSchema`, `FlowNodeSchema`, `FlowEdgeSchema`,
324+
`FlowVariableSchema`. A node's `config` stays an **open** record: it is
325+
per-node-type, owned by the executor's `configSchema` and the conversion
326+
layer (see the alias table above).
327+
328+
Every rejection is written to be self-fixing: it names the offending key and,
329+
where recognisable, the canonical spelling (`steps``nodes`, edge
330+
`from`/`to``source`/`target`, `read``allowRead`, `tabs`
331+
`tabPermissions`), a wrong-layer pointer (a flow-level `objectName` belongs on
332+
the start node's `config`; `apiOperations` is response-side only), or a
333+
retired-key tombstone (`contextVariables` → ADR-0105 D11's `rlsMembership`
334+
resolvers; `isProfile``isDefault`, ADR-0090 D2).
335+
336+
Migration is by construction behavior-preserving: any key now rejected was
337+
previously stripped, so it never had a runtime effect — remove it or rename it
338+
to the canonical key the error suggests; nothing about a working app changes.
339+
One gap the gate itself caught: `PermissionSetSchema` gains the previously
340+
unrepresentable-but-consumed **`description`** key (persisted to
341+
`sys_permission_set.description`, shown in Setup).
342+
307343
### `agent.tools[]` is removed — capability comes from skills (ADR-0109, #3820)
308344

309345
ADR-0064's invariant is "an agent's tool set is the union of its
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# Unknown-key strictness ledger — the authorable / wire / open triage (#4001)
2+
3+
**Date**: 2026-07-30 · **Issue**: [#4001](https://github.com/objectstack-ai/objectstack/issues/4001) · **Builds on**: #3405 / #3746 (the `ActionParamSchema` template), #1535 (`ObjectSchema.create` unknown-key error), ADR-0089 D3a (view/page strict), ADR-0078 (no-silently-inert-metadata), ADR-0049 (enforce-or-remove), ADR-0054 (ratchet idiom)
4+
5+
Zod's default is `.strip`: a key the schema does not declare is **silently
6+
discarded** and the instance goes on parsing. On an authorable surface that is
7+
the worst failure mode — the author (human or AI) gets a success envelope and
8+
ships metadata that quietly ignores their config. This ledger is #4001 step 1:
9+
the persisted classification of *which* `z.object` sites that default is wrong
10+
for, so the `.strict()` ratchet advances on evidence instead of a one-shot
11+
sweep. **1885 sites was never the target number** — a large share of the spec
12+
is wire/response shape where strictness would be a forward-compat bug.
13+
14+
## Classification rule
15+
16+
One question decides the class: **who writes this schema's input?**
17+
18+
| Class | Input written by | Unknown-key policy |
19+
|---|---|---|
20+
| **authorable** | A human or AI author, into `*.object.ts` / `defineStack` config / Studio / MCP | `.strict()` + fixable error (the ratchet target) |
21+
| **wire** | Another machine: server responses, connector payloads, runtime envelopes, persisted runtime state | stay tolerant (`.strip` / `.passthrough`); strictness here turns an upstream *addition* into our parse crash |
22+
| **open** | Deliberately schemaless user data (record bodies, per-node-type `config`, React props) | stay open; a *sibling* contract validates it (e.g. a node executor's `configSchema`, #4027/#4040) |
23+
24+
Mixed files carry both — classify per schema, not per file. A **response-side
25+
extension of an authoring schema** (e.g. `EffectiveObjectPermissionSchema`)
26+
must explicitly `.strip()` back, because `.extend()` inherits `.strict()`.
27+
28+
## Standard wiring
29+
30+
`strictUnknownKeyError` in `shared/suggestions.zod.ts` (generalized from the
31+
#3746 hand-rolled map) is the one factory every strict authoring schema wires:
32+
33+
```ts
34+
z.object({ ... }, { error: strictUnknownKeyError({ surface, knownKeys, aliases, guidance, history }) }).strict()
35+
```
36+
37+
- `aliases` — semantic near-misses edit distance cannot reach (`visibleWhen`
38+
`visible`, `from``source`, `read``allowRead`).
39+
- `guidance` — exact-key prescriptions: **tombstones for retired keys** (the
40+
rejection carries the upgrade — AGENTS.md Post-Task Checklist #3) and
41+
wrong-layer pointers (`apiOperations` is response-side; `objectName` belongs
42+
on the start node).
43+
- Key lists live beside the schema and are **drift-guarded by tests** (an
44+
"accepts every declared key" probe), because the schema body is lazy.
45+
46+
Every ratchet step ships only with the empirical zero-breakage pass: full
47+
`@objectstack/spec` suite + `tsc`, downstream consumer suites, and
48+
`objectstack validate` on all three example apps. Inference is not evidence —
49+
the first application of this gate (below) caught a real spec gap.
50+
51+
## Ratchet state
52+
53+
### Already strict before #4001 (for reference)
54+
55+
| Surface | Since |
56+
|---|---|
57+
| `data/object.zod.ts``ObjectSchema.create()` unknown-key error + `UNKNOWN_KEY_GUIDANCE` tombstones; strict capabilities / tenancy / CRUD-override blocks | #1535, #2377, #2763 |
58+
| `ui/view.zod.ts` / `ui/page.zod.ts` — form/page component schemas | ADR-0089 D3a |
59+
| `ui/dashboard.zod.ts`, `data/field.zod.ts` (blocks), `ai/agent.zod.ts`, `ai/tool.zod.ts` (blocks) | various |
60+
| `ui/action.zod.ts``ActionParamSchema` (the template) | #3405 / #3746 |
61+
62+
### This step (#4001 Tier-A slice)
63+
64+
| Schema | File | Evidence class |
65+
|---|---|---|
66+
| `PermissionSetSchema`, `ObjectPermissionSchema`, `FieldPermissionSchema`, `AdminScopeSchema` | `security/permission.zod.ts` | A silently dropped key on the capability container is the ADR-0049 asymmetry itself; retired `contextVariables` (ADR-0105 D11) / `isProfile` (ADR-0090 D2) get tombstones |
67+
| `FlowSchema`, `FlowNodeSchema`, `FlowEdgeSchema`, `FlowVariableSchema` | `automation/flow.zod.ts` | Most AI-authored surface; cloud#688 / #2419 class. Node `config` stays **open** (executor `configSchema` owns it, #4027/#4040) |
68+
| `ActionParamSchema` re-homed onto the shared factory | `ui/action.zod.ts` | #3746 template, byte-identical messages |
69+
70+
**Findings log** (what the gate caught the day it was turned on):
71+
`PermissionSetSchema` could not represent `description` — yet the built-in
72+
default sets author it and `plugin-security`'s Setup projection reads it
73+
(`permission-set-projection.ts`). It had been silently stripped at every parse
74+
(the ADR-0078 §3 inverse-drift class). Fixed contract-first: `description` is
75+
now a declared key. This is the empirical argument for the ratchet: the
76+
inference "no metadata in the repo carries unknown keys" was **false**, and
77+
only the strict gate could prove it.
78+
79+
## File-level triage — the five authorable directories
80+
81+
Site counts are `z.object(` occurrences per file (2026-07-30, this branch).
82+
Classification is per the rule above; **(p)** marks a provisional call made
83+
from the file's exports/JSDoc rather than a full read — verify before
84+
tightening (the #4001 "sharing-rule lesson": candidates, not verdicts).
85+
86+
### `ui/` — 192 sites
87+
88+
| File | Sites | Class | Note / next action |
89+
|---|---|---|---|
90+
| `action.zod.ts` | 8 | authorable | param schema strict (#3746); remaining blocks ride later steps |
91+
| `view.zod.ts` | 51 | authorable | partially strict (ADR-0089); long tail of sub-blocks |
92+
| `component.zod.ts` | 29 | authorable | **next candidate** — SDUI component defs; check React-prop open slots first (p) |
93+
| `theme.zod.ts` | 14 | authorable (p) | authored themes |
94+
| `app.zod.ts` | 11 | authorable | **next verified step**`AppSchema` + nav-item union; recursive `NavigationItemSchema` needs union-error care |
95+
| `dashboard.zod.ts` | 11 | authorable | partially strict |
96+
| `widget.zod.ts` | 9 | authorable (p) | |
97+
| `page.zod.ts` | 7 | authorable | partially strict (ADR-0089) |
98+
| `chart.zod.ts` / `i18n.zod.ts` / `responsive.zod.ts` | 6+6+6 | authorable (p) | i18n label shapes are wide-open records by design — verify |
99+
| `dataset.zod.ts` / `animation.zod.ts` / `dnd.zod.ts` / `keyboard.zod.ts` / `touch.zod.ts` | 4 ea | authorable (p) | interaction configs |
100+
| `notification.zod.ts` / `offline.zod.ts` / `report.zod.ts` | 3 ea | authorable (p) | |
101+
| `sharing.zod.ts` | 2 | authorable (p) | public-sharing config |
102+
103+
### `data/` — 163 sites
104+
105+
| File | Sites | Class | Note |
106+
|---|---|---|---|
107+
| `object.zod.ts` | 20 | authorable | top-level already guarded (#1535); inner blocks partially strict |
108+
| `data-engine.zod.ts` | 14 | wire (p) | engine contract shapes |
109+
| `external-lookup.zod.ts` | 12 | mixed (p) | authored config + wire results |
110+
| `seed-loader.zod.ts` | 12 | mixed (p) | seed file shapes are authored; loader state is runtime |
111+
| `field.zod.ts` | 11 | authorable | partially strict |
112+
| `filter.zod.ts` / `query.zod.ts` | 11+10 | open | query dialect — user data flows through; validated semantically elsewhere |
113+
| `driver-nosql.zod.ts` / `driver.zod.ts` / `driver-sql.zod.ts` | 10+9+2 | wire | driver capability contracts |
114+
| `datasource.zod.ts` | 9 | authorable (p) | stack-authored config — **candidate** |
115+
| `analytics.zod.ts` | 8 | mixed (p) | |
116+
| `document.zod.ts` | 8 | wire (p) | |
117+
| `hook.zod.ts` / `hook-body.zod.ts` | 6+2 | authorable (p) | `defineHook`**candidate** |
118+
| `mapping.zod.ts` | 3 | authorable (p) | |
119+
| `external-catalog.zod.ts` | 4 | wire (p) | |
120+
| `field-value.zod.ts` / `seed.zod.ts` / `validation.zod.ts` | 1 ea | mixed (p) | |
121+
122+
### `automation/` — 80 sites
123+
124+
| File | Sites | Class | Note |
125+
|---|---|---|---|
126+
| `flow.zod.ts` | 12 | authorable | **strict as of #4001** (4 schemas; `FlowVersionHistorySchema` is runtime — stays tolerant) |
127+
| `sync.zod.ts` / `etl.zod.ts` | 12+10 | authorable (p) | authored pipelines — **candidates** |
128+
| `trigger-registry.zod.ts` | 11 | mixed | descriptors are code-registered (wire-ish); bindings authored |
129+
| `execution.zod.ts` | 8 | wire | run-state envelopes — never strict |
130+
| `state-machine.zod.ts` | 7 | authorable (p) | |
131+
| `control-flow.zod.ts` | 6 | authorable (p) | validated structurally by `validateControlFlow` |
132+
| `bpmn-interop.zod.ts` | 5 | wire (p) | interop import shapes |
133+
| `approval.zod.ts` | 4 | authorable | **next candidate** — v17 approval nodes are new authoring surface |
134+
| `node-executor.zod.ts` | 4 | wire | executor contract |
135+
| `webhook.zod.ts` | 1 | authorable (p) | spec-only (#3461) |
136+
137+
### `security/` — 20 sites
138+
139+
| File | Sites | Class | Note |
140+
|---|---|---|---|
141+
| `explain.zod.ts` | 11 | wire | permission-explain responses — never strict |
142+
| `permission.zod.ts` | 4 | authorable | **strict as of #4001**; `EffectiveObjectPermissionSchema` explicitly `.strip()`s (wire) |
143+
| `rls.zod.ts` | 3 | authorable | **next candidate** — a stripped RLS key is a silent policy hole |
144+
| `sharing.zod.ts` | 2 | authorable | **next candidate** — same class |
145+
146+
### `studio/` — 27 sites
147+
148+
| File | Sites | Class | Note |
149+
|---|---|---|---|
150+
| `object-designer.zod.ts` | 12 | authorable (p) | Studio-written JSON — machine-authored but *our* machine; strict protects the builder itself |
151+
| `plugin.zod.ts` | 8 | mixed (p) | |
152+
| `flow-builder.zod.ts` | 7 | authorable (p) | independent of `FlowSchema` shapes |
153+
154+
## Other directories (coarse; classify per schema before touching)
155+
156+
| Dir | Sites | Dominant class | Rationale |
157+
|---|---|---|---|
158+
| `api/` | 426 | wire | REST/GraphQL request/response contracts — tolerant by design |
159+
| `system/` | 383 | mixed | manifest/datasource blocks are authored; runtime envelopes are wire |
160+
| `kernel/` | 351 | wire | plugin/kernel contracts, code-to-code |
161+
| `cloud/` | 83 | wire | multi-tenant runtime |
162+
| `ai/` | 75 | mixed | agent/tool/skill definitions authored (partially strict already); model/provider payloads wire |
163+
| `integration/` | 64 | wire | connector payloads — upstream adds fields freely |
164+
| `identity/` | 34 | mixed | position/user shapes authored; auth payloads wire |
165+
| `shared/` | 25 | n/a | utilities and building blocks; strictness decided at the consuming schema |
166+
| `qa/` | 6 | n/a | test fixtures |
167+
168+
## Next steps (verify-then-enforce, one shape at a time)
169+
170+
1. `ui/app.zod.ts``AppSchema` + navigation union (highest-traffic remaining
171+
authorable type; needs union-error design so the strict error is readable).
172+
2. `security/rls.zod.ts` + `security/sharing.zod.ts` — small, security-class.
173+
3. `automation/approval.zod.ts` — new v17 authoring surface, tighten while young.
174+
4. `data/hook.zod.ts`, `data/datasource.zod.ts``defineHook` / stack config.
175+
5. Promote this ledger to a machine-checked gate (pattern of
176+
`packages/spec/liveness/` + `check:liveness`) once enough of the surface is
177+
classified that the table above is enforceable rather than descriptive.
178+
179+
Long tail stays gated on a verification pass per shape — never a one-shot
180+
"make all ~453 sites strict" (ADR-0054 ratchet; #4001's own recommendation).

packages/spec/api-surface.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4469,6 +4469,7 @@
44694469
"SortItemSchema (const)",
44704470
"StaticMount (type)",
44714471
"StaticMountSchema (const)",
4472+
"StrictUnknownKeyErrorOptions (interface)",
44724473
"SystemIdentifier (type)",
44734474
"SystemIdentifierSchema (const)",
44744475
"TemplateExpressionInput (type)",
@@ -4498,6 +4499,7 @@
44984499
"resilientFetch (function)",
44994500
"safeParsePretty (function)",
45004501
"singularToPlural (function)",
4502+
"strictUnknownKeyError (function)",
45014503
"strictVisibilityError (const)",
45024504
"suggestFieldType (function)",
45034505
"tmpl (function)"

packages/spec/authorable-surface.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5721,6 +5721,7 @@
57215721
"security/ObjectPermission:viewAllRecords",
57225722
"security/ObjectPermission:writeScope",
57235723
"security/PermissionSet:adminScope",
5724+
"security/PermissionSet:description",
57245725
"security/PermissionSet:fields",
57255726
"security/PermissionSet:isDefault",
57265727
"security/PermissionSet:label",

packages/spec/liveness/permission.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
"evidence": "rendered as the record title of sys_permission_set in Setup's generic object views (objectui app-shell list/detail rendering via displayNameField)",
1515
"note": "display metadata."
1616
},
17+
"description": {
18+
"status": "live",
19+
"verifiedAt": "2026-07-30",
20+
"evidence": "persisted to sys_permission_set.description (plugin-security sys-permission-set.object.ts: textarea field, list columns + form); read from the authored set by permission-set-projection.ts (description: ps.description ?? null) and diffed on re-seed",
21+
"note": "Declared in #4001: the built-in default sets always authored it, but the schema could not represent it, so it was silently stripped at parse (ADR-0078 §3 inverse drift) — the .strict() gate surfaced the gap."
22+
},
1723
"packageId": {
1824
"status": "live",
1925
"verifiedAt": "2026-07-30",

0 commit comments

Comments
 (0)