Commit 736c63a
test(spec): record why fourteen top-level keys are never offered by a metadata form (#20064)
Part of #19333
Clause-②: no
This PR gives the top-level keys that no metadata form may offer a
recorded reason. The reasons live in the metadata-form reconciliation
ledger, at the root coordinate PR #19639 added. It does **not** switch
on the top-level `zodOnly` assertion. After this PR, 40 top-level keys
on the 16 object-rooted types still have neither a form row nor a
recorded reason, so the population does not close.
What stays open under #19333: the one residue key that fits none of the
card's buckets (`field.format`), and the open questions on the seven
`view` keys folded in from #19334, which is no longer open. The other 39
residue keys are the structured-control bucket, and they are carded on
#19332.
One file changes:
`packages/spec/src/system/metadata-form-zod-reconciliation.test.ts`. It
gets 14 ledger rows and a comment block. No schema, form, `describe()`,
liveness row or generated artifact changes.
## The population, re-derived on this tree
⛔ No number is carried over from the card or the thread. I copied the
reconciliation gate's own helper block byte for byte, from the top of
the file down to the first `describe(`, into a throwaway probe test next
to it. The probe runs the same `resolveCoordinate(form, root,
ROOT_PATH)` / `offerableKeysAt` / `omittedAt(LEDGER, …)` calls the gate
runs. It was deleted afterwards and is not in the diff. Final slice:
bytes 0..34234, sha256 `7b97432d8408…`, prefix verified byte-identical
on disk. The controls are asserted inside the probe:
| control | reading |
|:--|:--|
| LIT: `name` | offered by **17 of 17** forms |
| DARK: a fabricated key, form side | offered by **0** forms |
| DARK: the same key, schema side | declared by **0** schemas |
| stage (17 registered forms) | before (base `7e6ca1787a`) | after (head
`39590d226c`, merged with main `980bc05e5b`) |
|:--|--:|--:|
| top-level zod-only keys, overlay included | 229 | 229 |
| of those, the ADR-0010 overlay (skipped at the root since PR #19639) |
132 | 132 |
| non-overlay keys with no offer and no recorded reason | **97** |
**83** |
**Arithmetic.** 229 = 274 − 45. The census round measured 274 at
`596090efbe7`, and PR #19673 has since landed 45 scalar form rows. 97 =
229 − 132, which is also 142 − 45. 83 = 97 − 14, and 14 is the number of
rows this PR adds.
**The card's 145 and the thread's 142 count two different sets.**
Neither is a misreading.
- 145 = 132 overlay + 13 keys in the card's own three other sub-buckets
(4 + 4 + 5). The card's table adds up to it.
- 142 = 274 − 132, every non-overlay key, including the 47 scalar, 39
structured and 36 + 7 `view` keys that belong to other cards.
- On this tree the matching figures are **146** (132 + 14; the extra key
is `field.system`, see below) and **97**.
## The 229, by sub-bucket, measured
I assigned each bucket from the key's own `describe()` and its row in
`packages/spec/liveness/TYPE.json`. None was decided here:
| sub-bucket | criterion | keys | disposition |
|:--|:--|--:|:--|
| ADR-0010 provenance / lock overlay | in `FRAMEWORK_FIELDS` (the 7
`MetadataProtectionFields` keys on all 17 forms, 119, plus `protection`
on 13) | 132 | **one reason, already in place**: the `FRAMEWORK_FIELDS`
skip. No rows, and a root row naming an overlay key is refused by the
resolve test |
| platform-written, never authored | describe says not authored / never
authored / machine-managed / auto-injected | 5 | a root `omit` row each
|
| deprecated or legacy alias | describe carries `[DEPRECATED …]` or
`[LEGACY ALIAS …]` | 4 | a root `omit` row each, the shape of the
`page.interfaceConfig.sourceView` precedent |
| declared, not enforced yet | liveness verdict `planned` /
`experimental`, or every child of the row is | 5 | a root `omit` row
each, saying the key is out of this gate until enforced |
| the seven `view` keys from #19334 | no liveness verdict at any
coordinate | 7 | measured, **no row** (see below) |
| residue, object-rooted | fits no bucket above | 40 | 39 structured
(#19332) + `field.format` |
| residue, `view` | per-arm keys | 36 | outside the top-level direction
until the per-arm forms exist, per the #19330 ruling (letter A) |
132 + 5 + 4 + 5 + 7 + 40 + 36 = **229**.
### The 14 rows
- **Platform-written:** `app._unpublished`, `field.system`,
`view.columnState`, `view.isPinned`, `view.sortOrder`.
- **Deprecated / legacy alias:** `object.displayNameField`,
`object.titleFormat`, `view.drawerWidth`, `view.groups`.
- **Not enforced yet:** `object.externalSharingModel` (planned),
`field.useGrouping` (planned), `page.requires` (planned),
`agent.structuredOutput` (experimental), `action.onSuccess` (both
children `navigate` and `openIn` planned).
Why the five `view` rows are safe while `view` is outside the direction:
each of these reasons holds on every arm. None of the rows can excuse a
key that a future per-arm form ought to offer.
`field.system` is the one key not in the card's 145. PR #19673 held it
out of the scalar bucket, and the census round routed it here. It meets
the platform-written criterion on its own `describe()` (`Auto-injected
system/audit field`, set against `author-declared business fields`).
Every writer is platform code:
`packages/spec/src/data/injected-system-column-provenance.ts`,
`packages/objectql/src/search-companion.ts`,
`packages/metadata-core/src/audit-field-governance.ts`. The record
validator skips its required and multi-value checks for a flagged column
(`packages/objectql/src/validation/record-validator.ts`), so a control
would let an author turn those checks off by claiming a false
provenance.
### `app._unpublished`: what it is
It is the ADR-0045 §3 publish gate, amended to its own key. The AI
materialization path writes it, `POST /packages/:id/publish-drafts`
clears it, and `filterAppForUser` reads it. It shares the ADR-0010
envelope's `_` naming convention but is **not** a member of that
envelope: it is not in `MetadataProtectionFields`, and only `AppSchema`
declares it. So its absence from `FRAMEWORK_FIELDS` is correct, not a
gap, and nothing here widens that set. It now has its own root row,
which gives a machine a place to read its machine-written status.
## The seven `view` keys
**The planned mechanism does not work.** The plan was to give them
verdicts in `packages/spec/liveness/view.json`. That ledger's walk stops
at the `container` arm of the `view` union: the gate's `shapeOf` takes
the first OBJECT member, and the `viewItem` arm is a discriminated
union, so it is passed over. `--dump view` walks only name, label,
object, list, form, listViews, formViews and the overlay. A row for any
of the seven is therefore an ORPHAN. Measured by planting a `config`
row: `check:liveness` exited 1 with `✗ 1 ORPHAN ledger row(s) …
view/config`. The file was then restored, blob `21c15486454c` equal to
HEAD.
**What was measured instead.** Readings at framework `7e6ca1787a` and
objectui `62597c588`, re-read at framework `980bc05e5b` and objectui
`f8a9d0fb0596` (the console pin on that main), with the same results:
| key | writers | readers | reading |
|:--|:--|:--|:--|
| `config` | `defineViewItem`; the console's `viewEnvelope` (Save as
view); `expandViewContainer` | `MetadataManager.getViewsByObject` serves
it; the console's View editor edits `draft.config` | **authored**, live
|
| `viewKind` | `defineViewItem`; `viewEnvelope`; `expandViewContainer`,
the server's `viewIdentityPatch` and the console's
`buildPersistedViewBody` also stamp it | `getViewsByObject` filters on
it; the console's `listViews` drops the form family on it | **authored**
discriminator, live |
| `order` | `expandViewContainer`; the authoring door's own guidance
names it "the authored default" beside the per-user `sortOrder` |
`getViewsByObject` sorts on it | **authored**, live |
| `isDefault` | declared on the strict authoring door; the console's
set-default (`setDefaultViewPatches`) | the console's switcher |
**authored**, and also console-written as shared state |
| `scope` | `expandViewContainer` stamps `package`; nothing writes
`shared` or `personal` | the generic metadata list's `scope` filter |
platform-stamped; no runtime writer |
| `owner` | none in either repo | none in either repo | inert |
| `hidden` | none in either repo | none in either repo | inert |
None of the seven gets a row. The four authored keys would be excused by
a row, and whether an arm form should offer them is a question for the
first per-arm form, not for this ledger. The other three have no live
writer, so there is no platform-written state to give as the reason. The
readings are kept as a comment at the end of the ledger. They are not in
`view.json`, because `liveness/` is in `@objectstack/spec`'s published
`files[]` and a note there would change the tarball.
## Why the `zodOnly` check is not wired
The direction #19188 needs covers the 16 object-rooted types (per the
#19330 ruling, letter A). 40 of their keys still carry neither an offer
nor a reason. Wiring the check now would turn those 40 into red lines,
which is the shape the census round refused. `view` stays outside the
direction until its first arm form is registered.
## Ablation
Every leg went through `scripts/ablation-replace.mjs`. The anchor had to
hit, the mutation was verified on disk by marker count and blob change,
and the restore was proven by blob hash plus an empty `git diff HEAD`.
Every leg restored to `efc4637425b6` (the file at `9c63b38770`). The
subject is imported from `src` by relative path, so no build or `dist/`
sits between the mutation and the run.
1. **Remove the overlay reason.** `offerableKeysAt`'s root filter became
`return keys;`. The gate went red: 2 failed of 54, and `_lock is overlay
and must not be offerable at the root` names the keys. The census went
83 → **215**, exactly +132, all overlay keys.
2. **Delete one recorded reason** (the `app._unpublished` row). The
census went 83 → **84** and names `app._unpublished`. The gate stayed
**green**, 54 of 54. That is the measured statement of what "unwired"
means: while the `zodOnly` direction has no assertion, no gate notices a
row going missing.
3. **Point a row at a key the form offers** (`displayNameField` →
`nameField`). The resolve test went red: `object.(root).nameField: the
form offers it now — drop the ledger entry`. The new rows cannot outlive
the omission they excuse.
An earlier attempt at the `view.json` probe was a no-op: the replacement
contained its own anchor, so the tool refused before running anything.
It was re-run with a non-self-matching anchor, and that run is the
reading quoted above.
## Verification (head `39590d226c`)
- `@objectstack/spec` build: exit 0. Tree clean afterwards (no
`authorable-surface.base.json` or other artifact movement).
- `pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2`: **534 files, 15708 passed, 2 todo**. Pre-merge at
`9c63b38770`: 533 files, 15681 passed.
- `pnpm --filter @objectstack/spec typecheck` (`tsc --noEmit` +
`check:scripts-typecheck` + `check:test-typecheck`): exit 0. The test
layer holds its identity-pinned debt with no new signature.
- The reconciliation gate plus the probe: 2 files, 54 tests passed.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 77 commands, each run with its exit code
captured before any pipe. `--ran` first reported `77 derived, 73 run, 4
NOT-MEASURED, 0 UNRUN`: the four exit-3 families
`check:doc-formula-expressions`, `check:dual-build-cjs-loads`,
`check:lean-entry-closure` and `check:type-check-debt` were
`PREREQUISITE NOT MET` on other packages' `dist/` while the
whole-closure build waited for the shared lock. After that build ran
under the lock (turbo 72/72, verdict 0), all four were re-run and exit
0, and `--ran` reports `77 derived famil(ies) accounted for — 77 run, 0
NOT-MEASURED`. (Updated by the seat from the dev's report `5825062779`.)
- `check:liveness`: green, and `state-counts.md is current`.
- Lint, narrowed and measured: `eslint --no-inline-config --format json`
on the one changed file gives 1 file, 0 errors, 0 warnings. The
population comes from eslint's own `--print-config` (the file is linted,
not ignored). The config has no `parserOptions.project`, no
`projectService` and 0 typed rules. It is therefore not type-aware, and
a test-file edit cannot move any other file's verdict.
## Changeset
None. The only changed path is not published: `npm pack --dry-run` of
`@objectstack/spec` lists 2034 files, with 0 `*.test.ts` among them,
while the positive controls `src/ui/view.zod.ts` and
`liveness/view.json` are present. The documented no-release route is the
`skip-changeset` label (AGENTS.md, Post-Task Checklist step 3). This
PR's author does not write labels, so the seat applies it.
## Acceptance notes
- **The five not-enforced rows hold only while the verdict does.** No
leg re-reads the liveness verdict, so when one of these keys becomes
enforced its row goes stale silently. The ledger comment says to delete
the row at that point. Carrier: whichever PR enforces each key.
- **`object.actions` sits on the edge of the platform-written
criterion.** Its describe says "auto-populated from top-level actions
via objectName", but 8+ platform objects author `actions: […]` inline,
so it stays with the structured bucket (#19332).
- **The liveness ledger covers only one arm of the `view` union**
(`container`). The keys of the other arms can hold no verdict there at
all. This was noted by the census round, and it goes to whoever
registers the first per-arm form. The two inert keys above are the part
of it that is a finding, handed to the filing seat.
- The `274 … 132 of them this overlay` figures in the file's existing
comments are dated readings from the census tree and were left as
written.
---
_Generated by [Claude
Code](https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 60fdaa9 commit 736c63a
1 file changed
Lines changed: 154 additions & 0 deletions
Lines changed: 154 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
227 | 227 | | |
228 | 228 | | |
229 | 229 | | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
230 | 384 | | |
231 | 385 | | |
232 | 386 | | |
| |||
0 commit comments