Skip to content

feat(spec,cli,lint): translate a form section entry's label, placeholder and helpText from _sections.SECTION.fields.FIELD - #25620

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-25459-form-entry-pack-keys
Oct 24, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-25459-form-entry-pack-keys

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #25459

Clause-②: yes (widening)
Metadata-protocol: added

What

A form section entry ({ field, label, placeholder, helpText } in a form view's sections[].fields) overrides the object field's copy for that form. A translation pack had no key for those three strings, and translateView never read config.sections[].fields[], so a public form's entry overrides stayed in the authoring language beside translated headings and field labels.

The address, kept to the one _sections convention:

objects.OBJECT._sections.SECTION.fields.FIELD.label
objects.OBJECT._sections.SECTION.fields.FIELD.placeholder
objects.OBJECT._sections.SECTION.fields.FIELD.helpText

SECTION is the section's name and FIELD the entry's own field.

Landing (as the PM measured, confirmed)

  • packages/spec/src/system/translation.zod.ts: _sections.SECTION gains a strict fields record of { label, placeholder, helpText } (help / hint / tooltip are refused with the rename to helpText; entries is refused with the rename to fields).
  • packages/spec/src/system/i18n-resolver.ts: translateFormSections now calls a new translateFormSectionEntries for each named section. The rules are the section rules, one level down: only copy the entry authors is translated, a missing key keeps the authored string, and an entry string that diverged from the packaged section's entry with the same field keeps its value (ADR-0029 D9.2a, through the existing packagedPart + valueOverridesPackagedBase, no second comparison). No address: a section without a name, a bare field-name entry, and a composite entry's sub-fields.
  • packages/cli/src/utils/i18n-extract.ts (domain:server rider, scaffold only): walkObjectSections emits the key for every entry string a form view's named section authors, from the same three form-view anchors the heading walk reads (view.sections, form.sections, formViews.*.sections), in the section bucket. i18n-coverage.ts needs no hunk: coverage is computed from that same walker, so os i18n check / os lint see the keys with no other change (pinned).
  • packages/lint/src/validate-translation-references.ts: the _sections facts gain each form-view section's entry fields, and translation-target-unknown reports a _sections.SECTION.fields.FIELD key no form-view section of that name declares as an entry object. validate-translatable-sections.ts is unchanged (see Acceptance notes).
  • packages/rest/src/public-form-section-i18n.test.ts (domain:server rider, one door pin): GET /forms/:slug with Accept-Language: zh-CN serves the translated entry label, placeholder and helpText when the pack has them, and the authored copy when it has none or under en.

Tests (real readings, head 37a9e0f)

  • packages/spec: full --project local suite in three chunks: src/system 56 files / 1948 tests; src/ui and the root files 187 / 6603; every other directory 429 / 12167 (+1 todo). All passed. That includes i18n-resolver.form-sections.test.ts (new block: one pin per key, label, placeholder and helpText, beside the existing section pins; also missing key, authored-only, nameless section, explicit override vs packaged base, entry added after the fact, no input mutation) and translation.test.ts (accepts the node; strict; help goes to helpText).
  • packages/lint: full suite 158 files / 7238 tests passed (new case: an entry key that resolves is accepted; a key under a fieldGroups section and one for a bare field-name entry are reported as error).
  • packages/cli (unit tier, narrowed): every unit test file that imports the extractor or coverage module, 33 files / 384 tests passed, including the new test/i18n-form-section-entry-coverage.test.ts and the named i18n-form-title-coverage and i18n-bulk-action-coverage. The full unit tier needs more than the container's foreground cap; the integration tier is CI's.
  • packages/rest: the five translateView-reaching suites, 5 files / 128 tests passed (public-form-section-i18n 17, 3 of them new).
  • packages/metadata-protocol: it calls translateView in src/protocol.ts but has no translation test of its own. Its read path is exercised by the rest suites above over the real ObjectStackProtocolImplementation.
  • Typecheck: spec, lint, cli and rest exit 0.
  • Ablation, run once on committed HEAD with scripts/ablation-replace.mjs: the entry overlay's guard if (!Array.isArray(entries)) return entries; was replaced with if (entries !== undefined) return entries; (anchor 1 to 0, blob 5942acf6 to 1b8de47c). Result: 5 of 23 in i18n-resolver.form-sections.test.ts failed (the label, placeholder and helpText pins, authored-only, and override). Restored, blob back to HEAD, git diff HEAD empty. The suite reads src/, so the ablation needed no rebuild.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 117 commands on this diff. Exit codes were captured before any pipe and reconciled with --ran: 117 accounted for, 116 run with exit 0, 1 NOT MEASURED, 0 unrun.

  • NOT MEASURED: pnpm check:dual-build-cjs-loads (exit 3). It needs the whole workspace built, which is CI's job.
  • Run a second time after clearing a missing prerequisite, then exit 0:
    • check-plugin-teardown-shape --self-test: its pinned fixture commit was absent from this shallow clone.
    • check:skill-examples: needed @objectstack/client-react rebuilt.
    • check:i18n, check:i18n-coverage, check:i18n-walk-parity: needed @objectstack/cli built.
    • check:query-options-erasure: a repo-wide eslint ratchet. It ran detached until it finished.
  • pnpm --filter @objectstack/spec check:generated flagged two stale artifacts. --fix regenerated exactly those two, and they are committed: content/docs/references/system/translation.mdx and the system/ strictness-ledger count (377 to 378 sites, from the one new strict shape).

Acceptance notes

  • The resolver follows the section precedent for an inline locale map: an entry string authored as { en, 'zh-CN' } is authored copy and takes the catalog when the pack has a key. Not measured: whether the console renders such a map on an entry today. The crash that blocked it in objectui is closed, but this session did not run the console.
  • validate-translatable-sections still warns only about a nameless section that has a heading. A nameless section with no heading, whose entries author override copy, has no address and gets no warning. Left as is, because extending that rule is outside this card's ask.
  • Sub-fields of a composite or repeater entry are not addressed. Their names are relative to the parent entry, so keying them under the section would collide.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PkEdTSFyHwijHNXRdNTARE


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/lint, @objectstack/spec, touching 22 documentable anchor(s).

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

  • content/docs/api/error-catalog.mdx (via helpText (symbol, a field of interface FormSectionEntryLike), helpText (literal, a string literal in FormSectionEntryAttr; a string literal in ObjectTranslationDataSchema; a string literal in SECTION_ENTRY_ATTRS; a string literal in SectionEntryAttr; a string literal in translateFormSectionEntries))
  • content/docs/automation/flows.mdx (via helpText (symbol, a field of interface FormSectionEntryLike), helpText (literal, a string literal in FormSectionEntryAttr; a string literal in ObjectTranslationDataSchema; a string literal in SECTION_ENTRY_ATTRS; a string literal in SectionEntryAttr; a string literal in translateFormSectionEntries))
  • content/docs/deployment/validating-metadata.mdx (via globalFilters (literal, a string literal in packagedPart))
  • content/docs/protocol/kernel/i18n-standard.mdx (via helpText (symbol, a field of interface FormSectionEntryLike), helpText (literal, a string literal in FormSectionEntryAttr; a string literal in ObjectTranslationDataSchema; a string literal in SECTION_ENTRY_ATTRS; a string literal in SectionEntryAttr; a string literal in translateFormSectionEntries))
  • content/docs/protocol/objectui/actions.mdx (via helpText (symbol, a field of interface FormSectionEntryLike), bulkActionDefs (literal, a string literal in packagedPart), helpText (literal, a string literal in FormSectionEntryAttr; a string literal in ObjectTranslationDataSchema; a string literal in SECTION_ENTRY_ATTRS; a string literal in SectionEntryAttr; a string literal in translateFormSectionEntries))
  • content/docs/protocol/objectui/layout-dsl.mdx (via helpText (symbol, a field of interface FormSectionEntryLike), helpText (literal, a string literal in FormSectionEntryAttr; a string literal in ObjectTranslationDataSchema; a string literal in SECTION_ENTRY_ATTRS; a string literal in SectionEntryAttr; a string literal in translateFormSectionEntries))
  • content/docs/ui/actions.mdx (via bulkActionDefs (literal, a string literal in packagedPart))
  • content/docs/ui/dashboards.mdx (via globalFilters (literal, a string literal in packagedPart))
  • content/docs/ui/translations.mdx (via helpText (symbol, a field of interface FormSectionEntryLike), helpText (literal, a string literal in FormSectionEntryAttr; a string literal in ObjectTranslationDataSchema; a string literal in SECTION_ENTRY_ATTRS; a string literal in SectionEntryAttr; a string literal in translateFormSectionEntries))
  • content/docs/ui/views.mdx (via helpText (symbol, a field of interface FormSectionEntryLike), bulkActionDefs (literal, a string literal in packagedPart), helpText (literal, a string literal in FormSectionEntryAttr; a string literal in ObjectTranslationDataSchema; a string literal in SECTION_ENTRY_ATTRS; a string literal in SectionEntryAttr; a string literal in translateFormSectionEntries))

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

  • content/docs/releases/v16.mdx (via globalFilters (literal, a string literal in packagedPart))
  • content/docs/releases/v17/17-5.mdx (via helpText (symbol, a field of interface FormSectionEntryLike), helpText (literal, a string literal in FormSectionEntryAttr; a string literal in ObjectTranslationDataSchema; a string literal in SECTION_ENTRY_ATTRS; a string literal in SectionEntryAttr; a string literal in translateFormSectionEntries))
  • content/docs/releases/v17/17-6.mdx (via globalFilters (literal, a string literal in packagedPart))
  • content/docs/releases/v17/17-7.mdx (via bulkActionDefs (literal, a string literal in packagedPart))

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
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 151 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 7265b33e779e1d63afe50fb86e411beda7582838 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f34af0988cebc62ddf388bb9fe459e09d813ee79 — the merge of head 37a9e0fee342606e7c11b2c5877e27c009a22d28 into base 7265b33e779e1d63afe50fb86e411beda7582838, 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 f34af0988cebc62ddf388bb9fe459e09d813ee79 && git checkout f34af0988cebc62ddf388bb9fe459e09d813ee79
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7265b33e779e1d63afe50fb86e411beda7582838 37a9e0fee342606e7c11b2c5877e27c009a22d28 && git checkout -B drift-repro 7265b33e779e1d63afe50fb86e411beda7582838 && git merge --no-ff 37a9e0fee342606e7c11b2c5877e27c009a22d28

node scripts/docs-audit/affected-docs.mjs --json 7265b33e779e1d63afe50fb86e411beda7582838

⚠️ 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 7265b33e779e1d63afe50fb86e411beda7582838 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · #25459 · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-24T10:31Z · head 37a9e0fee3 · report 6383041298 · seat review at TIER_DEFAULT. Both clause-② limbs hit (path: packages/spec/src/system/translation.zod.ts, i18n-resolver.ts; declaration: Clause-②: yes), so a same-shape ## Contract review PASS at CONTRACT_REVIEW_TIER on the landing head is still owed. needs:contract-review is added in this stroke.

  1. Shape. Draft, base main, first line Fixes #25459, no other closing keyword. Clause-②: yes (widening) and Metadata-protocol: added are line-initial in the body and in the changeset (@objectstack/spec, @objectstack/cli and @objectstack/lint at minor). Twelve files, +658/−22 (get_files), all inside the claim's surface. The docs/audits/…counts/system.md 377 → 378 row is the strictness ledger the gate regenerates (spec lane), and content/docs/references/system/translation.mdx is generated. No governed path, no content/docs/releases/. Mergeable with no conflict.
  2. The card's asks, measured on the diff. (a) Address: _sections.SECTION.fields.FIELD.{label,placeholder,helpText}, strict, one convention with _sections. The aliases (entries → fields; help / hint / tooltip → helpText) are refusal-with-rename prescriptions, not new accepted spellings. (b) translateFormSectionEntries follows the section rules: authored copy only (sectionCopyAuthored), a missing key keeps the authored string, and an override on the packaged view keeps the edit (valueOverridesPackagedBase, ADR-0029 D9.2a). A section with no name and a bare field-name entry have no key. (c) The extractor scaffolds the key, and coverage reads the same walker. (d) translation-target-unknown reports an entry key that no form-view section of that name declares, and points a bare-field entry at objects.OBJECT.fields.FIELD. The door pins (zh-CN serves translated entry copy; a missing key and en serve authored copy) and one translateView pin per key are in the tests.
  3. Gates. Check-runs on 37a9e0fee3 at this record: 26 success, 6 skipped, 8 in progress, 0 failed. Local, per the report: --ran 117 derived, 116 run, 1 NOT MEASURED (check:dual-build-cjs-loads, declared; CI runs it). Spec full suite in three chunks, lint 158 files, cli unit narrowed to 33 extractor-reaching files (declared), rest 5 suites / 128 tests. Ablation on the entry walker: 5/23 red, then restored. Landing waits for every check green and the contract-review PASS.
  4. Deviations accepted. (a) The branch was opened on base 8801f2bb37 with no origin/main merge before the PR, and it is 10 commits behind with no conflict. The merge queue tests the merged result; this is not repeated as a rework. (b) The cli unit tier and the spec suite were narrowed or chunked for the foreground cap, declared. (c) The first push was refused for a model-named trailer and amended to the model-free pair before publishing. (d) The PR body footer follows the harness's attribution form rather than the role file's session-URL footer: a cosmetic gap, not re-worked. mcp_calls 0; relay writes 2. Container overhead for the card log: install 39 s + build closure 588 s = 627 s wall, with no turbo remote-cache pull.
  5. Out-of-scope outcomes.
    • An inline locale map on an entry string takes the catalog when the pack has a key (the section precedent); the console rendering of such a map is not measured: Acceptance notes (already in the PR body).
    • A nameless, heading-less section whose entries author copy has no address and no warning: Acceptance notes (extending validate-translatable-sections is a new warning face, outside the card).
    • Sub-fields of a composite or repeater entry have no address: Acceptance notes (no producer measured).
    • check:query-options-erasure (> 400 s locally) and check:dual-build-cjs-loads (whole-workspace build) are CI-shaped gates the derivation lists for a spec diff: dropped — an observation about local cost, with no wrong answer.
    • The cross-lane note for the packages/cli and packages/rest riders is posted on seat post [PM seat] domain:services — ⏳ vacant · archive of domain:server seat 1 (#6367) #6021 (6383123807).
  6. Landing. Not governed and under 3000 changed lines. Once the CONTRACT_REVIEW_TIER PASS is on record and every check on the then-current head is green, the seat sends pr_ready + automerge_enable through the relay. On merge, Web-to-Case: measure first whether a public form entry's label is served translated, then label the two submitter entries for the visitor ("Your name" / "Your email") only if it is (#2194 R3-D1) hotcrm#2248 can be reopened against the release that carries it.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 37a9e0fee342606e7c11b2c5877e27c009a22d28
Local-runs: none
Scope: packages/spec/src/system/i18n-resolver.ts, packages/spec/src/system/translation.zod.ts + packages/lint/src/validate-translation-references.ts, packages/cli/src/utils/i18n-extract.ts, packages/cli/src/utils/i18n-coverage.ts (unchanged; read for the no-hunk claim), packages/spec/src/ui/view.zod.ts (unchanged; FormFieldBaseSchema, FormSectionSchema), packages/spec/src/shared/strict-object.ts (unchanged; alias semantics), packages/spec/src/system/i18n-resolver.form-sections.test.ts, packages/spec/src/system/translation.test.ts, packages/lint/src/validate-translation-references.test.ts, packages/cli/test/i18n-form-section-entry-coverage.test.ts, packages/rest/src/public-form-section-i18n.test.ts, content/docs/references/system/translation.mdx, docs/audits/2026-07-unknown-key-strictness-ledger.counts/system.md, .changeset/25459-form-entry-pack-keys.md. Every path of the diff is read here; no remainder is left to a TIER_DEFAULT accept.

Inputs: card #25459 (body and every comment; the triage grade is the row in round record 6369194370 on #6015: p2, spec, i18n; no maintainer ruling on the card), PR #25620 body and file list, the net diff of 37a9e0fe against its merge-base with main (8801f2bb37; 12 files, +658/−22, matching the files API; main has not moved on any touched path since), and the check-runs on the head. Read-only: the head was fetched into the object store and read by sha; nothing was built, run or re-run.

① Derived judgments

  1. Schema widening — right. ObjectTranslationDataSchema._sections.SECTION gains an optional fields record of strict { label, placeholder, helpText }, keyed by the entry's field. The three names are the spelling a form section entry declares (FormFieldBaseSchema.label / .placeholder / .helpText, all I18nLabel, ui/view.zod.ts), so the pack key names the authored key one-to-one. The node is a strictObject with aliases; in shared/strict-object.ts an alias is a rename offered in the refusal (aliases[aliasProbe(key)] ?? findClosestMatches), never an accepted spelling, so help / hint / tooltip → helpText and entries → fields are prescriptions, not a lenient dialect (Prime Directive Add comprehensive test suite for Zod schema validation #12). Pinned in translation.test.ts: accepted node, .strict() refusal, help refused with the rename. help → helpText here against helpText → help on the FIELD translation one level up is deliberate and correct: each node suggests its own neighbour's spelling.
  2. Resolver overlay — right, and the D9.2a mechanics hold in the cited code. translateFormSections now hands each named section's fields to translateFormSectionEntries with the section's packagedSection. Rules read off the code: only an entry OBJECT with a non-empty field is addressed (a bare field-name entry and a nameless section are returned by reference); only authored copy is translated (sectionCopyAuthored, the same predicate the heading uses, so a non-empty string or an inline locale map); the packaged counterpart is packagedPart(packagedSection, 'fields', candidate.field === fieldName) and the override question is valueOverridesPackagedBase — one comparison, no second copy. The three-answer finder propagates correctly one level down: no packagedBase → undefined → the catalog applies; a section the base does not carry → {} → packagedPart({}, 'fields', …) is {} again → every entry string counts as diverged and keeps its value (an after-the-fact section keeps its entries' strings, consistent with its heading); a packaged entry present → exact comparison per attribute. packagedPart skips non-object candidates via asRecord, so a packaged bare string beside a served entry object reads as "authored after the fact" — the right answer under ADR-0029 D9.2a. Identity is preserved on no-change (fields === section.fields), so translateView still rebuilds config only when a section moved, and no input is mutated. All of it is pinned per key in i18n-resolver.form-sections.test.ts (label, placeholder, helpText, missing key, authored-only, nameless section, override vs packaged base with the no-base control, entry added after the fact, no mutation).
  3. Public surface — right. No export added, removed or renamed: translateFormSectionEntries, lookupSectionEntryText, FormSectionEntryLike, FormSectionEntryAttr are module-private and packagedPart's key union widens privately. The exported ObjectTranslationData type widens by an optional nested key only. check:api-surface and check:exported-any ran in Type Check · consumer gates → success.
  4. Lint gate for the new key — right, and it is one change with the schema. collectViewRecord.addSections records each named section's entry fields into sectionEntries from the same three anchors the served form view is composed from (formViews.*, the container's form, the container's own sections) with the same binding order the resolver's formSectionObjectName uses (the form's own data.object first). fieldGroups (:1190) and a page's record:details sections (:1323) still add a section NAME and no entry, so a _sections.SECTION.fields.FIELD key under either is reported by translation-target-unknown at error — exactly the set nothing reads. The known-section branch now continues after the entry check, so the pre-existing section-level orphan report is unchanged. Pinned: resolving key accepted; a key under a fieldGroups name and one for a bare field-name entry reported.
  5. Extractor and coverage — right. addSectionEntries reads the same three form-view anchors as the heading walk, skips a nameless section, a bare entry and composite sub-fields, and emits through pushEntry (authored string or inline map only, section bucket, { objectName }), so the scaffolded population equals the resolver's address set. i18n-coverage.ts is built from collectExpectedEntries (:473), so the coverage claim "no hunk needed" is true by construction and pinned in i18n-form-section-entry-coverage.test.ts. check:i18n, check:i18n-coverage and check:i18n-walk-parity (top-level groups only; objects was already walked) ran in Type Check · consumer gates → success.
  6. Generated artifacts — right, and complete. The two regenerated artifacts are the two the change implies: translation.mdx gains the fields row and the _sections type text, and the system/ strictness-ledger count moves 377 → 378 for the one new strict shape. authorable-surface/system.json records system/ObjectTranslationData:_sections at one level and liveness/translation.json's declared walk boundary is the group, so neither needs a row; check:authorable-surface, check:docs and check:strictness-ledger ran in Type Check · source gates → success, and Spec property liveness → success.
  7. Door pin — right. public-form-section-i18n.test.ts pins GET /forms/:slug under Accept-Language: zh-CN serving the translated entry label, placeholder and helpText, the authored copy for a missing key and under en, and that the anonymous lookup strip still runs over the rebuilt section object. No runtime hunk in rest or metadata-protocol is needed: both already call translateView.
  8. Declared equals enforced — right. The one new authorable address is read by the resolver, scaffolded by the extractor and gated by the linter in the same diff; no key is offered for a surface nothing reads (record:details, fieldGroups, composite sub-fields). Card "Done when" met on the diff: the three keys served, one translateView pin per key beside the section pins, lint and extractor see the key.
  9. Governance and shape — right. None of the 12 paths is a governed surface (Governed Surface Queue Guard → success); head repo equals base repo; 680 changed lines; branch claude/issue-25459-form-entry-pack-keys is the claimed branch and the PR body carries Fixes #25459.

② Semver level

.changeset/25459-form-entry-pack-keys.md: @objectstack/spec, @objectstack/cli, @objectstack/lint at minor; body carries Clause-②: yes (widening) and Metadata-protocol: added, both line-initial, identical to the PR body's pair. added is the right kind (a new key a translation author can write); kind not none ⇒ at least minor on every package the diff moves, and spec, cli and lint are the three it moves. @objectstack/rest is touched in a test file only and publishes nothing, so it correctly takes no entry. Non-breaking ⇒ no ADR-0087 disposition is owed and none is written; the breaking-migration clause does not apply. Check Changeset → success.

Clause-②: yes (widening)

③ Boundary flags

Dev report os-dev-report (card comment 6383041298):

  • Open question 1 — address shape _sections.SECTION.fields.FIELD.* (A, shipped): accepted. It is the card's own example and keeps one convention with _sections; the resolver, extractor and linter address it with one key family and no ADR names _sections (grep of docs/adr at origin/main), so no recorded decision is touched.
  • Open question 2 — an inline locale map on an entry counts as authored copy and takes the catalog (A, shipped): accepted. sectionCopyAuthored is the predicate the heading in the same function already uses; whether the console renders such a map on an entry is objectui's contract (objectui#13380 is closed), not this one's. Noted in the PR's acceptance notes.
  • Open question 3 — validate-translatable-sections not extended to a nameless, heading-less section whose entries author override copy (A, shipped): accepted for this card. The card asked that the linter ACCEPT the key; a new warning face is a separate decision and belongs on its own card (Prime Directive chore: version packages #10: file, never expand scope). No false "translated" artefact results — the entry copy renders authored in every locale, visibly, as the nameless heading does today.
  • Open question 4 — cross-lane declaration for the domain:server riders (packages/cli scaffold, one packages/rest door pin): escalated to the dispatching seat. Its own dispatch record says the PM posts it; it is a seat-post act, not a diff matter, and this record does not stand in for it.
  • Out-of-scope finding 1 — composite/repeater sub-fields have no pack address: accepted as a scoped exclusion. Their names are relative to the parent entry and the resolver's rule text and the PR both record the exclusion; the linter refuses nothing wrongly for it.
  • Out-of-scope finding 2 — check:query-options-erasure exceeds the local per-command cap and check:dual-build-cjs-loads needs a workspace build: answered by CI. Lint · scoped · query-options erasure → success on the head; check:dual-build-cjs-loads is hosted by ci.yml's Build Core → success on the head.
  • Deviation 1 — worktree attached to the existing branch without -b: process only. The base is the merge-base 8801f2bb37, verified with git merge-base.
  • Deviation 2 — commit trailer amended to the model-free pair: verified. All five PR commits end with Co-Authored-By: Claude at the noreply@anthropic.com address plus Claude-Session:; no model identifier anywhere in the five messages.
  • Deviation 3 — cli unit tier narrowed to 33 files, spec suite run in three chunks: the CI Test Core shards are the authority and their state at posting is stated below.
  • Deviation 4 — turbo remote cache not used: no bearing on the diff.

Check-runs on 37a9e0fe, read at 2026-10-24T10:49Z just before posting (46 runs): every concluded gate-carrying run is success — Lint & Repo Gates and its eight lanes, TypeScript Type Check and its four lanes (source gates, consumer gates, debt ledger, workspace), Build Core, Build Docs, Test Core (1/6), Test Core (2/6), Test Core (6/6), Governed Surface Queue Guard, Check Changeset, Check PR Size, Spec property liveness, Dogfood Regression Gate, and the claim and single-writer guards; no run concluded other than success or an expected skipped (the opt-in and full-battery runs). Still not concluded at posting: Test Core (3/6), Test Core (4/6) and Test Core (5/6) — three shards of the required Test Core context. This record judges the contract; the owning seat lands only once those three are green — and not while the maintainer's word stands: 「正在发版本,你可以派发但是不要合并。」

Implemented-by: session_01PkEdTSFyHwijHNXRdNTARE
Reviewed-by: session_01PC4NPXPgPmpDRG9DdDnyXT

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 24, 2026 11:00
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 24, 2026 11:00
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit 42d9fe8 Oct 24, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-25459-form-entry-pack-keys branch October 24, 2026 11:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/l tests tooling

Projects

None yet

2 participants