Skip to content

feat(spec,lint,cli): a page:tabs item label gains the key pages.PAGE.components.TABS.items.VALUE.label - #25005

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-24968-page-tabs-item-label-key
Oct 21, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-24968-page-tabs-item-label-key

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Closes #24968

Clause-②: yes (widening)

What changes

A page:tabs item's label had no translation key. TranslationBundle pages.PAGE.components is keyed by component id. A tab item is not a component and carries no id. Spec translatePage descended items[].children but never read items[].label. HotCRM measured this on sales_home_page: three English tabs under a translated header, in zh-CN, ja-JP and es-ES.

  • Spec (packages/spec/src/system/translation.zod.ts): pages.PAGE.components.TABS_ID.items.VALUE.label is a declared key. The component entry gains items, a record keyed by the tab item's value. Each entry is closed (strictObject, surface "this page tab item translation"). title, name and text are renames onto label. (The tabs alias onto items was dropped in f18a2f55dd.)
  • Resolver (packages/spec/src/system/i18n-resolver.ts): translatePage overlays each item's label from the key. It resolves across the locale chain item by item, under the same addressed collision arbitration as the component copy. Only page:tabs components are read. Two new exports are shared by the resolver, the extractor and the lint check, so the three cannot drift: PAGE_TABS_COMPONENT_TYPE and pageTabItemKey(item).
  • Walk (walkAddressedPageComponents): the walk used to rebuild a panel list from the authored node, so a visitor's edit to a panel's own key (its label) was overwritten. A panel's own keys now come from the visitor's return when the visitor keeps the list's authored shape (same length, an object at every panel position). Panel children are still descended from the authored document, so the walk still owns the descent. A visitor that changes the shape gets the authored list back. The docblocks state this, and a new walk test pins it.
  • objectstack validate (packages/lint/src/validate-translation-references.ts): the check builds a per-page universe with spec's walkAddressedPageComponents, the same walk translatePage runs. A components.ID.items.VALUE key resolves over a declared page:tabs item value. Otherwise it is refused as translation-target-unknown, in the existing one-line verdict style:
    • an unknown value (an index token such as tab-2 included): "...which page:tabs "home_tabs" on page "sales_home_page" gives no item as its value, so the tab keeps its source-locale label.", followed by "Did you mean";
    • a page:accordion: "...whose panels have no stable key, so nothing resolves them.", with a fix line pointing at the inline locale map;
    • another component type: "...declares as a record:related_list, not a page:tabs...";
    • an undeclared component id: "...a component id page "P" does not declare...", followed by "Did you mean" over the page's tabs components.
  • os i18n extract / coverage (packages/cli/src/utils/i18n-extract.ts): emitPageComponentCopy writes a slot for every valued item of an addressed page:tabs. A reader exists (translatePage), so the slot is demanded, which differs from feat(spec,lint): flows translation face gains approvals.NODE.label for an approval node's step label #24943. An inline locale map on the label is recorded as authored, through the same pushEntry path as the component keys.

Why this spelling (shape (a) over (b))

  • Keyed by value, under the tabs component's id. PageTabsProps.items[] already declares the identifier the key needs. value is the stable ?tab= token that the renderer reads and restores, so it is unique within the strip. The Inline I18nLabel maps are invisible to os i18n extract and check:i18n-coverage — should the extractor and the coverage gate see them? (objectstack#14412 ruling A follow-up) #14749 ruling (Q3 = C3, quoted in the extractor's header) recorded the direction "identity first: component.id / section.name / tabs item.value". Adding an id to tab items (shape (b)) would give one tab two identifiers. It would also make components.ID address something that is not a component, and it would need a new authorable key on a published schema. Shape (a) adds no authorable page key and reuses the grammar the face already has, the same way dashboards.D.widgets.ID and flows.F.screens.NODE key a child by the identifier its consumer already holds.
  • An item without a value has no key. Its renderer token is tab-INDEX. The Inline I18nLabel maps are invisible to os i18n extract and check:i18n-coverage — should the extractor and the coverage gate see them? (objectstack#14412 ruling A follow-up) #14749 ruling forbids array-index keys, because reordering two tabs would swap their translations with every check still green. The lint verdict names the remedy: give the item a value.
  • Inline locale maps keep working. translatePage writes only where the bundle has an entry. An item the bundle does not key keeps its map, and a bundle entry wins where both exist. This is the rule the component copy already follows.
  • Accordion is out on purpose. page:accordion items have no author-settable identity (the renderer derives panel-INDEX, and value is refused on them with guidance), so their labels stay an inline-map surface.

Tests

  • Repro first: commit 757aadb573 adds the tests before the fix. At that commit, on a rebuilt spec dist, the spec suite had 9 failing cases (4 schema, 4 resolver, 1 walk), the lint suite had 7 (5 refusal cases plus the two one-line arm coverage checks), and the new CLI file did not load, because the closure was not built yet.
  • Flipped pin: the BOUNDARY case in i18n-resolver.test.ts ("a tab panel label has no bundle key") said "If a key is added, this pin is the one to flip". It is flipped: its inline-map half is kept as a CONTROL, and the new translatePage — page:tabs item labels block pins the key.
  • Controls: panel children keep their own components.ID keys; the tabs component's own label key composes with its item keys; the same items key on a page:accordion translates nothing; an item without a value is untouched even if tab-2 is keyed; the locale-chain fallback works key by key; the component keys of the page validate clean beside an item key; a page the stack does not declare is not judged.
  • Package runs (merged head c8a248af73, local base origin/main 7adfaec2a1):
    • pnpm --filter @objectstack/spec test: 703 files, 21377 passed, 1 todo.
    • pnpm --filter @objectstack/lint test: 152 files, 7033 passed.
    • pnpm --filter @objectstack/cli exec vitest run --project unit: 316 files, 4629 passed, 29 skipped. Two published-subpath pins first refused because packages/cli itself was not built. That is a prerequisite refusal, not a measurement. After building the CLI they passed, with the new file: 3 files, 37 passed. The integration tier is declared to CI, since the diff touches no spawn entry.
    • typecheck for spec, lint and cli: exit 0 (each runs tsc --noEmit and check:test-typecheck).
  • Generated artifacts: pnpm --filter @objectstack/spec check:generated --fix regenerated api-surface/system.json and export-origins/system.json (the two new exports) and the strictness-ledger counts (system.md). The reference doc translation.mdx is unchanged, because its components cell is truncated with an ellipsis before items.
  • Reverse verification (after the fix was committed). Each mutation went through scripts/ablation-replace.mjs: the anchor hit once, and the restore was proven by blob == HEAD with an empty git diff HEAD. Each suite reads src.
    • A1, the translatePageTabItems call removed: spec resolver suite 4 failed | 363 passed.
    • A2, the walk forced back to the authored panel keys: 5 failed | 362 passed (the 4 above plus the walk pin).
    • A3, lint accepting every value: 4 failed | 190 passed (the two value refusals and the two one-line arm-coverage checks).
    • A4, the extractor's item slot removed: the CLI file 4 failed | 4 passed.
  • Derived gates (dispatch-gates --commands, reconciled with --ran with exit codes recorded): 94 derived, 93 run, all exit 0. check:i18n, check:i18n-coverage and check:i18n-walk-parity first refused with exit 3 (the CLI was not built). After the build they were re-run and exited 0: check:i18n reports 10 packages in sync, check:i18n-coverage reports 14 configs with none new, and check:i18n-walk-parity reports 11 declared groups, 9 walked and 2 exempted. NOT MEASURED: check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (exit 3), because packages this diff does not touch have no dist without a whole-repo pnpm build. Declared to CI. check:nul-bytes: exit 0.
  • eslint, narrowed to the 8 touched source and test files (--no-inline-config --format json, at c8a248af73): 8 files, 0 errors, 0 warnings. The config declares no type-aware linting (eslint.config.mjs: no parserOptions.project, no typed rules), so this diff cannot change the verdict on any untouched file.

Review tier

packages/spec is a published schema, so this PR owes a CONTRACT_REVIEW_TIER review of record before it enters the queue (pm-dispatch references/contract-review.md). It touches no governed surface (.claude/**, docs/adr/**, AGENTS.md, workflows, skills/**), so no Tier S or Tier H landing rule applies.

Liveness

packages/spec/liveness/translation.json's pages row is live and not drilled, so its blanket verdict covers the new member. The reader is translatePage, which the REST metadata read applies through translateMetadataDocument. objectui's page:tabs renders a string label as is.

Acceptance notes

  • objectstack validate still judges no other pages.PAGE member. An unknown page name, an unknown component id carrying only copy keys, and an unknown copy target are not checked by this rule today. This PR judges only the items member it adds, and skips pages the stack does not declare.
  • page:accordion panel labels have no bundle key (no stable identity). This is deliberate and recorded in the schema docblock.
  • #24947 (this seat's open PR) also touches i18n-extract.ts, in a different function (bulk-confirm template slots). No textual overlap is expected.
  • The branch carries a wip: commit (818b89e351) between the repro and the fix commit. This PR is squash-landed, so it does not reach main. I did not rewrite the history.

Generated by Claude Code

…n key

A pack key pages.PAGE.components.TABS.items.VALUE.label is refused by the
schema, never applied by translatePage, not judged by objectstack validate
and not offered by the extractor. Tests first; the fix follows.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…nents.TABS.items.VALUE.label

The tabs component's own id, then the item's value (the stable ?tab= token).
translatePage overlays the item label; walkAddressedPageComponents keeps a
visitor's edit to a panel's own keys; objectstack validate judges the key;
os i18n extract offers the slot and coverage demands it. Regenerated the
api-surface, export-origins and strictness-ledger counts.

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

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/lint, @objectstack/spec, touching 13 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/releases/v17/17-6.mdx (via translatePage (symbol, a top-level function))

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 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json) — pages documenting those are invisible to this run
  • 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 — 150 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 62bc1293080968f0ec8c3d31dc1f9ed8da4077df → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 62bc1293080968f0ec8c3d31dc1f9ed8da4077df

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

… the docs table names the tab-item key

The alias put a `tabs` key beside `emptyState` in one object literal,
which the list-view tabs retirement's tree-scoped absence pin reads as a
list-view payload carrying `tabs` (translation.zod.ts:1191). The alias was
not part of the card's ask, so it goes rather than widening the pin's
exclusions. content/docs/ui/translations.mdx gains the
pages.NAME.components.TABS.items.VALUE.label row.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f18a2f55dd050513dc8fe2dfdfab4217b5f48dff
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, packages/spec/src/ui/component.zod.ts, packages/spec/api-surface/system.json, packages/spec/export-origins/system.json, .changeset/24968-page-tabs-item-label-key.md, content/docs/ui/translations.mdx

Read against card #24968 (body, claim 6300704588, correction 6300738468, dev reports 6304394234 and 6307292111), the standing ruling on #6015 (comment 6292005854, rule 甲), the PR body and file list, and the three-dot diff origin/main...f18a2f55dd. Nothing was built, run or re-run.

Delta from the previously reviewed head c8a248af73 (the two three-dot diffs against origin/main compared line by line, so the origin/main merge 7e29242308 cancels out): one commit, f18a2f55dd. It drops the tabs to items alias from the page component entry's alias table (the view-list-tabs-retirement pin read the literal tabs key in that object as a retired ListView payload), drops the one schema test and the one changeset sentence that named it, and adds the content/docs/ui/translations.mdx table row this review had flagged. Nothing else differs: resolver, walk, lint, extractor, exports, generated artifacts and every other test are byte-identical to the previous head. The alias was an error-message hint on a key the strictObject refused on main and still refuses, so its removal changes no accept set; the head is narrower than what was reviewed before only in its guidance prose, and remains a pure widening against main. Every judgment below was re-read against the new head and holds.

① Derived judgments

  • New key pages.PAGE.components.TABS_ID.items.VALUE.label (translation.zod.ts). On main the component entry is a strictObject over title/description/label/placeholder/emptyText, so items (and tabs) under components.ID was refused as an unknown key. The head declares items as a record of closed entries (surface "this page tab item translation", one key label, aliases name/title/text to label); the component entry's own alias table is unchanged from main. Every input that parsed on main still parses; a previously refused input now parses. Widening. The "declares exactly the keys the resolver and the extractor act on" pin still holds because items is optional and absent from that parse. The item key carries the same I18nLabelSchema caveat as the component copy: the bundle entry wins, an unkeyed item keeps its inline map (pinned by the resolver test).
  • Identity choice (shape a). PageTabsProps.items[].value is optional and the renderer falls back to tab- plus the index (component.zod.ts, read point recorded in liveness/page.json); the extractor header already records the Inline I18nLabel maps are invisible to os i18n extract and check:i18n-coverage — should the extractor and the coverage gate see them? (objectstack#14412 ruling A follow-up) #14749 direction "identity first: component.id / section.name / tabs item.value". Keying by value under the tabs component id adds no authorable key to a published page schema and no second identifier per tab. An item without a value has no key, consistent with the array-index prohibition. page:accordion is excluded for a documented reason: its item value is refused with guidance because the renderer overwrites it with panel-INDEX. Accepted. Nit, not blocking: nothing in the schema or lint refuses two items in one strip sharing a value; both would then receive the same translation, which is also what the renderer's ?tab= token already does for them.
  • translatePage (i18n-resolver.ts). translatePageTabItems runs under the same addressed arbitration as the component copy, only for component.type === PAGE_TABS_COMPONENT_TYPE, before the if (nested) return next cut, so nested tabs components are reached by the id route. The lookup walks the locale chain per item and writes only where the bundle has a non-empty string, returning the component untouched otherwise. No existing key's resolution changes.
  • walkAddressedPageComponents. On main a panel list was rebuilt from the authored entry ({...entry, [panelKey]: visitedChildren}), and the docblock stated that a visitor's edit to a panel's own key is overwritten. The head spreads own[i] instead, where own is the visitor's returned list only when it keeps the authored shape (same length, a plain object at every authored panel position), else the authored list; [panelKey] is still the authored panel's children, visited. For a visitor that returns the component unchanged or re-spreads it with the same items array (the extractor, lint's new universe pass, objectui's validator, and translatePage before this change), own === list and the output is byte-for-byte what main produced. Behaviour differs only for a visitor that already edited panel keys, whose edits were previously discarded by contract and now stand. Descent order, depth cap, cycle guard, root-level collision arbitration, the two-positions-one-key compose path (rebuilt[key] reused on the second position) and the "no panel in the list, no rebuild" rule are unchanged. Signature and return shape unchanged. Widening of the visitor contract, pinned by a new walk test. Note the docblock change is itself a contract statement a downstream visitor may now rely on; it is consistent with the code.
  • Lint refusals (validate-translation-references.ts). The three translation-target-unknown arms (unknown value including an index token, items on a non-tabs component with the accordion given the inline-map remedy, items under an undeclared component id) judge only the items member. Under 甲 the question is whether any of them refuses something that passes today: no. On main, items under components.ID fails TranslationDataSchema as an unknown key before any reference check runs, so no bundle that validates today carries the member these arms read. They narrow nothing; they are the reference check for a key that did not exist. Severity error is correct. The universe is built on spec's walkAddressedPageComponents, the walk the reader runs, so the ids and item values lint accepts are the ones the resolver applies; sibling-package pages are collected with the existing ownDeclaration precedence. Pages the stack does not declare are skipped, consistent with the rest of the rule (acceptance note 1).
  • Extractor slot and coverage demand (i18n-extract.ts, i18n-coverage.ts). emitPageComponentCopy emits one slot per valued item of an addressed page:tabs, through the same pushEntry path as component copy, so an inline locale map on the label is recorded as inlineLocales and coverage treats it as authored for the locales it covers (pinned). Because a reader exists, the slot is demanded, which is the inverse of feat(spec,lint): flows translation face gains approvals.NODE.label for an approval node's step label #24943 and the right side of that line. Consequence for an existing downstream app: a page:tabs with valued items and no inline map now shows the tab labels as missing in check:i18n-coverage for each non-source locale. That is the measured defect itself (HotCRM's three English tabs) becoming visible to the gate the card asked to see it, and the remedy is the scaffold os i18n extract writes; an app already localizing by inline map stays green. Acceptable. In-repo, the dev reports check:i18n-coverage at 14 configs with none new and exit 0; CI's Test Core runs are the record of that on this head.
  • New exports PAGE_TABS_COMPONENT_TYPE, pageTabItemKey. Exported from i18n-resolver.ts through system/index.ts (export *), recorded in api-surface/system.json and export-origins/system.json. Additive. pageTabItemKey is total over unknown and returns undefined for a non-object, an array, a missing, non-string or empty value; the resolver, extractor and lint all address an item through it, which is the drift guard the PR claims.
  • Generated artifacts. The strictness-ledger counts (docs/audits/.../system.md) move by one closed entry; the reference translation.mdx is unchanged because its components cell is truncated. Consistent with the diff.

② Semver level

minor for @objectstack/spec, @objectstack/lint, @objectstack/cli (all three published at 18.0.0), with Clause-②: yes (widening) on the PR body and in the changeset. Matches what publishes: a new accepted key and two new exports in spec, a new refusal arm in lint over a key that was previously unparseable, a new extractor slot in the CLI. No existing accepted input is refused, no existing resolution changes, no signature or return shape changes. No ADR-0087 conversion is owed because nothing authored is rewritten. Check Changeset and Governed Surface Queue Guard are green on the head. Agreed: Clause-②: yes (widening).

③ Boundary flags

  • Dev out-of-scope 1 (lint judges no other pages.PAGE member: unknown page name, unknown component id carrying only copy keys): confirmed as pre-existing on main and untouched here. Not blocking; left as recorded, no card owed by this review.
  • Dev out-of-scope 2 (page:accordion panel labels have no bundle key): confirmed against component.zod.ts, where the accordion item value is refused because the renderer derives panel-INDEX. Deliberate and documented in the new schema docblock and the lint verdict. Not blocking.
  • Hand-written docs. Resolved at f18a2f55dd: content/docs/ui/translations.mdx gains a "Page tab labels (page:tabs items)" row naming the key, the value identity, the no-value case and the accordion exclusion. It matches the schema docblock and the lint verdicts.
  • PR body. The seat patched the Spec bullet to say the tabs alias was dropped in f18a2f55dd; the body, the changeset and the committed schema now agree.
  • Overlap with feat(spec,lint): a bulk action's confirmText is a template slot over count and its params, judged at validate, compile and the publish gate #24947 (same seat, i18n-extract.ts): the head's hunk is inside emitPageComponentCopy; the dev reports feat(spec,lint): a bulk action's confirmText is a template slot over count and its params, judged at validate, compile and the publish gate #24947 edits a different function. Merge-order risk only, no contract question.
  • wip: commit 818b89e351 in the branch history. The PR squash-lands; the net three-dot diff is what was reviewed. No flag.
  • Check-runs. On f18a2f55dd at the time of this review every completed check-run is success or skipped: Build Core, Build Docs, Test Core and its six shards, the four Type Check jobs, every Lint job, Check Changeset, Spec property liveness, Governed Surface Queue Guard, Dogfood Regression Gate and the card-claim guards; none is red. Two re-triggered runs (Check Changeset, No other open PR may claim the same single-writer path) were still in progress at that reading, each with an earlier successful completion on the same head. The seat posts this record once every run on the head has completed. Nothing was re-run locally.

Implemented-by: claude/issue-24968-page-tabs-item-label-key
Reviewed-by: session_01D6F9axfoPzR7t8G1J7Ytzz

VERDICT: PASS

Seat re-read of the check-runs at landing: all 49 runs on f18a2f55dd have completed: 41 succeeded, the rest were skipped, and none is red.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 21, 2026 03:32
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 21, 2026 03:32
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit 30d21f6 Oct 21, 2026
50 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24968-page-tabs-item-label-key branch October 21, 2026 04:07
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