Skip to content

refactor(types,plugin-kanban): retire ColumnWidthConfig / ColumnWidthConfigSchema (objectui#10582) - #10730

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-10582-retire-column-width-config
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-10582-retire-column-width-config

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10582
Clause-②: yes

What this does

Deletes ColumnWidthConfig (packages/types/src/complex.ts) and its Zod mirror ColumnWidthConfigSchema (packages/types/src/zod/complex.zod.ts), together with both @object-ui/types barrel exports (src/index.ts, src/zod/index.zod.ts) and the @object-ui/plugin-kanban re-exports (src/types.ts, src/index.tsx). This is ADR-0049 enforce-or-remove on a declaration that no key references and nothing reads. The governing direction is triage's written retire direction on the card (first grade, comment 5831964491), not a maintainer ruling.

  • The kanban retirement docblock in complex.ts no longer says the type survives. It now says ColumnWidthConfig lost its one reader to objectui#8522 and is deleted here, and it points at the pin.
  • zod-mirror-parity.test.ts: the complex.zod.ts#ColumnWidthConfigSchema pair leaves MIRRORS, Declared and the two import lists. EXPECTED_MIRROR_PAIRS follows the registry down by one, which is what the census failure message instructs ("update EXPECTED_MIRROR_PAIRS ... and nothing else"). The header sentence that names the pair under objectui#7664 is history and stays as it is.
  • New pin: packages/types/src/__tests__/column-width-config-retired-10582.test.ts.
  • .changeset/10582-retire-column-width-config.md: minor for @object-ui/types and @object-ui/plugin-kanban, with the breaking semantics and a migration section. No major.

Premise, re-measured on origin/main 5c61e5241 (in-repo evidence only)

  • git grep ColumnWidthConfig over the whole tracked tree. Outside .changeset/, it hits only the declaration, the mirror, the four re-exports, docblocks and the parity ledger. Per directory, case-insensitive, with a lit control on the same instrument:
    • content/: 0 hits (control KanbanSchema: 19);
    • examples/: 0 hits (control KanbanSchema: 3);
    • apps/: 0 hits (control @object-ui/types: 32);
    • skills/: 0 hits (control @object-ui/types: 17). There is no governed row to split out.
  • Shape level: defaultWidth appears only in the type and the mirror. No schema key references either name. The one key that did, the 'kanban' arm's columnWidths, retired with that arm (objectui#8802).
  • No reader, no producer and no doc that teaches building one were found, so the stop condition did not fire.
  • H2 measured: one-authority-per-exported-name-6273.test.ts has no ledger row for the name. It has one historical comment inside the objectui#7664 narrative, and that comment stays true as history. The file is not edited.
  • H4, sharpened: I read the published 17.6.0 tarballs from npm.
    • @object-ui/plugin-kanban@17.6.0 exports ColumnWidthConfig from its entry. There it is the package's own declaration, and KanbanSchema.columnWidths is typed with it.
    • @object-ui/types@17.6.0 exports neither name (lit control: BaseSchema appears in 42 files of the same tarball). The pair entered @object-ui/types in this unreleased window with objectui#7664, so for that package it leaves before any version publishes it. The changeset says so.
    • Consumers outside the repo cannot be measured from here.
  • H5, precedent followed: objectui#7068's retirement of ActionCallback / ActionCallbackSchema. There, a standalone retired pair is DELETED rather than tombstoned, and its absence is pinned (action-callback-retired-7068.test.ts). A tombstone (?: never / retirementTombstone()) exists for a member key under .passthrough(). A standalone type has no such hatch: an importer gets TS2305, "has no exported member", which is the loud refusal. The objectui#8522 retirement of useColumnWidths (PR objectui#10549) was a plain deletion too. I found no tombstone-export convention for retired type names in this repo.

The pin, and proof that it can fail

The pin has three readings, each with a lit control on the sibling CardTemplate / CardTemplateSchema that did not retire:

  1. The ColumnWidthConfigSchema name is in neither complex.zod.ts nor the @object-ui/types/zod barrel, checked at runtime.
  2. complex.ts declares no ColumnWidthConfig, and neither entry file (index.ts, zod/index.zod.ts) exports either name. Both are read off the AST, so the docblocks that tell the story do not count.
  3. Tree-scoped: no TypeScript source under packages/, apps/ or examples/ declares or exports either name. A re-declaration in another package would type-check cleanly, and only this reading sees it.

Each mutation leg went through ablation-replace.mjs. Its anchor had to hit, the landing was proven by the blob hash, and the restore was proven by blob == HEAD and an empty git diff HEAD. The pin imports relative src paths, not a package exports specifier, so no dist build sits between the mutation and the reading.

leg mutation predicted observed
A1 re-plant export const ColumnWidthConfigSchema in complex.zod.ts readings 1 and 3 red 2 failed / 4 passed
A2 re-declare export interface ColumnWidthConfig in plugin-kanban/src/types.ts only reading 3 red 1 failed / 5 passed; offender packages/plugin-kanban/src/types.ts: ColumnWidthConfig
A3 re-add ColumnWidthConfig to the index.ts export list readings 2 and 3 red 2 failed / 4 passed

The first A1 attempt was a null run. The replacement contained the anchor, so the tool refused because the anchor count could not drop. It restored the file and measured nothing. The leg was rerun with a disjoint anchor, and the table records that rerun.

Reverse check on the consumer side: the @object-ui/types dist was rebuilt, and its one ColumnWidthConfig hit is the docblock sentence. I re-planted ColumnWidthConfig in plugin-kanban/src/types.ts's re-export list and ran pnpm --filter @object-ui/plugin-kanban type-check. It went red with error TS2305: Module '"@object-ui/types"' has no exported member 'ColumnWidthConfig', and the restore was proven. This shows the green type-check reads the rebuilt .d.ts, not a cached one.

Verification, on HEAD 1f612da39

Each exit code was captured before any pipe.

  • pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-kanban^...' build: exit 0 (13 of 47 projects, including @object-ui/types).
  • pnpm --filter @object-ui/types type-check: exit 0.
  • pnpm --filter @object-ui/plugin-kanban type-check: exit 0.
  • pnpm exec vitest run packages/types/: exit 0, 241 files and 5283 tests passed.
  • pnpm exec vitest run packages/plugin-kanban/: exit 0, 64 files and 420 tests passed.
  • pnpm exec vitest run over the six scripts/__tests__ suites that read a touched file (one-authority-per-exported-name-6273, check-handler-key-read-sites, check-prompt-component-keys, docs-build-trigger, vite-objectstack-spec-dist, vite-types-zod-lazy): exit 0, 6 files and 235 tests passed.
  • node scripts/check-changeset-presence.mjs: exit 0. node scripts/check-changeset-no-major.mjs: exit 0. node scripts/check-changeset-fixed.mjs: exit 0.
  • pnpm check:new-line-citations: exit 0, with the verdict line reading 0 new citations. pnpm check:control-bytes: exit 0. pnpm check:spec-symbols: exit 0. pnpm check:test-path-roots: exit 0.
  • pnpm check:handler-key-reads: exit 0. pnpm check:doc-types: exit 0.
  • node scripts/check-governed-queue-guard.mjs --test over the 9 changed paths: exit 0, NOT GOVERNED.
  • ESLint with --no-inline-config on the 8 touched .ts/.tsx files: 1 error and 37 warnings. Every one of them is pre-existing. The per-file counts are identical when the same command lints the base blob of each file through --stdin, and the new pin has none. The 1 error is the no-restricted-imports finding on the FormField re-export in packages/types/src/index.ts, and it is present on the base blob.

Acceptance notes

  • Pending changesets that this retirement supersedes. check:changeset-claims (report-only) asked for 35 pending bodies to be re-read. Four of them name the type:

    • The objectui#8522 entry says "ColumnWidthConfig stays exported from @object-ui/types and from this package." That was true of that change.
    • The two objectui#7664 entries list the pair among the declarations that moved into @object-ui/types.
    • The objectui#7742 entry carries a dated note saying the type has no reader.

    This PR's own entry states the supersession in the same release section, for both packages. It edits none of those four files, because they are outside the claimed file surface. If the seat prefers an in-body dated note on the objectui#8522 entry, following the precedent where objectui#8522 added one to objectui#7742, it is a one-paragraph, prose-only edit.

  • Shared ledger. objectui#10608 may also delete rows from zod-mirror-parity.test.ts and move EXPECTED_MIRROR_PAIRS. Whichever PR lands second merges main (no rebase, no force-push) and takes the constant from the registry, as the census message says. PR objectui#10710 edits packages/types/src/index.ts in a separate hunk.

  • Spec Main Shape Gate: not applicable. Neither deleted declaration is spec-derived: both are plain local declarations with no @objectstack/spec reference, so objectui's compile against objectstack main cannot move with this diff.

  • check:readme-exports not run. No README names either symbol (whole-tree grep above), and the gate needs every package built.

  • check:test-path-roots classifies the pin's four filesystem reads as "unknown", because they go through a parameter or a loop binding. Every one of them is rooted at the pin's own import.meta.url, which is the same shape as the action-callback-retired-7068 walk.

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


Generated by Claude Code

…ConfigSchema

No schema key references the type since the kanban arm's `columnWidths`
retired with that arm (objectui#8802), and its one reader, `useColumnWidths`,
was removed by objectui#8522. ADR-0049 enforce-or-remove: the interface, its
Zod mirror, both `@object-ui/types` barrel exports and the `plugin-kanban`
re-exports go; the mirror-parity ledger drops its pair and the population
constant follows the registry.

The kanban retirement docblock in complex.ts no longer says the type survives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW
Neither `@object-ui/types` entry point exports `ColumnWidthConfig` /
`ColumnWidthConfigSchema`, `complex.ts` declares neither, and no TypeScript
source under packages/, apps/ or examples/ declares or exports either name.
Each reading carries a lit control on the sibling `CardTemplate` pair that
did not retire.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW
…eaking)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 35 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/index.ts → packages/types/src/index.ts — edited by this change

    GanttConfig itself gains nine members and is a published type, exported by name from packages/types/src/index.ts: lockField, objectField, summaryExtent, defaultCollapsedDepth, borderColorField, dependencyTypes, timeZone, exportFileName, interactions. The entry file's diff is empty only because the export list already named the type — the widening happened at the declaration.

  • 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/7664-plugin-kanban-declared-schema.md

  • names plugin-kanban/src/types.ts → packages/plugin-kanban/src/types.ts — edited by this change

    The shape is not member-for-member what this package declared — it is that shape plus four members, counted off origin/main's plugin-kanban/src/types.ts (19 members on KanbanSchema, 6 on KanbanColumn, 7 on KanbanCard) against the @object-ui/types declarations:

.changeset/7694-chart-series-chart-type-alias-refusal.md

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

    • Before: series: [{ name: 'revenue', chartType: 'line' }] validated green through @object-ui/types/zod (safeValidateSchema, objectui check / objectui validate, any pipeline that keeps parse()'s output) — and the key was gone from the output, so a consumer of the parse result drew that series in the chart's own family, precisely what the author was overriding. On the TypeScript face the key was merely an excess property on a fresh literal; a widened object carrying it assigned structurally. - After: the same document REFUSES at series[i].chartType (issue code invalid_type) with one message on both channels — the parse-time issue and the .describe() metadata: Unrecognized key(s) on this chart series: \chartType`. Did you mean `chartType` → `type`? …followed by the reason and the remedy. Writetype: 'bar' | 'line' | 'area'. On the TypeScript face ChartDataSeries.chartTypeis a?: nevertombstone, so both the fresh literal and the widened assignment aretsc errors. - **Both written** ({ type: 'bar', chartType: 'line' }) is refused at chartTypealone — the key is not folded ontotypeand no precedence is minted between the two spellings. - **Which documents to scan.** The narrowing does not stop atChartDataSeriesSchema; it reaches every document through the parents that embed it — ChartSchema.series (zod/data-display.zod.ts, z.array(ChartDataSeriesSchema)) and, one level further out, ReportSectionSchema.chart (zod/reports.zod.ts, ChartSchema.optional()). Authors meet it through safeValidateSchema() (zod/index.zod.ts, which parses AnyComponentSchema) and through the CLI's objectui validate command (packages/cli/src/cli.ts). In practice: every chartnode'sseries[], and every report section whose chart` carries one.

.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/7917-export-breadcrumb-object-tree-zod-schemas.md

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

    AnyComponentSchema declares 107 node component types. 105 of them could be named on the ./zod barrel — ButtonSchema.safeParse(node), which is what a designer, a form builder or a targeted test needs. The arms declaring type: 'breadcrumb' (navigation.zod.ts) and type: 'object-tree' (objectql.zod.ts) could not: both were already export const in their own module, but index.zod.ts — the package's only zod entry point — did not re-export them, so the schemas existed, were maintained, and were applied by the union while no consumer could name them.

.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/8344-node-recursion-point-redirect.md

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

    Two mechanical notes for anyone editing the wiring. AnyComponentSchema is built in zod/index.zod.ts from all 13 category modules and 14 modules import zod/base.zod.ts, so the arm cannot be an import — z.lazy defers evaluation, not the module graph. It is a written option slot that index.zod.ts fills inside AnyComponentSchema's own initializer, and it is a z.union option rather than a z.lazy holder because z.lazy memoises its getter: a holder would let whichever module graph parsed first decide the accept set for the whole process. Both constraints are measured, and the reasoning lives on defineNodeComponentUnion in zod/base.zod.ts.

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

    Two mechanical notes for anyone editing the wiring. AnyComponentSchema is built in zod/index.zod.ts from all 13 category modules and 14 modules import zod/base.zod.ts, so the arm cannot be an import — z.lazy defers evaluation, not the module graph. It is a written option slot that index.zod.ts fills inside AnyComponentSchema's own initializer, and it is a z.union option rather than a z.lazy holder because z.lazy memoises its getter: a holder would let whichever module graph parsed first decide the accept set for the whole process. Both constraints are measured, and the reasoning lives on defineNodeComponentUnion in zod/base.zod.ts.

.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/8499-node-slot-registered-arms.md

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

    Why 49 is acceptable here where 2 was a defect: the 2 were data blocks a consumer had a standing reason to validate on their own, and objectui#7917 (PR feat(types): re-export BreadcrumbSchema and ObjectTreeSchema from the zod barrel (objectui#7917) #8777, open at the time of writing, and the holder of zod/index.zod.ts) exists to export exactly those. The 47 added here are HTML primitives and two input aliases — they have no per-tag consumer to serve, and exporting a SemanticElementSchema / HtmlElementSchema pair would publish a NAMED authoring surface (z.enum families, not per-tag schemas) that this card's ruling does not cover: the ruling is "arm the registered renderers", not "add public exports to @object-ui/types". So the metric is left to move and said out loud instead. ⇒ Whoever next runs that measurement should expect 49, and whoever wants the number back down should treat naming these families as its own decision. If PR feat(types): re-export BreadcrumbSchema and ObjectTreeSchema from the zod barrel (objectui#7917) #8777 lands first, the same movement reads 0 to 47.

.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/8598-zod-subpath-single-bundled-module.md

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

    The defect. This package declares "sideEffects": false, and src/zod/index.zod.ts fills the node recursion point as the initializer of its AnyComponentSchema const. tsc emitted that barrel as a module whose only other content is re-exports — so a bundler resolving import { CardSchema } from '@object-ui/types/zod' followed the re-export to dist/zod/layout.zod.js, needed nothing from the barrel's own body, and the flag let it drop that body whole. The fill went with it, and every child slot then validated against the pre-spec(types): redirect the node recursion point from BaseSchemaCore to AnyComponentSchema — measured at 9 newly-refused documents, and it drops 118 phantom strict refusals #8344 BaseSchemaCore arm — the ~21 base keys and nothing type-specific — with no error, no warning and no way for the guard inside the dropped code to notice. objectui#8344 shipped that window DECLARED and pointed here to close 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/9067-zod-barrel-named-arms.md

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

    Two new exported symbols, both on packages/types/src/zod/index.zod.ts:

.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/9659-node-recursion-point-inert-clause.md

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

    Prose amended where this change falsified it, ⛔ not rewritten. zod/index.zod.ts said defineNodeComponentUnion "wraps it rather than replacing it" — true only while a wrapper existed. zod/base.zod.ts's note on the loose parameter bound still names ChatbotSchema's record body in the present tense; the paragraph is kept for the reason it records and carries an AMENDED note saying the exclusion set that pin reads is now empty.

.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 5c61e5241 (merge-base with origin/main): 8 file(s) changed outside .changeset/, read against 1561 pending declaration(s) that publish a body (2148 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-CsEvrEwl.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.82KB 131.15KB
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.05KB 35.61KB
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.41KB 39.70KB
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-27T02:06Z.

  • Failing on this head (1f612da39, job 108529945687): the step ends Process completed with exit code 1.
  • The same check is red on the base. main 5c61e5241 (runs 108526869853 / 108525123558) and f9c06ef6a both fail it. A local run of node scripts/check-vi-mock-specifiers.mjs on a detached origin/main 5c61e5241 names one offender, which this PR's diff does not touch: apps/console/src/__tests__/filterContextTokensSweep-10666.test.tsx:51, whose vi.mock of packages/plugin-map/node_modules/react-map-gl/dist/maplibre.js resolves to no file. It came in with PR objectui#10729.
  • Fix: none exists yet. The anchor card is objectui#10731, and objectui#10732 is a same-signature twin filed a few seconds later. The check is not in the merge queue's required set. For this PR's landing it counts as a merge-base red with the same signature, not as this PR's failure.
  • No re-run: the failure reproduces deterministically on the base, so a re-run of this job would only print the same line.

This PR otherwise waits on its remaining CI and on an at-tier contract review before it leaves draft.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1f612da392b6c35f3ac4cfd0fbce7e5b962da569

① Derived judgments

  • @object-ui/types root entry loses the type export ColumnWidthConfig. Evidence: git diff origin/main...1f612da39 deletes the 14-line export interface ColumnWidthConfig block in packages/types/src/complex.ts and the one ColumnWidthConfig, line in packages/types/src/index.ts; packages/types/package.json exports . and ./complex both point at those dist files. Verdict: RIGHT. A declaration nothing references narrows away (ADR-0049, triage direction on the card). The ./complex subpath is covered by the same deletion.
  • @object-ui/types/zod entry loses the runtime value ColumnWidthConfigSchema. Evidence: diff deletes the z.object in packages/types/src/zod/complex.zod.ts (-10) and the barrel line in zod/index.zod.ts (-1). Verdict: RIGHT.
  • @object-ui/plugin-kanban entry loses the ColumnWidthConfig re-export. Evidence: diff on packages/plugin-kanban/src/index.tsx and src/types.ts drops the name from both export type {...} lists; plugin-kanban/package.json exports only . and ./style.css. Verdict: RIGHT. This is the only surface a released consumer can feel (see ②: 17.6.0 shipped it).
  • Removal completeness. git grep -n -i columnwidthconfig over the head tree: 15 hits in packages/ (all prose or the pin: the two plugin-kanban retirement comments, the complex.ts docblock, the zod-mirror-parity header sentence under 7664, and the new pin), 1 in scripts/ (the narrative comment in the one-authority test), 13 in .changeset/. Per directory with lit controls on the same instrument: apps/ 0 (control @object-ui/types 32), examples/ 0 (control KanbanSchema 3), content/ 0 (control KanbanSchema 19), skills/ 0 (control @object-ui/types 17). Shape level: defaultWidth 0 hits tree-wide (control minWidth 56). No importer, re-export, reader or producer remains. Verdict: COMPLETE.
  • No key still references the type. git grep columnWidths on the head over the five roots hits only local useState in data-table.tsx, RecordPickerDialog.tsx, ObjectGrid prose, and the 8802 comments; none is typed with the retired name. The complex.zod.ts comment above KanbanCardSchema records that columnWidths was a tombstone on the kanban arm and left with it. Verdict: RIGHT.
  • Failure mode is loud; the pin can fail. An importer of the deleted name gets TS2305 (the PR's consumer reverse check reports exactly that message). The pin packages/types/src/__tests__/column-width-config-retired-10582.test.ts has 6 tests (4 in the entry-point describe, counting the two it.each rows, plus 2 tree-scoped) with a CardTemplate / CardTemplateSchema lit control in every one, a runtime in check on the module namespaces, an AST reader (ts.createSourceFile) for declarations and named-export specifiers, and a non-vacuity assertion that the walk sees plugin-kanban/src/types.ts and zod/complex.zod.ts. The ablation table is coherent with 6 tests: A1 (re-plant the Zod const) reddens readings 1 and 3 = 2 failed / 4 passed; A2 (re-declare in plugin-kanban) reddens only reading 3 = 1 / 5; A3 (re-add to index.ts) reddens readings 2 and 3 = 2 / 4. typescript is a devDependency of packages/types and of the root, and action-callback-retired-7068.test.ts imports it the same way. Verdict: RIGHT. Nit, non-blocking: the tree walk skips .d.ts (8 hand-written ones exist under the three roots); an ambient re-declaration there would escape reading 3 but would not restore a published export.
  • Shared ledger. zod-mirror-parity.test.ts: MIRRORS rows counted by awk over the object block = 160, Declared rows = 160, EXPECTED_MIRROR_PAIRS moved 161 to 160, which is what the census failure text instructs. Verdict: RIGHT.
  • Scope. 9 files, +191 -38, all inside the claim comment's file surface; nothing outside the card's scope. No cross-file path:line citation in added lines (AGENTS [WIP] Update documentation for project #11; Line Citation Gate green).
  • Check runs on the head, as they stand (final snapshot): 43 runs, 39 success, 3 skipped, 1 failure. The failure is Inert vi.mock Specifier Check: scripts/check-vi-mock-specifiers.mjs reports apps/console/src/__tests__/filterContextTokensSweep-10666.test.tsx mocking ../../../../packages/plugin-map/node_modules/react-map-gl/dist/maplibre.js, a path that does not exist without an install. That file is not in the diff (git diff --name-only ... | grep -c filterContextTokensSweep = 0); it was added to main by f9c06ef6a (fix: resolve context tokens in every directly authored data node's own filter (objectui#10666) #10729, 2026-09-26), and the same check is red on base 5c61e5241 and on f9c06ef6a. Verdict: red on the head, NOT caused by this PR; see ③(d).

② Semver level

  • Level: minor for @object-ui/types and @object-ui/plugin-kanban. Policy line, objectui AGENTS.md line 271: "changeset 里不要声明 major … objectui 自身的破坏性变更也标 minor(在正文里写清 breaking 语义即可)", enforced by scripts/check-changeset-no-major.mjs; Changeset Bump Policy, Changeset Declaration and Changeset Fixed Group Check are green on the head. The body opens with the BREAKING semantics and a Migration section. CONSISTENT.
  • Registry reachable: registry.npmjs.org answered 200 for both packages; dist-tags.latest is 17.6.0 for both and no later version exists, so 17.6.0 is the published baseline the changeset compares against.
  • "No schema key references the type and nothing reads it" — TRUE (greps in ①).
  • "The one key that carried the type, the 'kanban' arm's columnWidths, retired with that arm in this release (objectui#8802)" — IMPRECISE, not false: the pending 7742-kanban-arm-batch70.md records columnWidths as one of the four accepted spellings that arm retired as a ?: never tombstone, so the key stopped carrying the type at objectui#7742; the tombstone then left with the arm in objectui#8802 (the complex.zod.ts comment says exactly that). The card body carries the same 8802-only attribution and the dev copied it. Non-blocking.
  • "The one reader, the useColumnWidths hook … removed in this release too (objectui#8522)" — TRUE: 8522-retire-use-column-widths.md is pending, and the 17.6.0 plugin-kanban dist/index.d.ts still exports useColumnWidths (2 hits), confirming the hook was published and is unreleased-removed.
  • "@object-ui/plugin-kanban: 17.6.0 exports ColumnWidthConfig from its entry" — TRUE: tarball dist/index.d.ts line 23 reads export type { KanbanSchema, KanbanCard, KanbanColumn, CardTemplate, ColumnWidthConfig, InlineFieldDefinition } from './types'. "There it is the package's own declaration" — TRUE: dist/types.d.ts line 160 export interface ColumnWidthConfig. "KanbanSchema types its columnWidths member with it" — TRUE: dist/types.d.ts line 137 columnWidths?: ColumnWidthConfig;. "This release exports it no more" — TRUE per the diff.
  • "@object-ui/types: 17.6.0 exports neither ColumnWidthConfig nor ColumnWidthConfigSchema" — TRUE: 0 of 118 tarball files contain the token; lit control BaseSchema in 42 files (matches the PR body's 42).
  • "Both entered this package in this release, when objectui#7664 moved the kanban dialect's declarations here, and both leave before any version publishes them" — TRUE: 7664-kanban-arm-plugin-dialect.md lines 23-26 declare both names in @object-ui/types / @object-ui/types/zod, it is pending, and no version after 17.6.0 exists on the registry.
  • "For these two names, the objectui#7664 entries in this release are superseded by this one" — ACCURATE: both 7664 files exist on the branch and name the type (the plugin-kanban one names ColumnWidthConfig in its first sentence).
  • Migration bullet 1 (TS2305, type-only import erased at runtime) — TRUE.
  • Migration bullet 2: "The objectui#8522 entry … says ColumnWidthConfig stays exported from @object-ui/types and from @object-ui/plugin-kanban" — ACCURATE paraphrase of 8522-retire-use-column-widths.md lines 27-28 ("stays exported from @object-ui/types and from this package. This change does not retire it.").

③ Boundary flags

  • (a) One-authority ledger has no row — ACCEPTABLE. Rows in scripts/__tests__/one-authority-per-exported-name-6273.test.ts are ['Name', [paths]] tuples (e.g. ['CalendarEvent', [...]] at line 366); grep finds ColumnWidthConfig only inside the objectui#7664 narrative comment near line 440, which says the five names moved to @object-ui/types and plugin-kanban's re-export is "not an authority, so the entries stay retired". That sentence is still true as history. Not editing the file is correct; the card's ledger list was over-inclusive.
  • (b) 8522 changeset says the type stays exported; superseded in-body — ACCEPTABLE, follow-up recommended. Every sentence in the new entry is true and it names the supersession for both packages. But the repo's own precedent is an in-body dated note on the superseded entry (8522 says at lines 30-33 that it added one to 7742, and 7742 lines 36-39 carry it). Without one, plugin-kanban's next CHANGELOG will print "stays exported" (8522) and "removes it from both" (10582) in the same release section. One-paragraph prose edit for the seat; not a defect of this PR.
  • (c) No tombstone-export convention; delete-and-pin per objectui#7068 — ACCEPTABLE. AGENTS.md has zero lines on tombstone / retirementTombstone / ?: never for exported names. packages/types/src/__tests__/action-callback-retired-7068.test.ts exists and uses the same typescript AST reader; a standalone type has no .passthrough() hatch, so TS2305 is the loud refusal.
  • (d) Self-raised — NEEDS ESCALATION, not this PR's defect. Inert vi.mock Specifier Check is red on the head. Its workflow header states it is a required context named by branch protection and scripts/dependabot-merge-gate.mjs. The cause is main's f9c06ef6a (fix: resolve context tokens in every directly authored data node's own filter (objectui#10666) #10729), and base main is red on the same check; merging main into this branch will not clear it. The seat needs a fix on main (or a ruling) before this PR can land.
  • (e) Self-raised, non-blocking. The changeset, the pin docblock and the complex.ts docblock attribute the columnWidths retirement to objectui#8802 alone; objectui#7742 tombstoned it first (see ②).
  • (f) Self-raised, non-blocking. The pin's tree walk excludes .d.ts files (see ①).
  • (g) Self-raised, for the seat. The PR carries labels package: types, plugin, plugin: kanban, tests, no needs:contract-review label and no assignee (the dev report says the assign call was refused by the classifier).
  • Process note: a standdown.md I did not write was present in the scratch directory; it was not read. Worktree removed; /home/user/objectui and /home/user/objectstack untouched; zero GitHub writes.

Implemented-by: claude/issue-10582-retire-column-width-config
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 02:18
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit c3a26cc Sep 27, 2026
44 of 45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10582-retire-column-width-config branch September 27, 2026 02:31
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…se the DrillDownConfig members they never read (objectui#10685) (objectstack-ai#10710)

Fixes objectstack-ai#10685
Clause-②: no — a per-block narrowing, as the ruling recorded for
objectui#9002

## What this does

This applies objectui#9002's ruling B (comment `5643445104`, a per-block
refusal) to the sibling blocks of `object-metric`, as triage
`5837818071` directed. Each block refuses, by name, the
`DrillDownConfig` members it can never honour; the shared
`DrillDownConfig` keeps every member for the blocks that read them. The
seat's round-2 ruling (option A) widened the claim's file surface by
`packages/types/src/objectql.ts` and
`packages/types/src/zod/objectql.zod.ts`, so the `object-data-table`
half shuts BOTH doors here, and merging this PR completes the card.

- **`@object-ui/types`** adds two per-block shapes beside
`ObjectMetricDrillDownConfig` in `data-display.ts`, exported on the root
entry and on the `@object-ui/types/data-display` subpath:
- `ObjectPivotDrillDownConfig`: `DrillDownConfig` plus a `mode?: never`
tombstone (the objectstack-ai#10681 idiom: a `REFUSED BY NAME` docblock and
`@deprecated`, naming `object-data-table` as the block that reads
`mode`).
- `ObjectDataTableDrillDownConfig`: `DrillDownConfig` plus `filter?:
never`, `maxRows?: never` and `report?: never` tombstones, and `target?:
'drawer' | 'dialog'`. Each tombstone names the blocks that do read the
key.
- **TypeScript door.** `ObjectDataTableSchema.drillDown` in
`objectql.ts` is typed with `ObjectDataTableDrillDownConfig`. The data
table's component prop is anchored `Equal` to that schema by the two
objectui#6576 pins, so the widget's prop refuses exactly what the schema
refuses.
- **Zod door.** The `object-data-table` mirror's `drillDown` in
`objectql.zod.ts` is the shared `DrillDownConfigSchema` extended so that
`filter`, `maxRows` and `report` are `retirementTombstone` arms whose
messages name the key and the blocks that read it, and `target` is
`z.enum(['drawer', 'dialog'])` with an error naming the refused
`'navigate'`. `enabled`, `mode`, `title` and `columns` parse exactly as
before. Nothing else in `objectql.zod.ts` changes. This is the door
`objectui validate` and `objectui check` run (`safeValidateSchema`), so
a stored JSON table config carrying one of the four is now refused
there.
- **`@object-ui/plugin-dashboard`**: `ObjectPivotTableProps.schema`
gains `drillDown?: ObjectPivotDrillDownConfig`, and the component reads
its drill config through that type instead of through `any`.
- **Docblocks**: the shared `DrillDownConfig.mode` docblock names only
`object-data-table` (none of charts, pivot tables or metric cards reads
it) and says what each value does there; the `DrillDownConfig` class
docblock no longer says "`target` is honoured by all of them", and the
declared-keys pin's comment no longer names the table among the
`navigateOnly` deliverers.
- **`object-chart`: no type change** (see H4). Its refused set is
pinned.
- **Pins**: `drill-down-per-block-10685.test.ts` in `@object-ui/types`
(one `describe` per block, `@ts-expect-error` per refused member with
the read members as controls, plus a RUNTIME `describe` over the zod
door), and `ObjectPivotTable.drillDownRefusal-10685.test.tsx` in
`@object-ui/plugin-dashboard` (the published component's prop). The two
6576 anchor pins are re-pointed at the table shape, and the 7352 mirror
pin's table leg runs over the values the table now accepts, with a
non-vacuity check that the host-synthesised table configs stay in it.
- **Changesets**: `10685-drilldown-per-block.md` (`@object-ui/types`
`minor` with a breaking banner, `@object-ui/plugin-dashboard` `patch`)
opens with the banner "Breaking for authored metadata, graded `minor` by
this repo's convention": `objectui validate` / `safeValidateSchema` now
refuse a stored `object-data-table` config carrying `drillDown.filter`,
`.maxRows`, `.report` or `target: 'navigate'` that parsed green before,
none was ever read, and the migration is to delete the key or write
`'drawer'` / `'dialog'`. `minor` because the change narrows the
published validator's accept set and the published
`ObjectDataTableSchema` type, the grade every pending zod-door refusal
in this repo carries (6881, 6951, 6972, 7322, 7963, 8801, 7352);
objectui#9002's `patch` was graded on a TypeScript-door-only refusal,
which does not describe this change. `@object-ui/plugin-dashboard` stays
`patch`: a React-prop narrowing with no runtime door, the 9002 shape.
The changeset also says `object-pivot`'s refusal is at the TypeScript
door only (no zod mirror exists for it). H6:
`.changeset/7352-drill-down-config-mirror.md` and
`.changeset/6576-widget-schema-anchors.md` each have the falsified
sentence scoped "at this change" plus a `⚠️ Dated note, 2026-09-25 …
objectui#10685`; frontmatter md5 before and after: 7352
`ecedb10c5189b2c8841ea736b67c3cc6` both, 6576
`b25dbc26fe50c14d0b6f0cc8fe6cb9e1` both.

## H1: the table, re-measured per block

Read sites were re-read on `origin/main` `5c61e524` this round. The
one-member-at-a-time runtime probe (rendered DOM, data source calls and
`openRecordList` calls compared against a control, with and without a
`DrillNavigationProvider`) was run in round 1 at `bc97f9247` and is
recorded in report comment `5839013332`; it was not re-run, and no read
site moved since.

| block | reads | never reads |
|:--|:--|:--|
| `object-chart` | `enabled` (`isDrillEnabled`), `filter`
(`computeDrillFilter`), `title` (`resolveDrillTitle`), `target`
including `'navigate'` (`openRecordList`), `maxRows` (drawer
`pageSize`), `columns` | `mode`, `report`: already refused by the spec's
`ChartDrillDown` type and by the zod mirror (`unrecognized_keys`) |
| `object-pivot` | `enabled`, `filter`, `title`, and `target`,
`columns`, `maxRows`, `report` through `DrillDownDrawer` | `mode` |
| `object-data-table` | `enabled` (`isDrillEnabled`), `mode` (`'record'`
opens the row; `'filter'` turns the drill off), a non-template `title`,
`columns` (the record drawer's field list), `target` in two arms
(`'dialog'`, anything else drawn as a drawer) | `filter`, `maxRows`,
`report`; `target: 'navigate'` was drawn as a drawer |

`@object-ui/core`'s `drill-down.ts` helpers read `enabled`, `filter` and
`title` only; `DrillNavigationContext` carries one handler,
`openRecordList(objectName, filter)`; the `data-table` renderer the
table spreads its node into never names `drillDown`;
`RecordDetailDrawer` takes `fields`, `title` and a two-armed `target`.

The card's runtime-door bullet also names `ChartSchema.drillDown`. That
is the plain `chart` node, whose renderer `ChartRenderer.tsx` reads no
`drillDown`; it is outside this card's three blocks and deliberately
untouched, the plain-`chart` remainder.

## H2: can never honour, or not yet honoured (one line each)

- `object-pivot` · `mode`: never. Every pivot click point is an
aggregated bucket (a cell, a row or column header, a total), so the
pivot always drills through; `mode` chooses drill-to-record for a
clicked ROW, and a pivot has none. This is the metric's reasoning in
`5643445104`.
- `object-chart` · `mode`, `report`: never, by the contract. Its drill
type is `@objectstack/spec`'s `ChartDrillDown`, which declares neither;
the spec's schema refuses both by name ("A chart segment is always an
aggregate"). Already refused before this card; only pinned here.
- `object-data-table` · `filter`, `maxRows`, `report`: never. All three
configure a drilled LIST (its scope, its row cap, the report that
replaces it). The table drills to the RECORD its row already is,
`RecordDetailDrawer` lists nothing, and the block's `mode: 'filter'` arm
is pinned as "ignored", so there is no list drill for them to act on. A
row that drills through to a list of another object would need a
drill-target member `DrillDownConfig` does not have: a new capability,
not an unread member.
- `object-data-table` · `target: 'navigate'`: refused, not honoured
(H3).

## H3: `navigate` on the data table

`object-chart` honours `navigate` by calling `openRecordList(objectName,
merged filter)`, the object's LIST page scoped by the drill filter.
`DrillNavigationContext` has no record-level handler and
`@object-ui/app-shell`'s `useOpenRecordList` builds only the list route.
The table's click point is one record, so the list page is the wrong
destination, and a "navigate to this record" arm needs a new host seam,
not a small hunk. So the table's shape refuses `navigate` on both doors
and an author gets a type error, or a `validate` refusal, instead of a
drawer.

## H4: where the shape differs from PR objectui#10681

- **`object-chart` keeps its spec-bound type.**
`ObjectChartSchema.drillDown` is already per-block (the spec's
`ChartDrillDown`), pinned `Equal` to that symbol by
`object-chart-undeclared-keys-8885.test.ts`. A tombstoned local type
would unbind it from the spec. A fresh `mode` or `report` literal is
already a compile error and the zod mirror already refuses both by name.
- **Registration sentences: none added.** The `object-pivot` and
`object-data-table` registrations advertise no `drillDown` input (adding
one widens the designer palette, a shape decision). `object-chart`'s
description is pinned by `packages/plugin-charts/src/index.test.ts` to
list exactly the six spec keys and NOT `mode` or `report`.
- **Zod door narrowed for the table only**, because a runtime validator
(`safeValidateSchema`, run by `objectui validate`) reads that block's
drill members and accepted all four; the round-1 probe showed the same
door refuses a bad value on `enabled`, so it does reach `drillDown`.
`object-pivot` has no zod mirror, so its refusal is TypeScript-only and
the changeset says so.

## Verification (measured at `430223947f` = `a1217b515` merged with
`origin/main` `5c61e524`; head `64d43b6d67` = that head + a second merge
of `origin/main` `41ae65b26` + the changeset commit)

- **Round 3 delta and its gates (head `64d43b6d67`).** Against
`430223947f` the 14 files of this card differ in two places only: the
changeset (the `minor` grade and its banner) and one line of `index.ts`
that `main`'s own objectstack-ai#10730 deleted (a retired `ColumnWidthConfig` export);
the other 12 are byte-identical, and the branch's diff against `main` is
still exactly these 14 files. Re-run at this head, each exit 0:
`check-changeset-presence` (11 source files of 2 released packages, 1
changeset); `changeset:check`; `check-changeset-no-major` (the
`Changeset Bump Policy` step); `check:changeset-claims` (36 pending
changesets, unchanged reading); `check:pending-changeset-literals`;
`check:new-line-citations` (`0 new citation(s)`); `check:control-bytes`
OK; `check-changeset-overwrite` (report-only: it reports the 6576 and
7352 edits, the intended shape). No suite, type-check or ablation was
re-run: the delta is a changeset plus `main`'s already-green commits
merged with a clean `merge-tree`, and none of the measured sources
moved.

- Closure build `pnpm --workspace-concurrency=2 --filter
'@object-ui/plugin-dashboard^...' run build`, under the verify lock:
`VERDICT command-exit 0`. `dist/data-display.d.ts` and
`dist/objectql.d.ts` carry `ObjectDataTableDrillDownConfig` (2 hits
each), `dist/index.d.ts` carries `ObjectPivotDrillDownConfig`.
- `pnpm --filter @object-ui/types run type-check` then `pnpm --filter
@object-ui/plugin-dashboard run type-check`: `VERDICT command-exit 0`.
`tsc --listFilesOnly` on each `tsconfig.test.json` lists the four types
pins and the two plugin-dashboard pins, and plugin-dashboard's program
reads `@object-ui/types` through `dist/data-display.d.ts` and
`dist/objectql.d.ts`.
- `pnpm exec vitest run --maxWorkers=2 packages/types/
packages/plugin-dashboard/` from the repo root: `Test Files 378 passed
(378)`, `Tests 6562 passed (6562)`, `VERDICT command-exit 0`.
- Gates, each exit 0: `check-changeset-presence` (11 source files of 2
released packages, 1 changeset); `check:new-line-citations` (`0 new
citation(s)`); `check:control-bytes` OK; `changeset:check` (no `major`);
`check:pending-changeset-literals`; `check:changeset-claims` (36 pending
changesets name a touched file; the two whose prose is about drill-down,
7363 and 8885, were read and stay true, and the rest describe other
regions of these files); `check:spec-symbols`; `check:self-import`;
`check:esm-specifiers`; `check:test-path-roots`;
`check:vi-mock-specifiers`; `check:handler-key-reads`;
`check:unreferenced-sources`; `check:metadata-write-doors`;
`type-check:coverage`; `check:component-surface-parity`;
`check:doc-types`; `check-governed-queue-guard --test` over the 14
paths: NOT GOVERNED.
- NOT MEASURED: `check:readme-exports` exits 1 on a prerequisite (385
README self-imports unjudgeable because other packages'
`dist/index.d.ts` are not built); this diff edits no README. Left to CI.
- ESLint was run on the 11 touched source and test files, not the whole
repo. The narrowing is a measurement, for three reasons: the config is
the root `eslint.config.js` and neither package has its own; `--format
json` reports 11 files, 0 errors and 80 warnings (all `no-explicit-any`,
none on a line this branch added, by intersecting message lines with the
branch's added-line set); `--print-config` shows empty `parserOptions`,
so there is no type-aware linting and the diff cannot move any untouched
file's verdict. Under `--no-inline-config` the same run shows 1 error,
the block-disabled `SpecFormField` alias in `index.ts` that is on `main`
and outside this diff; the package `lint` scripts are `eslint .`, which
honours the disable. Full `pnpm lint` is left to CI.
- CI on this head: `Inert vi.mock Specifier Check` is red on `main`
since `f9c06ef6a` (PR objectui#10729), filed as objectui#10732.
Confirmed locally in a no-install probe worktree at `5c61e524`: exactly
one hit,
`apps/console/src/__tests__/filterContextTokensSweep-10666.test.tsx`,
outside this surface; in an installed worktree the specifier is found
through `node_modules` and the checker passes. Not touched here.

## Red on base, then green on head

The five touched sources (`data-display.ts`, `index.ts`, `objectql.ts`,
`zod/objectql.zod.ts`, `ObjectPivotTable.tsx`) were swapped to their
`origin/main` `5c61e524` blobs with the HEAD pins in place, then
restored to HEAD (every blob equal to `HEAD`, `git diff HEAD` empty,
types dist rebuilt, markers back to 2 and 1).

- Types test program (`tsc -p tsconfig.test.json`): exit 2.
`TS2305`/`TS2724` on the two new type names imported by the per-block
pin and the 6576 pin, and nine `TS2578 Unused '@ts-expect-error'` on the
per-block pin's refusals (the pivot's two, the table's seven).
- Vitest on the per-block pin and the 7352 mirror pin: `Tests 4 failed |
59 passed (63)`, the four failures being the zod describe's `filter`,
`maxRows`, `report` and `target` rows.
- Types dist built from base sources (markers 0 and 0), then the
plugin-dashboard test program: exit 2, `TS2305` on the 6576 anchor's
`ObjectDataTableDrillDownConfig` import and two `TS2578` on the pivot
pin.

## Ablations (each through `ablation-replace.mjs`: anchor hit once, blob
changed, command run, restored to the HEAD blob with an empty `git diff
HEAD`)

- **TS wiring.** `ObjectDataTableSchema.drillDown` back to the shared
`DrillDownConfig`, with a JSDoc marker `ABLATED_10685_TS1` on the line.
Types program exit 2: `TS2344` on `_SchemaTakesTheTableShape`, `TS2578`
on `nodeWithFilter` and `nodeWithNavigate`, `TS2344` on the 6576 pin's
`assertionDrillDownDeclared`, plus `zod-mirror-parity.test.ts` going red
with `objectql.zod.ts#ObjectDataTableSchema` (the repo's TS/zod parity
pin sees the two doors disagree: a second, independent instrument). Dist
leg: types rebuilt, marker count in `dist/objectql.d.ts` 1,
plugin-dashboard program exit 2 (`TS2344` on
`ObjectDataTable.schemaAnchor-6576`'s `Equal`); after the restore
rebuild the marker count is 0 and `ObjectDataTableDrillDownConfig` is
back to 2. A first pass of this leg read the dist marker with the wrong
grep (tsc emits the `import()` type with single quotes) and got 0; the
downstream red was the same in both passes, and the marker form above is
the evidence.
- **Each table tombstone alone** (`never` to `DrillDownConfig['KEY']`):
`filter` gives `TS2578` on `withFilter` and `nodeWithFilter`; `maxRows`
on `withMaxRows`; `report` on `withReport`; `target` widened back to the
shared union gives `TS2578` on `withNavigate` and `nodeWithNavigate`.
Each also turns `zod-mirror-parity` red as above.
- **Pivot tombstone** neutralised (marker `ABLATED_10685_PIVOT`): types
program `TS2578` on `withMode` and `handedAcross`; dist marker 1;
plugin-dashboard program exit 2 with two `TS2578` on the pivot pin;
marker 0 after the restore rebuild.
- **Chart binding** (`ObjectChartSchema.drillDown` to the shared type):
`TS2578` on the chart pin's `withMode` and `withReport`, plus the 8885
pin's `TS2344` and `TS2578`.
- **Zod door** back to the shared mirror: `Tests 4 failed | 59 passed
(63)`, the four refusal rows. **Each zod member alone** (the tombstone
renamed away, or `target` widened to include `navigate`): exactly one
failure each, its own row, `62 passed`.

Direction: red as predicted in every leg, with one increase (the parity
pin) beyond the prediction.

## Serial

`origin/main` `5c61e524` and PR objectui#8941's head `bb7d5e718`
(`claude/pr-7058-lucide-react-1.41.0`) were fetched into private refs;
`git merge-tree --write-tree` exits 0 against both. PR objectui#10707
and PR objectui#10643 have merged; objectui#10657 has no open PR and no
branch (its test is on `main`). `main` had moved in three of this PR's
files (the `filter` docblocks in `objectql.ts`, the `masked` column in
`data-display.ts`, the `filter` describes in `objectql.zod.ts`), so
`main` was merged in (`430223947f`, a merge commit, no conflicts). In
round 3 `main` had moved again in one file (`index.ts`, objectstack-ai#10730's retired
export), so `origin/main` `41ae65b26` was merged in the same way
(`4f2f7a530f`, no conflicts). At head `64d43b6d67`, `merge-tree
--write-tree` exits 0 against `origin/main` `41ae65b26` and against the
three open PRs that touch these files: PR objectui#10734 (`856ecc953`,
`ObjectChartSchema` regions of `objectql.ts` / `objectql.zod.ts`,
disjoint), PR objectui#10714 (`218f642484`, `data-display.ts`) and PR
objectui#8941 (`bb7d5e718`, `objectql.ts`). The branch diff against
`main` is exactly the 14 files of this card.

## Acceptance notes

- **`mode: 'filter'` on `object-data-table`** turns the row drill off,
as `ObjectDataTable.drill.test.tsx` pins, instead of drilling through.
The rewritten docblock says so. Value-level, outside the card's member
set; recorded here only.
- **`maxRows`** reaches each drill list as the drill table's `pageSize`
(`DrillDownDrawer` and the chart's drawer), a page size rather than the
"Hard cap on rows fetched" the shared docblock promises. Observation,
outside this card.
- **`object-chart`, non-fresh values.** A value typed as the shared
`DrillDownConfig` is still assignable to `ObjectChartSchema.drillDown`,
because the spec type omits `mode` and `report` rather than tombstoning
them. No producer in the tree hands a typed shared config across.
- **Spec guidance text.** `@objectstack/spec`'s `ChartDrillDownSchema`
refusal text for `mode` calls it "a TABLE / PIVOT / METRIC drill key";
on `object-pivot` and `object-metric` it is now refused. Reported to the
seat in round 1 for the spec repository; not touched here.
- **PR assignee.** Set to `os-elon-musk` in round 1 (read back
matching); found empty at the start of this round and set again to match
the card's assignee, per the dispatch rule.

Session: `https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN`

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…e the spec member on both faces (objectui#7759 group A) (objectstack-ai#10744)

Part of objectstack-ai#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 objectstack-ai#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 objectstack-ai#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 objectstack-ai#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 objectstack-ai#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](https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW)_

---------

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

Projects

None yet

1 participant