Skip to content

feat(types,plugin-dashboard): dashboard header and globalFilters state the spec member on both faces (objectui#7759 group A) - #10744

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-7759-group-a-dashboard-locale
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-7759-group-a-dashboard-locale

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #7759
Clause-②: yes

This PR covers group A of objectui#7759 only: the header / globalFilters row of the complex.zod.ts#DashboardComponentSchema pair. The card stays open for group C (FormSchema.layout), which this PR does not touch.

Dev session: https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW (PM seat domain:spec#1, claim comment 5851963524).

Determination per key (ruling 5617465269)

@objectstack/spec declares both keys, so principle 1 governs both: the zod mirror and the TypeScript declaration both follow the spec, in both directions.

The stop condition did not fire on either key. It covers the case where principle 1 and the objectui#4580 route point opposite ways. Here the spec types every nested label involved as I18nLabel (a string or an inline per-locale map), which is exactly the objectui#4580 form. So the two routes agree on the label, and principle 1 alone decides the remaining members.

key does the spec declare it? before after
header yes. DashboardSchema has header: DashboardHeaderSchema.optional(). In DashboardHeaderActionSchema, label is I18nLabelSchema, actionUrl is z.string() (required), and actionType is WidgetActionTypeSchema.optional(), the spec's ActionType mirror: the spec's member, by reference. Declaration: a hand-written restatement whose actions were typed label: string, actionUrl?: string, actionType?: string the declaration drops the extra 'header' omission and inherits the spec's member through the Omit projection its other spec-owned keys use
globalFilters yes. DashboardSchema has globalFilters: z.array(GlobalFilterSchema).optional(). In GlobalFilterSchema, each options item is a strict { value, label: I18nLabelSchema } pair. In GlobalFilterOptionsFromSchema, labelField is z.string() (required) and filter is FilterConditionSchema.optional() declaration: SpecGlobalFilter[], already by reference (objectui#4032). Mirror: the spec's shape spread into a local z.object, with options and optionsFrom restated the mirror's GlobalFilterSchema is stripImportedDefaults(SpecGlobalFilterSchema), the spec's schema whole. The TypeScript face is unchanged

The pair is also in the ledger's SPEC_DERIVED_PAIRS.

grep evidence. Measured on the installed pin, @objectstack/spec 17.4.0, against its src/ui/dashboard.zod.ts:

grep -nE 'header: DashboardHeaderSchema|globalFilters: z.array\(GlobalFilterSchema\)|label: I18nLabelSchema|actionUrl: z.string|labelField: z.string|filter: FilterConditionSchema' src/ui/dashboard.zod.ts
  • Pin (17.4.0): the command hits all six members.
  • objectstack main (8d1f7ab): the same file, read with git show, declares the same shape for both schemas. The only difference is an added .meta({ title }) on several members, which does not change the input type.

Measured on BASE 5c61e5241, before any edit: both keys were DISJOINT

I measured with a throwaway probe (deleted afterwards). It checked the type face with @ts-expect-error legs and ran safeParse on the mirror:

spelling mirror (safeParse) declaration (tsc)
header action label: { en: … } accepted refused
header action with no actionUrl refused accepted
header action actionType: 'bogus' refused accepted
filter options: ['EMEA'] (bare-string shorthand) accepted refused
filter option with no label accepted refused
optionsFrom with no labelField accepted refused
optionsFrom.filter as an array accepted accepted (the declared FilterCondition type has an index signature, so it admits an array; the spec's runtime schema refuses one)
unknown key inside a filter accepted, and stripped without a message n/a
filter option label: { en: … } refused accepted

Premise check (the dispatch's H1–H5)

  • H1: partly false. The ledger docblock said both keys "carry the inline-locale widening one level down". That holds for header only. globalFilters measured WIDER for a different reason: the objectui-local option shorthand and the relaxed optionsFrom. Its locale arm ran the other way: the mirror refused the map, which puts it on the KnownDrift side. The card body's group-A table inherits the same wording. The row was correct to exist; only its description was wrong.
  • H2: held. globalFilters is declared as SpecGlobalFilter[]. header was omitted from the spec projection and re-declared locally, and the spec does declare it (see the table above).
  • H3: held. The DashboardComponentSchema mirror line is globalFilters: z.array(GlobalFilterSchema). The z.array(z.any()) line belongs to DashboardConfigSchema, a different pair, which this PR leaves alone.
  • H4: held. The pin and objectstack main agree on both shapes.
  • H5: held. Both rows were retired because the pair now measures clean in both directions. KnownDrift keeps widgets for this pair, because it still measures as drift.

What changed

  • packages/types/src/complex.ts: DashboardComponentSchema extends the Omit projection over DASHBOARD_SPEC_EXCLUDED alone. The hand-written header is deleted, and the docblock records what it was and why it went.
  • packages/types/src/zod/complex.zod.ts: GlobalFilterSchema = stripImportedDefaults(SpecGlobalFilterSchema). The local options / optionsFrom restatement and its delegated superRefine are gone. The spec's own object-level refinement (the date defaultValue check) arrives with the schema. The SpecDashboardFields docblock no longer calls the filter config "deliberately looser than spec's".
  • packages/plugin-dashboard/src/DashboardRenderer.tsx: principle 1 requires the read site to implement every arm the spec declares. tActionLabel now collapses the I18nLabel to the active language with pickLocalized before using it, either as button text or as the dashboardActionLabel fallback. The ActionDef built for each header action carries the resolved label too; that memo depends on language rather than on a memoised resolver's identity (AGENTS.md [WIP] Enhance every detail of the designer #10). The handleClick fallback actionUrl || label was unreachable, because the guard above it already returns when actionUrl is empty; it is now executeAction(actionUrl).
    • Red-first on BASE source: both new renderer cases threw Objects are not valid as a React child (found: object with keys {en, zh-CN}). The mirror accepted that exact document.
  • packages/types/src/__tests__/zod-mirror-parity.test.ts:
    • WiderThanDeclared loses the pair's entry whole, and a departure note replaces it. WIDER_ARMS loses the pair's two rows.
    • KnownDrift for the pair goes from 'header' | 'widgets' | 'globalFilters' to 'widgets'.
    • The header figures move from 7 / 9 / 9 (split 5 / 4 / 0 / 0) to 6 / 7 / 7 (5 / 2 / 0 / 0) for WiderThanDeclared, and from 46 / 86 to 46 / 84 for KnownDrift. Each move is recorded in the sentence beside its figure.
  • New pin, packages/types/src/__tests__/dashboard-header-global-filters-spec-7759.test.ts, with checks on both faces for each key:
    • Equal rows showing the declaration and the mirror input each equal the spec member, plus a control row showing the old restatement is not the spec member;
    • @ts-expect-error legs for each refused spelling;
    • safeParse legs that assert the issue envelope (code plus path);
    • a check that no scope default is authored, and a control document.
  • Existing pins whose premise this PR removes (fixture triage):
    • twins-spec-by-reference-9736.test.ts: header is no longer a twin-only omission. The case now asserts it equals the spec member; the slots half stays as it was.
    • p1-spec-alignment.test.ts: the fixture { label: 'Refresh', actionType: 'refresh' } was legal only under the removed restatement, and the spec and the mirror both refuse it. It is replaced by a spec-legal action that uses a locale-map label, plus an @ts-expect-error leg for refresh.
    • report-chart-query-spec-parity.test.ts: the "pinned divergences" block, which asserted the local schema accepts what the spec refuses, now asserts both schemas give the same verdict on the same literals. The case that pinned the old composition is replaced by one that shows the date refinement still applies when the options are spec-legal.
  • Docs (AGENTS.md Add automated testing infrastructure and CI/CD workflows #2; ruling principle 3):
    • content/docs/plugins/plugin-dashboard.mdx gives header actions the spec's shape.
    • content/docs/guide/dashboard-filters.md says objectui's own validator refuses what the platform refuses, and that labelField is required.
    • The packages/plugin-dashboard/README.md example comment adds labelField. Before this PR it taught an optionsFrom the platform refuses at publish.
  • .changeset/7759-dashboard-header-global-filters-spec.md: @object-ui/types minor, because the validator's globalFilters accept set narrows and the header interface narrows actionUrl / actionType. @object-ui/plugin-dashboard patch, for the renderer fix. The changeset lists the breaking semantics in full.

Accept-set delta (why Clause-② is yes)

  • Validator, narrowed:
    • the bare-string option shorthand is refused: invalid_type at globalFilters.N.options.M;
    • a label-less option is refused: invalid_union at ….options.M.label;
    • an optionsFrom with no labelField is refused: invalid_type at ….optionsFrom.labelField;
    • an array optionsFrom.filter is refused;
    • an unknown key in a filter is now refused by name with unrecognized_keys at globalFilters.N, where before it was stripped without a message.
  • Validator, widened: an option label may be an inline per-locale map. The spec has always allowed it, and the filter bar already resolves it with pickLocalized.
  • Interface, narrowed: header.actions[].actionUrl is required, and actionType is limited to the spec's ActionType.
  • Interface, widened: header.actions[].label accepts I18nLabel.
  • Runtime, deliberately unchanged: @object-ui/core's normalizeFilterOptions still converts a STORED bare-string option on read and logs its deprecation warning, on the objectstack#7917 option-② window ("the spec stays strict; the renderer's lift retires behind a survey"). This is the split objectui#4165 established: the schema refuses the legacy spelling and the read path lifts it. The filter bar also still falls back to valueField when labelField is absent. Renderers never parse through the mirror; the only runtime callers of safeValidateSchema are the CLI's validate / check, which is the authoring door.

File surface beyond the claim, declared

The claim listed complex.ts, complex.zod.ts, this pair's ledger rows, one pin and one changeset. Measurement took this PR further, as the dispatch allows ("若实测表明…按生产者侧修,落点与理由写进报告和 PR 正文"):

  • DashboardRenderer.tsx and its test. Aligning the declaration with the spec is the forced consequence: without the read-site change, a spec-legal document the mirror accepts would still crash the renderer, and principle 1 requires the read site to implement the spec's arm. objectui#9092 / PR fix(types): restore the inline-locale declared face on group A's three pairs #9364 set the same precedent for plugin-grid.
  • The three existing packages/types pins listed above. Each one pinned the removed restatement or divergence.
  • Three docs files. They follow the implementation.

The PM should re-derive serial constraints on packages/plugin-dashboard/src/DashboardRenderer.tsx; I did not scan other open PRs (dev contract).

Tests and gates at head 70d26a8

The final union ran after the last commit, and after merging main, which brought in PR #10730 cleanly.

command reading
pnpm --filter @object-ui/types type-check exit 0 (VERDICT command-exit 0)
pnpm exec vitest run packages/types/ 242 files / 5294 tests passed
pnpm --filter @object-ui/core type-check / plugin-dashboard / plugin-designer / plugin-list exit 0 each, script name echoed. The dependency closure was built first with turbo run build --concurrency=2, and the rebuilt types dist was checked for the change
pnpm exec vitest run packages/core/ 173 / 3504 passed
pnpm exec vitest run packages/plugin-dashboard/ 136 / 1283 passed
pnpm exec vitest run packages/plugin-designer/ 24 / 175 passed
pnpm exec vitest run examples/schema-catalog/ 34 / 2198 passed
CLI retired-view-dashboard-key-9533 / app-generator plus app-shell dashboard-schema / DashboardWidgetInspector 4 files / 77 tests passed
eslint --no-inline-config --format json on the 9 changed TS files 9 files, 0 errors, 21 warnings; the same files at BASE carry the same 21
node scripts/check-changeset-presence.mjs / check-changeset-no-major.mjs exit 0 / exit 0
pnpm check:new-line-citations exit 0: "0 new citation(s)"
pnpm check:control-bytes / check:spec-symbols / check:test-path-roots exit 0 each. check:spec-symbols first caught my local alias named DashboardHeaderAction, a spec export name; it was renamed DashboardNodeHeaderAction
pnpm check:doc-types / doc-snippets / doc-examples / doc-fences / doc-example-ids / docs-route-closure / readme-exports / doc-example-readers / shell-escape-residue exit 0 each; doc snippets "676 of 676 block(s) judged, 0 failed" (after the gate's own scoped build)
pnpm check:changeset-claims / pending-changeset-literals / installed-pin-claims / handler-key-reads exit 0 each
node scripts/check-governed-queue-guard.mjs --test over the 13 branch paths NOT GOVERNED

Consumer direction. The dependents checked were core, plugin-dashboard, plugin-designer and plugin-list: the packages the census found importing DashboardComponentSchema. plugin-list's hit is a comment. examples/schema-catalog and the CLI / app-shell dashboard files cover the validator's accept set.

Cross-package reverse check. I pasted a header action with actionType: 'refresh' into a probe in plugin-dashboard's test project. It gave TS2322 against the rebuilt packages/types/dist, which shows the consumer reads the new .d.ts. The probe was then removed.

Reverse verification on committed HEAD

Each mutation went through ablation-replace.mjs, and each restore was proven the same way: the blob equals HEAD and git diff HEAD is empty. The expected direction was registered before each run.

  • A. Mirror: GlobalFilterSchema back to the local override (blob 72d0b6a85b32 to e52ba4acbdfe).
    • tsc -p tsconfig.test.json exit 2. The failures: the pin's mirror and local Equal rows, assertionDriftMatchesLedger and assertionWiderMatchesLedger (both naming complex.zod.ts#DashboardComponentSchema), and assertionWiderLedgerRecordsEveryKey (naming globalFilters).
    • vitest: 6 failed / 48 passed.
    • Restored to 72d0b6a85b32.
  • B. Declaration: hand-written header restored (blob ace7e75f2ade to cfb61c54e999).
    • tsc exit 2, with 10 errors. They cover: the pin's declared Equal row and its locale-map literal; two unused @ts-expect-error legs; the twins pin; p1-spec-alignment; and the three ledger reconciliations, which name the pair and the key header.
    • The runtime legs stayed 9 / 9 passed, as registered: the mirror does not move under this mutation.
    • Restored.
  • C. Renderer: tActionLabel passes the authored label through unresolved. vitest: 2 failed / 8 passed, with Objects are not valid as a React child. Restored.

NOT MEASURED

  • Spec Main Shape Gate (scripts/spec-main-shape-gate.mjs). Reason: its workflow builds @objectstack/spec from objectstack main, injects it, and runs TURBO_FORCE=true pnpm type-check across the whole workspace. That is farm-scale, and CI owns it. Mitigation: a source comparison of both touched spec schemas, pin 17.4.0 against main 8d1f7ab, shows no shape difference.
  • pnpm check:spec-floors. A prerequisite failure, not a finding: all 9 findings were [no-artifact] for packages outside the built closure. This diff does not move any @objectstack/spec range.
  • Repo-wide pnpm lint and the full pnpm test farm. Left to CI. The ESLint run above is a narrowed run. Three facts make it a measurement: the population is eslint's own **/*.{ts,tsx} config; the file count, 9, comes from the --format json output; and eslint.config.js declares no parserOptions / projectService, and no custom eslint-rules/* rule reads the filesystem, so type-aware linting is off and this diff cannot move a verdict in an untouched file.

Acceptance notes

  • Runtime lift kept. normalizeFilterOptions, the bare-string lift in @object-ui/core, remains. For a validated document it is now a read-path tolerance only, and its retirement stays on objectstack#7917's schedule. It is not touched here.
  • labelField fallback kept. DashboardFilterBar's from.labelField ? … : … fallback to valueField is unreachable for a validated document and was left in place for stored ones; nobody is assigned to carry it.
  • name fallback kept. The headerActionDefs fallback name: a.actionUrl || label survives only for a stored action with no actionUrl, which the mirror has always refused. It was left in place, matching the click guard.
  • Card body. The card body's group-A table calls the globalFilters difference "inline-locale widening". Measured, it is the local shorthand, and the locale arm ran the other way (see H1). This is recorded so the group-C round does not inherit the wording.
  • Commit trailers. The first commit carries only the Claude-Session: trailer. The later commits carry a model-free Co-Authored-By: Claude / Claude-Session: pair. History was not rewritten.
  • Scope. Group C (FormSchema.layout) is untouched and remains on the card, which stays open.

Generated by Claude Code

…e the spec member on both faces (objectui#7759 group A)

Both keys are declared by @objectstack/spec's DashboardSchema, so under the
card's ruling (principle 1) the zod mirror and the TypeScript declaration both
follow the spec, both ways. Each key was DISJOINT: ledgered in KnownDrift and
WiderThanDeclared.

- header: the declaration drops its hand-written restatement and inherits the
  spec's DashboardHeader through the Omit<Dashboard, ...> projection
  (actions[].label is I18nLabel, actionUrl required, actionType the ActionType
  enum). DashboardRenderer resolves the action label to the active language
  before rendering it or using it as the bundle fallback; an inline per-locale
  label used to reach React as a button child and throw.
- globalFilters: the mirror's GlobalFilterSchema is the spec's schema by
  reference. It no longer accepts the bare-string option shorthand, a label-less
  option, an optionsFrom without labelField, an array filter, or an unknown key;
  it now accepts the inline per-locale option label it refused. The runtime
  bare-string lift in @object-ui/core is untouched.

The pair leaves WiderThanDeclared whole and keeps only `widgets` in KnownDrift.

Claude-Session: https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW
…ocument the spec shape (objectui#7759 group A)

The dashboard docs follow the implementation: a header action's label is a
string or an inline per-locale map and its actionUrl is required; an
optionsFrom names labelField; objectui's validator refuses the bare-string
option shorthand, as the platform does. One changeset for the group:
@object-ui/types minor (the validator's globalFilters accept set narrows and
the header interface narrows actionUrl / actionType), @object-ui/plugin-dashboard
patch (the renderer resolves a per-locale header action label).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW
… the spec type (objectui#7759)

check:spec-symbols refuses a local declaration under a name @objectstack/spec
exports; `DashboardHeaderAction` is one. The alias is read off
DashboardComponentSchema, so it is named DashboardNodeHeaderAction.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW
@github-actions github-actions Bot added documentation Improvements or additions to documentation package: types plugin tests labels Sep 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 32 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/5903-objectgantt-declared-keys.md

  • names packages/types/src/__tests__/zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    ⚠️ The JUSTIFICATION for that parity clause was retired (objectui#9743); the clause itself stands and did not move. As first written it credited the zod-mirror-parity ratchet with a zero-drift reading for this pair — but at that time the ratchet measured three directions and was structurally blind to the MIRRORED-but-undeclared one (objectui#9711), so a zero from it recorded that it had not looked in that direction, not that nothing was there. objectui#9725 landed the fourth direction, and it covers this pair BY NAME: packages/types/src/__tests__/zod-mirror-parity.test.ts registers objectql.zod.ts#ObjectGanttSchema in both its mirror map and its declaration map; assertionMirroredUndeclaredMatchesLedger requires every registered pair's mirrored-but-undeclared key set to equal that pair's MirroredUndeclared ledger entry — never for a pair the ledger does not name — and assertionNoVacuousMirroredUndeclaredMeasurement refuses a measurement that has degenerated to any. ⛔ Read this pair's verdict off that reconciliation, which re-derives it on every run, rather than off any figure written here; when this paragraph was authored, on 2026-09-18, it required no MirroredUndeclared entry for the pair.

.changeset/5928-classname-style-props-rename.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Where the non-pair is recorded now. zod-mirror-parity.test.ts keys its existing EXCLUSIONS entry — the mechanism that accounts for every exported const with no TypeScript declaration to mirror, each with its stated reason — to ClassNameStylePropsSchema. Named for its own two keys, the const leaves no like-named declaration for a name-derived pairing to reach for.

.changeset/6051-gantt-flat-config-declared-keys.md

  • names packages/types/src/__tests__/zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    ⚠️ The JUSTIFICATION for that parity clause was retired (objectui#9743) — the same retirement objectui#5903's entry carries, for the same clause and the same reason; the clause itself stands and did not move. As first written it credited the zod-mirror-parity ratchet with a zero-drift reading for this pair, at a time when that ratchet measured three directions and was structurally blind to the MIRRORED-but-undeclared one (objectui#9711), so a zero from it recorded that it had not looked in that direction. objectui#9725 landed the fourth direction, and it covers this pair BY NAME: packages/types/src/__tests__/zod-mirror-parity.test.ts registers objectql.zod.ts#ObjectGanttSchema in both its mirror map and its declaration map; assertionMirroredUndeclaredMatchesLedger reconciles every registered pair's mirrored-but-undeclared key set against that pair's MirroredUndeclared ledger entry — never for a pair the ledger does not name — and assertionNoVacuousMirroredUndeclaredMeasurement refuses a measurement that has degenerated to any. ⛔ Read this pair's verdict off that reconciliation, which re-derives it on every run, rather than off any figure written here; when this paragraph was authored, on 2026-09-18, it required no MirroredUndeclared entry for the pair.

.changeset/6150-undeclared-but-consumed-keys.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    ⭐ AMENDED, and the amendment ships in this same release. objectui#7804's TreeViewSchema slice gave the key a zod arm after all — a NAMED REFUSAL (handlerKeyRefusal(key, 'runtime-slot', label)), never a shape — because "no mirror entry" is not neutral under BaseSchema.passthrough(): it meant an authored { "type": "tree-view", "onNodeClick": { "action": "toast" } } parsed GREEN, survived the parse, and reached a call site that expects a function. ⇒ the three clauses this bullet used to carry are no longer true of the code shipping beside it. The key is now a MEMBER of TreeViewSchema.shape and an authored value is refused BY NAME at path onNodeClick; it has LEFT zod-mirror-parity.test.ts's RuntimeOnlyDeclared for that file's KnownDrift; and it is no longer "the first pair to sit there without also sitting in UnmirroredDeclared" — draining it emptied that difference, so RuntimeOnlyDeclared is now a SUBSET of UnmirroredDeclared and the union of the two equals UnmirroredDeclared itself. ⛔ objectui#6152's ruling is untouched by any of this: what the key still does not have, and never will, is a z.function() shape — no serialized document could satisfy one.

.changeset/6175-column-state-persistence.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Nothing is retired. Both spellings remain declared on DataTableSchema; onColumnReorder stays declared and stays unwired, exactly as the RuntimeOnlyDeclared ledger in zod-mirror-parity.test.ts records it. Which of the two survives is a declared-surface ruling that stays open and is deliberately not settled here.

.changeset/6639-objectgrid-title-mirrored.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    The gain is the typed refusal: the mirror's .passthrough() base was already admitting any title unexamined, and it now enforces the declared string. zod-mirror-parity.test.ts's UnmirroredDeclared ledger records the key as worked off — the ledger's first shrink by repair (97 + 1 mirrored + 23 reclassified is what the seeded "121" now means).

.changeset/6687-chatbot-surface-authorable.md

  • names packages/types/src/complex.ts → packages/types/src/complex.ts — edited by this change

    Measured on both declaration faces before the fix, each with a control that had to hit: schema.surface appeared 0 times in renderer.tsx against schema.placeholder at 3 (one per registration) and schema.processVisibility at 1; and ChatbotSchema (packages/types/src/complex.ts) declared 34 keys, not this one. Two faces agreeing is what made the zero a reading rather than a bad query.

.changeset/6939-kanban-column-cards.md

  • names complex.ts → packages/types/src/complex.ts — edited by this change

    Breaking, deliberately. KanbanColumn declared its card list as items in complex.ts and in the zod mirror complex.zod.ts. Every board reads cards. Measured on origin/main 78a3cc238: KanbanImpl.tsx reads .cards on 12 lines, KanbanEnhanced.tsx on 8, and bucketCardsIntoColumns twice more as col.cards || []; .items had zero read sites in either board (a same-shaped .title control on the same two files returns 8 and 3, so those zeros are readings and not a mis-shaped probe). Both catalog entries, the plugin docs and content/docs/api/schema-reference.md all author cards.

  • names complex.zod.ts → packages/types/src/zod/complex.zod.ts — edited by this change

    Breaking, deliberately. KanbanColumn declared its card list as items in complex.ts and in the zod mirror complex.zod.ts. Every board reads cards. Measured on origin/main 78a3cc238: KanbanImpl.tsx reads .cards on 12 lines, KanbanEnhanced.tsx on 8, and bucketCardsIntoColumns twice more as col.cards || []; .items had zero read sites in either board (a same-shaped .title control on the same two files returns 8 and 3, so those zeros are readings and not a mis-shaped probe). Both catalog entries, the plugin docs and content/docs/api/schema-reference.md all author cards.

.changeset/7113-chart-data-model.md

  • names complex.zod.ts → packages/types/src/zod/complex.zod.ts — edited by this change

    .extend() with a NEW key still works and preserves the fold and the refinement; .optional(), z.discriminatedUnion, z.toJSONSchema and safeValidateSchema are all unaffected. Nothing in this repository calls the throwing combinators on either const, and the published surface already ships refined mirrors (objectql.zod.ts, complex.zod.ts, form.zod.ts, app.zod.ts), so the class is not new — but it is a real behaviour change on a published export and it belongs in the release note rather than in a reviewer's file.

.changeset/7295-chat-message-avatar-keys.md

  • names packages/types/src/complex.ts → packages/types/src/complex.ts — edited by this change

    packages/plugin-chatbot/src/index.tsx:173–178 reads message.avatar || userAvatarUrl and message.avatarFallback || userAvatarFallback (and the assistant twins), the authoring-to-runtime seam spreads every unlisted key through (chatMessageAdapter.ts, ...passthrough), and the SDUI renderer feeds the authored messages[] straight in — a per-message avatar override renders, is documented, and no authoring-facing type declared it. ChatMessage in packages/types/src/complex.ts has no index signature (objectui#5155, deliberately — none is added here), so an author annotating ChatbotSchema.messages was told a value that renders is an error (TS2353); the zod mirror ChatMessageSchema is a plain strip-mode z.object, so the value parsed green and was silently DROPPED from the parsed output.

.changeset/7344-handler-string-any-mirrors.md

.changeset/7654-floating-chatbot-trigger-icon-tombstone.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Every other tombstone in this package pairs ?: never with a retirementTombstone() refusal on the Zod twin. There is no twin here to carry one: FloatingChatbotConfig has no Zod mirror at all, and floatingConfig sits in the UnmirroredDeclared ledger (zod-mirror-parity.test.ts, complex.zod.ts#ChatbotSchema). BaseSchema is .passthrough(), so the whole floatingConfig object rides through unvalidated — before this change and after it. Minting a mirror to host a refusal would be the declared-but-UNMIRRORED axis (objectui#6152), a different defect: a key can be mirrored and inert, or unmirrored and live, and fixing one says nothing about the other. This change does not widen into it.

.changeset/7655-chatbot-registration-authoring-faces.md

  • names complex.ts → packages/types/src/complex.ts — edited by this change

    New published symbol: ChatbotSharedKey, the string-literal union of the twenty keys all three registrations read. It is exported from complex.ts because an exported interface may not extend a Pick over a private name (TS4022), so it is emitted into dist/complex.d.ts and is reachable through the published @object-ui/types/complex subpath (it is not re-exported from the package entry). It is a census, not an authoring face.

.changeset/7703-chatbot-dark-keys-retired.md

  • names packages/types/src/complex.ts → packages/types/src/complex.ts — edited by this change

    Each member goes to ?: never on packages/types/src/complex.ts and to retirementTombstone(...) on packages/types/src/zod/complex.zod.ts — both halves, in lockstep, the convention MarkdownSchema.sanitize (objectui#6972), TimelineSchema.timeScale (objectui#6355) and ObjectViewSchema.viewTabBar (objectui#7779) already carry. Each refusal names the key, says why it is retired, and points at what to write instead; one string feeds both the parse-time message and the .describe() metadata, so the two cannot drift.

  • names packages/types/src/zod/complex.zod.ts → packages/types/src/zod/complex.zod.ts — edited by this change

    Each member goes to ?: never on packages/types/src/complex.ts and to retirementTombstone(...) on packages/types/src/zod/complex.zod.ts — both halves, in lockstep, the convention MarkdownSchema.sanitize (objectui#6972), TimelineSchema.timeScale (objectui#6355) and ObjectViewSchema.viewTabBar (objectui#7779) already carry. Each refusal names the key, says why it is retired, and points at what to write instead; one string feeds both the parse-time message and the .describe() metadata, so the two cannot drift.

.changeset/7804-tree-view-handler-slot.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    The pair moves from zod-mirror-parity.test.ts's RuntimeOnlyDeclared to its KnownDrift, which empties the former of the one entry the latter did not also hold — so the two unmirrored ledgers are now in a containment relation, and the cross-ledger figure that recorded their difference states the containment instead.

.changeset/8114-detail-tab-activity-timeline.md

  • names plugin-dashboard/README.md → packages/plugin-dashboard/README.md — edited by this change

    README.md ships in this package's files, so the example went out in every tarball. DetailTabs renders a tab's content through ANGLE-BRACKETS(SchemaRenderer schema={toRenderableSchema(tab.content)} /), which makes content.type an SDUI node position judged by the component registry — so a reader copying the tab got the registry's Unknown component type panel (OBJUI-001) where the timeline should be. Same shape as the line-chart widget in plugin-dashboard/README.md (objectui#7896's census; fixed by objectui#7951) and the fourth known instance.

.changeset/8338-retire-toast-action.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    The parity ledgers drain with it, every figure re-derived by zod-mirror-parity.test.ts's own AST and mirror instruments rather than stepped by hand: KnownDrift 42 entries / 64 keys → 41 / 63 (the entry's whole content, so the entry went too — the ledger's first loss by RETIRING a key rather than by moving either face toward the other), and WiderThanDeclared 23 / 36 / 47 arms, split 6 / 30 / 0 / 11 → 22 / 35 / 45, split 6 / 29 / 0 / 10. The pair itself stays registered, so EXPECTED_MIRROR_PAIRS does not move.

.changeset/8415-filter-builder-condition-id.md

  • names complex.ts → packages/types/src/complex.ts — edited by this change

    Breaking for authored metadata: a filter-builder CONDITION must now declare id (objectui#8415). It is declared on both published faces — the TypeScript interface FilterBuilderCondition in complex.ts and the Zod mirror FilterBuilderConditionSchema in zod/complex.zod.ts — so a key the renderer has always required is finally validated instead of silently discarded.

  • names zod/complex.zod.ts → packages/types/src/zod/complex.zod.ts — edited by this change

    Breaking for authored metadata: a filter-builder CONDITION must now declare id (objectui#8415). It is declared on both published faces — the TypeScript interface FilterBuilderCondition in complex.ts and the Zod mirror FilterBuilderConditionSchema in zod/complex.zod.ts — so a key the renderer has always required is finally validated instead of silently discarded.

.changeset/8478-describe-line-addresses.md

.changeset/8478-zod-pins-complex.md

.changeset/8478-zod-pins-form-layout.md

  • names zod/complex.zod.ts → packages/types/src/zod/complex.zod.ts — edited by this change

    The remaining 6 addresses (zod/complex.zod.ts) stayed out of scope for this PR and returned to the queue rather than riding this PR's scope — the card did not close here.

.changeset/8572-chatbot-body-retired.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Why this key and not another. It was the ONE place in this vocabulary where body did not mean "what goes inside this component": zod-mirror-parity.test.ts carried the pair under KnownDrift as "two different meanings of one key", and the same collision was the whole reason chatbot was the single arm of the component union whose output was not assignable to SchemaNode. Both ledger rows move with this change, and the two pins that recorded the old state are INVERTED rather than deleted (see below).

.changeset/8604-record-details-columns-enum.md

  • names p1-spec-alignment.test.ts → packages/types/src/__tests__/p1-spec-alignment.test.ts — edited by this change

    ⚠️ The census behind this narrowing covers this repository only: one in-repo call site wrote the number (p1-spec-alignment.test.ts), and it is corrected in the same change. A TypeScript consumer of @object-ui/types outside this repo that wrote columns: 2 is not observable from here and gets a compile error (TS2322) naming the key — which is why the FROM/TO is spelled out above. Nothing to migrate at runtime: the renderer passes the authored value straight through, and the registry manifest (@object-ui/plugin-detail) already published this key as type: 'enum', enum: ['1','2','3','4'], so the published TypeScript face was the only layer that disagreed.

.changeset/8760-unfulfilled-chart-stubs.md

  • names content/docs/plugins/plugin-dashboard.mdx → content/docs/plugins/plugin-dashboard.mdx — edited by this change

    • At render. SchemaRenderer's lazy branch re-checks hasLazy(type) on every pass and returns the Loading ANGLE-BRACKETS(type)… placeholder. Registry.register() deletes a lazy entry only for keys the loaded module actually registers, so for an unfulfilled key the entry SURVIVES the load and every later pass takes the same branch. Measured on b775500af through the real chain: { "type": "line-chart" } painted role="status" / data-lazy-loading="line-chart" / Loading line-chart…, permanently. Not the OBJUI-001 panel the card expected — no alert, no error, no console warning. A skeleton that never resolves reads to a user as a slow network. - At authoring. A stub is enough to put a key into getKnownTypes(), so check:doc-types and the CLI's generated KNOWN_SCHEMA_TYPES snapshot both blessed all three. content/docs/plugins/plugin-dashboard.mdx taught "type": "line-chart" inside a card body, and every gate was green on it.

.changeset/8801-object-kanban-allow-collapse-retired.md

  • names packages/types/src/zod/complex.zod.ts → packages/types/src/zod/complex.zod.ts — edited by this change

    • the declarations retired here — packages/types/src/objectql.ts and its mirror packages/types/src/zod/objectql.zod.ts; - the pins that assert the retirement — object-kanban-allow-collapse-retired-8801.test.ts and bare-kanban-node-key-retired-8802.test.ts; - a comment in packages/types/src/zod/complex.zod.ts, recording that the deleted retiredZeroReadKanbanKey helper once carried this spelling on the SIBLING arm; - one row of content/docs/api/schema-reference.md; - the .changeset/ release notes that discuss it — this one, the two historical entries covering the sibling arm's own spelling, and objectui#9629's note recording the correction to this paragraph.

.changeset/8802-8257-8008-kanban-gantt-family-retirement.md

  • names complex.ts → packages/types/src/complex.ts — edited by this change

    What each retirement was, measured. Three of the four were registration-only: no schema face in @object-ui/types ever declared kanban-ui, kanban-enhanced or gantt as a component node type, so unregistering is the whole retirement. The bare kanban key was the exception — it had a declared arm on both faces (KanbanSchema in complex.ts and its Zod mirror), and a plain deletion there would have been the objectui#7664 failure: BaseSchema is .passthrough(), so a document naming a dropped key validates green and renders nothing. It therefore retires as a named refusal: the Zod union keeps an arm claiming the literal and answers a { "type": "kanban" } document with a message naming object-kanban as the remedy, while the TypeScript half is the absence of the arm from ComplexSchema and of the key from SchemaRegistry, so tsc refuses it at the authoring site.

.changeset/9040-retire-record-details-layout.md

  • names packages/types/src/__tests__/p1-spec-alignment.test.ts → packages/types/src/__tests__/p1-spec-alignment.test.ts — edited by this change

    • packages/types/src/__tests__/p1-spec-alignment.test.ts wrote layout: 'stacked' on this interface and read it back. Both lines are gone, with the reason stated at the site and a live-key read-back put in their place so the leg is not quietly shrunk. Its RecordHighlightsComponentProps neighbour keeps its own layout — a different key on a different face. - packages/types/src/__tests__/record-details-top-level-9040.test.ts ledgered the divergence as OPEN. Those legs become the retirement's own: a keyof absence assertion (with a declared key read through the identical form as its control), a @ts-expect-error literal, and a new source-text leg. - packages/types/src/__tests__/record-highlights-layout-9187.test.ts read this declaration as TEXT, using its three-value layout as the lit control for a two-member assertion about the sibling. That control MOVED to RecordChatterComponentProps.position — a three-value union the contract genuinely declares — as that file's own instruction required. It was never to be deleted outright, and it is not.

.changeset/9187-record-highlights-layout-two-values.md

  • names p1-spec-alignment.test.ts → packages/types/src/__tests__/p1-spec-alignment.test.ts — edited by this change

    ⚠️ The census behind this narrowing covers this repository only, and it found no in-repo authoring to migrate: every in-tree layout: 'grid' belongs to a different component (detail-view in content/docs/api/schema-reference.md and phase2-schemas.test.ts, ai-recommendations in packages/plugin-ai/README.md), and the one in-repo consumer of this interface that writes a layout (p1-spec-alignment.test.ts) writes 'horizontal'. So no document in this repository stops type-checking. A TypeScript consumer outside this repo that wrote grid is not observable from here and gets a compile error (TS2322) naming the key — which is why the FROM/TO is spelled out above.

.changeset/9511-record-id-is-a-string.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    ⭐ The declaration alone would not have been enough, and this is the reusable part. A TypeScript declaration does not run at parse time. The hand-written zod mirror is the only face in this repository that can refuse an authored number, so narrowing the declaration without the mirror would have shipped declared !== enforced on a published surface — and zod-mirror-parity.test.ts would have stayed GREEN through it, because that instrument asserts a mirror accepts everything its declaration declares and a mirror left WIDER passes. Both faces moved together for that reason.

.changeset/9628-kanban-column-collapsed-honoured.md

  • names complex.ts → packages/types/src/complex.ts — edited by this change

    The key was declared on both published faces of the object-kanban arm — the lane element of ObjectKanbanSchema (objectql.ts and its Zod mirror) and the runtime lane KanbanColumn (complex.ts and its mirror) — and read by KanbanEnhanced alone, a module no production source imports. An authored { "id": "todo", "title": "To Do", "collapsed": true } therefore parsed green on both faces and reached a board that did nothing with it: KanbanImpl's only collapse is the SWIMLANE row's, held in viewer state under objectui:kanban-collapsed:ANGLE-BRACKETS(swimlaneField) and never keyed to a lane's declared value. That is the ADR-0049 declared-but-unhonoured shape.

.changeset/calendar-readme-schema-keys-5045.md

  • names packages/types/src/complex.ts → packages/types/src/complex.ts — edited by this change

    README.md's "Schema API / CalendarView" block described a CalendarViewSchema that does not exist. Measured against the interface itself (packages/types/src/complex.ts) and its zod mirror: events — the schema's only required key besides type — was published as events?, so a reader following the README omits it and TypeScript rejects the node; defaultDate was string where the schema says string | Date; and onDateClick was listed as a schema key when it is a CalendarViewProps component prop, sending readers to a different package's surface for a key calendar-view does not have (the schema's key is onDateChange). The block also listed 6 of the schema's 13 keys with nothing saying it was a summary (objectui#5045).

.changeset/object-view-unmirrored-keys-7779.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Who is NOT affected: every correctly typed document, and every document that never wrote these keys — absent stays valid on all nine. No renderer changed. The parity ledger (zod-mirror-parity.test.ts) records the move: UnmirroredDeclared 14 entries / 96 keys to 14 / 87, the ObjectViewSchema entry re-derived into the SPEC-DERIVED half because the mirror now references the spec in code.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Angle-bracketed names in the quoted prose above are rewritten as ANGLE-BRACKETS(name): GitHub deletes tag-shaped fragments from a stored body, and a quote that silently loses the identifier it is about is worse than a visible repair.

Compared the checked-out tree with 41ae65b26 (merge-base with origin/main): 12 file(s) changed outside .changeset/, read against 1564 pending declaration(s) that publish a body (2151 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3062.4 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-BCn4fP5i.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 547.88KB 131.16KB
core (index.js) 9.52KB 3.79KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 259.29KB 65.64KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.65KB 14.69KB
plugin-charts (index.js) 81.82KB 22.51KB
plugin-chatbot (index.js) 198.40KB 47.22KB
plugin-dashboard (index.js) 134.11KB 35.62KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 233.23KB 61.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 155.50KB 39.71KB
plugin-gantt (index.js) 169.75KB 41.96KB
plugin-grid (index.js) 218.12KB 59.72KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 114.70KB 28.42KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.91KB 9.05KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 87.83KB 22.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Red check that is not this PR's: Inert vi.mock Specifier Check. Read at 2026-09-27T03:31Z on head 70d26a8e9.

The same check is red on main (5c61e5241 and later heads). Its one offender is apps/console/src/__tests__/filterContextTokensSweep-10666.test.tsx:51, which PR objectui#10729 added and this diff does not touch. No fix exists yet; the anchor card is objectui#10731. The check is not in the main ruleset's required set (Lint, Type Check, Build & E2E, Build Docs, Changeset Declaration, Test). It reproduces deterministically on the base, so it is not re-run here.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 70d26a8e9457c5734975e861e63340788b73886f

① Derived judgments

Measured on a detached worktree at the head (removed afterwards), objectui origin/main = 41ae65b268 (the CI merge base), @objectstack/spec 17.4.0 from the npm tarball (the pnpm-lock.yaml pin), and objectstack origin/main = 49144fc (moved past the dev's 8d1f7ab; packages/spec/src/ui/dashboard.zod.ts is byte-identical between those two).

Accept-set / public-surface changes, each named and judged

  1. DashboardComponentSchema['header'] (TS interface) narrows and widens. The hand-written member is gone; the interface inherits Dashboard['header'] through the Omit projection over DASHBOARD_SPEC_EXCLUDED (name, label, description, widgets, globalFilters; unchanged by this PR). Literals now refused: an action with no actionUrl; an actionType outside script | url | modal | flow | api | form. Literal now admitted: label as an inline locale map. Each refusal matches the spec: pin 17.4.0 and objectstack main both declare label: I18nLabelSchema, actionUrl: z.string() (required), actionType: WidgetActionTypeSchema.optional() where WidgetActionTypeSchema is ActionType = z.enum(['script','url','modal','flow','api','form']); the only pin-vs-main difference in the header and global-filter regions is .meta({ title }) on members, which does not move the input type. The mirror's header was already the spec's by reference on base (SpecDashboardFields never excluded it), so the validator is unchanged here, as the changeset says. In-repo TS producers of the refused shapes: only the p1-spec-alignment fixture ({ label: 'Refresh', actionType: 'refresh' }), which the PR rewrites; apps/**, examples/**, content/**, skills/**, app-shell, plugin-designer author no header action at all (grep on actionUrl).

  2. GlobalFilterSchema (Zod, published export) becomes stripImportedDefaults(SpecGlobalFilterSchema). Base was a z.object spread of the spec shape with options: z.array(z.union([z.string(), z.object({ value, label: z.string().optional() })])), optionsFrom.labelField optional, optionsFrom.filter: z.any(), plus a delegated superRefine. Against the spec (options items strictObject({ value, label: I18nLabelSchema }), GlobalFilterOptionsFromSchema with object/valueField/labelField all z.string() and filter: FilterConditionSchema.optional(), the whole thing a strictObject with .superRefine(checkGlobalFilterDateDefaultValue)), every claimed change verifies: bare-string shorthand refused, label-less option refused, labelField-less optionsFrom refused, array filter refused (FilterConditionSchema is a lazy z.record(...).and(z.object({$and,$or,$not})), which rejects an array), unknown filter key refused by name (strictUnknownKeyError only rewrites the message and returns early unless issue.code === 'unrecognized_keys', so the code the pin asserts is the spec's own), inline-locale option label admitted. stripImportedDefaults rebuilds the object through cloneWithDef, which spreads the full zod def (checks and catchall included), so strictness and the date refinement survive; the pin's "no scope default" leg and report-chart-query-spec-parity's last_7_dayz refusal both ran green in CI shards.

    • In-repo producers of a now-refused shape: none. The three examples/schema-catalog filtered-dashboard*.json entries carry pair options and labelField (fixed by PR fix(core): bare-string filter options — docs/examples stop teaching it, runtime lift warns (#4356) #4601), the catalog guardrail imports the spec's GlobalFilterSchema directly (unaffected), app-shell dashboard-schema.ts / DashboardWidgetInspector.tsx pass globalFilters through without authoring options, designer.ts globalFilters?: any[] is the separate designer config type, skills/** has no dashboard filter, docs and README were the only teachers of a labelField-less optionsFrom and are corrected here.
    • objectui#4356 cross-check: consistent, no collision, no pre-emption. That card's Phase 0+1 (docs + warn-now lift) landed in PR fix(core): bare-string filter options — docs/examples stop teaching it, runtime lift warns (#4356) #4601; its remaining scope is Phase 2, retiring normalizeFilterOptions' lift in packages/core/src/utils/dashboard-filters.ts, released for dispatch on 2026-09-02 (comment 5511028727) after objectstack#7917 closed on the maintainer ruling 「不考虑现有数据」. This PR does not touch that file, keeps the lift, and narrows only the schema, which is the objectui#4165 split the card itself describes. Non-blocking nit: the new complex.zod.ts docblock and the report-chart-query-spec-parity docblock say the lift "retires behind a survey" on the "objectstack#7917 option-② window", and the changeset says it "keeps its own retirement schedule (objectstack#7917)". objectstack#7917 is closed; the survey condition was dropped by that ruling; the live schedule is objectui#4356 Phase 2 ("first minor after 17.6.0"). Stale pointer, not a behaviour defect.
  3. DashboardRenderer.tsx. tActionLabel collapses action.label through pickLocalized(label, language) before both the button text and the dashboardActionLabel fallback; the bundle key stays actionUrl || resolved label, identical to base for string labels. headerActionDefs moved below tActionLabel, still declared before its only consumers (engine in the effect at the getRunner call, executeAction in the click handler), so no TDZ; keyed on language, per AGENTS.md [WIP] Enhance every detail of the designer #10. executeAction(actionUrl || label) became executeAction(actionUrl): the guard above returns on empty actionUrl, and the def's name is actionUrl || label, so the dispatched name is unchanged in every reachable case. Nothing else the renderer draws changes (title/description/filter bar untouched). The new test is red on base by construction: base returned the map object from tActionLabel straight into a Button child, which is the Objects are not valid as a React child the dev reported from ablation C; pickLocalized('zh') upgrades to the zh-CN key, so getByRole('button', { name: '转换线索' }) is the right oracle. Test shards 1–8 are green at the head.

  4. Ledger. Counted at head vs base by parsing the interfaces: KnownDrift 46 entries / 84 keys (base 46 / 86), WiderThanDeclared 6 entries / 7 keys (base 7 / 9), WIDER_ARMS 7 arms split 5 SCHEMA-NODE / 2 CONCRETE (base 9, 5 / 4). The pair's WiderThanDeclared entry and both WIDER_ARMS rows are gone; KnownDrift keeps 'widgets' only. The rewritten header figures match these counts exactly. The reconciliation against measurement is type-level (assertionDriftMatchesLedger, assertionWiderMatchesLedger, assertionWiderLedgerRecordsEveryKey, compiled by tsconfig.test.json in the package's type-check), and Type Check is green at the head, so both ledger states are true by measurement, not by prose. The rewritten docblocks (the KnownDrift entry, the departure note replacing the WiderThanDeclared entry, the MirroredUndeclared note) match what base actually had: header wider through the spec's I18nLabel action label against a string restatement; globalFilters wider through the local shorthand / label-less / labelField-less / z.any() filter arms, with its locale arm narrower.

  5. Three rewritten pins: re-pointed, not weakened. twins-spec-by-reference-9736: the slots half is kept verbatim; the header case flips from Equal(..., Dashboard['header']) = false to = true, a stronger guard, and the dropped HeaderAction['label'] equals string row pinned exactly the restatement that is removed. p1-spec-alignment: the illegal fixture is replaced by a spec-legal one and an @ts-expect-error leg for refresh is added. report-chart-query-spec-parity: every literal the old "pinned divergences" block asserted is still asserted, now with both schemas returning the same verdict, plus array filter and locale-map legs; the composition case is replaced by a positive-and-negative refinement pair over spec-legal options. No guarded surface shrank; the derives from the spec block is untouched.

  6. Docs. plugin-dashboard.mdx: actionUrl required, the six-member actionType enum and the map-resolving label are all true of the spec; LocaleMap is the file's pre-existing pseudo-type name in a plaintext fence. dashboard-filters.md: objectui validate is the CLI bin (packages/cli/package.json bin objectui), and validate.ts runs safeValidateSchema = AnyComponentSchema.safeParse, which reaches the dashboard mirror and therefore the spec's GlobalFilterSchema; "object, valueField and labelField are all required" and "filter is a filter-condition object, not an array" are true of the spec; the example now carries labelField. README.md: true. The retained "still lifted by the runtime" callouts are true today.

② Semver level

@object-ui/types: minor, @object-ui/plugin-dashboard: patch. Consistent with AGENTS.md 版本号策略 (objectui's own breaking changes are scored minor, major is mechanically refused); the validator narrowing and the header interface narrowing are the breaking semantics and the changeset states them in full with codes and paths that the new pin asserts. plugin-dashboard patch is right for a renderer crash fix with no API change. Every changeset sentence checks out against the spec, base and head, with one stale pointer: "keeps its own retirement schedule (objectstack#7917)" names a closed card; the schedule lives on objectui#4356 Phase 2 (non-blocking).

③ Boundary flags

  • (a) Surface beyond the claim. The diff's 13 files are exactly the claim's five (changeset, complex.ts, complex.zod.ts, the ledger, one pin) plus the eight the addendum 5852309928 names (renderer + its test, three pins, three docs). Nothing outside either list. The PR body declares the extension and the reason (principle 1: a spec-legal document crashed the read site).
  • (b) Spec Main Shape Gate. Not run locally; CI at this head: Spec Main Shape Gate = success (completed 2026-09-27T03:36:36Z). Independently, dashboard.zod.ts on objectstack main (49144fc) differs from the 17.4.0 tarball only by .meta({ title }) and doc text in the two touched schemas, and is unchanged since the dev's 8d1f7ab. All other checks at the head are green (Type Check, Lint, Build & E2E, Build Docs, Test shards 1–8, dist pins, changeset gates, doc gates); the single red is below.
  • (c) Read-path tolerances untouched. normalizeFilterOptions (core) and DashboardFilterBar's labelField fallback are left in place. Correct for this card: the lift's removal is objectui#4356 Phase 2, dispatchable and not claimed here; the fallback is unreachable for a validated document. The filter bar passes optionsFrom.filter through to $filter / runtimeFilter, the platform's filter-condition slots, so the spec's object form is what the read site already serves. Nit as in ①.2: the "retires behind a survey" wording in the two new docblocks is stale.
  • (d) Premise correction. Verified on base source: the local options element was label: z.string().optional() inside a union with z.string(), so the mirror refused the locale map (narrower, the KnownDrift side) while admitting the shorthand, a label-less option, a labelField-less optionsFrom and any filter (wider). The old ledger docblock's "inline-locale widening one level down" held for header only. The dev's H1 reading is right and the rewritten departure note records it correctly.
  • CI context, Inert vi.mock Specifier Check. Same signature at the head (merge commit 7ed2db2 of 70d26a8 into 41ae65b) and on main (41ae65b): one offender, apps/console/src/__tests__/filterContextTokensSweep-10666.test.tsx:51, vi.mock("../../../../packages/plugin-map/node_modules/react-map-gl/dist/maplibre.js"), from PR fix: resolve context tokens in every directly authored data node's own filter (objectui#10666) #10729; anchor card objectui#10731 is open. Not this PR's.
  • Raised by me, non-blocking: headerActionDefs gives label: undefined for an action whose resolved label is the empty string (was ''); ActionDef.label is optional, so no consumer changes.

Implemented-by: claude/issue-7759-group-a-dashboard-locale
Reviewed-by: session_014fWVhLzhxR8qrFsJ5o8TYW

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 05:07
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 1422a92 Sep 27, 2026
44 of 45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-7759-group-a-dashboard-locale branch September 27, 2026 05:21
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…e html tier's intrinsic elements, marked tier html — div stays out (objectui#10735) (objectstack-ai#10753)

Fixes objectstack-ai#10735
Clause-②: yes — the published manifest's declared vocabulary grows.

Executes the maintainer's ruling A on objectstack#20112 (batch objectstack-ai#225 item
1, 「其他同意」): the published `sdui.manifest.json` declares the html tier's
registered intrinsic set exactly as the registry declares it, with each
tag's `inputs` and child slot; `div` stays undeclared. Base
`origin/main` 5ad3b88; head 6417e8d (round 2: wording only, one commit
on top of 5bd6270).

## What changed

- `packages/core/src/registry/html-tier-intrinsics.ts` (new):
`HTML_TIER_INTRINSICS` — the 37 tags of `html-elements.tsx`'s `TAGS`,
plus `span`, `table`, `label` and the seven sectioning tags of
`semantic.tsx` (47). Exported from `@object-ui/core` beside
`PUBLIC_BLOCKS`.
- `packages/core/src/registry/Registry.ts`: `getPublicConfigs()` appends
the roster's registered tags LAST, each stamped `tier: 'html'` in the
projection (never written onto the registration);
`PublicComponentConfig.tier` admits `'html'`. `PUBLIC_BLOCKS` is
untouched.
- `packages/sdui-parser`: `ManifestComponent.tier?: 'html'`;
`manifestFromConfigs` writes exactly `'html'` or omits the key (every
curated entry serialises byte-identically); `generateBlockList` sections
the html tier under its own count; `RegistryConfigLike.tier` admits
`'html'`. `gen-manifest.ts` header updated, behaviour unchanged.
- `packages/components/src/renderers/layout/react-page.tsx`: the
`kind:'react'` scope builder skips `tier === 'html'` entries, so no `P`
/ `A` / `Img` wrapper is injected (file-surface amendment, forced by the
card's ⛔ on silent widening — see H2).
- Tests: new pins in core, sdui-parser, console;
`react-page-scope.test.tsx` and `container-declaration-census.test.tsx`
updated; `public-contract.test.ts` prose amended (two ledger passages
that said curating a sectioning tag would newly widen the manifest).
- Docs: `content/docs/guide/react-pages.md` (the html tier is not
injected on the react tier), `content/docs/guide/plugin-development.md`
(the component-level `tier: 'html'` marker).
- `.changeset/10735-html-tier-manifest.md`: `minor` for
`@object-ui/core`, `@object-ui/sdui-parser`, `@object-ui/components`.
- Round 2 (contract review, wording): the react-page skip line is quoted
as it now reads — `if (!tag || cfg.isContainer || cfg.tier === 'html')
continue;` — in `content/docs/guide/react-pages.md` ("skips every
container and every html-tier entry"; frontmatter lines 1–4 md5
unchanged, 9c5f94a7…), in both `public-contract.test.ts` passages, in
the `container-declaration-census` pin, and in
`layout-containers-declare-containment.test.tsx` (comment-only,
previously untouched). `git grep -F` of the old line at head: 2 hits,
both inside `.changeset/6740-flex-is-container.md` — its original
paragraph and the dated note that marks it. The 10735 changeset now says
"200 ledgered findings" (10 ledger rows whose counts sum to 200).
- `.changeset/6740-flex-is-container.md` (pre-existing,
`@object-ui/components: patch`): reconciled in the objectui#10533
dated-note form, scoped "at this change" — `⚠️ **Dated note, 2026-09-27
— the skip line quoted above has since gained a third arm —
objectui#10735.**` — frontmatter byte-identical (md5 ce888ebd… before
and after). `check-changeset-overwrite` REPORTS the modification
(report-only, exit 0) and did not refuse it.
- ⛔ Not touched: `html-elements.tsx`, `div.tsx`, `PUBLIC_BLOCKS`, the
registration-uniqueness test.

## H1 — the census (measured from `ComponentRegistry.getKnownTypes()`
after `@object-ui/components` + `apps/console/src/register-plugins.ts`
load, in the console vitest project)

Instrument for "is an HTML tag": the keys of `JSX.IntrinsicElements` in
`@types/react` (178 names, HTML and SVG; lit control: `div`, `p`, `svg`
in, `object-grid` out). Registry: 485 known keys, 208 bare.
Intersection: **64** bare registrations carry an element name.

- **(a) `html-elements.tsx` `TAGS` — 37, all registered:** h1 h2 h3 h4
h5 h6 p a blockquote pre strong em b i u small mark sub sup del ins abbr
ul ol li dl dt dd figure figcaption img hr br time address cite q.
- **(b) registered elsewhere, declared — 10:** `span` (basic/span.tsx;
inputs className, children), `table` (complex/table.tsx; caption,
footer, columns, data, className — no child slot), `label`
(form/label.tsx; text required, className — no child slot), and `aside
main header nav footer section article` (layout/semantic.tsx; className,
children; `isContainer`).
- **(c) excluded — 17:**
- `div` — deprecated on the JSON surface in favour of `box`
(objectui#3965, PR objectui#6878); kept out by the ruling.
- `code`, `summary`, `object` — bare namespace fallbacks of `field:code`
/ `field:summary` / `field:object` (inputs `[]`, no child slot); `view`
— `plugin-view`'s bare key (SVG name coincidence). ⚠️ The ruling's list
names `code` as "registered elsewhere"; measured, the only `code`
registration is the field widget (a code editor / `pre` block reading
`value`). Declaring it would teach a tag whose children the runtime
drops, so it is NOT declared — see Acceptance notes and the report's
open question.
- `kbd` — a `ui` component reading `keys` / `label`, not named by the
ruling; not declared (additive later on a named need; retracting is a
narrowing).
- `form input textarea select switch` — JSON-surface form components;
the tier excludes form controls by design (the exclusion list in
`html-elements.tsx`). `progress`, `dialog` — `ui` components under tag
names, not passthroughs, not named.
- `button text image html` — already curated in `PUBLIC_BLOCKS`; they
reach the manifest from there, unstamped.

The (c) list is not prose:
`apps/console/src/__tests__/html-tier-manifest.test.ts` holds every
HTML-named bare registration to one of `PUBLIC_BLOCKS`,
`HTML_TIER_INTRINSICS` or its exclusion ledger `EXCLUDED_HTML_NAMED` (13
entries, each with a reason, each pinned live), so a registration that
newly squats on a tag name lands there by absence.

## H2 — the fork, the consumer map, and the marker decision

**Premise measured true:** `getPublicConfigs()` through
`manifestFromConfigs` is the sole source of the published manifest —
objectui's `apps/console/dev/manifest-dump.tsx` /
`scripts/dump-public-manifest.mjs`, and objectstack's
`scripts/gen-sdui-manifest-node.mjs` (which imports the 16 registry
modules from objectui's built tree at `.objectui-sha` and calls exactly
`ComponentRegistry.getPublicConfigs()` then its lockstep copy's
`manifestFromConfigs`). The manifest is ONE flat `components` map with
no tier marker; `compile()` derives `allowedTags =
Object.keys(manifest.components)`. So listing the html tags in
`PUBLIC_BLOCKS` would have widened the curated JSON-surface vocabulary
silently — the fork the card names.

**Marker chosen: a per-entry `tier: 'html'` on `ManifestComponent`,
carried from a projection-only stamp in `getPublicConfigs()`.** Why not
a second registry read or a second manifest section: the objectstack
generator calls `getPublicConfigs()` and nothing else, and `compile()`
reads only `components` — either alternative needs an objectstack-side
change before the gate accepts a single html tag, and the second-section
shape would leave the gate refusing the tags until its lockstep copy was
ported. With the per-entry marker the objectstack gate accepts the tags
with NO reader change: it ignores the extra key.

Consumers, and what each does with the marker:

| reader | reads | change |
|---|---|---|
| objectstack `packages/lint` `validateJsxPages` → `compile()` | keys as
whitelist; `validateTree` reads `inputs` / `namespace` | none — accepts
the tags; ignores `tier` |
| objectstack `scripts/check-sdui-manifest.mjs` | `components[k].type
=== k`, count, sha | none |
| objectstack `scripts/gen-sdui-manifest-node.mjs` |
`getPublicConfigs()` + lockstep `manifestFromConfigs` | none to run; ⚠️
its lockstep copy of `packages/sdui-parser/src/{index,types,codegen}.ts`
needs this port for the marker to reach the tracked file — until then
the tags are declared there WITHOUT `tier`, which the gate treats
identically |
| objectui `react-page.tsx` `buildComponentScope` | `getPublicConfigs()`
| skips `tier === 'html'` (else `P` / `A` / `Img` wrappers would be
injected) |
| objectui `generateBlockList` | manifest | sections the html tier;
curated count unchanged (59) |
| objectui `generateDts` | manifest | emits the tags into
`JSX.IntrinsicElements` (the html-tier authoring surface) |
| objectui `page.tsx` html-tier compile | `getKnownTypes()`, not the
manifest | none (already accepted these tags) |
| console census tests | `getPublicConfigs()` |
`container-declaration-census` reads the curated contract as the
unstamped entries; `public-contract` coverage lists filter by
`PUBLIC_BLOCKS` and are unchanged |

Reverse verification that consumers read the REBUILT `.d.ts`: a scratch
console file assigning `tier: 'json'` to a `ManifestComponent` and to a
`PublicComponentConfig` — `tsc --noEmit` exit 2, `TS2322: Type '"json"'
is not assignable to type '"html"'` and `… to type '"html" | "public" |
"internal" | undefined'`; the `'html'` assignments compiled. Probe
removed; `git status` clean.

## H3 — the regenerated artefacts

Generated with the repo's own generator (`buildArtifacts` in
`packages/sdui-parser/scripts/gen-manifest.ts`) over the console
registration graph with the 16 plugin modules eager-imported in
`manifest-dump.tsx` order, once on the base tree and once on head:

| artefact | base | head |
|---|---|---|
| `sdui.manifest.json` components | 59 | **106** (+47 added, 0 removed,
0 curated entries changed) |
| `div` / `code` / `kbd` present | no | no |
| entries with `tier: 'html'` / without | 0 / 59 | 47 / 59 |
| `sdui-intrinsics.d.ts` intrinsic entries | 59 | 106 (+253 lines, −0) |
| `sdui-blocks.md` | 65 lines, one table | 119 lines: `# SDUI public
blocks (59)` unchanged + `## html tier intrinsic elements (47)` |

Sample head entries: `a` → inputs className, href, target, rel, title,
children (slot); `br` → className only (void, no slot); `label` → text
(required), className (no slot). The base run's manifest differs from
objectstack's tracked `sdui.manifest.json` only because the pin f8a9d0f
predates this base (`cmp` differs at byte 1183) — the regeneration at
the next pin bump is the `domain:cli` lane's step.

## Pins — red on base, green on head

Two mutation legs (implementation files checked out at base, tests at
head; trap-guarded restores to `HEAD`, proven by `git diff HEAD` empty
and blob-hash equality — "RESTORE PROVEN" printed twice):

- Leg 1 — `Registry.ts`, sdui-parser `index.ts` / `types.ts` /
`codegen.ts`, `react-page.tsx` at base: `Test Files 5 failed (5)`,
`Tests 17 failed | 61 passed (78)` — every behavioural pin red (the
roster-only pins stay green by design).
- Leg 2 — `react-page.tsx` alone at base: `Tests 1 failed | 8 passed
(9)` — `a PascalCased html-tier tag is an unknown identifier on this
tier` red (an `Img` wrapper was injected).
- Green on head 5bd6270 (round 1) after the restores: `Test Files 9
passed (9)`, `Tests 114 passed (114)` over the five pin files plus
`registration-uniqueness.test.tsx` (unchanged, green),
`public-tier.test.ts`, `tier.test.ts`, `public-contract.test.ts`.

Round 2 (head 6417e8d): the three re-quoted test files plus
`react-page-scope` and `html-tier-manifest` re-run — `public-contract`,
`container-declaration-census`, `layout-containers-declare-containment`,
`react-page-scope`, `html-tier-manifest` — `Test Files 5 passed (5)`,
`Tests 89 passed (89)`.

Broader affected suites at head 5bd6270: console `html-tier-manifest`,
`public-contract`, `public-block-binding-reach`,
`record-block-record-reach`; components `react-page-scope`,
`container-declaration-census`, `container-declaration-ratchet`,
`layout-containers-declare-containment`, `registration-uniqueness`;
plugin-tree `registration.publicTier-10064` — `Test Files 10 passed`,
`Tests 137 passed` (plus the scratch generator run).

## Gates (exit captured before any pipe; verdict lines quoted)

- Build: `turbo run build --filter="@object-ui/console^..."
--concurrency=2` under the verify lock — `Tasks: 34 successful, 34
total`, `VERDICT command-exit 0`.
- Type-check (`pnpm run type-check`, script echoed): `@object-ui/core`
exit 0, `@object-ui/sdui-parser` exit 0, `@object-ui/components` exit 0,
`@object-ui/console` exit 0. `--listFiles` counts the new / edited test
files inside their programs (1, 1, 2, 1).
- Lint, CI-parity: `turbo run lint` over the four packages — `Tasks: 5
successful, 5 total`, 0 errors; eslint `--format json` over the 14
touched files: 0 errors, 22 warnings, all pre-existing `no-explicit-any`
lines not introduced here.
- `check:control-bytes` — `✅ check-control-bytes: OK (scanned 8928
tracked text file(s))`.
- `check:new-line-citations` — `VERDICT new-cross-file-line-citations: 0
new citation(s)`.
- `check-changeset-presence` — round 2: `✅ 15 source file(s) of 4
released package(s) changed, and this change declares 1 changeset(s)`;
`changeset:check` — `✅ No changeset declares a major bump` and `✅ All
workspace packages are in the changeset fixed group`.
- Round 2 text gates on head 6417e8d: `check:control-bytes` `✅ OK
(scanned 8928 tracked text file(s))`; `check:new-line-citations`
`VERDICT new-cross-file-line-citations: 0 new citation(s)`;
`check-changeset-claims` exit 0 (2 bodies judged, no negation; the
re-read list is unchanged); `check-changeset-overwrite` exit 0
report-only — `1 changeset(s) added, 1 modified, 0 deleted`, the
modified one being `.changeset/6740-flex-is-container.md` (the dated
note above); `check-doc-links` — `Links are valid across 17 scan roots.`
- `check:unreferenced-sources` OK; `check:pending-changeset-literals` `✅
No test source names a pending changeset`; `check:test-path-roots` OK;
`check:doc-types` `✅ Every documented component type is registered`;
`check:doc-fences` OK; `check:registry-bare-names` `✓ no new bare-name
collision`; `check:esm-specifiers` (specifier leg) OK;
`check:component-surface-parity` report-only exit 0.
- `check:changeset-claims` (report-only): 13 pending changesets name a
file this change touches; each re-read — the "forwards exactly seven
keys per input" claims stay true (this adds a component-level key, no
input key), and "none of the 8 is in the curated public contract" stays
true (the seven now enter the manifest as html-tier entries, not
curated).
- NOT MEASURED locally, declared to CI: `check:readme-exports` (exit 1 =
8 self-imports unjudgeable because `packages/cli` and
`packages/plugin-ai` are outside the built closure; 538 judged, 0
fabricated), `check:doc-snippets` (exit 2 PRECONDITION NOT MET, same
unbuilt packages; the two edited guides gained prose only),
`check:eager-closure` and `check:sdui-registration-pins` (need a console
build), repo-wide `pnpm lint` (per-package eslint via turbo covered the
four touched packages exactly as CI runs them; `eslint.config.js`
declares no type-aware `project`, so this diff moves no untouched file's
verdict).

## Serial

Claim read: no open PR touches `gen-manifest.ts`, `public-blocks.ts` or
`html-elements.tsx`. Round 1: `origin/main` fetched into the private ref
`refs/issue-10735/main` (01700dc, four commits past base), `git
merge-tree --write-tree HEAD refs/issue-10735/main` exit 0 (tree
368f01d). Round 2, before the push of 6417e8d: main re-fetched at
704e05b (nine commits past base: objectstack-ai#10741, objectstack-ai#10574, objectstack-ai#10736, objectstack-ai#10745,
objectstack-ai#10749, objectstack-ai#10734, objectstack-ai#10733, objectstack-ai#10744, objectstack-ai#10752), `merge-tree --write-tree` exit
0 (tree 0d9bcb9); the set of files main moved since base is disjoint
from this branch's files (`comm -12` empty), so no merge commit was
needed.

## Acceptance notes

- **`div` on the html tier is now split across two faces.** The
published contract refuses it (`forbidden-tag`), while the renderer's
declaration still reads `deprecated: { surfaces: ['json'] }`
(objectui#4000) and the console's html-tier compile (`page.tsx`,
`getKnownTypes()`) still accepts and renders it. The ruling's item 3
parks the renderer-side retirement in these notes: the reconciliation is
a separate card (declare the `html` surface on `div`'s deprecation with
`box` as the replacement, and decide whether the console compile should
read the same roster).
- **"Refused with `box` named as the replacement" has no mechanism
today.** The gate's refusal is the parser's generic `forbidden-tag` text
(the tag is not an allowed component); nothing in the manifest or the
parser names `box` for an absent `div`. `Registry.deprecationFor('div',
'json').replacement` names it, but the gate never reaches the registry.
Not implemented here (a new contract shape); raised in the report's open
questions.
- **`code`:** the `html-elements.tsx` header and ADR-0081 §2 both say
`code` is "registered elsewhere"; the only registration is
`field:code`'s bare fallback. On an html page today `code` resolves to
that widget and drops its text — a reproducible runtime defect
independent of this card; reported as an out-of-scope finding.
- **`label` and `table` are declared as registered**, so the HTML-shaped
`label` with text children draws `missing-required-prop` (`text`) and
`not-a-container`, and a `table` authored with element children draws
`not-a-container`; the registered spellings (`text="…"`, `columns` /
`data`) are clean. Pinned.
- **objectstack side, for the `domain:cli` lane:** regenerate
`sdui.manifest.json` at the pin; port
`packages/sdui-parser/src/{index,types,codegen}.ts` to the lockstep copy
so the marker reaches the tracked file; the ledger's 200 `a` / `div`
rows go stale (the `a` rows) or stay refused (the `div` rows, to be
rewritten to `box` as the ruling says).
- `semantic.tsx`'s comment ("none of these seven tags is in the curated
`PUBLIC_BLOCKS` contract, so there is no injected identifier") remains
literally true and its conclusion holds on two grounds now; not edited
(no registration change).

## Pending changesets re-read (round 2, head 6417e8d)

The `changeset-claim-re-read` bot lists 12 pending changesets naming
files this diff touches (13 in round 1, `6740` now being one this change
modifies). Each re-read paragraph by paragraph against head:

- `5905-componentinput-inputtype-tombstone.md` — still true:
`packages/sdui-parser/src/index.ts` still forwards exactly seven keys
per input (`name`, `type`, `of`, `required`, `enum`, `binding`,
`description`); this change's `tier` write sits at component level,
beside `type` / `namespace` / `isContainer`, and adds no per-input key.
- `5905-componentinput-retire-constraint-keys.md` — still true, same
seven-keys-per-input claim, same reason.
- `7493-componentinput-retire-label-defaultvalue-advanced.md` — still
true, same seven-keys-per-input claim; `min` / `max` / `step` /
`placeholder` / `label` / `defaultValue` / `advanced` still reach no
manifest.
- `6067-component-meta-derive-from-canonical.md` — still true: it
describes the pre-6067 shape of `Registry.ts` in the past tense; at head
`ComponentMeta` is still `CanonicalComponentMeta &
RegistryComponentMetaExtras`, and `RegistryComponentMetaExtras.tier` is
unchanged (`'public' | 'internal'`). The `'html'` value lives only on
the `PublicComponentConfig` projection.
- `componentinput-reexport-4972.md` — still true: `Registry.ts` still
re-exports `ComponentInput` from `@object-ui/types` (`export type {
ComponentInput } from '@object-ui/types'`).
- `default-children-retired-5051.md` — still true: none of the three
`ComponentMeta` declarations offers `defaultChildren`; this change adds
no `ComponentMeta` key.
- `6764-container-declaration-census.md` — still true as written:
`react-page.tsx` drops containers from the react scope and reads
`getPublicConfigs()`, and none of the 8 is in the CURATED public
contract. The seven sectioning tags now ride that read as html-tier
entries, stamped `tier: 'html'` and skipped, which leaves the claim's
operative words ("curated public contract") intact; not corrected.
- `9280-record-highlights-entry-icon-retired.md` — still true:
`gen-manifest.ts` still serialises registry `inputs` into
`sdui.manifest.json` and `sdui-intrinsics.d.ts` (the header now also
names the html tier's stamped entries; behaviour unchanged).
- `components-react-page-published-dts-text-5666.md` — still true:
`react-pages.md`'s injected-scope note and §Styling still say plain HTML
with inline `style`, not Tailwind; the guide's edited sentence quotes
the skip line and adds "and every html-tier entry", nothing about
styling.
- `page-source-tailwind-framing-5461.md` — still true: `react-page.tsx`
keeps both styling comments (the header note and the
`buildComponentScope` note); this change adds a comment inside the loop
and removes none.
- `page-source-tailwind-prose-retraction-5469.md` — still true:
`react-pages.md` §Styling wording is unchanged.
- `palette-canonical-discussion-5495.md` — still true:
`public-blocks.ts` still records `record:chatter` as the same block
"under a Salesforce-familiar name"; this change adds a header paragraph
and touches no roster entry.
- `6740-flex-is-container.md` — CORRECTED (prose-only): its paragraph
quotes the react-page skip line as `if (!tag || cfg.isContainer)
continue;`, which this PR changed; a dated note in the objectui#10533
form (`⚠️ **Dated note, 2026-09-27 — the skip line quoted above has
since gained a third arm — objectui#10735.**`) records the current line
and keeps the entry as the reading of that change. Frontmatter
byte-identical, md5 ce888ebde4729564f67bca69a7928743 before and after.
`check-changeset-overwrite` reports the modification (report-only, exit
0).

---
_Generated by [Claude
Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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 package: types plugin tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants