Skip to content

test(spec): record why fourteen top-level keys are never offered by a metadata form - #20064

Merged
os-litant merged 4 commits into
mainfrom
claude/issue-19333-zod-only-recorded-reasons
Sep 25, 2026
Merged

os-litant merged 4 commits into
mainfrom
claude/issue-19333-zod-only-recorded-reasons

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

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 (#19188 split: 39 top-level zod-only keys are structured controls needing a designed widget, not a row #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

… metadata form

The metadata-form reconciliation ledger learned a root coordinate and an
ADR-0010 overlay skip, but no top-level key carried a recorded reason yet.
This adds fourteen root `omit` rows, each reason read off the key's own
describe() or its liveness verdict:

- platform-written, never authored: app._unpublished, field.system,
  view.columnState, view.isPinned, view.sortOrder
- deprecated or legacy alias: object.displayNameField, object.titleFormat,
  view.drawerWidth, view.groups
- declared, not enforced yet: object.externalSharingModel,
  field.useGrouping, page.requires, agent.structuredOutput, action.onSuccess

The overlay keeps its single reason in FRAMEWORK_FIELDS and gets no row.

The seven view keys with no liveness verdict (config, viewKind, order,
isDefault, scope, owner, hidden) cannot carry a row in liveness/view.json:
that ledger's walk stops at the union's container arm, so a row for any
of them is an orphan. Their measured writers and readers are recorded in
the file's note instead. None of the seven gets a reconciliation row.

The top-level zodOnly direction stays unwired: 40 object-rooted keys
still have neither an offer nor a recorded reason.

Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d
Co-authored-by: Claude <noreply@anthropic.com>
…n a shipped file

liveness/ is in @objectstack/spec's published files, so a note in
view.json would change the tarball for a record that only the
reconciliation ledger's readers need. The measured writers and readers of
config, viewKind, order, isDefault, scope, owner and hidden move into a
comment at the end of the ledger, and view.json returns to its base bytes.

Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d
Co-authored-by: Claude <noreply@anthropic.com>
The objectui pin moved on main while this branch was open, so the seven
view-key readings were re-taken at the new pin and at the merged main,
and the comment names both.

Claude-Session: https://claude.ai/code/session_019c3Hi6ZMU1p6m6aA6Bz45d
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

PM review — ACCEPT at head 39590d226c, 2026-09-25T01:57Z

domain:spec seat 4 (session_019c3Hi6ZMU1p6m6aA6Bz45d), claim 5824229416. Governing thread: unlock 5771402501, re-grade 5808359068, #19334 fold-in 5807281727.


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 25, 2026 01:58
@os-litant
os-litant enabled auto-merge September 25, 2026 01:58
@os-litant
os-litant added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 736c63a Sep 25, 2026
47 of 48 checks passed
@os-litant
os-litant deleted the claude/issue-19333-zod-only-recorded-reasons branch September 25, 2026 02:38
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…etadata form (objectstack-ai#20362)

Part of objectstack-ai#19332
Clause-②: no

## What this changes

One file changes:
`packages/spec/src/system/metadata-form-zod-reconciliation.test.ts`,
+314 / -1. It adds 11 root `omit` rows to the reconciliation `LEDGER`,
under three new reason classes, and one `describe` block of four
admission tests for those classes. No schema, form, `describe()`,
liveness row or generated artifact changes.

This is the ledger flight of ruling record `5861442317` (letter A on
groups G3, G4 and G5; objectstack-ai#19332). The G1 and G2 form-row flights are still
to come, which is why this PR says `Part of`.

It does **not** wire the top-level `zodOnly` direction. That is objectstack-ai#19333
item 2, which comes after every flight. It also leaves out
`action.aria`, because objectstack-ai#20323 is re-grading its liveness verdict.

| class (the `why` opens with it) | rows | what admits a row |
|:--|:--|:--|
| authored through its own editor | `object.actions` (the `action`
type), `object.listViews` (the `view` type, per the objectstack-ai#19330 ruling),
`page.slots` (the page designer), `object.external` (the import flow) |
the ruling's key list; **and**, when the editor is a registered metadata
type, the key must store that type's own node |
| code-declared platform configuration | `object.tenancy`,
`object.systemFields`, `app.contextSelectors`, `action.resultDialog` |
the ruling's key list only (see below) |
| no registered widget spells this union | `object.stageField`,
`field.inlineEdit`, `field.relatedList` | the ruling's key list; **and**
the node must be a union with an arm `FormSelectOptionSchema` refuses as
an option `value` |

Each row's `why` is one sentence. It names the editor, the writers or
the union arms, and it cites the ruling record, with the tracker number
beside it as a link.

## The admission tests (file lines 1251–1454)

These five root reasons were already in the file: platform-written,
deprecated alias, not enforced yet, overlay, renderer-owned vocabulary.
Each of them is read off the key itself. The three new ones were
**ruled**: for each key, the maintainer chose between a form row and a
recorded reason. So the first admission is the ruling's own key list,
transcribed as `RULED_ROOT_REASONS`.

**Test 1: each class holds exactly the keys the ruling put in it.** A
row that gives a ruled reason for any other key fails, and so does a
ruled key that has lost its row. A key that a form offers later already
has to drop its row, through the existing resolve test. The same edit
then takes it out of the list.

Where this package can read the class off the key's own node, the row
must pass that reading too:

- **Own editor: `editorTypeOf(type, key)`.** This returns the registered
type whose own node the key stores, judged by node identity. It is never
judged by name or by overlapping key sets. `object.actions` reads as
`action`, because its element is `getMetadataTypeSchema('action')`.
`object.listViews` reads as `view`, because its value is the node the
`view` container declares under `listViews`. The existing
`object.fields` subset row reads as `field`, and it serves as the lit
control. For the dark controls, `object.tenancy` and `object.access`
read as nothing. `page.slots` and `object.external` also read as
nothing, and that is a measurement, not an assumption: the page designer
lives in objectui, and the import flow lives in the datasource service.
So those two rows rest on the ruling alone.
- **Union: `unspellableArmValuesOf(type, key)`.** The node must be a
union, and it must have an arm whose value
`FormSelectOptionSchema.safeParse` refuses. That refusal is read from
the schema, not from a list kept in the test. The lit controls are
`stageField`, which returns `[false]`, and `inlineEdit`, which returns
`[true, false]`. The dark controls are `object.requiredPermissions`, a
union with no such arm, which returns `[]`, and `object.tenancy`, which
is not a union. The half that says no widget renders the whole union is
objectui's registry, and it rests on the ruling (the objectui readings
are below).
- **Code-declared platform configuration.** No reading in this package
admits it. The four keys' `describe()` text does not say "platform
only", and their writers are platform objects declared in other
packages. A spec-package test could not read those writers without
becoming a cross-package test. So this class is admitted by the ruling's
key list alone, and each row names the writers I measured.

**One dark control is deliberate: `object.systemFields`.** Its `false`
arm passes the union reading (`[false]`), but the ruling put it in
platform configuration. The test pins that fact, so the reading alone
never decides the class.

## Mechanism assumptions (PM), measured

1. **Population on today's main: holds.** I re-derived it with the
gate's own helper block, sliced verbatim into a throwaway probe beside
the gate, with the prefix checked byte-identical on disk. The probe was
deleted after each run.
- Controls: LIT `name` is offered by 17 of 17 forms and declared by 17
of 17 schemas. DARK, a fabricated key, is offered by 0 and declared by
0.
- At base `c74de10a9` (PM's `826f3279c` is an ancestor), the
object-rooted residue is **39**, the ruling's 39. None were added,
removed or retired, and all 11 keys of this flight are in it. The
liveness rows are `live`, or drilled with every child `live`.
- After the rows, the residue is **28**. Exactly the 11 keys left it,
and none came in. All types: 81 → 70 (`view` 42). Root rows: 15 → 26.
2. **Root-coordinate discipline: partly false as stated.**
- No existing root reason is admitted by a *test*. The phrase "its
admission test heads its group below" refers to the group's head
**comment** (the renderer-owned criteria). The three new classes get the
file's first real admission tests.
- Two of them are partly mechanical, as set out above. The
platform-configuration class is not mechanical at all: it rests on the
ruling's key list, and I say so plainly.
- A reading I tried and dropped: "no registered form row sits over such
a union" is **false**. The forms already spell one arm of unions with a
boolean arm: `object.fields.unique` (`type: 'boolean'`), `field.unique`
(derived), `action.visible` and `action.disabled` (`textarea`), and
`dashboard.globalFilters.defaultValue`. So that fact cannot be the
class's reading.
3. **Wording: holds.** Each row is one sentence naming the concrete
editor, writers or arms, and each cites `5861442317`.

## What the rows rest on, measured

Framework readings are at `154da111e`. objectui readings are at the
`.objectui-sha` pin `f8a9d0fb0596`, from a shallow clone. HotCRM
readings are at `2f7b2326`.

- **The page designer exists.** objectui
`app-shell/.../inspectors/PageBlockInspector.tsx:387` writes `slots: {
...slotsObj, [slotName]: nextArr }`.
- **The import flow exists.**
- REST routes: `POST /datasources/:name/external/tables/:remote/draft`
and `/import` (`packages/rest/src/external-datasource-routes.ts:31-32`).
- The draft writes `external: { remoteSchema?, remoteName }`
(`packages/services/service-datasource/src/external-datasource-service.ts:486`,
`:931-932`).
- Callers: the Studio's `ImportObjectDialog.tsx` and `os datasource
introspect`.
- **List views are authored through the view type.** HotCRM writes
`listViews` in 13 files, and every one is a `*.view.ts`. The examples
write them in 9 files, all `*.view.ts`.
- **Platform-configuration writers.** Examples and HotCRM author none of
the four keys; the HotCRM lit control `fieldGroups` hits 18 files. The
code writers are:
  - `tenancy`: `sys_api_key`, `sys_sso_provider`;
  - `systemFields`: `sys_metadata_activation` (`{ tenant: false }`);
  - `contextSelectors`: `studio.app.ts`;
- `resultDialog`: actions on `sys_user` (4), `sys_two_factor` (2),
`sys_oauth_application` (2), `sys_sso_provider` (1).
- **The union bounds.**
- `FormSelectOptionSchema` refuses `true` and `false` as an option
`value`, and accepts `primary`, `grid` and `form` (measured by
`safeParse`).
- objectui `WIDGETS` (`widgets.tsx:2909`) registers 20 widgets, and none
of them is a union selector.
- `resolveUnionBranch` falls back to the first branch in create mode
(`SchemaForm.tsx:238-250`).

## Ablation

Every leg ran from committed state (`154da111e`, blob `d2fe718f77e5`)
through `scripts/ablation-replace.mjs` in WRAP mode, under the verify
lock. In every leg, the anchor hit once, the mutation was verified on
disk (anchor count and blob change), and the gate plus a probe
regenerated from the mutated file ran. The restore was proven each time:
blob equal to HEAD, and `git diff HEAD` empty. The subject is imported
from `src` by relative path, so there is no `dist/` between the mutation
and the run.

| leg | residue (28 before) | gate |
|:--|:--|:--|
| delete the `object.external` row (own editor) | 29, adds
`object.external` | 1 red: *each class holds exactly the keys…* |
| delete the `action.resultDialog` row (platform config) | 29, adds
`action.resultDialog` | 1 red: the same test |
| delete the `field.relatedList` row (union) | 29, adds
`field.relatedList` | 1 red: the same test |
| swap `key: 'actions'` → `'access'` | 28 (`object.actions` in,
`object.access` out) | 2 red: the list test, and the own-editor test
(`object.access: the ruling names no editor for this key`) |
| swap `key: 'tenancy'` → `'access'` | 28 (`object.tenancy` in,
`object.access` out) | 1 red: the list test |
| swap `key: 'inlineEdit'` → `'readonlyWhen'` | 28 (`field.inlineEdit`
in, `field.readonlyWhen` out) | 2 red: the list test, and the union test
(`field.readonlyWhen: an option can spell every arm`) |

Every other test stayed green on every leg (57 of 58, or 56 of 58). The
resolve test stayed green on the swaps, because the swapped-in keys are
authorable and unoffered.

## Verification (head `154da111e`)

- **Gate and census probe:** 2 files, 58 tests passed, under the lock,
`VERDICT command-exit 0`. This ran at `8f31b4a5d`; the gate file's blob
there, `d2fe718f77e5`, is byte-identical at `154da111e`, and the
ablation legs re-ran the same pair at `154da111e` before each mutation's
verdict.
- **`@objectstack/spec` build:** exit 0. The tree stayed clean: no
`authorable-surface.base.json` or other artifact movement.
- **`pnpm --filter @objectstack/spec typecheck`** (tsc,
scripts-typecheck and test-typecheck): exit 0.
- **`pnpm --filter @objectstack/spec exec vitest run --project local
--maxWorkers=2`:** 554 files, 16431 passed, 1 todo, exit 0. The gate
file is in the `local` project; it is not in `vitest.repo-tests.json`.
- **`pnpm --filter @objectstack/spec test:repo`** (`--project repo`, run
under the lock as the seat asked): 33 files, 604 passed, `VERDICT
command-exit 0`.
- **Gates:**
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 78 commands. Each was run with its
exit code captured after a redirect, never through a pipe. **74 exited
0.**
- Four exited 3, `PREREQUISITE NOT MET` on other packages' `dist/`:
`check:doc-formula-expressions`, `check:dual-build-cjs-loads`,
`check:lean-entry-closure` and `check:type-check-debt`. The objectstack-ai#20064 and
objectstack-ai#20322 precedents hit the same four families.
- The whole-closure build that would satisfy them queued for about 18
minutes under the shared lock, behind other seats' heavy runs, and never
acquired. So these four are **NOT MEASURED** locally, a declared
narrowing. They read built output of packages this test-only diff does
not touch, and CI runs them.
- `--ran` reports `78 derived famil(ies) accounted for — 74 run, 4
NOT-MEASURED (4 DERIVED from a recorded exit 3)`, exit 0.
- **Lint, narrowed with measurement:**
- `eslint --no-inline-config --format json` on the one changed file
reports 1 file, 0 errors and 0 warnings.
- `--print-config` resolves a config for the file, so the file is inside
the linted population.
- That config has no `parserOptions.project` and no `projectService`,
and it enables 4 rules, none of them type-aware. So this edit cannot
move any other file's verdict.

## Changeset

None is owed. The change is test-only and publishes nothing. `npm pack
--dry-run` of `@objectstack/spec` lists 2027 files, and none is a
`*.test.ts`. The changed path is absent from the list. The positive
controls, `src/ui/view.zod.ts` and `liveness/object.json`, are present.
The no-release route is the `skip-changeset` label, as on objectstack-ai#20064.

## Acceptance notes

- **`object.external.introspectedAt` claims a writer that does not write
it.** Its describe says "Set by `os datasource introspect`", but that
command's draft path writes only `remoteSchema` and `remoteName`
(`external-datasource-service.ts:486`, `:931-932`). This is a source
reading only; I did not run it. Carrier: none. The own-editor reading
deliberately does not lean on that describe.
- **The forms already spell one arm of a boolean-armed union.**
`field.unique` and `object.fields.unique` offer only the boolean arm:
`object.form.ts:130` has `type: 'boolean'`, and `field.form.ts:26` is
derived, so the first arm is the boolean. That makes `'global'` /
`'organization'` (ADR-0120) unreachable from those Studio rows. It is
the same shape the ruling refused as option C for G5. I read it from
source and did not browser-run it. Carrier: the G2 flight that curates
`indexes.unique`, or whoever builds a union-aware control.
- **`action.aria` is still in the 28-key residue.** objectstack-ai#20323 carries it.
- The section comments still quote the census figures ("274 … 132").
They are historical and were left as written, as the precedents did.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

protocol:system size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants