Skip to content

test(spec): the last ui/ file group's test titles state each cited decision in words instead of a tracker number (stage 23) - #21947

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20749-test-strings-n
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20749-test-strings-n

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #20749
Clause-②: no

Stage 23 of this card: the next area of class (e), the test strings shipped under packages/spec/src, as ruled in 5902360492 on #20513. This stage takes the last name-ordered ui/ group: the 16 id-bearing test files directly under packages/spec/src/ui/ from view-item-config-type.test.ts to widget.test.ts. Those files carried 100 messages and 106 tracker ids, citing 53 records. All 106 now either state what their record decided, in words (form D), or are dropped where the title already says it. No needle sits in this group. Text only: no assertion, identifier, test count or code comment changes, and no file is renamed.

Census at the base (9e33ee7c59)

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 22 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 9e33ee7c59, the claim's base. Both instruments read 571 messages / 604 ids in 127 files, the seat's reading and stage 22's head reading.

directory files messages / ids titles other
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
ui/ (this PR: 16 of the 21 files) 21 107 / 113 97 / 103 10 / 10
ai/ 1 2 / 2 0 2 / 2
contracts/ 1 1 / 1 0 1 / 1
total 127 571 / 604 523 / 553 48 / 51

The group reads 100 messages / 106 ids in 16 files, the seat's figures file for file:

file (under ui/) messages / ids titles other
view-item-config-type.test.ts 1 / 1 1 / 1 0
view-metadata-schema.test.ts 8 / 8 8 / 8 0
view-metadata-type.test.ts 2 / 2 2 / 2 0
view-overlay-options-bag.test.ts 6 / 6 6 / 6 0
view-overlay-options-type.test.ts 1 / 1 1 / 1 0
view-overlay-owner-hidden-retirement.test.ts 1 / 1 1 / 1 0
view-overlay-viewkind-arm.test.ts 10 / 10 10 / 10 0
view-overlay-viewkind-type.test.ts 1 / 1 1 / 1 0
view-strictness-batch18.test.ts 10 / 11 10 / 11 0
view-submit-redirect-url.test.ts 5 / 5 5 / 5 0
view-union-branch-focus.test.ts 7 / 7 6 / 6 1 / 1
view-union-diagnostics.test.ts 4 / 5 4 / 5 0
view-union-retirement-prescription.test.ts 1 / 1 1 / 1 0
view.test.ts 39 / 43 38 / 42 1 / 1
widget-i18n-retirement.test.ts 3 / 3 2 / 2 1 / 1
widget.test.ts 1 / 1 1 / 1 0
16 files 100 / 106 97 / 103 3 / 3

