Skip to content

feat(lint): os validate refuses an action translation's resultDialog title / description / acknowledge when the action declares no resultDialog - #21304

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21264-result-dialog-leaves
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21264-result-dialog-leaves

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #21264
Clause-②: no (narrowing)

What changed

validateTranslationReferences (packages/lint/src/validate-translation-references.ts) is the rule behind translation-target-unknown, which os validate, os build and os lint all run. Its action-entry walk (checkActionEntry) now judges the result dialog's prose leaves, resultDialog.title, resultDialog.description and resultDialog.acknowledge, at both action addresses, objects.OBJECT._actions.ACTION and globalActions.ACTION.

bundle key judged against code · level the runtime read it mirrors
resultDialog.title / .description / .acknowledge whether the action declares a resultDialog at all translation-target-unknown · error (new) resolveActionResultDialog
resultDialog.fields.PATH each literal resultDialog.fields[].path translation-target-unknown · error (unchanged) resolveActionResultDialog
  • One finding per leaf, at the level the walk already uses for an undeclared target. No second vocabulary.
  • Under a declared dialog every leaf passes, including a leaf the dialog does not set itself. The resolver overlays a translated title onto a dialog that declares no title, so declaring the dialog is the whole condition.
  • The action-entry coverage pin (coverage vs the action translation shape) moves the three leaves from leaf-copy to reference-checked. Each is backed by a ghost that must report at both addresses, under a new host noDialog (an action that declares no resultDialog). resultDialog itself stays a container.
  • Docs: the rule table in content/docs/protocol/kernel/i18n-standard.mdx gains every keyed child of an action entry the walk judges: params.NAME.options.VALUE, outcomeMessages.OUTCOME, resultDialog.fields.PATH and this card's leaves. A lead sentence says these rows hold at both action addresses. The globalActions row states only what os validate does today: it resolves when it names an action with no objectName, and a key naming a bound action is an error whose message names the objects.OWNER._actions.ACTION key to write instead, OWNER being the action's own objectName. It says nothing about where the copy is read from; both resolvers still fall back to globalActions.ACTION for a bound action until lint: os validate refuses a bound action's globalActions translation key as "never read", but the spec's own i18n resolver reads it as the object-scoped key's fallback #21261 lands.
  • Changeset: .changeset/21264-result-dialog-leaves.md, @objectstack/lint minor, with Clause-②: no (narrowing), a BREAKING banner heading its "What changes for a project" paragraph, and one ADR-0087 disposition, not-required (no-migration-prescription), with its reason in writing.

Why

resolveActionResultDialog (packages/spec/src/system/i18n-resolver.ts) returns the action's own resultDialog untouched when the action declares none, before any lookup. So a dialog leaf under such an action is read by nothing, while os validate passed it clean. A bundle can translate a dialog the action declares. It cannot add one.

Measured

All readings below are on this branch, objectstack-ai/objectstack. The base is origin/main 4e6dc2338a, and HEAD is 46b40dc845.

The door, os validate --json, before and after. The probe stack has a bound action plain_probe with no resultDialog. Its bundle carries title, description and acknowledge under resultDialog, plus a params.ghost control. A bound action dialog_probe declares a dialog with no title, and its bundle carries all three leaves. An object-less plain_global with no dialog carries resultDialog.title. An object-less dialog_global declares a dialog and carries all three leaves.

key base 4e6dc2338a HEAD 46b40dc845
..._actions.plain_probe.params.ghost (control) error error
..._actions.plain_probe.resultDialog.title / .description / .acknowledge no finding 3 errors
globalActions.plain_global.resultDialog.title no finding error
..._actions.dialog_probe.resultDialog.* (declared dialog) no finding no finding
globalActions.dialog_global.resultDialog.* (declared dialog) no finding no finding

Before: 1 error. After: 5 errors, and both runs exit 1.

The runtime read. translateAction (@objectstack/spec/system, dist), on the same bundle in zh-CN:

  • plain_probe and plain_global return no resultDialog, so the leaves are never read.
  • dialog_probe returns title 令牌已生成, description 请妥善保存 and acknowledge 已保存, though its own dialog sets no title. The field label token is 令牌.
  • dialog_global returns 已生成, 全局说明 and 好的.

