Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .changeset/adr-0087-v17-strictness-wave-entry.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
---
"@objectstack/spec": minor
---

chore(spec): register the #4001 unknown-key strictness wave as one v17 ADR-0087 entry (#7630)

PR #7624's stock reconciliation judged **eleven** declared-breaking v17 changesets
*owed* an ADR-0087 ledger entry and deliberately did not write one — the grouping
was the open question, and the registries are consumed as a **set**, so a wrong
grouping produces no error anywhere. It would simply be wrong, silently, in the
artifact whose entire purpose is to be the trustworthy record. The maintainer ruled
on #7630 (2026-08-12): **one entry per major**, mirroring the registry's only two
precedents of this shape — `ui-schemas-strict-unknown-keys` (15) and
`dashboard-widget-strict-unknown-keys` (16) — against the alternatives of eleven
entries or a per-surface grouping.

So the eleven batches fold into a single D3 semantic migration,
`authoring-schemas-strict-unknown-keys`. An upgrader reads one prescription covering
the whole wave; the per-batch trace is written **inside** the entry, so an
archaeologist still walks it batch by batch:

| batch | closed |
| --- | --- |
| `unknown-key-strictness-tier-a` | `security/permission.zod.ts`; `automation/flow.zod.ts`'s four outer shapes |
| `unknown-key-strictness-step2` | `security/rls.zod.ts`, `security/sharing.zod.ts`, `identity/position.zod.ts` |
| `strict-automation-control-flow-state-machine` | five `control-flow` + six `state-machine` shapes |
| `unknown-key-strictness-automation-batch11` | flow's six nested blocks, time-relative trigger, flow function, webhook |
| `unknown-key-strictness-ui-batch13` | all four shapes in `ui/responsive.zod.ts` |
| `unknown-key-strictness-ui-batch15` | `ui/theme.zod.ts` 14/14, `ui/chart.zod.ts` 5/7 |
| `unknown-key-strictness-ui-batch16` | `AriaPropsSchema`, carried on ~30 live shapes |
| `view-subblock-strictness-batch18` | fifteen `ui/view.zod.ts` sub-blocks |
| `rare-jars-shave` | `ViewItemSchema` split into a strict authoring gate + `.strip()` wire variant |
| `user-filters-allow-add-tab-promote-and-close` | `userFilters` promoted then closed; the three page-only keys |
| `view-union-identity-precondition` | the `view` union stops matching every object |

Each of the eleven stock changesets now carries its
`<!-- adr-0087: registered … -->` disposition marker, so the judgement is recorded
where the next auditor reads rather than only in a PR body. `spec-changes.json` and
`docs/protocol-upgrade-guide.md` are regenerated from the registry.

Measured with `check-adr-0087-registration.mjs --audit-stock` on a full (unshallow)
history, before → after: **answered 51 → 62, residue 97 → 86, flagged (`!`) 52 →
41**. The two adjacent seams stay exactly as recorded on #7630 and are **not**
touched here: the 45 `~` rows (a ledger file was touched by some commit, which does
not prove the entry covers that face) and the 7 borderline candidates PR #7624
judged not-owed.

No runtime behaviour changes and no schema changes — this is the ledger half of
breaks that already shipped.
2 changes: 2 additions & 0 deletions .changeset/rare-jars-shave.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,3 +47,5 @@ validation — the write-path mirror of `stripReadDecorations` — and
round-trip to the renderer untouched. `id` was deliberately **not** declared:
it is a React list key, and declaring it would put a UI artifact on the
authorable surface.

<!-- adr-0087: registered authoring-schemas-strict-unknown-keys -->
2 changes: 2 additions & 0 deletions .changeset/strict-automation-control-flow-state-machine.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -70,3 +70,5 @@ behaviour was not openness but strip — an authored `meta` arrived as `{}`.

All three example apps (`app-showcase`, `app-crm`, `app-todo`) validate unchanged, so no
ADR-0087 conversion accompanies this change.

<!-- adr-0087: registered authoring-schemas-strict-unknown-keys -->
2 changes: 2 additions & 0 deletions .changeset/unknown-key-strictness-automation-batch11.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,3 +74,5 @@ feature: both metadata load paths call `applyProtection` on every type, so a
package-loaded webhook already carried those keys when `plugin-webhooks`
re-parsed it at boot. Closing the shape without declaring them would have turned
every package-shipped webhook into a skipped subscription after a redeploy.

<!-- adr-0087: registered authoring-schemas-strict-unknown-keys -->
2 changes: 2 additions & 0 deletions .changeset/unknown-key-strictness-step2.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,3 +42,5 @@ The error carries the fix; FROM → TO mappings baked in include:
bindings (`sys_position_permission_set` / `sys_user_position`), never
authored on the position; `parent` is rejected with the flatness rule
(ADR-0090 D3 — hierarchy is the business-unit tree, not a position tree).

