Skip to content

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

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-19332-ledger-reasons
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-19332-ledger-reasons

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #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; #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 #19333 item 2, which comes after every flight. It also leaves out action.aria, because #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 #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 test(spec): record why fourteen top-level keys are never offered by a metadata form #20064 and test(spec): record why field.format is never offered by a metadata form #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 #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. [finding] action.aria is graded live in the liveness ledger, but no action surface reads it — and the Studio action editor still renders an aria control #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

…etadata form

Three ruled root reason classes in the metadata-form reconciliation
ledger: authored through its own editor (object.actions,
object.listViews, page.slots, object.external), code-declared platform
configuration (object.tenancy, object.systemFields,
app.contextSelectors, action.resultDialog) and no registered widget
spells this union (object.stageField, field.inlineEdit,
field.relatedList). Each class holds exactly the keys the ruling put in
it, and the own-editor and union classes are also read off the key's
own node.

Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ
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 29720975b6775390d7c54cb3e51f0d70d36c6cd7 → packageMentionDocs.

@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 28, 2026
@objectstack-fleet objectstack-fleet Bot assigned os-warren and unassigned os-sales Sep 28, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 04:22
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit b1cbd92 Sep 28, 2026
41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-19332-ledger-reasons branch September 28, 2026 04:43
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…bjectstack-ai#20383)

Fixes objectstack-ai#20339
Clause-②: no

## What this changes

Comment text only, in `packages/cli`. Four sentences there said nothing
reads the `flows` translation group, and two named an objectui runner's
landing as the flip trigger. Since PR objectstack-ai#20328, the shipped liveness
ledger's `flows` row (`packages/spec/liveness/translation.json`) says
otherwise: `children.screens` is `live`, because the console's
screen-flow runner reads it, and only `children.label` is still read by
nothing (objectstack-ai#20318). The container keeps `planned` + `authorWarn: true`,
and that warn bit is group-level. So the gating these comments describe
is unchanged and still correct. Only the reason they gave was stale.

| where | before | after |
|---|---|---|
| `packages/cli/src/utils/i18n-extract.ts`, the
`authorWarnedTranslationGroups` docblock (site 2) | "no shipped runner
reads the group, so a translated wizard string really is stored and
never shown" | "Only part of the group is read: the console's
screen-flow runner reads `screens`, but the flow's own `label` is read
by nothing yet (objectstack-ai#20318), so a translated flow label really is stored and
never shown. The warn is group-level, so it still covers the whole
group." |
| the same docblock, its "Shape" paragraph | "the day the objectui
screen-flow runner lands and the row flips to `live`" | "the day the row
flips to `live` (dropping its `authorWarn`; for `flows` that waits on
objectstack-ai#20318)" |
| `packages/cli/test/i18n-flow-liveness-gate.test.ts`, the header's
ledger sentence (site 3) | "no shipped runner reads the group" | "only
part of the group is read: the console's screen-flow runner reads
`screens`, but the flow's own `label` is read by nothing yet (objectstack-ai#20318)" |
| the same header, its warn-side paragraph (site 3) | "Nothing reads the
group, so a translated wizard string really is stored and never shown" |
"The flow's own `label` is read by nothing yet (objectstack-ai#20318), so a translated
flow label really is stored and never shown [...] The warn is
group-level, so it still covers the whole group." |
| the same file, the comment inside the cell `never lets both rules
speak about the same keys` | "nothing reads the group, so this copy is
stored and never shown" | "the flow's own `label` is read by nothing
yet, so its translated copy is stored and never shown" |
| `packages/cli/test/i18n-flow-screen-coverage.test.ts`, the header's
flip-trigger sentence (patch round 1) | "it turns itself back on the day
an objectui screen-flow runner lands and the row flips" | "it turns
itself back on the day the row flips to `live` (dropping its
`authorWarn`; for `flows` that waits on objectstack-ai#20318)" |

The "Shape" sentence is changed because it sits in the same docblock and
named the wrong trigger. The screen-flow runner has already shipped
(objectui at the `.objectui-sha` pin `f8a9d0fb`), and the row did not
flip. The ledger's `screens` note says the container bit drops when
objectstack-ai#20318 lands. The in-cell comment is changed because the claim's file
surface names every `packages/cli` sentence that gives this reason. Only
its comment lines move, and the cell's code is unchanged (proof below).
The header of `i18n-flow-screen-coverage.test.ts` named the same wrong
trigger under the same rule, so patch round 1 gives it the Shape
clause's wording. `packages/cli` now states one trigger. The file is the
one the gate header points readers to, and the claim names it.

## Evidence

- **Premise.** Measured on base `5a6267f48`, and unchanged through the
merge of `862b6ce86`. The `flows` row is `status: planned` and
`authorWarn: true`; `children.label` is `planned`; `children.screens` is
`live` (`verifiedAt: 2026-09-27`). Its `authorHint` reads: "Only part of
this group is read. The console's screen-flow runner reads `screens`
[...] The flow's own `label` is read by nothing yet, so a translated
flow label is stored and never shown". The new sentences follow that
row.
- **Comment-only proof.** Instrument: the TypeScript parser, printing
both files with `removeComments: true`. The print at base and the print
at HEAD are identical: 48261 of 48261 characters for `i18n-extract.ts`,
5544 of 5544 for the gate test, and 18534 of 18534 for
`i18n-flow-screen-coverage.test.ts` (patch round 1; measured against
both `bd7b473e2` and merge base `c577e6663`). Control legs: the same
instrument detects a one-identifier rename of the exported function, and
a one-identifier rename of the screen-coverage file's mocked
`authorWarnedProperties`. No assertion, fixture or behaviour moves.
- **Published surface.** After `pnpm --filter @objectstack/cli build`,
both `dist/utils/i18n-extract.d.ts` and `dist/utils/i18n-extract.js`
carry the new sentence (1 hit each) and not the old one (0 hits each).
Positive control: the `authorWarnedTranslationGroups` export line gets 1
hit in each. `files: ["dist"]` ships both files, so this PR carries an
`@objectstack/cli` `patch` changeset.

## Local verification (HEAD `bd7b473e2`; patch round 1 at `dbff132d7`)

- **Patch round 1**, at `dbff132d7`, after merging `origin/main`
`c577e6663`:
  - `pnpm lint` exit 0;
- `node scripts/check-issue-citations.mjs --base origin/main` exit 0: 2
added citations judged, both live;
- `vitest run --project unit --maxWorkers=2` over
`i18n-flow-screen-coverage.test.ts` and
`i18n-flow-liveness-gate.test.ts`: 2 files and 29 tests passed;
- the 63 derived gates, re-run on this head: 63 run, 0 NOT-MEASURED, 0
UNRUN.

  The bullets below are the first round, at `bd7b473e2`.
- `pnpm --workspace-concurrency=2 --filter '@objectstack/cli^...' build`
exit 0. `pnpm --filter @objectstack/cli build` exit 0.
- Targeted unit run (`vitest run --project unit --maxWorkers=2`) over
the 18 test files that import `i18n-extract`: 18 files and 250 tests
passed. The gate file on its own, verbose: 8 of 8 cells passed.
- The full `unit` layer of `@objectstack/cli`: 231 files and 3309 tests
passed. The `integration` layer is left to CI, because no spawn entry or
integration-tier file is touched.
- `pnpm --filter @objectstack/cli run typecheck` exit 0 (`tsc --noEmit`,
then `check:test-typecheck` OK).
- `pnpm --filter @objectstack/spec run check:liveness` exit 0.
- `pnpm lint` (the full `eslint . --no-inline-config`) exit 0, no
findings.
- `node scripts/check-issue-citations.mjs --base origin/main` exit 0. It
judged the 2 added citations (objectstack-ai#20318, objectstack-ai#20339), and both are live issue
numbers.
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` derived 63 commands, and all 63 ended at exit 0. The `--ran`
reconciliation found 63 run, 0 NOT-MEASURED and 0 UNRUN. Three first
attempts measured nothing and were re-run green after their
prerequisites were built:
- `check:dual-build-cjs-loads` (exit 3, PREREQUISITE NOT MET): 104 entry
points load.
- `check:i18n-coverage` (exit 3, PREREQUISITE NOT MET): 13 configs, none
new.
- `check:type-check-debt` (hit a 300 s per-command cap): 4 entries, none
above its record.
- The roster gates whose roster directory holds one of these paths all
exit 0: `check-changeset-fixed`, `check:authz-resolver`,
`check:error-code-casing`, `check:filter-alias-parity`. So does
`check:nul-bytes`.

## Acceptance notes

- **Site 1 is not in this PR.** Site 1 is
`packages/spec/src/system/metadata-form-zod-reconciliation.test.ts`, the
`why` of the `onSuccess` omit entry. Triage routed it to the spec lane.
It is still stale on `origin/main` `c577e6663`. objectstack-ai#20362 was a spec-lane
PR that touched that file after triage, and it did not carry the fix.
- **Other copies of the same stale wording, not edited here:**
- `packages/cli/CHANGELOG.md`, the same entry in
`packages/lint/CHANGELOG.md`, and `packages/spec/CHANGELOG.md`. These
are release-owned, and each sentence was true when it was released.
- The `flows` bullet in `content/docs/ui/translations.mdx`. This
customer-facing page still says "no shipped screen-flow runner reads the
group yet, so a wizard still renders the strings authored on the flow".
It is outside `packages/cli`, so it goes to the seat.
- **One sentence left as it is.** The test header's "(a) it
self-activates when the row flips to `live` with the objectui runner"
stays byte-identical, as the dispatch directs. It holds under objectstack-ai#20318's
ENFORCE proposal, whose reader surface is objectui `FlowRunner.tsx` plus
the launcher. objectstack-ai#20318 still waits for the maintainer's ENFORCE-or-RETIRE
decision.

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

---------

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.

3 participants