So the control is a real read.

Census.

Ablation. Each leg is committed first, then mutated through scripts/ablation-replace.mjs, which uses a literal anchor, proves the mutation is on disk by blob hash, and restores with git checkout HEAD. The test imports the rule from src by a relative path, so no dist build sits between the mutation and the run.

  • Leg 1 drops the new leg's call, checkActionResultDialogLeaves(findings, ctx);. Blob de2dc9a541a9 becomes 8133e71705ce. 3 tests fail and 149 pass: both new refusals, and the coverage pin's backing test. Restored: blob equals HEAD and git diff HEAD is empty.
  • Leg 2 drops the declared-dialog guard, if (isRec(ctx.action.resultDialog)) return;. Blob de2dc9a541a9 becomes 1892f4a64d54. 4 tests fail and 148 pass: this card's declared-dialog control, the resultDialog.fields control (accepts every declared path…), accepts the real instance of every keyed group…, and the existing dotted-path refusal. Each of the four carries a translated leaf under a declared dialog. Restored the same way.
  • The direction is the usual one: the tests turned red.

Tests (at HEAD 46b40dc845)

  • pnpm --filter @objectstack/lint test: 119 files and 5518 tests pass.
  • pnpm --filter @objectstack/lint typecheck: exit 0, including check:test-typecheck.
  • pnpm --filter @objectstack/cli exec vitest run --project unit: 243 files and 3439 tests pass. The integration layer is left to CI, because this diff touches no spawn entry.
  • Filter direction: downstream (...@objectstack/lint), narrowed to @objectstack/cli, the os validate door the dispatch names. No other importer owes tests, because @objectstack/lint's export surface is byte-unchanged: the new function is module-private.
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths) derives 88 commands, and all 88 exit 0. --ran reconciles 88 derived, 88 run, 0 NOT-MEASURED, with every exit code recorded.
    • Two gates first answered PREREQUISITE NOT MET (exit 3): check:skill-examples (no client-react dist) and check:dual-build-cjs-loads (8 packages with no dist). They passed once those packages were built.
    • origin/main moved to 5a9292e6f6 during the run, and only .github/workflows/ci.yml changed among the derivation's inputs. A derivation from a tree at 5a9292e6f6, given this diff's 4 paths, yields the same 88 commands.
  • ESLint, narrowed and measured. The population, read from eslint.config.mjs: only ts, tsx, mts, cts, js, jsx, mjs and cjs globs are linted, so of this diff's 4 paths the 2 .ts files are the population. Running eslint --no-inline-config --format json on them gives 2 files, 0 errors and 0 warnings. Invariance: the config enables no type-aware linting (no parserOptions.project, no typed rules), so this diff cannot move a verdict on an untouched file. The full pnpm lint is left to CI.
  • Round 2 (head b82d990cb9: the changeset and the docs row only; code and tests unchanged since 46b40dc845): node scripts/check-adr-0087-registration.mjs --base origin/main exits 1 with the arm corrected and no marker (signals BREAKING and clause-②-narrowing), and exits 0 with the marker. node scripts/check-changeset-no-major.mjs --base origin/main exits 0. With this body passed as the pull_request event (--event), it also exits 0 and its level axis reads no (narrowing), a BREAKING change that ships minor during the launch window. The 40 families dispatch-gates.mjs derives for the .mdx path all exit 0, as do the changeset path's check-empty-changeset, check:changeset-gate-self-tests, check:objectui-changeset, check:pm-changeset-deadline-census and check:published-files.

