spec: GlobalFilterSchema gains optional object for i18n label resolution (#7804) - #7892
Conversation
…l resolution (#7804) GlobalFilterSchema gains an optional `object?: string` (Route A, ruled on the triage-seat comment on #7804, 2026-08-11) so a dashboard global filter's field label (e.g. "Sales Channel:") and its option labels resolve through the same `fields.<object>.<field>` translation-bundle convention lists/forms already use -- zero new i18n vocabulary, one resolver path. Rejects Route B (a new `dashboards.*.filters` bundle node -- duplicate vocabulary, no precedence rule) and Route C (inline I18nLabelSchema forms -- orthogonal, tracked separately). Deliberately independent of `optionsFrom.object`, which names the object dynamic OPTIONS are fetched from and may differ (e.g. filtering `opportunity` by `owner` with options sourced from `user`). Additive optional key: nothing that parsed before stops parsing. Unblocks objectui#4324, the dashboard filter-bar renderer half already landed behind this key. Also: content/docs/references/ui/dashboard.mdx and packages/spec/authorable-surface/ui.json regenerated to match (`pnpm --filter @objectstack/spec gen:docs`; check:generated all 13 green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY2Q5Xto1u8YHADgrZDTnk
…ema.object (#7804) pnpm --filter @objectstack/spec gen:docs after adding the schema key; check:generated all 13 artifacts green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY2Q5Xto1u8YHADgrZDTnk
…e-match) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY2Q5Xto1u8YHADgrZDTnk
pnpm --filter @objectstack/spec gen:schema; check:authorable-surface green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY2Q5Xto1u8YHADgrZDTnk
…tion (#7804) The schema-source half of the change: `object?: string` on GlobalFilterSchema between `field` and `label`, documented as distinct from `optionsFrom.object`. See the changeset for the full rationale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY2Q5Xto1u8YHADgrZDTnk
New describe block: accepts string, optional/absent, rejects non-string, independence from optionsFrom.object, unknown-key strictness unchanged, JSON-Schema pin (string slot, not required). pnpm --filter @objectstack/spec test: 378 files / 9954 tests passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY2Q5Xto1u8YHADgrZDTnk
…e-match local) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY2Q5Xto1u8YHADgrZDTnk
…transcription) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JY2Q5Xto1u8YHADgrZDTnk
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
…s-regen driver's second half
`authorable-surface/ui.json` carries `merge=os-regen`, so a merge resolves it by
taking a side and leaving it REGEN-PENDING rather than by textually splicing two
generated files. That is the driver working as designed; the half that must
follow is the regeneration, and it is not optional here — measured on the merged
tree before pushing, `check:authorable-surface` fails:
❌ authorable-surface/ is out of date (1 key(s) not recorded).
+ ui/GlobalFilter:object
The missing key is main's own, from #7892 (`GlobalFilterSchema` gains an optional
`object` for i18n label resolution). Nothing of this branch's is involved: the
regenerated diff is exactly that one line, and batch A's five declared keys were
already recorded.
Worth stating because the failure mode is quiet: `pnpm build` REWRITES this
artifact, so a `check:generated` run that happens to follow a build reports all
13 green against the file the build just fixed. Only `check:authorable-surface`
on its own, against the merged bytes, shows the drift — which is how this was
caught before the merge queue caught it.
Merged tree verified: spec 381 files / 10091 tests, lint 71 / 1932,
check:generated 13/13, check:strictness-ledger current (438 sites / 149 strip /
9 authorable — unmoved by the merge). The ledger counts and liveness counts were
regenerated too and came back byte-identical.
Part of #4001
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012NLJ8PWLdwLSyi84LHzrAx
Rebase round for PR #7851 (#7521) after the domain:spec os-regen landing relay tail landed on main (#7758, #7759, #7763, #7782, #7813, #7892). Every `merge=os-regen` generated path is taken from origin/main WHOLESALE (`git checkout origin/main -- <paths>`, the 12 entries .gitattributes names), so this commit is a known-good base: the `os-regen` merge driver exits 0 without conflict markers while silently dropping one side, so a clean merge is indistinguishable from a dropped baseline. Regeneration lands as a SEPARATE commit on top of this one, and asserts the relay PRs' entries survived. Committed with --no-verify deliberately: the os-regen pre-commit hook wants the artifacts regenerated into this same commit, which would mix "what main brought" with "what my change produces" into one indistinguishable blob. The very next commit regenerates them, so the pushed tip is current. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018tmmVmCkr4QtvGKMcn5s96
…ee (#7521) Step 4 of the os-regen rebase, on top of the committed merge (cbea40d) whose generated baselines came from origin/main wholesale. `check:generated` proved exactly 2 of 13 artifacts stale — api-surface/ and export-origins/, both this PR's own — and the other 11 current, so main's baselines carried across the merge intact. Regenerated only those two. The diff is 4 pure insertions and zero removals: the four exports managed-api-affordance.ts adds. The relay tail's entries are asserted present in the regenerated tree rather than assumed — the os-regen driver drops a side silently, so a clean merge is not evidence: #7763 TranslationBundle `flows` — content/docs/references/system/translation.mdx:161 #7813 Field.number `useGrouping` — packages/spec/authorable-surface/data.json #7892 GlobalFilterSchema `object` — content/docs/references/ui/dashboard.mdx #7782 InvitationStatus `canceled` — content/docs/references/identity/organization.mdx Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018tmmVmCkr4QtvGKMcn5s96
…keys (objectstack-ai#4001 batch A) (objectstack-ai#8008) * feat(spec): close the 31 SDUI component-props shapes against unknown keys (objectstack-ai#4001 batch A) `ComponentPropsMap` — the declared shape of every `page:*`, `record:*`, `element:*`, `nav:*` and `ai:*` node a page can carry — stripped unknown keys in silence. All 31 object sites in `ui/component.zod.ts` are `strictObject` now, including the two union arms (`RecordHighlightsField`'s object arm and `record:related_list`'s sort entry) and every nested block, since strictness does not recurse. objectstack-ai#5068 already REPORTED these keys, by walking a strip-mode object and reconstructing what the parse would have dropped. Now the parse rejects the key itself — same rule id, same warning tier, and three things the reconstruction could not give: curated per-surface prescriptions, a rejection that holds for every caller rather than only inside the gate, and coverage of union arms. Five renderer-honoured props are DECLARED rather than rejected, on the rule this file has applied three times before (objectstack-ai#5611/objectstack-ai#5775/objectstack-ai#6276 — the delivered, authorized shape is the contract): `page:header` `maxVisible`/`mobileMaxVisible`, `page:tabs.alwaysShowStrip`, `record:details` `inlineEdit`/`showHeader`. Each is read by objectui through `schema?.X ?? schema?.properties?.X` with its own comment inviting authors. All optional with no schema default — the defaults are the renderer's, and declaring them would turn an unset key into an authored one. Curated tables, each backed by a producer measured in the wild rather than guessed: a tab item's `key` → `value` (objectui's Studio designer publishes `key`; the renderer reads `it.value`), a header's `description` → `subtitle` (the rename its own ADR-0087 conversion performs — the one path that had no diagnostic at all), a container's `body` → `children`, and a wrong-layer family for keys that belong on the component NODE. `@objectstack/lint` gains one piece of wiring: zod 4 collapses union-arm failures into a single `invalid_union`, so a lone arm's `unrecognized_keys` is unpacked back onto the unknown-key rule id — and deliberately is not when two arms could both have been meant. Deliberately unchanged: the carrier is still `z.record(z.string(), z.unknown())` (direction B stays declined, unregistered `record:*` types are still skipped), the storage path still parses no props (objectstack-ai#4463), and the gate is still warning level. Verified with direct build-artifact `.parse()` probes over the example corpus — `objectstack validate` never parses through `PageSchema` (objectstack-ai#5000), so "the examples validate clean" would have been no evidence. 244 registered props bags across the three example apps' build artifacts, the same three from source, and the three published platform pages: zero undeclared keys, zero new refusals. The probe's negative control moved from 0/244 injections caught to 244/244. Filed out of scope: objectstack-ai#7973 (objectui's Studio designer publishes three page component inputs no renderer reads). Part of objectstack-ai#4001 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012NLJ8PWLdwLSyi84LHzrAx * docs(ui): stop teaching `record:highlights.actions` — nothing has ever rendered it Found by the objectstack-ai#3746-style corpus scan this batch owes: every `content/docs/**` and `skills/**` code block carrying a registered component type was parsed and its props judged against the now-closed `ComponentPropsMap`. One hit, in the "Complete Example" of the pages guide — an `os:check` block, so it is an example the docs gate compiles and an AI author copies. `RecordHighlightsProps` has never declared `actions`, and objectui's `record-highlights.tsx` reads `schema.fields` and nothing else. Under strip the key was dropped in silence, which is precisely why a doc could teach it for this long: the example parsed clean and rendered without the actions, and no layer disagreed. With the shape closed it is a rejection, so the doc had to be right before the batch could land. The actions move to `record:quick_actions`, which is the component that renders them (`actionNames`, per objectui's own designer config) and which the same page already lists two sections up. Scan after the fix: 392 mdx + 2299 markdown files, 8 registered-type props bags, 0 teaching a key these schemas reject. `check:skill-examples` — 209 prose examples type-check. Part of objectstack-ai#4001 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012NLJ8PWLdwLSyi84LHzrAx * chore(spec): regenerate authorable-surface after merging main — the os-regen driver's second half `authorable-surface/ui.json` carries `merge=os-regen`, so a merge resolves it by taking a side and leaving it REGEN-PENDING rather than by textually splicing two generated files. That is the driver working as designed; the half that must follow is the regeneration, and it is not optional here — measured on the merged tree before pushing, `check:authorable-surface` fails: ❌ authorable-surface/ is out of date (1 key(s) not recorded). + ui/GlobalFilter:object The missing key is main's own, from objectstack-ai#7892 (`GlobalFilterSchema` gains an optional `object` for i18n label resolution). Nothing of this branch's is involved: the regenerated diff is exactly that one line, and batch A's five declared keys were already recorded. Worth stating because the failure mode is quiet: `pnpm build` REWRITES this artifact, so a `check:generated` run that happens to follow a build reports all 13 green against the file the build just fixed. Only `check:authorable-surface` on its own, against the merged bytes, shows the drift — which is how this was caught before the merge queue caught it. Merged tree verified: spec 381 files / 10091 tests, lint 71 / 1932, check:generated 13/13, check:strictness-ledger current (438 sites / 149 strip / 9 authorable — unmoved by the merge). The ledger counts and liveness counts were regenerated too and came back byte-identical. Part of objectstack-ai#4001 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012NLJ8PWLdwLSyi84LHzrAx * chore(spec): recompute the strictness ledger counts from the tree merged with batch B Second turn of the os-regen relay. `objectstack-ai#7985` (batch B — the memory driver's five persistence sub-shapes) and `objectstack-ai#7972` landed while this branch was in the queue, so the shared counts artifact met two batches that each decremented it correctly and independently. That is the exact arithmetic objectstack-ai#5107 built this artifact to prevent: the rows do not overlap, git merges them without complaint, and the SUBTOTAL — which overlaps nothing — merges clean and wrong. The artifact carries `merge=os-regen` so the merge defers rather than splices, and the only correct resolution is to recompute from the merged tree. Regenerated, never hand-edited: still-open (strip) 149 → 144 (batch B's five) files carrying at least one 26 → 25 authorable — forced scope 9 → 4 data/ strip 107 → 102, data/ strict 57 → 62 total strict 283 → 288 Neither batch's own numbers moved; the combined ones did. `check:strictness-ledger` agrees with the merged tree in both directions (25 open files / 144 strip sites, no closed file still carrying a worklist row — batch B's `driver/memory.zod.ts` row left with their side of the prose). Merged tree verified: spec 382 files / 10109 tests, lint 71 / 1932, check:generated 13/13. Part of objectstack-ai#4001 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012NLJ8PWLdwLSyi84LHzrAx --------- Co-authored-by: Claude <noreply@anthropic.com>
…thor is (objectstack-ai#7521) (objectstack-ai#7851) * feat(spec,lint): gate managed `apiMethods` ⊆ affordances where the author is (objectstack-ai#7521) `reconcileManagedApiMethods` has always caught a managed object advertising a write verb its own affordances refuse, and stripped it — fail-closed, nothing ever exposed. What it could not do is tell anyone: the only signal was a `console.warn`. `sys_environment` / `sys_package` declared `apiMethods: ['get','list','create','update']` against `userActions` refusing all three writes, and that warning fired on every control-plane boot for the life of the divergence unread. The split was found by hand-driving the HTTP seam, not by any gate. Per the maintainer's 2026-08-11 ruling (the middle option — lint/gate-visible): - **New shared predicate** `checkManagedApiMethodAffordances` (`@objectstack/spec/data`), beside `resolveCrudAffordances` — the affordance authority both sides already read. The verb → affordance table moves here from objectql's registry. - **`reconcileManagedApiMethods` is now a pure reaction to it.** Behaviour is unchanged and pinned so: still warn-and-strip, still never throws — failing registration closed would let one metadata typo kill a control-plane boot. The warning now cites the lint rule id so a boot log leads to the gate. - **New author-time rule** `object/managed-api-method-unaffordable` (`error`, gating, pre-parse) wired into the authoring registry, so `os lint`, `os validate` and `os build` all report it. One predicate, two consumers: a second copy of this table at either end would BE the declared≠enforced drift the rule exists to detect. Same shape, and same reason, as `checkFieldCompleteness` under ADR-0078. `validateManagedApiMethods` and its rule id are exported so a repo whose object definitions live in code — which `os lint` never walks — can run the same rule over its own registry instead of hand-rolling the table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018tmmVmCkr4QtvGKMcn5s96 * chore(spec): regenerate api-surface + export-origins on the merged tree (objectstack-ai#7521) Step 4 of the os-regen rebase, on top of the committed merge (cbea40d) whose generated baselines came from origin/main wholesale. `check:generated` proved exactly 2 of 13 artifacts stale — api-surface/ and export-origins/, both this PR's own — and the other 11 current, so main's baselines carried across the merge intact. Regenerated only those two. The diff is 4 pure insertions and zero removals: the four exports managed-api-affordance.ts adds. The relay tail's entries are asserted present in the regenerated tree rather than assumed — the os-regen driver drops a side silently, so a clean merge is not evidence: objectstack-ai#7763 TranslationBundle `flows` — content/docs/references/system/translation.mdx:161 objectstack-ai#7813 Field.number `useGrouping` — packages/spec/authorable-surface/data.json objectstack-ai#7892 GlobalFilterSchema `object` — content/docs/references/ui/dashboard.mdx objectstack-ai#7782 InvitationStatus `canceled` — content/docs/references/identity/organization.mdx Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018tmmVmCkr4QtvGKMcn5s96 --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #7804
Problem
A dashboard global filter renders its field label (e.g. "Sales Channel:") and its option labels untranslated, and there was no key to fix it with:
GlobalFilterSchemadeclared noobject, and neither doesDashboardSchema, so the canonicalfields.<object>.<field>translation-bundle convention that lists/forms already use has nothing to resolve against. Measured in objectui#4324's implementation, and hit in production on a hotcrm-heimao dashboard.Fix (Route A, ruled on the triage-seat comment on #7804, 2026-08-11)
Adds an optional authorable
object?: stringtoGlobalFilterSchemainpackages/spec/src/ui/dashboard.zod.ts, betweenfieldandlabel. When set, it names the objectfieldlives on, and the filter's field label and option labels resolve through the samefields.<object>.<field>bundle entry lists/forms already use — zero new i18n vocabulary, one resolver path.Deliberately independent of
optionsFrom.object, which names the object dynamic options are fetched from and may differ (e.g. filteringopportunitybyownerwith options sourced fromuser).Rejects Route B (a new
dashboards.*.filtersbundle node inTranslationData— duplicate vocabulary, no precedence rule) and Route C (inlineI18nLabelSchemaforms — orthogonal, tracked separately on objectui#4163).Additive and optional: a filter that omits
objectrenders exactly as it always has. Nothing that parses today stops parsing. Unblocks objectui#4324, the dashboard filter-bar renderer half already landed behind this key.This PR is a continuation
Branch
claude/issue-7804-globalfilter-objectwas pushed by an earlier dev session that died before opening a PR or reporting (handoff note on #7804). This session fetched the existing branch into a dedicated worktree, reviewed every commit (schema change, tests, changeset, regenerated docs/authorable-surface), mergedorigin/main(clean, no conflicts), and re-ran full verification from scratch — nothing here was trusted blindly.Changes
packages/spec/src/ui/dashboard.zod.ts—GlobalFilterSchema.object(optional string)packages/spec/src/ui/dashboard.test.ts— newdescribeblock pinning: accepts string, optional/absent (no default materializes), rejects non-string, independence fromoptionsFrom.object, unknown-key strictness unchanged, JSON-Schema pin (string slot, not required)content/docs/references/ui/dashboard.mdx— regenerated (gen:docs)packages/spec/authorable-surface/ui.json— regenerated (gen:schema).changeset/global-filter-object-i18n-7804.md— minor bump on@objectstack/specVerification (this session, re-run in full)
pnpm --filter @objectstack/spec build— green, tree clean after (generated artifacts already matched, no diff)pnpm --filter @objectstack/spec typecheck— green (tsc --noEmit,check:scripts-typecheck,check:test-typecheck)pnpm --filter @objectstack/spec test -- --maxWorkers=2— 379 test files / 9989 tests passedpnpm --filter @objectstack/spec check:generated— all 13 generated artifacts up to date, includingcheck:authorable-surfaceandcheck:docspnpm check:i18n(after building@objectstack/cli) — 9 packages, all bundles in sync, no undeclared authoring keyspnpm --filter @objectstack/lint run check:doc-formula-expressions— green (self-test 24/24, 22 + 9 example sites judged clean)pnpm check:adr-anchors,check:changeset-gate-self-tests,check:docs-audit-scope,check:merge-driver,check:release-body,check:spec-parsed-alias,check:nul-bytes— all greenCI (ESLint job, TypeScript Type Check job, and the rest of the gate farm) has not been waited on from this session per the current reporting contract — the PM reads real gate-job conclusions before any ready-flip.
Generated by Claude Code