Three more test files sit in the same name range and carry no id (view-item-owner-hidden-retirement.test.ts, view-list-tabs-retirement.test.ts, vocabulary-derivation.test.ts). The three "other" strings are rewritten and declared to the text-only tool: the table label at view-union-branch-focus.test.ts:139, which prints inside two for … of test titles, and the expect messages at view.test.ts:4099 and widget-i18n-retirement.test.ts:135.

  • Controls. Lit: ui/notification.test.ts and api/api-error-code-type.test.ts, outside the group, read 1 id each at the base and at the head. Dark: view.test.ts reads 0 at the head while 92 of its comment lines still carry a number. Planted in a scratch tree: an id put into a widget.test.ts title reads 1 / 1 (title:describe), and an id put into a view-metadata-type.test.ts comment reads 0.
  • A wider pattern (any # plus digits) reads the same as the gate pattern in 15 of the 16 files at the base. view.test.ts reads 2 more, and keeps them at the head: the CSS colours '#00cc00' (:2770) and '#22c55e' (:3537), fixture values that cite nothing.
  • At the head: 471 messages / 498 ids in 111 files. The 16 files read 0 / 0, ui/ reads 7 / 7, 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 22's re-cut named this group at 106 ids, and this census reads 106, so no re-cut was needed. view.test.ts (43 ids) is one file inside one text-only proof here, so it is not split.

ui/ after this PR reads 7 / 7, all kept items: stage 20's component-props-unknown-members.pin.test.ts:322, stage 21's four colour literals (dashboard-chart-structure-refusal.test.ts:94, dashboard.test.ts:124), and stage 22's two needles (notification.test.ts:123, strictness-batch14.test.ts:395).

Named for the next stages (cut from the head census, 471 / 498):

  • api/, 201 ids in 40 files, with no subdirectory. Its first name-ordered group near the bound is ai-agents-envelope.test.ts through package-lifecycle.test.ts: 27 files, 100 messages / 106 ids (95 / 101 titles, 5 / 5 other: auth.test.ts, discovery-environment-subset.pin.test.ts and three in export-job-family-retirement.test.ts). The second is plugin-rest-api.handler-status-retirement.test.ts through zod-issues-to-fields.test.ts: 13 files, 89 / 95, protocol.test.ts alone 50.
  • system/ 167, two stages. The files directly in src/, 120, one.
  • The needles: the three docblock needles, the kept :322 and stage 22's two. One stage, with an at-tier review.

What each id became

  • 25 literals (27 ids) now state a decision in words.
  • 20 literals (22 ids) get their subject back in words, where the number stood for a thing.
  • 55 literals (57 ids) drop a number the title already explains.

Every cited record was fetched with all its comments through REST, and its decision was read from its ruling, ACCEPT and landing comments: 53 records, 51 answer 200 and 2 answer 404. Five citations are objectui's and were read from objectui: objectui#5233, objectui#2231 (cited bare at view-strictness-batch18.test.ts:309), objectui#6237 (cited bare at view.test.ts:949), objectui#5435 and objectui#3289. Three same-number records in the other repository were fetched first and set aside: objectstack#2231 is a version-packages PR, objectstack#6237 a datasource PR, and objectui#2998 a form PR; framework#1894 / #2998 are this repository's #1894 and #2998 under its old name. The two that answer 404 were read from what landed:

One citation names the wrong record, and the titles now state what landed instead. #3896 close-out (twice in view.test.ts) names #3896, the sharing-rule criteria card, which records no decision about these keys; the two titles state the decision from the landed tombstones of form.defaultSort and view.responsive / view.performance, as stage 20 did for action.test.ts.

Where a record's first decision was corrected later, the title follows the correction:

Stated in words:

record literal (under ui/) now reads the decision
#5599 view-metadata-schema.test.ts:95 "REJECTS a bare {} — the pin this line used to make, reversed by the identity precondition" Maintainer ruling 2026-08-06, direction B: a minimal identity precondition ahead of the union's four members; each member's .strip() is untouched.
#7741 view-metadata-schema.test.ts:125 "… NO object binding — a row no read path could serve, with located guidance" Maintainer ruling 2026-08-12, direction B: a row that cannot be expanded or served by any read path is not stored and badged valid; the inline arm requires the binding, refused with defineView's guidance.
#5599 view-metadata-schema.test.ts:215 "identity precondition — a body must read as a view before any member judges it" The same direction B.
objectui#5233 view-metadata-schema.test.ts:413 "… a columnState-only patch (the patch-only write the console persists)" Maintainer ruling 2026-08-12 (on #7494): persistViewPatch stores the patch only, not the merged base.
#17152 view-overlay-owner-hidden-retirement.test.ts:335 "… names the family's D2 conversion (ruled: a D3 entry per family, even beside a lossless D2)" Ruling B (director seat, 2026-09-10, upheld 2026-09-11): one D3 semantic entry per retired family, beside its D2 conversion even when D2 is lossless.
#7494 view-overlay-viewkind-arm.test.ts:102 "the console %s toggle (a patch-only write, as ruled) is ACCEPTED on listOverlay" Maintainer ruling 2026-08-12: the overlay store is org-wide, and the toolbar write stores the patch only.
#4001 view-strictness-batch18.test.ts:91 "批 18, unknown keys refused — the doors these shapes are reachable through" The strictness campaign: an unknown key on the authorable surface is refused, not silently stripped.
#15469 view-strictness-batch18.test.ts:149 "… a CLOSED entry, and since the renderer-ahead .passthrough() was removed a CLOSED parent too" Maintainer ruling A (decision batch #41, 2026-09-05): every key the gantt and tree renderers read is declared, and both .passthrough() calls go.
#5074 view-strictness-batch18.test.ts:364 "[RESOLVED by the ruled split] ViewItemSchema SPLIT — …" Maintainer ruling A (2026-08-04): split — a strict authoring ViewItemSchema and a reopened wire member in the union.
#5074 view-strictness-batch18.test.ts:403 "[RESOLVED with the ruled split] ListViewSchema.sort CLOSED — …" The split's scope addendum: the wire door strips the console's decoration keys before validating, so sort[] closed again with no declared id.
#7025, #7741 view-union-diagnostics.test.ts:246 "the acceptance face of ViewMetadataSchema — frozen by the diagnostics work; every move since is deliberate and pinned" #7025's sweep rule: the diagnostic face improves, the acceptance face does not move; #7741's ruled binding requirement is the first pinned move since.
#9463 view.test.ts:342 "viewMode — the granularities the gantt renderer honours, measured" Declare viewMode with exactly the granularities objectui's GanttView honours, measured, not invented (the spec half of objectui#5074's ruling).
#17053 view.test.ts:441 "the legacy string sort clause is retired — one spelling, the array" objectui's ruling (director batch #77, option B): one spelling, the array; the spec stops producing the string.
#13704 view.test.ts:873 "wizard tightening — sections are the steps, the inert step keys are refused, no key is added" The ruled shape of #13622 (2026-08-31): sections are the steps, the wizard-inert step keys are refused at parse, zero new keys.
objectui#6237 view.test.ts:949 "… stay accepted on tabbed/simple (the ruled split confines it to wizard steps)" Maintainer ruling 2026-08-30 (director batch #3): FormSectionConfig is split, so tabbed sections take a predicate and wizard steps carry none.
objectui#2231 view.test.ts:2876 "ListColumnSchema summary object form and prefix — spec-owned, no longer an objectui-local extension" The derive-by-reference unification: 677b591 moved prefix and the { type, field } summary form into the spec, closing objectui's local .extend().
#3896 (see above) view.test.ts:3144 "FormViewSchema — retired defaultSort (audit close-out: nothing read it)" The landed tombstone: form.defaultSort was removed because nothing read it.
objectui#5435 view.test.ts:3386 "… defaults asymmetry, copied from what the renderer reads" Ruling A (2026-08-22): the spec adopts group / hideFields / rowColor, with the defaults copied from ListView's reads.
#3896 (see above) view.test.ts:3826 "ListViewSchema — retired responsive/performance (audit close-out: no renderer read them)" The landed tombstones: no renderer or runtime read either key.
#7176 view.test.ts:3847 "ListViewSchema — retired striped/bordered/virtualScroll (every reader only passed them through)" Maintainer ruling 2026-08-10: retire under ADR-0049, since every measured reader copied the keys forward and none applied them.
#5832 view.test.ts:4099 (expect message) "HttpMethodType was renamed to HttpMethodSubset" Maintainer ruling 2026-08-06: rename the 5-value subset; the 7-value HttpMethod keeps its name and its wire contract.
#16577, #13817 view.test.ts:4708 "… the type: 'calendar' axis is NOT gated by the allowedVisualizations check (ruled: a completeness warning)" Ruling B (director seat, 2026-09-11): the #13817 guard keeps gating allowedVisualizations only; the type: 'calendar' route is carried at warning by checkViewCompleteness.
#19228 view.test.ts:4793 "view row bound — pagination.pageSize is the one bound; no per-kind limit on the view configs" Maintainer ruling D (2026-09-23): one row bound per view, pagination.pageSize; the per-kind limit was removed before it shipped.
#5055 widget-i18n-retirement.test.ts:70 "ui/ widget + i18n family retirement — doorless vocabularies removed, not tightened" Maintainer ruling A (2026-08-06): ADR-0049 enforce-or-remove retires the unreachable widget and locale vocabularies; closing them would only dress a dead slot as a checked one.
objectui#3289 widget-i18n-retirement.test.ts:196 "the surviving error slot is exactly the one objectui renamed its own slot onto, with no alias" objectui followed the spec: its widget errorMessage slot became the spec's error, with no alias, and the form renderer produces it.

Subject back in words (20 literals): "(binding pair, #7741)" becomes "(the object + viewKind binding pair)"; "union error behaviour (#5014)" becomes "union error behaviour (where a branch prescription gets buried)"; the five #7496 prefixes become "the ruled redirect url shape —" (twice) and "ruled bullet 1 / 2 / 3 —", the file's own name for the ruling's three bullets; "the pre-#7510 ranking" becomes "the pre-fix ranking"; "the #4001 wrap prescription" becomes "the defineView wrap prescription"; the acceptance-face describe at view-union-branch-focus.test.ts:261 (above); "the #6926 fold" becomes "the producer-side groups fold"; "(#7025 membership)" becomes "and the union corpus pins it accepted"; "(#8321/#12174)" becomes "(a negative or fractional scale)", what that test probes; "the exact declaration #9340 exists to make legal" and "the gap #9340 closes" name "this block"; "(acceptance criterion, objectstack#11195)" becomes "(the acceptance criterion for adopting the three keys)"; "(#7176 rides …)" becomes "(the retirement rides …)"; "the axis #13817 does not gate" becomes "the axis the allowedVisualizations check does not gate"; "zero holders after #5055" becomes "after the widget + i18n retirement"; "#5055 — the one surviving shape" becomes "the widget retirement — the one surviving shape".

Dropped where already stated (55 literals, 57 ids). A number goes only where the title already says its decision. Examples: the four [#19920] prefixes ("… typed by its arm, not unknown", "… a parsed view body, not unknown") and [#19871]; the six [#20051] describes on the options bag and the one in view-union-retirement-prescription.test.ts; the eight #20186 describes ("a column-less list PATCH is judged by the list member", "each member judges ONE viewKind", …); the three [#6391] describes, three [#7510] titles and the [#21180] table label ("the retired publicPicker key itself"); "(#3095)", "(#5074)" after ".strip() round-tripping is untouched", "(#9933)" after "runtime-only overlay key", and the view.test.ts tails (#15469), (#6926), (#12174), (#19088), (#7084), (#9340 — …), (#17499), (#18791), (framework#1894 / #2998), (#5073 — …) x2, (#8010), [#4688], [#4691], (#6416 / #6619), (#17063), (#16885), (#13817) and (#16577). The batch label 批 18 stays, in stage 20's "批 19, unknown keys refused" form on the file's first describe and bare on the other four. W2 stays: the file's own header defines W1 and W2. The commit ce70876e stays in "(measured on origin/main ce70876)": a commit, not a tracker id.

No file is renamed.

Readers

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 three lines: view-union-branch-focus.test.ts:139, view.test.ts:4099 and widget-i18n-retirement.test.ts:135.
  • Result: 16 of 16 files SAME on all three legs, with the per-file counts predicted in writing before the run.
  • Totals: 100 changed string leaves in 100 literals: 97 titles and 3 declared. The diff's + and - lines are exactly the 100 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; the declared table label given a new id VIOLATION; a template-literal title given a new id VIOLATION.
  • Templates and tables: one .each title changes, view-overlay-viewkind-arm.test.ts:102, a %s template whose placeholder and rows are untouched. The table label at view-union-branch-focus.test.ts:139 feeds two for … of template titles, which print it whole. The template-literal title at :173 changes only its text before ${label}.

Test counts: the 16 files were run at the base, in a separate base worktree, and at the head, with --project local --project repo. Both sides read 862 tests in 16 files, all passed, with the same count and status sequence per file in 16 of 16. 574 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 16 touched files are in it, and no *.test.ts at all. The controls src/ui/view.zod.ts, src/ui/widget.zod.ts and dist/index.mjs are in it.
  • In the built dist/, a new phrase 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 75022207b3)

  • 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 16 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 22, 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 16 files reads 0 errors and 0 warnings. The population comes from ESLint's own config: 16 configured, 0 ignored. No file sets parserOptions.project or projectService, so no untouched file's verdict can move.
  • check-governed-merges --test: NOT governed, 200 changed lines (+100 / -100).
  • A control-byte scan over the 16 changed files finds none.

main since the base

Re-fetched just before this PR opened, origin/main was four commits past the base (1f0469655f: #21939, #21937, #21943, #21928). They touch 32 files, none of the 16; two are under packages/spec (a step-18 semantic migration entry and the migration registry, neither a test file). So main was not merged. The census on that tree still reads 571 / 604 in test files and 0 elsewhere. git merge-tree onto 1f0469655f is clean, and none of the 8 open PRs touches any of the 16 files.

Acceptance notes


Generated by Claude Code

…cision in words instead of a tracker number (stage 23)

Stage 23 of the spec lane's share of the runtime-string burn-down: the
test strings shipped under packages/spec/src, the last name-ordered
ui/ group (view-item-config-type.test.ts through widget.test.ts).

100 literals in 16 files (97 test titles and 3 declared strings: one
table label that prints inside for-of titles, and two expect messages)
lose 106 tracker ids: 25 literals now state the cited decision in words,
20 get their subject back in words, and 55 drop a number the title
already explains. No needle sits in this group.

Text only: one line per literal, no assertion, identifier, test count,
code comment or file name changes.

Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026

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 1f0469655f1707e47b7fefb4459222ff09f66736 → packageMentionDocs.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 04:50
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 6, 2026 04:50
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit a3bd157 Oct 6, 2026
41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20749-test-strings-n branch October 6, 2026 05:28
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
… an undeclared `help` (objectstack-ai#21956)

Fixes objectstack-ai#21948

Clause-②: no

## What changed

`translateObject` used to put a bundle's
`objects.OBJECT.fields.FIELD.help` entry on a `help` key that
`FieldSchema` does not declare. The served field then failed
`FieldSchema` with `unrecognized_keys`. A consumer that reads only
declared keys rendered the English `description`, and the console logged
one ingestion warning per such field.

- **Target key, measured.** The extractor writes that bundle entry from
`field.help ?? field.description`
(`packages/cli/src/utils/i18n-extract.ts:1223`). `FieldSchema` refuses
`help`, so on a spec-valid field the only source is `description`. The
nine bundle source trees author 0 `inlineHelpText` and 0 field `help`.
The nine `en` bundles carry 523 `help` entries.
- **`translateField`**
(`packages/spec/src/system/i18n-resolver.ts:3054`) now serves the
translated help on `description` and writes no `help` key.
`inlineHelpText` is not touched.
- **Precedence: ADR-0029 D9.2a**, the resolver's own rule, applied
through the existing `valueOverridesPackagedBase` predicate. There is no
second comparison. The catalog applies only while the served field's
`description` equals the packaged field's.
- No packaged base supplied: the catalog applies (the conservative edge
objectstack-ai#8284 established).
- A field the base does not declare counts as diverged, which is
`packagedPart`'s convention. The new finder `packagedObjectField`
(`:1434`) reads both field-map shapes.
- A field's `label` is unchanged and stays a flat `catalog ?? document`,
as its existing pin scopes it.
- **`ObjectFieldLike`** (`:2453`) drops its `help?: string` member. This
is safe:
- its `[key: string]: any` index signature still accepts and types a
`help` key, so a caller that writes or reads one still compiles (probe
below);
- `packages/spec/api-surface/system.json` records only `ObjectFieldLike
(interface)`, not its members, and `check:api-surface` stays green;
- the one in-repo importer (`service-analytics`, which reads `options`
only) still typechecks.
- The `translateObject` docblock no longer says it translates a field's
`help`. A new section states the target key and the D9.2a precedence.
- `.changeset/21948-spec-field-help-served-on-description.md`:
`@objectstack/spec` `patch`.

## Clause-② — measured `no`

- No Zod schema, parse or export changed. These all stay green:
`check:authorable-surface`, `check:api-surface`, `check:export-origins`,
and `check:generated` (15 artifacts up to date).
- **Type probe** (scratch, not committed), compiled against the rebuilt
`dist/system/index.d.ts`, exit 0:
- `const legacy: ObjectFieldLike = { name: 'x', help: 'legacy' }`
compiles, and so does reading `legacy.help`;
- a declared member still refuses a wrong value (an `@ts-expect-error`
on `label: 42` is consumed);
- `const proof: number = ({} as ObjectFieldLike).help` compiles, which
shows the probe read the rebuilt declaration.
- **Control leg:** the same probe against a copy of that `.d.ts` with
BASE's `help?: string` put back gives exit 2, with exactly one error:
TS2322 on the `proof` line. Writing and reading `help` compile under
both declarations.
- So no value the type or the schemas used to accept is refused now. The
one type-level delta is that `ObjectFieldLike['help']` is now read
through the index signature (`any`) instead of `string | undefined`.
That is stated here for the at-tier review.

## Measured

All at HEAD `3ad2f22bed` unless stated.

**Corpus at the resolver seam (scratch script, not committed).** Every
object in `packages/platform-objects/scripts/i18n-extract.config.ts` (48
objects, 617 fields), with that package's real bundles, using each
object as its own packaged base:

| | served `help` keys | `help` refused by `FieldSchema` | `sys_user`
fields with `help` | zh-CN `description` equal to the bundle entry |
|---|---|---|---|---|
| BASE behaviour (ablation leg 1 below) | 332 | 332 | 18 of 26 | 0 |
| HEAD | 0 | 0 | 0 of 26 | 332 |

- At HEAD in zh-CN, `sys_user.two_factor_enabled.description` =
`该用户是否已启用双因素认证。由 better-auth 的 \`twoFactor\` 插件维护。`.
- In `en`, 0 descriptions change from source, because the en entries
repeat the source.
- **NOT MEASURED: the HTTP door** (`GET /api/v1/meta/object/sys_user`).
The spec change invalidates the showcase build closure: 62 of 63 turbo
tasks miss the cache. The pins and the corpus run read the same
`translateMetadataDocument` dispatch the REST read calls.

**Pins** (`packages/spec/src/system/i18n-resolver.test.ts:3552`, 9
cases):
- a zh-CN served field carries no `help`, has the translation on
`description`, and parses with no `unrecognized_keys`;
- the untranslated control is unchanged;
- a field with both `description` and `inlineHelpText` gets the
translation on `description`, and `inlineHelpText` is untouched;
- a diverged `description` is kept in zh-CN and en, through the type
dispatch, while the undiverged sibling is translated;
- a field the base does not declare counts as diverged;
- with no packaged base (`undefined`, `null`, or omitted) the catalog
applies;
- an absent `description` is filled by the catalog;
- array-shaped fields are judged against an array-shaped base;
- the input is not mutated.

No existing pin asserted a served field `help`. I searched every test
outside the bundle suites, so none had to move.

**Ablation**, run on the committed head. Each leg used
`scripts/ablation-replace.mjs` in WRAP mode plus a trap. Predictions
were written down before the run. Both legs were restored, with the
restored blob equal to HEAD (`a8eade91`) and `git diff HEAD` empty:

| leg | mutation (anchor 1 → 0 on disk) | predicted | observed |
|---|---|---|---|
| 1 | restore BASE's two lines (write `next.help`, never `description`)
| 7 failed / 2 passed (control and no-mutation stay green) | `Tests 7
failed \| 2 passed` |
| 2 | flat overlay (the D9.2a comparison replaced by `true`) | 3 failed:
the diverged, undeclared-field and array cases | `Tests 3 failed \| 6
passed` |

- Leg 1 sample: `expected 'Whether two-factor authentication is …' to be
'该用户是否已启用双因素认证。…'`.
- Leg 2 sample: `expected '该用户是否已启用双因素认证。…' to be 'Edited by the
tenant.'`.

**Tests**
- `@objectstack/spec`, full local project: `Test Files 619 passed`,
`Tests 18480 passed | 1 todo`.
- `pnpm --filter @objectstack/spec typecheck`: exit 0. Its test layer
compiles under `tsconfig.test.json` with the identity-pinned debt held.
- `@objectstack/service-analytics`: `typecheck` exit 0. The three
dimension/label suites that call `translateObject`: `Tests 53 passed`.
- Targeted eslint over the two changed `.ts` files: 0 errors and 0
warnings. That run is not the repo-wide lint, which is CI's.

**Gates.** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --ran` derived 85 families: 84 ran with exit
0 and 1 is NOT MEASURED.
- The NOT MEASURED one is `pnpm check:dual-build-cjs-loads`, which exits
3 with `PREREQUISITE NOT MET` because 78 packages have no `dist/`.
- Narrowed in its place: all 19 `require` entries of
`@objectstack/spec`'s `exports` load from the rebuilt `dist`, and
`system.translateObject` is a function.
- `pnpm check:lean-entry-closure` first answered exit 3 (`objectql/core`
unbuilt). After `turbo run build --filter=@objectstack/objectql...` it
answered exit 0, and that is the recorded code.

**Ships:** `npm pack --dry-run` of `@objectstack/spec` lists
`dist/system/index.js`, `.mjs` and `.d.ts`. Both runtime files carry
`packagedObjectField` and no `next.help = translatedHelp`.

**Base:** origin/main moved to `a3bd157730` (objectstack-ai#21947, test titles under
`packages/spec/src/ui/` only). None of this PR's files is touched, so it
is not merged.

## Known readers in objectui

objectui at `.objectui-sha` `0abd4f9f`, read-only. No objectui file is
edited here.

- `packages/plugin-form/src/ObjectForm.tsx:1033` and
`packages/plugin-form/src/sectionFields.ts:311` (`field.help ||
field.description`), and
`packages/app-shell/src/utils/resolveActionParams.ts:714`
(`param.helpText ?? field.help ?? field.description`).
- All three fall back to `description`, so they render the same
translated text, and their `help` arm is now dead. Retiring those arms
is objectui's job.
- The ingestion warning is
`packages/core/src/utils/reference-keys.ts:362`, reached through the
`no-declared-twin` arm (`:415`) of `canonicalizeRetiredFieldKeys`. It
fires for any undeclared key on a served field def, so it goes quiet
once no `help` is served.

## Acceptance notes

None of these is addressed in this PR.

1. **Studio `saveFields` write-back.** NOT MEASURED; this is a read-only
inference.
- objectui `packages/app-shell/src/services/MetadataService.ts:898` to
`:939` carries per-field server keys from the translated read back into
the object PUT.
- `help` is not in objectui's `RETIRED_FIELD_KEYS`
(`packages/types/src/internal/retired-field-keys.ts`). So a field save
on a platform object with bundle `help` entries should have sent a key
`FieldSchema` refuses.
   - This PR removes the source.
   - Carrier: this PR. Reach would need one measured PUT.
2. **The extractor's `field.help ??` arm**
(`packages/cli/src/utils/i18n-extract.ts:1223`) reads a key
`FieldSchema` refuses, so it is dead on every spec-valid field. Carrier:
`domain:cli`; none in flight.
3. **Field `placeholder` is never overlaid.** `FieldTranslationSchema`
declares `placeholder`, and the extractor emits
`objects.OBJECT.fields.FIELD.placeholder` (`:1224`), but
`translateField` never overlays it. The nine shipped bundles carry 0
field-level placeholder entries (the 4 per locale are action params), so
it is dormant. Carrier: none.
4. **`inlineHelpText` has no translation path.** The extractor never
reads it, so an authored one is never offered for translation. The nine
bundle sources author 0 of them. Carrier: none.
5. **Docs line.** `content/docs/protocol/kernel/i18n-standard.mdx:166`
lists a field's `label` / `help` / `placeholder` as display labels, but
`FieldSchema` declares no `help`. Carrier: none.

---
_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

Labels

protocol:ui 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