Acceptance notes

  • skills/objectstack-i18n/SKILL.md (Tier H, domain:skills, a separate PR, not edited here). It would need three changes:
    • The _actions.{action_name} row of its sub-key table (:164) lists params.{param_name} and resultDialog only. It should also list params.{param}.options.{value}, outcomeMessages.{outcome}, resultDialog.title / .description / .acknowledge and resultDialog.fields.{path}.
    • The orphan paragraph (:198-:202) enumerates what translation-target-unknown names. It should add an action's undeclared outcome, an undeclared result-dialog field, and a result dialog the action does not declare.
    • globalActions (:170) should state what os validate does: a key naming a bound action is an error whose message names the object-scoped key to write instead.
  • The Clause-② line reads no (narrowing): this diff narrows what os validate accepts and widens nothing. Spelled that way, check:adr-0087-registration asks for a disposition. PR feat(lint): os validate judges every keyed child of an action translation entry against its declaration #21258's landed changeset carries the same bare yes this PR first copied; that is left to the dispatching seat.

claude added 2 commits October 2, 2026 02:16
… a declared dialog

A `resultDialog.title`, `.description` or `.acknowledge` key under an action
that declares no `resultDialog` is now `translation-target-unknown` at error,
under both `objects.OBJECT._actions.ACTION` and `globalActions.ACTION`.
`resolveActionResultDialog` returns before any lookup when the action declares
no dialog, so that copy was read by nothing while `os validate` passed it.
Under a declared dialog every leaf is read and nothing changes.

The action-entry coverage pin moves the three leaves from leaf-copy to
reference-checked, each backed by a ghost at both addresses.

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

The `os validate` rule table in i18n-standard gains the action-entry keys the
walk judges: `params.NAME.options.VALUE`, `outcomeMessages.OUTCOME`,
`resultDialog.fields.PATH` and the dialog's `title` / `description` /
`acknowledge` leaves, at both action addresses. The `globalActions` row states
that a bound action's copy lives only under its object. Adds the
`@objectstack/lint` changeset.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

5 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 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 — 4 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 4e530568a27d750f6b895a2f8b2c131be849857d → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 4e530568a27d750f6b895a2f8b2c131be849857d

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 46b40dc845b7a50f555befa5896d914761dca24d
Local-runs: none

Inputs read: card #21264 (body; comments 5943006586, 5943050672, 5944251774, 5944702775); card #21261 (body; ruling 5942994297); PR #21304 (body, file list, net diff of origin/main... the head, fetched into an owned ref whose sha equals the head above; merge-base 4e6dc2338a); the head's check-runs; packages/spec/src/system/i18n-resolver.ts on origin/main; objectui at the pin 31971ff1e28f (packages/i18n/src/useObjectLabel.ts, packages/core/src/actions/ActionRunner.ts, packages/app-shell/src/hooks/useConsoleActionRuntime.tsx, packages/app-shell/src/views/RecordDetailView.tsx, packages/app-shell/src/views/metadata-admin/MetadataTypeActions.tsx); the gate sources scripts/pm/clause2-line.mjs, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs. Nothing was built, run or re-run.

① Derived judgments

  1. The refusal is honest — right. Spec resolver: resolveActionResultDialog (i18n-resolver.ts :638-:644) reads const spec = action.resultDialog; if (!spec) return spec; before any lookup; lookupActionResultDialogNode and lookupActionResultDialogText are called from nowhere else, and translateAction (:1090) is the only path that reads dialog copy out of a bundle. A git grep over packages/** (non-test, non-dist) on origin/main finds no other reader of _actions.*.resultDialog or globalActions.*.resultDialog copy. objectui at the pin: useObjectLabel().actionResultDialog(objectName, actionName, spec) (useObjectLabel.ts :587-:609) returns spec unchanged when spec is absent; its only callers are the two resultDialogHandlers (useConsoleActionRuntime.tsx :218, RecordDetailView.tsx :690), which ActionRunner.handlePostExecution invokes only when hasResultDialog = !!(action.resultDialog && result.success) (ActionRunner.ts :1452, :1508-:1510); MetadataTypeActions.tsx :202 gates on action.resultDialog and passes the literal spec. So under an action with no declared dialog no reader shows resultDialog.title, .description or .acknowledge, at either address. The lint's guard if (isRec(ctx.action.resultDialog)) return; is the resolver's !spec read off the declaration. The producer agrees: packages/cli/src/utils/i18n-extract.ts emits dialog leaves only when action.resultDialog is an object (:714-:719), so i18n:extract never writes the refused shape.
  2. The pass side is honest — right. Spec: title = lookupActionResultDialogText(...) ?? spec.title, likewise description and acknowledge (:645-:649), overlays each leaf whether or not the declared dialog authors it. objectui: textFor(attr) resolves the bundle key with spec[attr] ?? '' as the fallback, likewise. The control (mint_token declares a dialog with no title; rotate_secret declares one with no description or acknowledge) is a real read at both readers. The lint judges nothing further under a declared dialog, and that is right because the resolver reads every leaf there.
  3. Vocabulary and level — right. The new leg pushes TRANSLATION_TARGET_UNKNOWN at TRANSLATION_TARGET_UNKNOWN_SEVERITY (error), the code and level checkActionParams, checkActionOutcomeMessages and checkActionResultDialogFields already use; one finding per leaf present; message and hint carry no tracker number. checkActionEntry is reached from the _actions loop (:1537-:1562) and from the globalActions loop, so one function covers both addresses.
  4. Coverage pin and ledger — right. The pin requires COVERAGE's key set to equal the translation schema's action-entry keys at both addresses (classifies every key…), so a leaf cannot be left out of the ledger; the three leaves move to reference-checked under the new host noDialog (plain_env bound, check_updates object-less; neither declares a resultDialog in actionStack, and coverageStack adds only params to check_updates), and the backing test asserts each ghost reports at both addresses with the right rule; resultDialog stays a container whose why now says its leaves are judged below. The remaining leaf-copy rows (label, description, confirmText, successMessage, params.*.label, .helpText, .placeholder) are prose on a node that resolved, which is true. The ledger now tells the truth. The ablations as described exercise both directions and are consistent with the code: leg 1 (drop the call) can only turn the two new refusal tests and the backing test red while the control stays green; leg 2 (drop the guard) refuses leaves under declared dialogs, and each of the four tests named carries one (the control at :3270-:3277; the fields control at :3216-:3217 with title; the real-instance test; the dotted-path refusal at :3193 with title).
  5. Census — method adequate; limits stated. os validate --json on the four examples is a door-level read. The platform-objects bundle was run through the rule rather than the door (platform-objects is not a stack app), and its generated leaves sit under declaring actions by construction of the extractor (item 1). The git grep over 98 tracked bundle files is the right population for this repository. Not covered, and not coverable from here: bundles outside this repository, for which the changeset's "What changes for a project" paragraph is the only notice; the sibling objectui holds no stack translation bundle at the pin (a git grep for _actions under **/translations/**, **/locales/** and *.json returns nothing; its packages/i18n/src/locales/*.ts hits are objectui's own UI strings actions.resultDialog.*).
  6. Public surface — unchanged, right. packages/lint/src/index.ts is not in the diff; the export list of validate-translation-references.ts is identical on origin/main and the head (TRANSLATION_TARGET_UNKNOWN, TRANSLATION_OPTION_KEY_UNKNOWN, TranslationRefSeverity, TranslationRefFinding, validateTranslationReferences); checkActionResultDialogLeaves and RESULT_DIALOG_LEAVES are module-private. @objectstack/lint ships no generated artefact (its tracked non-source files are CHANGELOG, README, package.json, two scripts, the tsconfigs, the tsup and vitest configs and the test-typecheck debt ledger), and check:api-surface covers spec only. content/docs/protocol/kernel/i18n-standard.mdx is hand-written (scripts/docs-audit/handwritten-docs.json); no page is added, so no meta.json is owed.
  7. The one accept-set change, named. os validate, os build and os lint now refuse at error a bundle key resultDialog.title, .description or .acknowledge under an action that declares no resultDialog, at objects.OBJECT._actions.ACTION and at globalActions.ACTION. Nothing is widened: no new rule id, no new export, no newly accepted key.

② Semver level

  • Level — right. @objectstack/lint minor. The diff is an accept-set narrowing; check-changeset-no-major.mjs (header, "END CONDITION") grades a breaking change minor during the launch window, patch would be wrong and major is refused. Check Changeset concluded success on this head.
  • Direction arm — wrong, and owed. PR body line 2 and the changeset body both read Clause-②: yes, bare. By scripts/pm/clause2-line.mjs, yes declares a widening and an absent arm declares no direction. Item ①.7 finds no widening and one narrowing; the PR body itself says "This diff narrows what os validate accepts". The exact spelling this diff owes, in both carriers, is Clause-②: no (narrowing) — the CLAUSE2_ARMS docblock's own case, "NOT a widening, but breaking. This is the whole point of the arm". The precedent the PR cites, feat(lint): os validate judges every keyed child of an action translation entry against its declaration #21258's changeset, wrote the same bare yes for the same kind of narrowing; a precedent is not a ruling (see the escalation in ③).
  • Does the bare form change a gate verdict? Yes, one of the two. check-changeset-no-major's LEVEL axis: unchanged. yes and no (narrowing) both carry the axis (judgeLevel: "clean … the axis is CARRIED (yes, or no (narrowing))"), @objectstack/lint minor satisfies it, exit 0 either way. check-adr-0087-registration: changed. breakingDeclaration (:629-:641) reads breaking-ness from the changeset body alone, from major, a **BREAKING banner, a bang summary, or a Clause-② arm reading narrowing. Today's changeset carries none of the four, so the gate asks nothing and exits 0. With the owed no (narrowing) it would read the signal clause-②-narrowing and demand exactly one ADR-0087 disposition marker, which the changeset does not carry, so it would exit 1. The bare yes greens a gate the honest declaration would red — the measured shape (feat(platform-objects): sys_job.timezone and sys_report_schedule.timezone are validated against the IANA domain #16296) the arm was added to remove.
  • BREAKING banner — owed, absent. check-changeset-no-major.mjs's header and the Check Changeset step's WHICH LEVEL prose name the **BREAKING** banner plus the ADR-0087 disposition as the mandatory carriers of breaking-ness during the window, because the level is not one. The changeset's "What changes for a project" paragraph is a real migration (move the keys under the declaring action, declare the dialog, or delete them), but no **BREAKING** banner heads it.
  • ADR-0087 marker — owed once the arm is spelled. One marker in the changeset body, in the HTML-comment form the gate's header prescribes (it opens with adr-0087: and names either registered with ids or not-required with one of the gate's CATEGORIES and a reason; the gate prints the set when it fails). No spec key, export or stored value moves here, so a not-required disposition is the shape to argue; which category is the author's answer in writing, re-validated by the gate.

Remedy, one patch round, code and tests untouched: in .changeset/21264-result-dialog-leaves.md and in PR body line 2, Clause-②: no (narrowing); a **BREAKING** banner heading the "What changes for a project" paragraph; one ADR-0087 disposition marker in the changeset body; then node scripts/check-changeset-no-major.mjs and node scripts/check-adr-0087-registration.mjs both exit 0 on the new head, and a record on that head.

③ Boundary flags

Check-runs on the head, read at 2026-10-02T03:10Z: 35 check-runs, every one concluded — 33 success, 2 skipped (Console Pin Gate, Packed-tarball smoke (opt-in)), none red, none still running. The seven required contexts all concluded success: Lint & Repo Gates, TypeScript Type Check, Test Core (all six shards), Dogfood Regression Gate (all three shards), Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard; Check Changeset also success. The green reads as the check-runs say and no more: on ② above, two of those greens stand on a declaration that hides the fact they would otherwise read.

Implemented-by: claude/issue-21264-result-dialog-leaves
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: FAIL

In one line: ① holds at the code and the pin; ② the declaration misstates the direction and the changeset omits both mandatory breaking carriers, and the bare yes keeps check-adr-0087-registration from asking for the disposition it would otherwise demand. One patch round on the changeset and the PR body line, then a record on the new head.


Generated by Claude Code

claude added 2 commits October 2, 2026 03:14
…lobalActions row states the os validate rule

The changeset's Clause-② line reads `no (narrowing)`: the diff refuses keys
`os validate` passed and widens nothing. A BREAKING banner heads the
paragraph that tells a project what changes. The i18n-standard
`globalActions` row now states only what `os validate` does: a key naming a
bound action is an error that names the object-scoped key to write instead.

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

One `not-required (no-migration-prescription)` marker: no spec key, export
or stored value moves, so there is nothing for `objectstack migrate meta` to
convert, and the refusal's remedy is the author's choice, not a mechanical
rewrite.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b82d990cb98b126724907c43b8fb7018a087bdcf
Local-runs: none

A narrow record on patch round 2. It answers the FAIL on ② in record 5944914497 (head 46b40dc), where ① and ③ were found right at the code and at objectui's pin; those two are re-judged here only where the delta reaches them.

Inputs read: record 5944914497 in full; card #21264 comments 5944933068 (seat response), 5944937163 (claim revision) and 5945035954 (round-2 dev report); PR #21304's current body and file list; the delta 46b40dc845..b82d990cb9, fetched into an owned ref whose sha equals the head above (two commits, 5729fa3 and b82d990; the merge-base with origin/main is still 4e6dc23); the head's check-runs; the gate sources scripts/pm/clause2-line.mjs, scripts/check-changeset-no-major.mjs and scripts/check-adr-0087-registration.mjs (CATEGORIES, breakingDeclaration, readDisposition, the category table); on origin/main, packages/spec/src/system/i18n-resolver.ts, packages/spec/src/system/translation.zod.ts, packages/spec/src/ui/action.zod.ts and packages/lint/src/reference-integrity-suite.ts; the lint source at the head, for the globalActions refusal and the new leaves' hint. Nothing was built, run or re-run.

① Derived judgments

Carry, verified by blob id. packages/lint/src/validate-translation-references.ts is blob de2dc9a541a959c86288484c6bcb833aca7be1b2 at both 46b40dc and b82d990; packages/lint/src/validate-translation-references.test.ts is blob df3e5712f4b2354b8e768303142ba4df48e34de9 at both. The delta touches exactly two files, .changeset/21264-result-dialog-leaves.md (+4 / -2) and content/docs/protocol/kernel/i18n-standard.mdx (+1 / -1), and no packages/** path. So the lint walk, its tests, the coverage pin, the census and the public surface are the ones 5944914497 judged, and its items ①.1 to ①.7 carry unchanged: the one accept-set change is still the refusal, at error, of resultDialog.title, .description and .acknowledge under an action that declares no resultDialog, at objects.OBJECT._actions.ACTION and at globalActions.ACTION; nothing is widened, and no export moves.

② Semver level

  • Direction arm — right, in both carriers, in the reader's spelling. Changeset line 7 reads Clause-②: no (narrowing); PR body line 2 reads Clause-②: no (narrowing) (line 1 is Fixes #21264). Against clause2-line.mjs: each line is key-initial and undecorated, so CLAUSE2_KEY_LINE matches; matchValueToken reads no as the first thing after the colon; readArmToken finds the parenthetical opening with the exact token narrowing. The reading is declared, value no, arm narrowing — the CLAUSE2_ARMS docblock's "NOT a widening, but breaking" case, and the spelling 5944914497 named as owed. Neither line names the key twice or quotes it, so neither reads as describing.
  • BREAKING banner — in the prescribed form. Line 15 opens **BREAKING** — an accept-set narrowing at the os validate, os build and os lint doors, shipped as minor under the launch-window convention. and heads the "What changes for a project" paragraph. check-changeset-no-major.mjs's header (END CONDITION) names the **BREAKING** banner plus the ADR-0087 disposition as the mandatory carriers of breaking-ness during the window; breakingDeclaration (:629-:642) reads the banner as signal (2) by the pattern **BREAKING, case-insensitive, which this line satisfies; and the form is the one .changeset/21229-object-grid-export-options-closed.md carries on origin/main. On this head the gate reads two signals, BREAKING and clause-②-narrowing, and asks for the disposition it could not ask for on 46b40dc.
  • ADR-0087 marker — exactly one; the right member; a true reason. readDisposition (:1921-:1949) finds one HTML comment opening adr-0087: in the body (line 9; adr-0087 occurs once in the file) and parses it as not-required, category no-migration-prescription, no ids, the rest as the reason. no-migration-prescription is the third of the five CATEGORIES (:494-:500). The member, judged against the gate's own table rather than the dev's words: unpublished is false, @objectstack/lint is published; already-registered names no entry, and no ledger entry covers this change; runtime-interface-only and type-surface-only name a PATH#SYMBOL that lost a runtime member or moved a type, and no exported symbol changes here (①). no-migration-prescription is the gate's catch-all, refused only when "the changeset's own body carries a migration prescription", that is, instructions for rewriting a consumer's code from one shape into another — the FROM → TO mapping the ledger exists to carry and objectstack migrate meta to apply. The body carries none, and rightly: no spec key is removed or renamed. translation.zod.ts on origin/main still declares resultDialog: ActionResultDialogTranslationSchema.optional() (:152) with title, description and acknowledge (:92-:94), and the PR does not touch packages/spec, so there is no retired shape to convert and no single TO to write. The refused key is a reference whose target the action does not declare, the class an undeclared params.NAME already belongs to, and the remedy is the author's: the hint at the lint code (:2083-:2085) reads "Move the resultDialog translations under the action that declares the dialog, declare the resultDialog on this action first if it should show one, or drop them." The "What changes for a project" paragraph states that same three-way choice as the project-facing notice the window demands; it is not a rewrite of one shape into another, and AGENTS.md's FROM → TO mandate binds only when something authorable is removed or renamed, which nothing is. So the banner paragraph and the marker are consistent, not contradictory. The reason's clauses, one by one: "no spec key, export or stored value moves" — true (①; no packages/spec path in the diff); "TranslationDataSchema parses these three keys exactly as before" — true (the schema above, untouched); "objectstack migrate meta has nothing to convert" — follows; "the runtime publish door never runs this rule on a translation write (the member's runtime types default to flow)" — true: reference-integrity-suite.ts registers validateTranslationReferences with no runtimeTypes (:466), DEFAULT_MEMBER_RUNTIME_TYPES is ['flow'] (:192), and validateReferenceIntegrity skips a member whose types exclude the runtimeWriteType (:677-:680), so a write of any type but flow never reaches this member; "the refusal is a lint finding at the authoring doors that names the key" — true, the finding's path is the bundle key ending .resultDialog.LEAF, at TRANSLATION_TARGET_UNKNOWN_SEVERITY; "its remedy is the author's choice …, not a mechanical rewrite of one shape into another" — true, the hint quoted above.
  • Level — unchanged, right. Line 2 reads '@objectstack/lint': minor. A declared narrowing is breaking and ships minor during the window: judgeLevel (:1547, :1640-:1646) treats no (narrowing) as carrying the axis and grades a minor bump clean; major is refused by the launch-window guard.
  • Gate verdicts, read off the head's check-runs. Both gates run inside the changeset-check job, check-run Check Changeset (pr-automation.yml :243-:244; the ADR-0087 step at :970-:980, the no-major step at :1130); lint.yml runs only their self-test halves (:3562-:3566). Check Changeset concluded success on this head, twice (the push run and the body-edit run). The two greens now stand on the honest declaration: the ADR-0087 gate asked for a disposition and was answered, which is the fact 5944914497 found hidden.
  • Other changeset sentences. Lines 5, 11 and 13 and the "What changes for a project" sentences are byte-identical to 46b40dc and were judged true there (①.1, ①.2, ①.7, the census). The new text is the banner clause and the marker, judged above. No sentence became false. PR body: the "Measured" section still names 46b40dc as the head it measured and says so; a round-2 bullet under Tests names b82d990; the changeset and docs bullets describe this head truthfully.

③ Boundary flags

  • The globalActions row. At the head it reads: an action with no objectName — a key naming a bound action is an error whose message names the objects.{owner}._actions.{action} key to write instead, {owner} being the action's own objectName. At the lint code (byte-identical to 46b40dc): universe.globalActions holds the actions with no objectName (:1141-:1150; action.object is objectName's ADR-0087 alias, action.zod.ts :859), so the key resolves exactly for an object-less action; a key naming a bound action falls to orphan(…) (:1357-:1366, TRANSLATION_TARGET_UNKNOWN_SEVERITY, error) with universe.actionOwners.get(actionName) as the owner (:1568-:1590), and the hint is "Move these keys under objects.OWNER._actions.ACTION." — the key the row names. True at the lint code. The row now says nothing about where copy lives or what the resolvers read, so the fallback on main — lookupActionField (:559-:564), lookupActionResultDialogNode (:608-:612), the outcome lookup (:716) and the param lookup (:744) all read globalActions.ACTION.* for a bound action after the object-scoped miss — contradicts nothing in it. The restatement 5944914497's ③ asked for is made. One boundary note outside this diff: the lint's own refusal message on origin/main (:1575-:1576, pre-existing, untouched by this PR) still says the resolver looks a bound action up "never under globalActions … This key is never read", which becomes true when lint: os validate refuses a bound action's globalActions translation key as "never read", but the spec's own i18n resolver reads it as the object-scoped key's fallback #21261's ruling (a) lands and is lint: os validate refuses a bound action's globalActions translation key as "never read", but the spec's own i18n resolver reads it as the object-scoped key's fallback #21261's fact until then. Named, not graded: this PR neither wrote nor moved that sentence.
  • Dev's two round-2 out_of_scope_findings, one line each. (1) skills/objectstack-i18n/SKILL.md (Tier H): out of scope for this PR, a Tier H surface the claim excludes; none of (a), (b), (c) — missing members are incomplete, not wrong, no declared contract is contradicted, and the trap is the one this PR makes loud — so acceptance notes with a carrier, and the carrier now exists: seat post [PM seat] domain:spec — 🟢 os-sales · session_01UtnxvdiN376GF3sgXwAw4d · R1 #6017, per 5944933068. (2) platform-readings.md :345-:349 (an edit-side PATCH appends a footer) against the relay's measured no-footer write: out of scope for this PR, a .claude/** seat reference the dispatching seat owns; none of (a), (b), (c) — a seat-protocol observation on one channel, not a product defect, not a declared contract a user or published package reads, no metadata — so it belongs on the seat post or the shift brief, with the dispatching seat as carrier, as the dev wrote. Nothing is filed by this review.
  • Trailers and identifiers. Both round-2 commits end with the session-URL trailer (https://claude.ai/code/session_…) and Co-authored-by: Claude, the model-free pair AGENTS.md prescribes; no model identifier appears in the commits, the changeset, the PR body or the comments read.
  • Base. origin/main is at c2cd651 at the time of reading; the branch's merge-base with it is still 4e6dc23; the queue rebuild remains the arbiter, as in 5944914497.

Check-runs on the head, read at 2026-10-02T03:41Z: 42 check-runs, every one concluded — 38 success, 4 skipped (Console Pin Gate, Packed-tarball smoke (opt-in), and the body-edit run's Auto Label and Check PR Size), none red, none still running. The required contexts all concluded success: Lint & Repo Gates, TypeScript Type Check (with its four Type Check · legs), Test Core (all six shards), Dogfood Regression Gate (all three shards), Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard; Check Changeset concluded success on both of its runs. The green reads as the check-runs say and, on ② above, now stands on a declaration that states the fact it carries.

Implemented-by: claude/issue-21264-result-dialog-leaves
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

In one line: ① carries by blob id; ② the arm reads no (narrowing) in both carriers, the banner and one not-required (no-migration-prescription) marker are present and true, the level stays minor, and Check Changeset is green on the honest declaration; ③ the globalActions row now states only the os validate rule and is true at the lint code.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 03:44
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 2, 2026 03:44
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 1371dc9 Oct 2, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21264-result-dialog-leaves branch October 2, 2026 04:08
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 size/m tests tooling

Projects

None yet

2 participants