<!-- adr-0087: registered authoring-schemas-strict-unknown-keys -->
2 changes: 2 additions & 0 deletions .changeset/unknown-key-strictness-tier-a.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,3 +54,5 @@ error message carries the fix; the FROM → TO mappings baked into it include:
`guard`/`when`/`expression`→`condition`; a top-level `object`/`objectName`/`schedule`
belongs on the START node's `config` (`{ objectName, triggerType, condition,
schedule }`), not on the flow.

<!-- adr-0087: registered authoring-schemas-strict-unknown-keys -->
2 changes: 2 additions & 0 deletions .changeset/unknown-key-strictness-ui-batch13.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,3 +64,5 @@ any of them exists in this repo, objectui, or the example apps. `.strict()` is a
property of a parse; there is no parse. Retiring them or giving them a carrier is
ADR-0049 enforce-or-remove, tracked in #4988 — not a breaking change to spend
here.

<!-- adr-0087: registered authoring-schemas-strict-unknown-keys -->
2 changes: 2 additions & 0 deletions .changeset/unknown-key-strictness-ui-batch15.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,3 +50,5 @@
`.strict()` 是 parse 的属性,这里没有 parse —— 收紧只会让文件看起来完成,并留下*一个被精确校验的死槽位*(#4583)。账本因此新增第四类 **`no gate`**(承载键活、parse 缺),与批 13 的 `no door`(承载键本身不存在)并列:两者处方相反,前者该接闸门,后者该走 ADR-0049 退役。已归档为独立 issue。

主题里那些**发出后无人读取**的 CSS 变量(`--font-size-*` / `--z-*` / `--duration-*` …)是 ADR-0049 的 liveness 题目,不是未知键题目,同样单独归档 —— 收紧能让被丢弃的键变响,不能让一个槽位变活。

<!-- adr-0087: registered authoring-schemas-strict-unknown-keys -->
2 changes: 2 additions & 0 deletions .changeset/unknown-key-strictness-ui-batch16.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,3 +93,5 @@ spec `check:*` gates, `objectstack validate` on app-showcase / app-crm / app-tod
and an ADR-0087 direct-parse probe over the three apps' **built** artifacts —
zero `aria` slots present, with the probe's negative control proven red on a
legacy-spelled block.

<!-- adr-0087: registered authoring-schemas-strict-unknown-keys -->
2 changes: 2 additions & 0 deletions .changeset/user-filters-allow-add-tab-promote-and-close.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,3 +39,5 @@ FROM → TO:未声明的键 → 删除它,或改成它想表达的那个已声
FROM → TO:`userFilters.tabs` → 对象的 `listViews` 具名条目;`showAllRecords` → 默认列表视图本身就是「全部记录」入口;`allowAddTab` → 由 ViewTabBar 自带的新增控件承担。三条拒绝各自带 `guidance` 处方,不是裸的 "unrecognized key"。

派生变体同时改为携带**自己的**错误映射:`.omit()` 会连基类的 `knownKeys` 一起继承,而那份候选列表是从基类形状读的、仍然含被 omit 掉的键 —— 实测在对象视图上写 `tab` 会被答复 *"Did you mean `tab` → `tabs`?"*,把作者指向这个形状唯一拒绝的键。形状仍由 `.omit()` 派生(#2231 不变),候选池改为按 omit 后的形状构建。

<!-- adr-0087: registered authoring-schemas-strict-unknown-keys -->
2 changes: 2 additions & 0 deletions .changeset/view-subblock-strictness-batch18.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,3 +40,5 @@ closed nested block 422s a console PUT regardless). `UserFiltersSchema`,
`ViewItemSchema` and the private `FormFieldBase` also stay open, each for a measured reason recorded
in the schema's own JSDoc, in `view-strictness-batch18.test.ts`, and in the `ui/` row
of `docs/audits/2026-07-unknown-key-strictness-ledger.md`.

<!-- adr-0087: registered authoring-schemas-strict-unknown-keys -->
2 changes: 2 additions & 0 deletions .changeset/view-union-identity-precondition.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -94,3 +94,5 @@ Direction A from the issue — giving the form arm a required floor — remains
deliberately **not** taken. It needs Studio's flattened round-trip bodies
measured first, or it 422s writes the platform itself makes; the ruling on #5599
deferred it as a possible second tightening on top of this one.

<!-- adr-0087: registered authoring-schemas-strict-unknown-keys -->
3 changes: 3 additions & 0 deletions docs/protocol-upgrade-guide.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -331,6 +331,9 @@ The action LOCATION vocabulary loses `global_nav` in this step (#6888, ADR-0049,
- **`auth-config-unadvertised-reserved-features`** — `api.authConfig.features.passkeys / api.authConfig.features.magicLink` → (removed — no replacement flag; the capabilities are not advertised)
- Why not automatic: Both flags were served by `GET /api/v1/auth/config` from introduction and read by no client: no login UI anywhere renders a passkey or magic-link affordance off them, so the payload advertised two sign-in methods a user could never reach, and a deployer setting `plugins.passkeys` / `plugins.magicLink` flipped a switch with no observable effect (ADR-0049 enforce-or-remove; maintainer ruling 2026-08-11 on #7481 chose remove over keep-as-reserved). The two are not equally empty: nothing at all is wired behind `passkeys`, whereas `magicLink`'s better-auth endpoints are live and only their advertisement was withdrawn. This is a RESPONSE surface — nobody authors or persists an `AuthFeaturesConfig` — so there is no source for the chain to rewrite; the schema tombstones both keys via retiredKey() and consumers drop their read. The withdrawal is conditional: both return to the payload in the change that ships the login UI (objectui#4179). ADR-0049, #7481.
- Done when: No client reads `features.passkeys` or `features.magicLink` off `/api/v1/auth/config`; a client that gated UI on either now treats the capability as absent rather than reading `undefined` as false by accident, and constructing an `AuthFeaturesConfig` with either key fails to parse with its own prescription instead of being silently stripped. Magic-link deployments keep working: `plugins.magicLink` still mounts `/api/v1/auth/magic-link/send` and `/magic-link/verify`, which a custom UI may call directly.
- **`authoring-schemas-strict-unknown-keys`** — `the protocol-17 authoring schemas closed against undeclared keys (#4001) — `automation/` (flow and its six nested blocks, control-flow, state-machine, webhook, time-relative trigger, flow function), `security/` (permission sets, RLS policies, sharing rules) and `identity/position`, `ui/` (responsive, theme, chart, `AriaProps`, fifteen `view` sub-blocks, `ViewItem`, `userFilters`) — plus the `view` write-path identity precondition one level above them` → declared keys only. Each rejection names the surface, echoes the offending key and — where the word is recognisable — gives the canonical spelling, a retired-key tombstone, or a prescription where a rename would be wrong. A `view` body must additionally carry at least one key some union member declares, discounting the identity keys the write path stamps itself (`VIEW_WRITE_PATH_IDENTITY_KEYS`)
- Why not automatic: zod's default `.strip` discarded any key these schemas did not declare and let the parse SUCCEED, so the author — increasingly an AI — got a success envelope and shipped metadata that quietly ignored what they wrote. Closing them turns that into a loud parse error (ADR-0049 enforce-or-remove, ADR-0078 no-silently-inert). It is not losslessly convertible for the same reason the two precedent entries at majors 15 and 16 are not: an arbitrary unknown key has no mapping target, and auto-deleting it would be exactly the silent data loss ADR-0078 bans — so each occurrence needs the author to decide, fix the typo, move it to the layer that owns it, or delete dead metadata. The named renames the errors carry are a help, not a transform: a large share of this wave is PRESCRIPTIONS rather than renames precisely because renaming would be wrong (`inputSchema.optional` is the opposite polarity of `required`; `errorHandling.maxAttempts` counts the first attempt where `maxRetries` counts the ones after it; a `responsiveStyles` bucket written on `responsive` is a wrong-layer pointer, and the two breakpoint vocabularies sixteen lines apart cannot be bridged by edit distance; `aria.live` is real on exactly one renderer and `ariaLabelledBy` has nothing to rename to; `finally` on `try_catch` and `context` on a state machine have no key at all). The eleventh member is not an unknown-key close but the same defect one level up — the `view` union had an arm that both stripped and required nothing, so it matched every object and `saveMetaItem` persisted garbage as an ACTIVE view overlay that read back badged valid. This is ONE entry for the whole major by the ruling on #7630 (2026-08-12), mirroring the registry's only two precedents of this shape; the eleven batches it folds are the changesets `unknown-key-strictness-tier-a`, `-step2`, `-automation-batch11`, `-ui-batch13`, `-ui-batch15`, `-ui-batch16`, `strict-automation-control-flow-state-machine`, `view-subblock-strictness-batch18`, `rare-jars-shave`, `user-filters-allow-add-tab-promote-and-close` and `view-union-identity-precondition`, each carrying its own FROM → TO table in `CHANGELOG.md`. ADR-0049 / ADR-0078 / ADR-0087, #4001, #5073, #5599 (registered #7630, backfilling #6350).
- Done when: `objectstack validate` passes with no unknown-key parse errors on any authoring surface — the sweep is "fix until nothing raises", and every rejection carries its own fix. ⚠️ Parsing clean is the weaker half on three of these faces, because the key was being dropped rather than refused and a config that silently did nothing looked exactly like one that worked: re-check that responsive/theme/chart styling actually renders as authored, that every `aria` block still names the element it was written for, and that each state machine and loop config still carries the transitions and caps you declared. For stored `view` bodies, `GET /api/v1/meta/diagnostics?type=view` lists every overlay the identity precondition now rejects, one row per view with the reason; each is fixed by giving the body a real view shape or deleting an overlay that was never a view.
- **`batch-options-validate-only-retired`** — `api.batchOptions.validateOnly` → (removed — no dry-run today; open an issue to design a no-commit batch preview)
- Why not automatic: The `validateOnly` key promised a dry-run ("validate records without persisting") but no batch surface ever read it — updateManyData / deleteManyData / batchData persist regardless. There is no behaviour to preserve and nothing stored to rewrite (it only ever appeared in an HTTP request body). Callers must stop sending it.
- Done when: No /batch, /updateMany or /deleteMany call sends `options.validateOnly`; a request that includes it answers 400 VALIDATION_FAILED with the retirement prescription.
Expand Down
Loading
Loading