fix(spec,cli): walk a slotted page's slots and a tabs panel's items[].children, and give dashboard global filters a bundle group - #16883
Conversation
…d dashboards.*.globalFilters Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
…st typing Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
…undary pin Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
…8n-unaddressable-surfaces
…aches under slots; declare the platform-objects bundle rows The platform-objects extract config explained its missing `pages` key by the shared walk rooting at `regions[].components[]` only. The walk now also roots at `slots.<slot>`, so the reason moved: it reaches every component on the three shipped record pages, and none of them carries an `id`, so the extractor still offers the page label alone. The changeset gains `@objectstack/platform-objects` — its shipped Setup bundles carry the new `dashboards.system_overview.globalFilters.created_at.label` row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
📓 Docs Drift CheckThis PR changes 3 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 003c982669227e3ab42b300f8c3f36577a40d2c1 && git checkout 003c982669227e3ab42b300f8c3f36577a40d2c1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d958b345fe5a0e26980741bdfe981344ba38cf73 af0593780f0ec2b9689b53f457ab3eb66eef89dd && git checkout -B drift-repro d958b345fe5a0e26980741bdfe981344ba38cf73 && git merge --no-ff af0593780f0ec2b9689b53f457ab3eb66eef89dd
node scripts/docs-audit/affected-docs.mjs --json d958b345fe5a0e26980741bdfe981344ba38cf73
|
⛔ Blocked, and this is the seat saying so once rather than leaving a red unattended — 2026-09-08T13:51Z
What is red, and why it is a fair red
⇒ The changeset declares ⭐ The gate is right and the declaration is the part that is correct. Why it is not fixed in this commentThe That leaves no route that is both available and legal:
⇒ This PR parks on tier capacity. ⛔ It is not abandoned and it is not waiting on review — it is waiting on one thing a person controls, and it resumes the moment fable capacity returns: raise the two levels, push, then the at-tier contract review this diff owes for widening What is already established, so the next dev re-derives none of itThe build itself was at tier — the dev's transcript carries 165 harness-stamped Docs drift is clean, measured by this seat (⛔ do not re-derive): Everything else on this head is green: 35 checks, The per-package readings the next push still owes
If a measurement shows one of them genuinely publishes nothing new, that is route 2 — a producer-side correction stated with its reading, ⛔ never a level bump used to silence the gate. ⛔ Still draft, ⛔ not enqueued, ⛔ auto-merge unarmed, Generated by Claude Code |
Contract review (
|
Seat concurs with the director seat's review, and corrects its own blocker comment — 2026-09-08T14:11Z
⭐ Finding 2 is real, and I verified it myself rather than concurring on the report
⇒ A breaking change to a published export, graded
|
Fixes #16772
Clause-②: yes
Container & model:
M,mode:cloud,model: claude-fable-5-1(CONTRACT_REVIEW_TIER, passed explicitly on the resumption dispatch).node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/spec/src/system/i18n-resolver.ts packages/spec/src/system/translation.zod.ts packages/spec/api-surface/system.json packages/cli/src/utils/i18n-extract.ts, run by the dispatching seat for THIS dispatch, printed a "Clause ② SUSPECT surface" block namingpackages/spec/src/system/i18n-resolver.tsand.../translation.zod.tsunderpackages/spec/src/**— "the contract surface … the normal landing zone of a clause-② card" — plus "whichever tier is dispatched, the PR's actual diff passes the clause-② enqueue gate before the card may enqueue." This diff movespackages/spec/api-surface/system.json(+3) ⇒ it widens the published surface ⇒ 强制条款② ⇒ fable.What changed
Two surfaces the console renders that no translation bundle could address, both in
packages/spec, both the benign direction of the seam #16735 describes (nothing to address, so nothing claimed coverage).Finding A — the shared page walk now reaches a slotted page and a tabs panel.
walkAddressedPageComponents(the ONE traversal behindtranslatePageand the CLI extractor) rooted atregions[].components[]only and descendedproperties.childrenonly. Now:regions[].components[]ANDslots.SLOT(PageSchema.slotsdeclares each slot as onePageComponentSchemaor an array of them — the walk accepts both and passes any other value through). Regions first, then slots in authored key order; both are root level (depth 0,nested: false) for the ruled id arbitration and for the page-namepage:headerroute, so a slotted page'sslots.headeris translated as THE page's header by page name, and the id route is not read for it (the same exception a region-level header already had).properties.childrenAND apage:tabs/page:accordionpanel'sproperties.items[].children— matched by SHAPE (an object entry ofitemscarrying achildrenarray), becausepropertiesis an open bag. The panel object itself is not a component and is not visited; its children sit one level below the tabs node.body/footerstay undescended (renderer back-compat fallback, not an authorable spelling). Depth cap, cycle guard and thetranslatePagestops at region-level components, so copy on components nested in another component'sproperties.childrenis authorable but never resolved — 4 KPI labels stay English on hotCRM's zh-CN landing page #12961 collision arbitration are unchanged.AddressedPageRoots(a Pick ofPageLikeonregionsandslots) instead of a Pick onregionsalone, and the walk returns the rebuilt roots pair{ regions, slots }(each key present exactly when present on the input) instead of the regions array.PageLikegainsslots;translatePagecarries the rebuiltslotsback onto the document. In-repo consumers:translatePage(updated) and the CLI extractor (enumeration-only, ignores the return). objectui at the pinned.objectui-sha(53ded82b) imports none ofwalkAddressedPageComponents/AddressedPageRoots/GlobalFilterLike/globalFilterKey— measured withgit grepat that commit, positive controlpickLocalizedhit.Finding B —
dashboards.NAME.globalFilters.KEYis a new bundle group (translation.zod.ts):labelplus anoptionsmap from the optionvaluespelled as a string to its translated label. The KEY is the filter'sname, else itsfield— not a lenient fallback:GlobalFilterSchema.nameis declared as "Stable filter name (variable key); defaults to field", so a filter that authors nonameIS keyed by itsfieldeverywhere the platform reads it. The exportedglobalFilterKey()is the one derivation both the resolver and the extractor use. Alias tables mirror the dashboard document's own (filters/globalFilterat the dashboard level;title/name/textforlabel,choices/values/itemsforoptions).optionsFromoptions are fetched rows and deliberately have no key.translateDashboardoverlays the group on the served document; only filters the bundle addresses are rebuilt, andglobalFiltersis left off the copy when none moved.@objectstack/cli(i18n-extract.ts):collectExpectedEntriesoffersdashboards.NAME.globalFilters.KEY.label/.options.VALUEfor every static filter, and the page-name header route (pages.NAME.title/.subtitle) for apage:headerat ANY root by asking the shared walk (nested: false) instead of a hand loop overpage.regionsthat would have offered nothing forslots.header. Component keys underslotsand tab panels follow from the shared walk with no extractor change.Bundles born under the ratchet:
@objectstack/platform-objectsSetup bundles (en,zh-CN,ja-JP,es-ES) gaindashboards.system_overview.globalFilters.created_at.label(the date-range bar; the filter authors noname, so it is keyed byfield); the showcase example bundle gains its two dashboards' filter rows inenandzh-CN.Docs / generated:
content/docs/ui/translations.mdx(hand-written tree) gains the two key rows;content/docs/references/**,api-surface/system.json(+3:AddressedPageRoots,GlobalFilterLike,globalFilterKey),export-origins/system.json,liveness/translation.jsonand the strictness-ledger counts file were regenerated by the repo's own tooling (check:generated: all 15 artifacts up to date after theorigin/mainmerge). The platform-objects extract-config note that explained its missingpageskey by the old root set was re-measured: the walk now reaches every component on the three shipped record pages, none of them carries anid, so the extractor still offers the page label alone (the CLI boundary pin holds both halves:reached: true,addressed: 0,offered: ['label']).Changeset:
@objectstack/specminor,@objectstack/clipatch,@objectstack/platform-objectspatch.minorfor the signature change follows the launch-window conventionscripts/check-changeset-no-major.mjsenforces ("we ship breaking changes asminor") and the precedent in the spec CHANGELOG ("Markedminorrather thanpatchbecause of that signature");check:api-surfacereports 3 added / 0 breaking because its signatures snapshot is scoped to thedefineXfactories.Resumption note
The predecessor session died on an account usage limit holding "two spec assertions failed and the CLI file failed at load". Re-run on the inherited HEAD
f3e92ee1c(dist built 11:28Z, after the last wip commit at 11:25Z) BEFORE any change:i18n-resolver.test.ts+translation.test.ts391/391 pass; the whole spec suite 465 files / 12978 tests pass;packages/cli/test/platform-page-i18n-parity.test.ts27/27 pass. The failure bodies could not be reproduced on the committed tree — they belonged to a transient state (uncommitted edits or a pre-build dist) that no longer exists. The three wip commits were re-read adversarially against the schema premises (PageSchema.slotsunion per slot,GlobalFilterSchema.namedefault,PageTabsProps.items[]) and stand; the one thing they missed was the platform-objects changeset entry, added here.Verification
Tree:
origin/mainc930f8597merged viabash scripts/pm/os-regen-merge.sh(merge commit718bff099; step 2 had no both-sides regen path, step 3 nothing extra to commit);pnpm install --frozen-lockfile; spec rebuilt; CLI dependency closure rebuilt (turbo, 56 tasks). Final commitaf0593780(comment + changeset only; no test input).origin/mainhas since moved 8 more commits toa5d4e286b(scripts/check-cli-test-child-env.mjsamong them) — not re-merged here; the queue rebuild covers it.Post-merge, all through
scripts/pm/os-verify-lock.sh, verdicts quoted from itsVERDICT command-exitline:pnpm --filter @objectstack/spec test—VERDICT command-exit 0· 465 files / 12989 tests passed (at718bff099)pnpm --filter @objectstack/spec typecheck— exit 0 (tsc + scripts +check:test-typecheck: OK)pnpm --filter @objectstack/cli exec vitest run --project unit—VERDICT command-exit 0· 186 files, 2556 passed | 6 expected fail;pnpm --filter @objectstack/cli typecheckOK. Theintegrationtier is declared to CI (the diff touches no integration file or spawn entry).pnpm --filter @objectstack/platform-objects test— 37 files / 545 passed;typecheckOK.pnpm --filter @objectstack/spec check:generated— exit 0, "All 15 generated artifacts are up to date".node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ranreconciles 107 derived / 107 run / 0 NOT-MEASURED / 0 UNRUN. Every family exited 0 on its own verdict line, includingcheck:i18n,check:i18n-coverage(13 configs, 621 baselined, none new),check:i18n-walk-parity(11 declared groups, 8 walked, 3 exempted),check:api-surface,check:authorable-surface,check:strictness-ledger,check:liveness,check:pm-widening-tells,check:changeset-no-major("introduces nomajorbump"; LEVEL AXIS not measured locally — no PR payload),check:nul-bytes,check:partof-closing-keywordself-test. One family needed the CI-shaped heap:check:type-check-debtOOMed under--max-old-space-size=4096(exit 3, PREREQUISITE NOT MET) and passed under 6144, the ceiling its own script pins ("5 ledger entries re-measured, none above its recorded number").eslint --no-inline-config --format jsonover the 12 changed.tsfiles ataf0593780— 12 files linted, 0 errors, 0 warnings. Population read from eslint's own config (--print-configreturns a config for the changed files, i.e. none is ignored); invariance:eslint.config.mjsstates it never enables type-aware linting for any file (noparserOptions.project, no typed rules — quoted at its line 326-332), so this diff cannot move any untouched file's verdict. The repo-widepnpm lintis CI's run.Ablation (from the committed state, each leg restored under
trap … EXIT INT TERM, landing proved by anchor counts and blob hashes, tree empty ongit diff HEADandgit status --porcelainafterwards):slotsnever a root (i18n-resolver.ts): speci18n-resolver.test.ts7 failed / 257 passed (the slots-root trace, the measured-zero fixture, cross-root arbitration, rebuild/no-mutation, and the three slotted-pagetranslatePagepins). Dist-mediated leg for the CLI pin: markerABLATION-M1-SLOTS-NEVER-A-ROOTproved INpackages/spec/distbyscripts/ablation-dist-preflight.mjsafter a rebuild, thenplatform-page-i18n-parity.test.ts3 failed / 24 passed (walk parity,slots.headerroute, the boundary pin); restore leg rebuilt,--absentpreflight "marker absent from all 218 built files", green control 27/27.items[].childrennever descended: 6 failed / 258 passed.globalFiltersgroup and its dashboard-level aliases removed fromtranslation.zod.ts:translation.test.ts8 failed / 119 passed.Source blobs after every leg equal
HEAD:(a94f3ce15…for the resolver,b6663c982…for the schema).验收备注
PageTabsProps.items[].label) still has no bundle key. It is anI18nLabelSchema, so its route is the inline locale map (the ruled authoring route, 2026-08-06); a bundle key for a panel that carries no id and whosevalueis optional is a naming decision this PR does not make. Pinned as a BOUNDARY test (translatePage — slotted page roots and tab panels, last case) so the day a key is added the pin is the one to flip. Raised as an open question in the report for the contract reviewer; 承接者:无 unless the reviewer wants it.LEVEL AXIShalf ofcheck:changeset-no-majorreads the PR payload and is measured only in CI._views.VIEW.descriptionis declared, walked and counted as covered but never rendered — plus three surfaces a bundle cannot address at all #16735 and analytics: chart category labels drawn from aselectfield's options render untranslated on a SAME-object dataset dimension, while the identical values translate on a dotted cross-object one #16773 remain open (split siblings, other lanes).🤖 Generated with Claude Code
https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
Generated by Claude Code