Skip to content

feat(spec): offer onSuccess and outcomeMessages in Studio's action form; field.picklist joins the own-editor class - #21901

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21863-stale-reconciliation-rows
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-21863-stale-reconciliation-rows

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21863

Clause-②: no

This lands the director seat's ruling 1A / 2B on the card (record 5995552118, maintainer 「同意」). Studio's action form now offers onSuccess and outcomeMessages, using the controls ruling 5861442317 already gave their shapes. The field.picklist ledger row moves into the ruled class "authored through its own editor". No Zod key, liveness row or gate logic changes.

What changed

  • The form rows (packages/spec/src/ui/action.form.ts), in the Behavior section after successMessage / errorMessage:

    • outcomeMessages is one widget: 'json' row, spelled like the file's patch and bodyExtra rows (G1: a map-shaped value takes json). Its helpText says each key is a snake_case outcome value the handler returns, and each value is the label shown for that outcome. It also says what happens with no entry, and that a key the handler never returns is never shown.
    • onSuccess is one composite row (G2: an object-shaped value takes curated sub-rows), the face the body row above already uses. It has two sub-rows:
      • navigate is type: 'text', required: true, because the Zod requires it inside the block. Its helpText names the route or URL template and its three interpolation scopes (${param.*}, ${ctx.*}, ${result.*}), taken from the key's describe.
      • openIn is type: 'select' with no inline options. The member newTab has a capital letter, and FormSelectOptionSchema.value is a lowercase system identifier, so an inline list cannot spell it. The select reads its two members (self, newTab) off the served schema instead. Their meanings are in the helpText, and nothing is retyped.
    • Both rows carry visibleWhen: "data.type == 'api' || data.type == 'script'". These are the two types the parse accepts them on (the onSuccess type refinement and refuseInertOutcomeMessages in action.zod.ts), so the control is shown where the value is accepted.
    • No designed control and no new widget.
  • The ledger (packages/spec/src/system/metadata-form-zod-reconciliation.test.ts):

    • The action.onSuccess and action.outcomeMessages omit rows are deleted from the "Declared, not enforced yet" group.
    • 2B: field.picklist is added to the "authored through its own editor" key list in RULED_ROOT_REASONS, and RULED_EDITORS gains 'field.picklist': { surface: "the object designer's shared-picklist picker" }.
    • The picklist row moves into that class's group and its why is rewritten. It states the live verdict (the server resolves the named picklist onto the served field's options and judges a write against that set). It names the surface, and gives the class's citation with the placement ruling inside the same parenthetical: (ruling record 5861442317, #19332; the picker placed there by ruling record 5755653853, #18164 batch #209 item 1 A).
    • Comments only: the four comment blocks that described the class as holding exactly ruling 5861442317's keys now name field.picklist and the ruling that admitted it (5995552118).
    • object.externalSharingModel and view.groups are untouched.
  • The catalogs: the four packages/platform-objects/src/apps/translations/*.metadata-forms.generated.ts files were regenerated with node scripts/check-i18n-bundles.mjs --write --filter=platform-objects, following PR feat(spec): offer object.imageField in Studio's object form beside nameField #21854:

    • The first --write added the four new keys (outcomeMessages, onSuccess, onSuccess.navigate, onSuccess.openIn). The zh-CN / ja-JP / es-ES label and helpText leaves were then written in place.
    • A second --write kept those leaves and dropped the eight provenance entries per locale that the first extract had added to the *.source-hashes.generated.ts companions.
    • The three companions are byte-identical to BASE: blob 2decb02bf6f1 (es-ES), a96e8eb503d5 (ja-JP) and 31882e433b83 (zh-CN), before and after.
  • Three count pins in platform-objects, each re-taken with one comment line naming the rows:

    • The translated row-label positive control in object-lifecycle-panel-echo-decisions.test.ts: 661 → 665 per locale (four new row labels).
    • In action-body-panel-echo-decisions.test.ts, the count of composite children the repeater pin does not walk: 5 → 7 (onSuccess.navigate, onSuccess.openIn).
    • In the same file, the set of their parents: ['body'] → ['body', 'onSuccess'].

    The same file's echo rule ("no leaf on this panel reads its en source unless the ledger decided it is an echo") stays green, so all eight new leaves are authored in all three locales.

  • Changeset: @objectstack/spec patch and @objectstack/platform-objects patch, in one file (see What ships).

Controls (reverse verification, from the committed state)

Both legs ran against HEAD a5106c1c78, in a throwaway detached worktree at that commit, through scripts/ablation-replace.mjs in wrap mode. Each leg also had its own trap that restores from HEAD. Command: pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/system/metadata-form-zod-reconciliation.test.ts, through scripts/pm/os-verify-lock.sh.

state reading
baseline (unmutated) 1 file, 76 passed (76)
the onSuccess form row deleted (anchor x1 → x0, blob da0842c71614 → 0c90358ff49f) 1 failed / 75 passed: action.(root): accepted by the Zod but unauthorable in the form — offer it, or add a root ledger entry that records why it is not offered: expected [ 'onSuccess' ] to deeply equal []. Restored: blob da0842c71614 == HEAD, git diff HEAD empty
field.picklist removed from the class's key list (anchor x1 → x0, injected text x0 → x1 read on disk during the run, blob c3bc2007ee93 → aa5ba299af81) 1 failed / 75 passed: "authored through its own editor": the root rows giving this reason are not the keys ruling record 5861442317 put in it. A key the ruling did not name needs a ruling of its own; …: expected [ 'field.picklist', …(4) ] to deeply equal [ 'object.actions', …(3) ]. Restored: blob c3bc2007ee93 == HEAD, git diff HEAD empty, git status --porcelain 0 lines

The first run of leg 1 timed out in the lock queue (exit 99, never acquired). The tool restored the file then (blob == HEAD), and the leg was re-run once the lock freed; the reading above is the re-run. The test imports only relative src paths and the spec package has no workspace dependencies, so no dist/ is involved in either leg.

What ships

Measured with npm pack --dry-run --json --ignore-scripts, with a positive control:

  • @objectstack/spec: src/ui/action.form.ts is not in files[] (0 entries; src/ui/action.zod.ts, the control, is 1). The form ships compiled, though. The new onSuccess and outcomeMessages helpTexts are each in 6 packed dist/ files, and the control, successMessage's helpText, is in 6.
  • @objectstack/platform-objects: the new en helpText is in 6 packed files, against 6 for the control. The zh-CN and ja-JP labels are in 6 each, matched as unicode-escaped text, and the es-ES label is in 6. The control, zh-CN successMessage, is in 6.

Both packages publish the change, so skip-changeset does not apply.

Verification (head a5106c1c78)

All suites ran through scripts/pm/os-verify-lock.sh.

  • pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: 618 files passed, 18450 tests passed, 1 todo.
  • pnpm --filter @objectstack/spec exec vitest run --project repo --maxWorkers=2: 53 files passed, 902 tests passed.
  • pnpm --filter @objectstack/platform-objects exec vitest run --maxWorkers=2: 59 files, 949 passed. Run before the pins were re-taken, it gave exactly the three failures above (expected 7 to be 5, expected [ 'body', 'onSuccess' ] to deeply equal [ 'body' ], zh-CN positive control: expected 665 to be 661).
  • pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: 215 files passed, 3 skipped; 27906 tests passed, 19 skipped. No served-schema count moved: the change adds no Zod key.
  • pnpm --filter @objectstack/spec run typecheck: exit 0. tsc -p tsconfig.test.json --listFilesOnly compiles both changed spec files.
  • pnpm --filter @objectstack/platform-objects run typecheck: exit 0. Its tsconfig.test.json compiles both changed test files.
  • pnpm check:i18n: exit 0, platform-objects in sync (11 bundle(s)).
  • pnpm check:i18n-coverage: exit 0, 13 config(s), 621 baselined untranslated string(s), none new. The first attempt exited 3 (PREREQUISITE NOT MET, the example closure unbuilt), and the reading here is from after the build.
  • pnpm --filter @objectstack/spec check:generated: exit 0 (15 artifacts up to date).
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 87 commands from 9 paths against merge base 607463d73. All 87 ran on a5106c1c78. --ran reconciliation: 87 derived famil(ies) accounted for — 87 run, 0 NOT-MEASURED. pnpm check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET, no dist/ for nine packages), and exited 0 after a full workspace build. The record keeps that last reading.
  • eslint: --no-inline-config --format json on the 8 changed TS files gave 8 files, 0 errors, 0 warnings. The population is eslint.config.mjs:971 (**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}). The config never enables type-aware linting (eslint.config.mjs:326-328: no parserOptions.project), so the verdict on an untouched file cannot move.
  • Patch round 1, at bff24d6190 (claim revisions 5999807822 and 6000249318; one commit on a5106c1c78): the two red shards on a5106c1c78 were this PR's own pins.
    • Test Core (4/6): packages/lint/src/validate-predicate-path-refs.test.ts re-takes the shipped-corpus pins 83 → 85 (predicates) and 58 → 62 (== / != literal comparisons), each with a history comment. Measured against merge base 607463d736 by <form>::<field>::<source>: added exactly action :: outcomeMessages and action :: onSuccess, both data.type == 'api' || data.type == 'script'; none removed.
    • Test Core (2/6): packages/cli/test/i18n-extract-outcome-messages.test.ts leaves the metadataForms. subtree out of its .outcomeMessages. selection. The new row's metadataForms.action.fields.outcomeMessages.{label,helpText,placeholder} paths are form-row text, so the equality stays over the four action-outcome keys. Ablation control: with the clause made a no-op, the file reddens exactly as CI did (expected [ …(7) ] to deeply equal [ …(4) ]).
    • Local runs after a full build: @objectstack/cli 352 files, 4683 passed, 2 skipped; @objectstack/lint 119 files, 5629 passed; plus objectql, service-automation, plugin-approvals, cloud-connection, http-conformance, plugin-dev, example-embed-objectql, connector-rest, connector-slack and rest, all green. dispatch-gates derived 89 commands from 11 paths, and all 89 exited 0. ESLint 0 / 0 on the 2 edited files. No source line moves in packages/lint or packages/cli.

Acceptance notes

  • The picklist citation. The gate requires every row of a ruled class to contain 5861442317 ("cite the ruling record that decided the reason"). The dispatch asked that the row name the placement ruling (5755653853). So the row carries both, in the one parenthetical the class's citation form uses. The two-ruling spelling follows the object.listViews row in the same class (ruling record 5861442317, #19332; per-arm view forms under the #19330 ruling, letter A). The ruling that admitted the key to the class (5995552118) is cited in code comments only.
  • The picker does not exist yet. At the objectui pin 0abd4f9f87, ObjectFieldInspector has the inline option editor and no shared-picklist picker; the picker is objectui#10202 phase 2. The row's why therefore says the offer "was decided as that picker", not that the picker is there. Until it lands, field.picklist is authorable only in source.
  • Renderer reading, not a browser run. Read at the objectui pin, not run in a browser. SchemaForm.tsx resolves a composite sub-row's schema from the parent's properties. select is a passthrough widget, and with no fieldSpec.options the select falls back to the sub-schema's enum, so openIn should render self / newTab. The JSON Schema this head emits (packages/spec/json-schema/ui/Action.json) carries onSuccess as a plain object, with navigate required and openIn.enum equal to self and newTab, which is the shape that fallback reads. json is a passthrough widget too, so outcomeMessages takes the same face as patch and bodyExtra. NOT MEASURED: a browser run of either face.
  • Surface, stated. The claim named action.form.ts, the four catalogs, the reconciliation test, any platform-objects count pin, and a changeset. action-body-panel-echo-decisions.test.ts is the second platform-objects pin the new composite moves. It falls under the claim's "any platform-objects count pin" and under the cross-lane declaration 5998582591 on [PM seat] domain:engine — ⏳ vacant #6367. No metadata-protocol pin moved.
  • origin/main gained one commit after the branch point (87712ab823, global search in metadata-protocol). It touches none of this PR's paths, so it was not merged.

Generated by Claude Code

claude added 4 commits October 5, 2026 16:31
…form; picklist joins the own-editor class

Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ
Co-authored-by: Claude <noreply@anthropic.com>
…and outcomeMessages, in four locales

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

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec, touching 4 documentable anchor(s).

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/protocol/kernel/i18n-standard.mdx (via outcomeMessages (literal, a string literal in sections))
  • content/docs/protocol/objectui/actions.mdx (via openIn (literal, a string literal in sections))
  • content/docs/ui/actions.mdx (via openIn (literal, a string literal in sections))
  • content/docs/ui/react-pages.mdx (via onSuccess (literal, a string literal in sections))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-1.mdx (via newTab (literal, a string literal in action; a string literal in sections), onSuccess (literal, a string literal in sections), openIn (literal, a string literal in sections))
  • content/docs/releases/v17/17-3.mdx (via onSuccess (literal, a string literal in sections))
  • content/docs/releases/v17/17-6.mdx (via outcomeMessages (literal, a string literal in sections))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see

Coarse fallback — 138 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 e6dc7a240617eaeef9a64e788bf6e5561c107f1b → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c31b008230ed57024eb74ee271523f72c0a89681 — the merge of head 1bd3d8e20922291b8e526dfed7ed04bc3f19c04a into base e6dc7a240617eaeef9a64e788bf6e5561c107f1b, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c31b008230ed57024eb74ee271523f72c0a89681 && git checkout c31b008230ed57024eb74ee271523f72c0a89681
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e6dc7a240617eaeef9a64e788bf6e5561c107f1b 1bd3d8e20922291b8e526dfed7ed04bc3f19c04a && git checkout -B drift-repro e6dc7a240617eaeef9a64e788bf6e5561c107f1b && git merge --no-ff 1bd3d8e20922291b8e526dfed7ed04bc3f19c04a

node scripts/docs-audit/affected-docs.mjs --json e6dc7a240617eaeef9a64e788bf6e5561c107f1b

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs e6dc7a240617eaeef9a64e788bf6e5561c107f1b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

claude added 3 commits October 5, 2026 18:15
The lint shipped-corpus pins move 83 to 85 predicates and 58 to 62 literal
comparisons (outcomeMessages and onSuccess each carry a two-literal type
gate). The cli outcome-messages equality now leaves out the metadataForms
subtree, where the new form row's own strings match the substring.

Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ
Co-authored-by: Claude <noreply@anthropic.com>
…conciliation-rows

Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ
Co-authored-by: Claude <noreply@anthropic.com>
…conciliation-rows

Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 5, 2026 23:43
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 5, 2026 23:43
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 5, 2026
Merged via the queue into main with commit 8e35895 Oct 6, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21863-stale-reconciliation-rows branch October 6, 2026 00:23
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ecision in words instead of a tracker number (stage 22) (objectstack-ai#21931)

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

Stage 22 of this card: the next area of class (e), the test strings
shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513.
This stage takes the third name-ordered `ui/` group: the 29 id-bearing
test files directly under `packages/spec/src/ui/` from
`dataset-filter-nested-relation-list.test.ts` to
`view-inline-object-binding.test.ts`. Those files carried 96 messages
and 102 tracker ids, citing 52 records. 100 of those ids now either
state what their record decided, in words (form D), or are dropped where
the title already says it. Two stay: they are needles, ids that an
assertion reads in another file's text (below). Text only: no assertion,
identifier, test count or code comment changes, and no file is renamed.

## Census at the base (`be97cf3c93`)

Instruments: `census10.cjs` (md5 `9d08602ab972b4b8643c90d64d40fa41`),
`census.cjs` (md5 `6e42a45a926d375013c32d62f16a296e`), `census-wide.cjs`
(md5 `c98410a19529c439adb0afbfb00026a2`) and `dirtable.cjs` (md5
`dda605c54745b4a60cc14c9a686e4eff`), byte-identical to the copies stages
10 to 21 used. A literal counts as a test title when its folded message
is argument 0 of a `describe` / `it` / `test` call, `.each` / `.skip` /
`.only` chains included. Everything else is an "other" string.

The worktree was cut from `origin/main` at `be97cf3c93`, four commits
past the claim's `3dbd084209`. At the claim's base both instruments read
**665 messages / 702 ids**, the seat's reading and stage 21's head
reading. At `be97cf3c93` they read **665 / 704 in 154 files**: the two
extra ids are in `system/metadata-form-zod-reconciliation.test.ts` (22
to 24 ids), a ledger `why` string that objectstack-ai#21901 rewrote. No `ui/` file
moved.

| directory | files | messages / ids | titles | other |
|:--|--:|--:|--:|--:|
| `ui/` (this PR: 29 of the 48 files) | 48 | 201 / 213 | 186 / 198 | 15
/ 15 |
| `api/` | 40 | 189 / 201 | 181 / 193 | 8 / 8 |
| `system/` | 34 | 154 / 167 | 128 / 138 | 26 / 29 |
| (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 |
| `ai/` | 1 | 2 / 2 | 0 | 2 / 2 |
| `contracts/` | 1 | 1 / 1 | 0 | 1 / 1 |
| **total** | **154** | **665 / 704** | **612 / 648** | **53 / 56** |

The group reads **96 messages / 102 ids in 29 files**, the seat's
figures file for file:

| file (under `ui/`) | messages / ids | titles | other |
|:--|--:|--:|--:|
| `dataset-filter-nested-relation-list.test.ts` | 5 / 5 | 5 / 5 | 0 |
| `door-reachability.testkit.test.ts` | 4 / 4 | 3 / 3 | 1 / 1 |
| `expression-scope-app-root.pin.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `form-layout-inline-grid-retired.test.ts` | 4 / 4 | 3 / 3 | 1 / 1 |
| `form-option-enum-derive.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `i18n-label-resolver.test.ts` | 5 / 6 | 5 / 6 | 0 |
| `i18n.test.ts` | 6 / 6 | 5 / 5 | 1 / 1 |
| `inline-action-type.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `inline-action.test.ts` | 3 / 3 | 3 / 3 | 0 |
| `interaction-config-retirement.test.ts` | 4 / 4 | 2 / 2 | 2 / 2 |
| `joined-report-block-type.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `master-detail-detail-sort-field-retirement.test.ts` | 1 / 1 | 1 / 1 |
0 |
| `notification-embed-retirement.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `notification.test.ts` | 4 / 4 | 3 / 3 | 1 / 1 |
| `page.test.ts` | 2 / 3 | 2 / 3 | 0 |
| `react-blocks.test.ts` | 3 / 4 | 3 / 4 | 0 |
| `report-joined-block-dataset.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `report.test.ts` | 3 / 3 | 3 / 3 | 0 |
| `responsive.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `section-group-reference.test.ts` | 2 / 2 | 2 / 2 | 0 |
| `strictness-batch14.test.ts` | 4 / 5 | 3 / 4 | 1 / 1 |
| `view-authoring-wire-split.test.ts` | 11 / 11 | 11 / 11 | 0 |
| `view-console-round-trip-keys.test.ts` | 5 / 5 | 5 / 5 | 0 |
| `view-field-order-composition.pin.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `view-filter-rule-value-shape.test.ts` | 8 / 9 | 8 / 9 | 0 |
| `view-filter-rule-wire-id.test.ts` | 4 / 5 | 4 / 5 | 0 |
| `view-form-features-root.test.ts` | 2 / 2 | 2 / 2 | 0 |
| `view-gantt-tree-config-closed-15469.test.ts` | 4 / 4 | 4 / 4 | 0 |
| `view-inline-object-binding.test.ts` | 4 / 4 | 4 / 4 | 0 |
| **29 files** | **96 / 102** | **89 / 95** | **7 / 7** |

Seventeen more test files sit in the same name range and carry no id.
The seven "other" strings are the two needles below and five strings
rewritten and declared to the text-only tool: the expect messages at
`door-reachability.testkit.test.ts:216`, `i18n.test.ts:166-167` (the id
is on `:167`), `interaction-config-retirement.test.ts:148` and `:189`,
and the door name at `form-layout-inline-grid-retired.test.ts:61`, which
`describe(door.name, …)` prints as a title.

- **Controls.** Lit: `ui/view.test.ts`, outside the group, reads 43 ids
at the base and at the head. Dark: `view-authoring-wire-split.test.ts`
reads 0 at the head while 11 of its comment lines still carry a number.
Planted in scratch copies of head files: an id put into an
`inline-action-type.test.ts` title reads 1 / 1, and an id put into a
`report.test.ts` comment reads 0.
- **A wider pattern** (any `#` plus digits) reads the same as the gate
pattern in all 29 files at the base.
- **At the head:** 571 messages / 604 ids in 127 files. The 29 files
read 2 / 2 (the two needles), `ui/` reads 107 / 113, and no other file
moved.

## How the area was chosen

`ui/` has no subdirectory test file with an id, so it is taken in
name-ordered file groups near the ~100-id bound. Stage 21's re-cut named
this group at 102 ids, and this census reads 102, so no re-cut was
needed.

**Named for the next stages** (cut from the head census, 571 / 604):
- `ui/` 113 ids. 106 sit in the last group, the 16 files from
`view-item-config-type.test.ts` to `widget.test.ts` (100 messages / 106
ids; `view.test.ts` alone 43, `view-strictness-batch18.test.ts` 11,
`view-overlay-viewkind-arm.test.ts` 10). The other 7 are kept items:
stage 20's `component-props-unknown-members.pin.test.ts:322`, stage 21's
four colour literals, and this stage's two needles.
- `api/` 201, two stages. `system/` 167, two. The files directly in
`src/`, 120, one.
- The needles: the three docblock needles (`ai/build-progress.test.ts`
x2, `contracts/approval-service.test.ts`), the kept `:322`, and this
stage's two. One stage, with an at-tier review.

## The two needles, kept

- **`notification.test.ts:123`**, `source.indexOf('// [objectstack-ai#4610]')`. The
`./ui notification tombstone` pins read `ui/notification.zod.ts` and
slice it at this anchor, the comment that opens the tombstone at
`ui/notification.zod.ts:94`; `:134` then asserts the slice starts with
it. The id is the anchor text of a source comment, so it can only leave
together with that comment.
- **`strictness-batch14.test.ts:395`**,
`expect(source).toContain('objectstack-ai#5015')`. It reads `ui/notification.zod.ts`
and `ui/sharing.zod.ts` and asserts that both record the retirement by
citing the record. Those citations sit in source comments at
`ui/notification.zod.ts:48` and `:103`, and `ui/sharing.zod.ts:22` and
`:106`.

The five other "other" strings are failure messages of assertions whose
expected values carry no id, plus one door name. None is a needle.

## What each id became

- **24 literals (29 ids)** now state a decision in words.
- **22 literals (22 ids)** get their subject back in words, where the
number stood for a thing.
- **48 literals (49 ids)** drop a number the title already explains.

Every cited record was read with its comments through REST: 51 answer
200 and 1 answers 404. Three citations are cross-repo, `objectui#3907`,
`ui#6206-B` and `objectui#6262`, and were read from objectui. Two
records closed with no comment, objectstack-ai#3916 and objectstack-ai#4413; their decisions were
read from what landed: `f752ee3` ("give reports a sort declaration")
with `a831df1` ("`report.order` is live"), and `ebb209c` ("withdraw the
`record:*` blocks from the react tier — no renderer read the props it
published"). objectstack-ai#11284 answers 404; it was read from its landing commit
`5383fa6` (PR objectstack-ai#11695) and that commit's CHANGELOG entry.

Where a record's first decision was later corrected, the title follows
the corrected one:
- **objectstack-ai#15184:** its first ruling retired `fieldOrder`; ruling B superseded
it on a measured false premise (keep the key, declare the `columns` x
`hiddenFields` x `fieldOrder` composition). The title reads "the
list-view field composition is declared, not implied", which is ruling
B.
- **objectstack-ai#6227 and objectstack-ai#19514:** objectstack-ai#6227 recorded `equals` + array as accepted;
objectstack-ai#19514 reversed that on measurement. The two titles say so, in that
order.
- **objectstack-ai#20456:** not every census key is declared (a key the census mapped
to an existing spelling stays undeclared, which `:147` pins), so the
census describe names what the census records, not "every key is
declared".

**Stated in words:**

| record | literal (under `ui/`) | now reads | the decision |
|:--|:--|:--|:--|
| objectstack-ai#20080 | `dataset-filter-nested-relation-list.test.ts:116` | "§1 —
both analytics carriers refuse a list inside a nested relation, at save"
| Remedy A (triage `5825670610`): the two analytics carriers refuse the
list when the filter is saved, not when it is charted; the shared
`FilterConditionSchema` stays as ruled. |
| objectstack-ai#5056 | `door-reachability.testkit.test.ts:156` | "regression — the
any-one-shared-property bridge stays dead; a share of the shape decides"
| The derived-clone bridge stops firing on any one shared property and
requires a whole-shape overlap of at least 0.5. |
| objectstack-ai#5828 | `door-reachability.testkit.test.ts:216` (expect message) |
"the residual false-reachable case — no threshold excludes it" | Closed
not planned: no threshold separates a small all-shared-leaf shape from a
real derivation that also scores 1.0, so the `KNOWN BOUND` pin is the
record. |
| objectstack-ai#17203 | `expression-scope-app-root.pin.test.ts:84` | "no UI prose
face advertises `app` as an expression-scope root — the renderer no
longer mounts it" | Option B of decision batch objectstack-ai#67: objectui stopped
binding `app`, and the engine's `SCOPE_ROOTS` is the contract. |
| objectstack-ai#6761, objectstack-ai#6765 | `i18n-label-resolver.test.ts:281` | "resolveI18nLabel —
rule parity with objectui pickLocalized (ruled: one shared resolver, on
the server)" | Maintainer ruling B on objectstack-ai#6761: an inline locale map is
resolved to a string server-side, by one shared resolver in
`packages/spec`; objectstack-ai#6765 is that resolver, held to `pickLocalized`'s rule.
|
| `objectui#3907` | `i18n-label-resolver.test.ts:340` | "… the rule
departures converged once objectui read only own, string-valued entries;
one departure survives" | `pickLocalized` gained the own-property check
and the string filter on every limb. |
| objectstack-ai#10492 | `i18n.test.ts:99` | "rejects a lone `key`, which used to
parse as a locale map" | A lone `{ key }` parsed as a map for a language
called `key`; it is refused by name, under the retired key-reference
ruling. |
| objectstack-ai#6828 | `inline-action.test.ts:225` | "object-form `params` prescribes
per action type — its url meaning is retired, not re-keyed" | Maintainer
ruling 2026-08-10: retire the third meaning; no new key, and the refusal
guidance branches by action type. |
| objectstack-ai#4988 | `interaction-config-retirement.test.ts:60`, `:189` (expect
message) | "ui/ interaction config family retirement — renderer
behaviour, not authored metadata"; "… being undone — these are renderer
behaviour, not authored metadata" | Maintainer ruling A (2026-08-04):
the five files are retired; they are renderer built-in behaviour, not
per-page metadata. |
| objectstack-ai#21768 | `master-detail-detail-sort-field-retirement.test.ts:489` |
"the narrowing exempts only an inline grid field's own `sortField`, a
key the grid widget declares" | The `object-form` runtime form field
declares the grid widget's eight camelCase keys, `sortField` among them.
|
| objectstack-ai#4610 | `notification.test.ts:78` | "does not re-expose the bare
Notification/NotificationConfig names from ./ui — the bare
`Notification` belongs to ./api alone" | The `./ui` names were deleted;
`./api`'s `Notification` is the live contract. |
| objectstack-ai#11027 | `page.test.ts:494` | "PageComponentSchema — retired
`responsive`, which no renderer read" | Maintainer ruling B
(2026-08-22): retire it, since its renderer hook had zero callers. |
| `ui#6206-B`, objectstack-ai#15442 | `page.test.ts:696` | "ElementDataSourceSchema
`filter` — one filter orthography platform-wide, the ViewFilterRule
array" | Ruling B on objectui#6206 (one orthography), and ruling A on
objectstack-ai#15442: the binding-level `dataSource.filter` converges on
`ViewFilterRule[]`. |
| objectstack-ai#4413 | `react-blocks.test.ts:92` | "REACT_BLOCKS — the record:*
family is out, since no renderer read the props it published" |
`ebb209c`: the `record:*` blocks are withdrawn from the react tier. |
| objectstack-ai#11284 | `react-blocks.test.ts:147` | "REACT_BLOCKS — vocabulary
converges on the metadata tier, and the ListView alias retirement" |
`5383fa6`: the react tier adopts the metadata-tier spelling. objectstack-ai#14791 in
the same literal is dropped: the title names its retirement. |
| objectstack-ai#3916 | `report.test.ts:334` | "Report ordering — a report declares
its own sort" | `f752ee3`: the time axis is ordered by default, and a
report gets its own sort declaration. |
| objectstack-ai#13855 | `section-group-reference.test.ts:86`, `:181` | "… the
field-group reference form, members derived from the group" | Maintainer
ruling B (2026-08-31): a section names a field group and inherits its
members through `deriveFieldGroupLayout`. |
| objectstack-ai#5011 (and objectstack-ai#4001) | `strictness-batch14.test.ts:206` | "dashboard
compareTo: no longer a union but the executor contract — the strictness
arm-error limit does not apply to it" | Maintainer ruling 2026-08-04:
`compareTo` converges on the executor's `{ kind, dimension? }`. objectstack-ai#4001
becomes its subject, the strictness campaign. |
| objectstack-ai#5074 | `view-authoring-wire-split.test.ts:105` | "the two doors,
which is the whole point of the split: strict at authoring, reopened on
the wire" | Maintainer ruling A (2026-08-04): a strict authoring shape,
and a reopened wire member in the union. |
| objectstack-ai#19514 | `view-filter-rule-value-shape.test.ts:249` | "the scalar arm,
in both directions: a single-valued operator refuses an array" | The
protocol half of objectui#9050's ruling C′. |
| objectstack-ai#5114, objectstack-ai#5074 | `view-filter-rule-wire-id.test.ts:107` | "a
console-written filter row, judged per door: refused by name when
authored, stripped on the wire" | objectstack-ai#5114's provisional reopen ended when
objectstack-ai#5074's split landed. |
| objectstack-ai#15811 | `view-form-features-root.test.ts:208` | "an AST-only envelope
no longer reaches this scanner — an evaluated slot requires a `source`,
so it is refused one layer up" | Ruling A (decision batch objectstack-ai#122): every
engine-evaluated expression slot requires a non-blank `source`. |

**Subject back in words** (22 literals): "objectstack-ai#5056 premise" becomes "the
derived-clone bridge premise"; "the objectstack-ai#5068 props gate" becomes "the props
gate"; "the objectstack-ai#19331 shape" becomes "as its form row writes it"
(`object.form.ts`'s labelled `sharingModel` select); "the producer call
shape objectstack-ai#6761 needs" becomes "… the dataset compiler needs"; "the one
departure objectui#3907 did NOT touch" becomes "… the objectui map-limb
fix did NOT touch"; "the calls that caused objectstack-ai#6761" becomes "the calls
behind the dropped dataset label"; "retired at objectstack-ai#4988" becomes "retired
with the interaction-config family"; "after objectstack-ai#4988" becomes "after the
family retirement"; "objectstack-ai#4738 left it to ./ui alone" becomes "the
connector-side rename left it to ./ui alone"; "the objectstack-ai#4610 note" becomes
"the tombstone note"; "(objectstack-ai#5015 took the other half)" becomes
"(EmbedConfig, the other half, was retired)"; "objectstack-ai#4721, the silently
REVERSED sort" becomes "it once parsed as a silently REVERSED sort"; the
four `objectstack-ai#5599 —` titles become "the identity precondition …" / "identity
precondition — …" where the title needs the subject (`:272`, `:278`,
`:298`); "the census record (objectstack-ai#20456)" becomes "the census record of the
keys the console reads back"; "objectstack-ai#6227 — the reported shape" becomes "the
reported shape — a set operator carrying a scalar —"; "recorded as
ACCEPTED at objectstack-ai#6227" becomes "recorded as ACCEPTED by the first
value-shape rule"; "objectstack-ai#6227 — the refinement" becomes "the value-shape
refinement"; "the card's probe … (objectstack-ai#15469)" becomes "the probe that found
the gap"; "the objectstack-ai#14471 typo" becomes "the `colourField` typo", the key
the test writes; "objectstack-ai#6391's union membership" becomes "its union
membership".

**Dropped where already stated** (48 literals, 49 ids). A number goes
only where the title already says its decision. Examples: the three
`objectstack-ai#20080 §2` / `§3` / `§4` prefixes (the `§n` markers stay: the file's
own header numbers its sections with them); "[objectstack-ai#19920] InlineAction is an
inline action body, not unknown"; "… the retired arms are refused with
the prescription (objectstack-ai#20221)"; "InlineActionSchema — `bodyExtra` is the
payload key, `params` is not (objectstack-ai#5777)"; "ListView: objectName / viewType
are RETIRED — … (objectstack-ai#14791)"; the six `objectstack-ai#5074 —` prefixes beyond the first;
the four `[objectstack-ai#7741]` / `objectstack-ai#5114 —` prefixes; "what stays accepted (the objectstack-ai#5685
side: never stricter than the runtime)", which keeps "(never stricter
than the runtime)", objectstack-ai#5685's ruling in words. The batch labels `批 14`, `批
16` and `(batch 13)` stay in the earlier stages' form, and `ADR-0089
D3a` stays as a decision-record citation.

**No file is renamed.** `view-gantt-tree-config-closed-15469.test.ts`
keeps its name; its four title strings are rewritten.

## Readers

- **Test-name filters:** none. No tracked script, workflow or package
config passes `-t` / `--testNamePattern` (the 31 hits are `mapfile -t`,
`docker build -t`, `type -t`, a `create-objectstack -t` template flag
and a self-test's probe strings).
- **Snapshots:** none. No `__snapshots__` directory is tracked under
`packages/spec`, and none of the 29 files calls a snapshot matcher.
- **Projects:** `master-detail-detail-sort-field-retirement.test.ts` is
in the `repo` project (`packages/spec/vitest.repo-tests.json:50`); its
base and head runs below include it. The other 28 run in `local`.
- **By substring:** every old literal, its id-bearing fragment and a
window around each id (283 needles) was searched with `git grep` at the
base, across the tracked tree outside its own file. No gate, doc,
filter, snapshot, QA checklist entry or `scripts/check-*.mjs` self-test
reads one. The 4 hits are two code comments that quote the
`page.test.ts:494` title verbatim: `ui/dashboard.test.ts:585` and
`ui/responsive.test.ts:14`, both reading ("[objectstack-ai#11027] PageComponentSchema
— retired `responsive`"). Code comments are not this card's share. The
new title keeps "PageComponentSchema — retired `responsive`" as its
prefix, so a reader following either comment still finds it.

## Text-only proof

Stage 10's scratch tool (`textonly10.cjs`, md5
`d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file
on three legs:
1. **Skeleton:** the full AST, with string pieces masked. It must be
identical.
2. **Comments:** every comment, byte-equal.
3. **Strings:** each changed string leaf must sit in a test-call title
position or on a declared line, must carry a tracker id before, and must
carry no `#` plus digits after. This stage declares five lines:
`door-reachability.testkit.test.ts:216`,
`form-layout-inline-grid-retired.test.ts:61`, `i18n.test.ts:167`, and
`interaction-config-retirement.test.ts:148` and `:189`.

- **Result:** 29 of 29 files SAME on all three legs, with the per-file
counts predicted in writing before the run.
- **Totals:** 94 changed string leaves in 94 literals: 89 titles and 5
declared. The diff's `+` and `-` lines are exactly the 94 planned lines
as multisets, and every file keeps its line count.
- **Controls (14 of 14 as predicted on the first run, on scratch copies,
each anchor hit once):** identifier rename DIFF; numeric literal DIFF;
comment edit COMMENT DIFF; a non-title string given an id VIOLATION; a
rewritten title given a new id VIOLATION; a title that was id-free at
base edited VIOLATION; one title reverted to base SAME; an `it.each` row
given an id VIOLATION; an undeclared expect message changed VIOLATION; a
title re-split into a `+` chain DIFF; a declared expect message reverted
to base SAME; a declared expect message given a new id VIOLATION; a kept
needle edited VIOLATION; a kept needle's id dropped VIOLATION.
- **Templates and tables:** one `.each` title changes,
`view-filter-rule-value-shape.test.ts:255`, a `%s` template (`refuses %s
— …`): its placeholder and rows are untouched, and the printed names
below match the plan. One template-literal title loses only its tail
(`form-layout-inline-grid-retired.test.ts:141`).

**Test counts:** the 29 files were run at the base, in a separate base
worktree, and at the head, with `--project local --project repo`. Both
sides read 858 tests in 29 files, all passed, with the same count and
status sequence per file in 29 of 29. 596 full test names change, and
each changed name equals the base name with the planned replacements
applied (0 mismatches). No full name repeats on either side.

## Changeset: `skip-changeset`

Measured, not assumed:
- `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the
29 touched files are in it, and no `*.test.ts` at all. The controls
`src/ui/view.zod.ts`, `src/ui/report.zod.ts` and `dist/index.mjs` are in
it.
- In the built `dist/`, two new phrases and an old one each read in 0
files. The control `Unrecognized key` reads in 42.

So this PR publishes nothing, and no changeset is added.

## Verification (at `612375dc0c`)

- `pnpm turbo run build` over all packages: 71 / 71, through the shared
verify lock (`VERDICT command-exit 0`).
- `@objectstack/spec`:
  - `vitest run --project local`: 619 files, 18471 passed, 1 todo.
- `typecheck`: exit 0, including `check:test-typecheck` (52 files / 246
errors / 135 pinned signatures held). Its program holds all 29 group
files, counted by path with `tsc --listFilesOnly -p tsconfig.test.json`.
- `check:generated`: all 15 generated artifacts up to date, against the
`dist/` the build above wrote.
- **Gates:** `dispatch-gates --commands` derived 79 families, the same
set as stage 21, and all 79 exit 0. `--ran` reconciles: 79 derived, 79
run, 0 NOT-MEASURED, 0 UNRUN, every family with its exit code recorded.
The five roster families whose rosters sit under a touched directory
were also run, and each exits 0: `check:meta-url-spelling`,
`check:spec-changes`, `check:authz-resolver`, `check:error-code-casing`
and `check:filter-alias-parity`.
- **ESLint, a proven narrowing:** `--no-inline-config` over the 29 files
reads 0 errors and 0 warnings. The population comes from ESLint's own
config: 29 configured, 0 ignored. No file sets `parserOptions.project`
or `projectService`, so no untouched file's verdict can move.
- `check-governed-merges --test`: NOT governed, 188 changed lines (+94 /
-94).
- A control-byte scan over the 29 changed files finds none.

## `main` since the base

Re-fetched just before this PR opened, `origin/main` was two commits
past the base (`faf8dce482`: objectstack-ai#21917, objectstack-ai#21906). Neither touches
`packages/spec` or any of the 29 files, so `main` was not merged. `git
merge-tree` onto `faf8dce482` is clean, and none of the 8 open PRs
touches any of the 29 files.

## Acceptance notes

- **The two needles** stay, as above. They leave with their source
comments, in the needles' stage.
- **The census base moved by two ids** after the claim: objectstack-ai#21901
(`8e35895832`) rewrote a `why` string in
`system/metadata-form-zod-reconciliation.test.ts`, which now carries 24
ids where it carried 22. It is a ledger value, not a title, and it rides
the `system/` stages.
- **Same-id test titles in this card's later stages** go with those
stages: 34 lines in `packages/spec/src`, for example
`api/api-error-code-type.test.ts:71` ("[objectstack-ai#19920] …"),
`system/i18n-resolver.test.ts:2652` ("(objectstack-ai#5377)"),
`ui/view-metadata-schema.test.ts:215` ("identity precondition (objectstack-ai#5599)"),
`ui/view-strictness-batch18.test.ts:364` ("[RESOLVED at objectstack-ai#5074] …") and
`ui/view-union-diagnostics.test.ts:62` ("[objectstack-ai#6391] …").
- **Same-id test titles in other packages** stay: 38 lines in 9 packages
(`lint` 8, `objectql` 8, `service-analytics` 7, `cli` 4,
`metadata-protocol` 4, `spec/scripts` 3, `plugin-security` 2,
`plugin-sharing` 1, `service-automation` 1), each package's share under
the objectstack-ai#20513 lane children. Three of them cite `objectstack-ai#6262` (`objectql`) and
two cite `objectstack-ai#6206` (`plugin-security`, `plugin-sharing`): those are
objectstack records, different from the objectui records this group
cites.
- **Code comments with live ids** remain in these files and their
sources, for example the `[objectstack-ai#4610]` / `[objectstack-ai#5781]` banners in
`notification.test.ts`, the `objectstack-ai#5056` section headers in
`door-reachability.testkit.test.ts`, and the two comments above that
quote the old `page.test.ts:494` title. Code comments are not this
card's share.

---

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

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

Projects

None yet

2 participants