Skip to content

test(types): the JoinedReportBlock pin compiles on the installed spec and on spec main, and trips at the bump (objectui#10916) - #10934

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-10916-joined-report-block-pin
Sep 28, 2026
Merged

os-zhuang merged 3 commits into
mainfrom
claude/issue-10916-joined-report-block-pin

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10916
Clause-②: no — a test-side tripwire and the ledger text beside it move to what spec main now says; no published type, no export and no accepted document changes (the local JoinedReportBlock interface and the index.ts exports are untouched).

⚠️ Generic parameters are written as UPPERCASE WORDS in this body (for example "IsUnknown OF SpecJoinedReportBlock"), never in angle brackets: GitHub's body sanitizer deletes tag-shaped fragments on save, and backticks do not protect them (AGENTS.md, "GitHub mutates body BYTES"). Diagnostics are quoted by code and text, without the file's line and column, per AGENTS.md #11.

What was red, and why the pin was right to go red

objectstack#20369 (681868ca7) removed the bare z.ZodTypeAny annotation from JoinedReportBlockSchema on objectstack main. The inverted pin _specJoinedReportBlockIsStillUntyped (true satisfies IsUnknown OF SpecJoinedReportBlock) in packages/types/src/__tests__/report-chart-query-spec-parity.test.ts then failed with TS1360 ("Type 'true' does not satisfy the expected type 'false'.") on the Spec Main Shape Gate, which compiles every head against spec built from objectstack main. Its docblock said that failure is the instruction: re-run the triage and burn it down.

The pull-request type-check compiles the SAME file against the INSTALLED spec, the published 17.4.0 (the newest release on npm, published 2026-09-09, so before #20369). There the type is still unknown. Whatever stands in the file has to compile against both until a spec bump installs a release carrying #20369.

The re-run triage: spec main against the local interface, member by member

Read with the TypeScript checker off the source-built spec's dist (objectstack df3ba164a588); JoinedReportBlock is neither any nor unknown there and has no index signature.

member local JoinedReportBlock (spec-report.ts) spec main JoinedReportBlock (input) verdict
name string, required string, required (snake_case at runtime) agree
type optional tabular / summary / matrix the same enum, optional (defaults to tabular) agree
label, description string, or an object with default and translations string, or an inline locale map whose every value is a string diverge: the local object arm is not a spec label
columns REQUIRED array of column OBJECTS (field, aggregate, ...) optional array of dimension NAMES (matrix across-axis) one name, two concepts
objectName declared not declared; the closed schema refuses it (its alias table points at dataset) local-only
groupingsDown, groupingsAcross declared not declared; refused local-only
filter declared not declared; refused (alias table points at runtimeFilter) local-only
chart declared retired by objectstack#20161, refused with the retirement prescription local-only
index signature [k: string]: unknown none (closed schema) diverge
dataset, rows, values, runtimeFilter, order not declared declared (the ADR-0021 dataset-bound selection) spec-only

Verdict. The spec type is now precise, and it describes a DIFFERENT shape: guard case 2c on spec main, no longer 2b. The local interface is the legacy inline-query block, and the spec's closed schema refuses every one of its five local-only keys. So it is derivable, but not by a re-export beside the local interface: the burn-down REPLACES the published JoinedReportBlock (and the blocks of JoinedSpecReport) with the spec's type. That changes a published type, so it is its own slice, and it cannot land before the bump, because against the installed spec the derived type would be unknown. Nothing in this tree reads the local interface's legacy members (from a grep of the five local-only keys and a reading of the renderer, not a runtime probe): the joined branch of DatasetReportRenderer reads dataset-bound blocks, and the local type is only re-exported and used by JoinedSpecReport.

The chosen shape, and the precedent it follows

objectui#10080 (PR #10089) is the precedent for "the gate compiles against spec main, the PR check against the installed pin": it kept both jobs of the red declaration and changed its form so the file compiles against both specs, then proved both legs with a control. objectui#3177 (case 2c for NavigationItem) is the precedent for what replaces an IsAny / IsUnknown pin once the spec is typed: per-key probes that each stop compiling the day that one divergence moves. This PR does both. The guard now has two halves, because two different runs read two different specs:

  • Compile time, both runs. A state pin, _specJoinedReportBlockTyping, admits erased and typed and refuses any; the IsAny pin stays as it was. On a typed spec, seven divergence and agreement probes (_localBlockIsNotTheSpecBlock, _specOnlyBlockKeys, _localOnlyBlockKeys, _blockColumnsAreTwoConcepts, _blockLabelObjectArmIsNotASpecLabel, _blockNameAgrees, _blockTypeAgrees) record the triage above and stop compiling the day one of them moves, on the Spec Main Shape Gate first. On the erased spec they are not asked (OnTypedSpec), since an erased type declares no member to compare.
  • Test time, the installed spec only. The Spec Main Shape Gate type-checks and runs no test, so a vitest tripwire can act on the INSTALLED spec alone. It reads the installed spec's built declarations with the TypeScript checker, and it fails on the first bump past the published 17.4.0. If that release types the block, the failure is the burn-down's start signal: derive the published interface in its own slice, and delete the state pin's erased arm so that an erasure stops compiling again. If that release still erases the block, the spec erased it again, which is an upstream regression; the erased arm loses its licence either way. A lit control (ReportSort must read typed) keeps the reader from passing vacuously.

⚠️ Declared limit. An erasure on objectstack main BEFORE that bump cannot be told apart, at the type level, from the installed 17.4.0: both are the same bare z.ZodTypeAny. The state pin therefore admits it on the gate. The producer's own pin covers that window (packages/spec/src/ui/joined-report-block-type.test.ts in objectstack, landed with #20369), and the tripwire's version row catches it at the bump at the latest.

Nothing was skipped, disabled or ts-ignored, and no workflow changed. The ledger row (the JoinedReportBlock ALLOW entry in scripts/check-spec-symbol-derivation.mjs, which is objectui#3162's ledger in machine-readable form) and that script's case-2b header now say what is true of both specs, and when the burn-down fires.

Both compile legs, from committed state

Legs taken with pnpm run type-check in packages/types (all three tsc programs, tsconfig.test.json included). The spec-main legs follow the gate's own procedure: @objectstack/spec built from objectstack df3ba164a588 (a detached worktree, pnpm install --frozen-lockfile --filter @objectstack/spec..., pnpm --filter @objectstack/spec build), npm pack --ignore-scripts, then node scripts/spec-main-shape-gate.mjs inject into this worktree's install. The inject printed its consumer list and re-pointed zod to 4.6.1, as the gate does. The installed legs ran after a clean reinstall, with the gate's injection marker confirmed absent.

leg spec under the compiler tree result
installed, before published 17.4.0, block is unknown 733fd5ac6 (base) exit 0, 0 diagnostics
spec main, before source-built df3ba164a588, block typed 733fd5ac6 exit 2, 1 diagnostic: TS1360 "Type 'true' does not satisfy the expected type 'false'." at _specJoinedReportBlockIsStillUntyped (the same diagnostic the gate reports)
spec main, after same injected build 8ce61bad3 exit 0, 0 diagnostics
installed, after published 17.4.0 restored 8ce61bad3, and again at head 22226a8e9 exit 0, 0 diagnostics

The two commits after 8ce61bad3 touch only scripts/ and .changeset/, which no packages/types program reads.

⚠️ Narrowed on purpose: the gate runs turbo run type-check over every package. Locally only @object-ui/types was compiled against spec main, because it is the only package the gate reported and the only one this diff touches. The full farm is the gate's own run on this head.

The tripwire, fired for real

Under the injected spec-main install, pnpm exec vitest run packages/types/src/__tests__/report-chart-query-spec-parity.test.ts gives 1 failed, 46 passed (47). The one failure is the typing row, with its message: "the installed @objectstack/spec 17.4.0 now types JoinedReportBlock — this is the bump the burn-down is owed at. [...] expected 'typed' to be 'erased'". The lit control and the version row pass (a source-built main still reports version 17.4.0). Under the installed spec the same file gives 47 passed (47).

Ablations, each proved on disk

All through ablation-replace.mjs (objectstack scripts/) in wrap mode. For every leg the tool reported "mutation landed", and after it "restored: blob == HEAD (a1b2c707461e) and git diff HEAD is empty".

Spec main injected, tsc -p tsconfig.test.json:

leg mutation result
A1 drop order from _specOnlyBlockKeys' expected set TS1360 at that pin
A2 drop chart from _localOnlyBlockKeys' expected set TS1360 at that pin
A3 spec columns expected as number[] TS1360 at _blockColumnsAreTwoConcepts
A4 state pin without its typed arm TS1360 "Type '"typed"' does not satisfy the expected type '"erased"'."
A5 state pin without its erased arm 0 diagnostics (the burn-down's future edit is valid on a typed spec)
A6 _blockTypeAgrees compares name against type TS1360 at that pin
A7 local label replaced by a plain string in _blockLabelObjectArmIsNotASpecLabel TS1360 at that pin

Installed 17.4.0:

leg mutation result
A5 state pin without its erased arm TS1360 "Type '"erased"' does not satisfy the expected type '"typed"'." (the arm is load-bearing today)
A1 as above 0 diagnostics: the typed-spec probes are not asked of an erased spec, as declared
V tripwire version row expects 17.3.0 vitest: that row alone red, 1 failed, 46 passed

⚠️ The first attempt at A1, A2 and A5 on spec main was a no-op. The replacement text was a substring of the anchor, so ablation-replace refused it ("the replacement count moved 1 to 1", paraphrased) and restored. Those three legs were re-run in --delete mode, and the table shows the re-run.

Tests and gates, on head 22226a8e9

  • pnpm exec vitest run --maxWorkers=2 packages/types/: 264 files, 5856 tests, all passed (installed spec).
  • Every test that references scripts/check-spec-symbol-derivation.mjs (31 files, scripts/__tests__ and package suites): 31 files, 630 tests, all passed.
  • pnpm check:spec-symbols: exit 0 ("0 untriaged collisions in 0 packages").
  • check-changeset-presence: RED first ("1 source file(s) of 1 released package(s) changed, and this change adds no changeset"). That gate counts every file under a released package's src/, tests included. So this PR carries .changeset/10916-joined-report-block-pin-two-specs.md with an EMPTY frontmatter, and the gate then reads exit 0 ("declared as releasing nothing").
  • Exit 0 as well: check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite, check:changeset-claims, check:pending-changeset-literals, check:control-bytes, check:new-line-citations (0 new citations), check:test-path-roots, check:installed-pin-claims, check:shell-escape-residue, check:vi-mock-specifiers, check:vi-mock-inherit.
  • check-governed-queue-guard --test on the three paths: NOT GOVERNED.
  • eslint (--no-inline-config) on the two changed source files: 2 files linted, 0 errors, 0 warnings. Narrowed on purpose; the config enables no type-aware linting, so this diff cannot move the verdict on an untouched file.
  • NOT MEASURED, prerequisite: check:spec-floors needs every package built (it reports no-artifact for all 19 spec consumers in an unbuilt worktree); this diff moves no manifest or dist.

The second slice this owes (not in this PR)

At the first spec bump past 17.4.0, derive JoinedReportBlock from @objectstack/spec/ui and converge JoinedSpecReport's blocks. Reading: Clause-② yes (narrowing). The published local type accepts any key through its index signature and types the legacy members; the spec's is closed and dataset-bound. It should land WITH the bump: on a typed spec, JoinedSpecReport intersects the spec's blocks (from ReportParsed) with the local ones, which makes columns both names and objects (read from the two declarations, not compiled).

Acceptance notes

  • objectui#3162 is CLOSED and its body still records the 17.2.0 verdict for this symbol. It is not edited here; the in-tree ledger row is.
  • spec-derived-unions.test.ts keeps its batch-8 note that this symbol was "STILL erased"; that note is a dated reading at spec 17.2.0 and is left as history.

This change was made in Claude Code session https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk.


Generated by Claude Code

… and on spec main, and trips at the bump (objectui#10916)

objectstack#20369 typed JoinedReportBlockSchema on objectstack main, so the
inverted `true satisfies IsUnknown` pin fired on the Spec Main Shape Gate,
as its docblock intended. The PR type-check still compiles the same file
against the published 17.4.0, which erases the type.

The single pin becomes two halves. At compile time a state pin admits
`erased` and `typed` and refuses `any`; on a typed spec, divergence probes
record that the spec's block is the ADR-0021 dataset-bound shape and the
local interface the legacy inline-query one. At test time, a tripwire reads
the installed spec's declarations and fails on the first bump past 17.4.0,
which is when the burn-down becomes possible.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
…d the burn-down owed at the bump (objectui#10916)

The ALLOW reason said the spec declares `JoinedReportBlockSchema` as a
bare `z.ZodTypeAny` and that the pin fails the day the spec types it.
Both became false on objectstack main with objectstack#20369. The row now
records the installed 17.4.0 as case 2b, spec main as case 2c (precise,
but the ADR-0021 dataset-bound shape), and the burn-down as a published
type change owed at the first spec bump past 17.4.0.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
…ing nothing (objectui#10916)

check-changeset-presence counts every file under a released package's
src/, tests included, so the test-only rewrite needs an empty-frontmatter
declaration.

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

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 4 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/6286-explain-batch-cap-from-spec.md

  • names scripts/check-spec-symbol-derivation.mjs → scripts/check-spec-symbol-derivation.mjs — edited by this change

    No value changes and no behaviour changes. The spec exports 200, which is what the local copy said, verified by resolving the installed package and reading the export — both statically (dist/security/index.d.mts) and at runtime through the same specifier the source now uses. What changes is reference identity: if the server relaxes or tightens the cap and the spec follows, the client follows too, instead of paginating at the old boundary with no signal anywhere. The cap's whole point is that an over-cap request is refused with 400 VALIDATION_FAILED rather than truncated, so a client that silently disagrees with it is exactly the drift scripts/check-spec-symbol-derivation.mjs argues about — and could not catch here, because both of its scanners skip non-exported declarations and this const was module-local (objectui#5899).

.changeset/8841-tree-view-config-derive.md

  • names scripts/check-spec-symbol-derivation.mjs → scripts/check-spec-symbol-derivation.mjs — edited by this change

    What was wrong. @object-ui/types published TreeViewConfig as a hand-written interface — a copy of the protocol's ListView.tree block under a second name — and the copy declared a fifth key, titleField. @objectstack/spec@17.4.0 refuses that key there by name: TreeConfigSchema is a strictObject since spec #15469 closed the .passthrough() window 17.3.0 left open. So this package's published face accepted what the contract rejects, and an author who followed @object-ui/types was refused at publish with Unrecognized key(s) on this tree configuration: 'titleField'. The copy was invisible to scripts/check-spec-symbol-derivation.mjs, which matches spec symbols BY NAME — a hand copy renamed away from the spec's symbol has nothing for its rule 1 to match (objectui#4592's recorded blind spot).

.changeset/console-form-container-specs-one-declaration-5596.md

  • names scripts/check-spec-symbol-derivation.mjs → scripts/check-spec-symbol-derivation.mjs — edited by this change

    Both types are therefore derived from the spec's own FormSection / FormView with named narrowings — the repo's sanctioned form for a spec-shaped local type (scripts/check-spec-symbol-derivation.mjs) — rather than restated. Every key the two layers agree on comes from the spec and cannot fall behind it; the four positions where this layer is deliberately narrower are each named in an Omit list and restated once next to its reason: fields keeps the converged 26-key leaf (deriving it would silently re-open [finding] A THIRD inline copy of the form-field authoring contract lives in apps/console FormPage.tsx — objectui#5040 converged only the app-shell two #5542), and label / description / visibleWhen / visibleOn keep the shapes this repo's renderers and evaluators actually consume rather than the spec's I18nLabel and ExpressionInput. apps/console's submitBehavior union — previously hand-written under the comment "Mirrors the spec FormView.submitBehavior union" — is now read back off the shared type, making the mirror structural. @object-ui/app-shell re-exports both names from its package root (type-only, erased at build — nothing is added to the bundle), because a type that cannot be imported is a type that gets retyped.

.changeset/listview-comment-pair-4559.md

  • names scripts/check-spec-symbol-derivation.mjs → scripts/check-spec-symbol-derivation.mjs — edited by this change

    objectui#4966 — formatActionLabel's docblock now sits above formatActionLabel. It had drifted two declarations up, so the exported parseSortConfig carried two stacked leading comments and the helper carried none. This one was not cosmetic: because parseSortConfig is exported, vite-plugin-dts copied the misattributed block into dist/ListView.d.ts, so every consumer's editor hover and TypeDoc introduced the sort parser with a sentence about action labels. Moving the block removes it from the .d.ts; formatActionLabel is module-private, so its now-correct docblock does not appear there. It also matters to scripts/check-spec-symbol-derivation.mjs, whose rule 2 reads the comment block attached to a declaration — a misattributed docblock is the mechanism by which a claim gets scored against the wrong symbol. This block carries no spec-alignment phrase, so nothing fired today.

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.

Compared the checked-out tree with 733fd5ac6 (merge-base with origin/main): 2 file(s) changed outside .changeset/, read against 1669 pending declaration(s) that publish a body (2263 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3096.1 KB 3104.5 KB
Main entry chunk (gzip) 148.6 KB 350 KB
Entry file index-CXe0iCYr.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.58KB 6.17KB
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.22KB 10.61KB
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.70KB 2.23KB
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.70KB 10.94KB
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) 557.59KB 133.60KB
core (index.js) 9.93KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 226.44KB 63.00KB
fields (index.js) 261.01KB 66.28KB
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.32KB 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.96KB 14.83KB
plugin-charts (index.js) 83.99KB 22.86KB
plugin-chatbot (index.js) 197.67KB 46.90KB
plugin-dashboard (index.js) 136.93KB 36.48KB
plugin-designer (index.js) 215.03KB 44.21KB
plugin-detail (index.js) 233.48KB 61.79KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 161.21KB 41.41KB
plugin-gantt (index.js) 170.35KB 42.19KB
plugin-grid (index.js) 228.33KB 62.59KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.86KB 28.64KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.17KB 12.20KB
plugin-timeline (index.js) 31.00KB 9.09KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 88.55KB 22.21KB
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.81KB 3.58KB
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) 119.16KB 39.05KB
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) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
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.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.22KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
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.26KB
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.27KB 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 22226a8e9bde1f06f47c2faaa698cfb9148793de

Isolated adversarial contract review of PR #10934 (card objectui#10916). Inputs: the card and its five comments (the dev's os-dev-report 5866360163 read as a claim), objectui#9860 (ruling C) with all 17 comments, the gate workflow and scripts/spec-main-shape-gate.mjs on origin/main, precedents objectui#10080 / PR #10089, objectui#3177 and objectui#3162, objectstack#20369 and objectstack origin/main 2c310705f (packages/spec/src/ui/report.zod.ts, joined-report-block-type.test.ts, i18n.zod.ts, identifiers.zod.ts, strict-object.ts), the PR body, diff, file list and the 43 check-runs on the head, and the objectui tree at the head and at origin/main 733fd5ac6. Every reading below is this seat's own unless marked as the dev's claim. Generic parameters are written as UPPERCASE WORDS.

① Derived judgments

Not a weakening of the gate. The gate (ruling C) is objectui compiling against spec main; the old line was a consumer-side tripwire, true satisfies IsUnknown OF SpecJoinedReportBlock, whose docblock said its own failure is the instruction to re-run the triage and burn down. The typed schema landed upstream (objectstack#20369, merge 681868ca7c, an ancestor of objectstack main), the tripwire fired as designed, and the burn-down cannot land yet: on the installed 17.4.0 the derived type would be unknown (read from the installed dist/page.zod-DiFhqKNm.d.ts: declare const JoinedReportBlockSchema: z.ZodTypeAny and type JoinedReportBlock = z.input OF typeof JoinedReportBlockSchema, with zod 4.4.3 resolving from the spec's own directory). The claim comment 5865635869 fenced exactly this: a burn-down that changes the published declaration stops and becomes a second slice. What the old pin caught — the spec typing the block — is now a recorded fact rather than a pending event; any is still refused (the IsAny pin is unchanged and the state pin refuses any). Nothing is skipped, disabled or ts-ignored, no workflow moved, and the gate is green on the head because the file compiles, not because it was excluded.

The dual admission is justified and load-bearing. Read at the head: erased is what the installed 17.4.0 declares (the pull-request Type Check compiles this file against it); typed is what spec main declares (the gate compiles the same file against it). One file, two compilers, so one pin admitting both is the only shape that compiles on both without a skip. Verified in a worktree at the head with a frozen offline install: tsc -p tsconfig.test.json in packages/types gives exit 0 with 0 diagnostics on the installed spec; deleting the erased arm gives exit 2 with exactly one diagnostic, TS1360 Type '"erased"' does not satisfy the expected type '"typed"' at _specJoinedReportBlockTyping — so the arm is load-bearing today and the dev's ablation A5 reproduces. The file was restored to blob a1b2c707461e, equal to HEAD.

Declared limit, judged real and acceptable. A re-erasure on spec main before the bump is type-identical to the installed 17.4.0 (both are the bare z.ZodTypeAny), so the state pin admits it on the gate. This seat looked for a compile-time marker caused by this schema that could refuse it: objectstack#20369 added JoinedReportBlockParsed to the ui entry, but it is a type-only export and a type's presence cannot be probed conditionally at compile time (a missing type import is a hard error on 17.4.0); the schema value's own annotation is the same predicate as the erasure and adds nothing. So the dev's falsification of Zone 2 item 3 stands. The window is covered at the producer: packages/spec/src/ui/joined-report-block-type.test.ts on objectstack main carries seven @ts-expect-error lines that become TS2578 under check:test-typecheck the moment the block is unknown again (objectstack#20369's own reverse leg reports 7 TS2578 and 2 TS18046 on reverting item 1).

What forces the burn-down at the bump, and it fires. The Spec Main Shape Gate runs TURBO_FORCE=true pnpm type-check --continue and no test (workflow read on origin/main), so the forcing lives in vitest, which the required Test aggregator runs on every pull request including a spec-bump PR. Two rows read the INSTALLED spec's built .d.ts with the TypeScript checker: the typing row (must read erased) and the version row (must read 17.4.0). Verified: the file gives 47 passed on the installed spec; with the version row's expectation changed to 17.3.0, 1 failed / 46 passed with the row's own message; with the reader forced to answer erased for every symbol, the lit control (ReportSort must read typed) is the one row that fails, 1 failed / 46 passed — so the control is a real control and the typing row cannot pass through a dead reader. The reader resolves @objectstack/spec/package.json through createRequire(import.meta.url) from the test file's own directory, which is packages/types/node_modules/@objectstack/spec, the same symlink tsc resolves; the lockfile holds exactly one 17.4.0 entry and the worktree's virtual store one copy, so there is no hoisted duplicate to read instead. The exact 17.4.0 match is sound rather than churn: npm's latest is 17.4.0 (the newest of eight 17.x releases), objectstack's own retirement text in report.zod.ts names 17.5.0 as the next release, and #20369 is on main behind that release, so any bump moves the version row and the typing row together. ⚠️ What the row forces is an edit to this file; which edit (delete the erased arm, derive the published interface in its own Clause-②: yes slice) is prose in the failure message and the docblock, which is the same forcing the old pin offered. That slice will carry its own contract review, so this is recorded, not held against the PR.

The seven probes, each read against spec main today and mutated. Spec main's JoinedReportBlockSchema is lazySchema wrapping strictObject(options, shape), with lazySchema returning its factory's type and strictObject a closed z.object(shape).strict(); the shape declares name (SnakeCaseIdentifierSchema, a z.string()), label and description (I18nLabelSchema.optional(), a union of z.string() and InlineLocaleMapSchema, whose input is Record OF string, string with key and defaultValue typed never), type (enum tabular/summary/matrix, defaulted), dataset, rows, columns (z.array(z.string()), the matrix across-axis), values, runtimeFilter, order; objectName, object, filter, groupings and friends are alias-table entries aimed at dataset / runtimeFilter / rows, and chart is a guidance entry (objectstack#20161's retirement prescription), not a declared key. So the recorded divergences are TRUE of spec main today: name and type agree; the local { default, translations } label arm is not assignable to the locale map; columns is dimension names there and column objects here; the five local-only keys are undeclared and the closed schema refuses them; the five spec-only keys are exactly dataset, rows, values, runtimeFilter, order; the spec type has no index signature. To ask whether the probes can be satisfied vacuously, this seat hand-derived the input type from that shape, pointed a verbatim copy of the PR's type machinery at it beside the REAL local interface from spec-report.ts, and compiled twelve variants under the package's tsconfig.test.json: the as-read shape gives 0 diagnostics (the triage table is consistent with the schema); spec columns as objects fires _blockColumnsAreTwoConcepts alone; spec label admitting the local object arm fires _blockLabelObjectArmIsNotASpecLabel alone; a new spec key fires _specOnlyBlockKeys alone; a branded name fires _blockNameAgrees alone; type gaining joined fires _blockTypeAgrees alone; chart declared as a never tombstone fires _localOnlyBlockKeys; unknown gives 0 diagnostics; any fires the state pin and the IsAny pin; never fires five pins. Two vacuities found: if the spec DROPS columns, BlockMember yields never and [never] extends [string[]] is true, so probe 3 stays green; if the spec drops both label and description, probe 4 stays green by the same route. Both cases fire _localOnlyBlockKeys (the dropped key becomes local-only), so the file as a whole is not vacuous — only the per-probe attribution is. No boolean-leaking conditional: every probe is tuple-wrapped and SpecTyping checks a non-naked type, so no probe can resolve to boolean and slip through true satisfies.

Published surface untouched. git diff origin/main refs/review/pr-10934 -- packages/types/src/spec-report.ts packages/types/src/index.ts is empty; the diff is one test file, one script and one changeset. Clause-②: no is honest.

Ledger edit. ALLOW["@object-ui/types:JoinedReportBlock"].reason and the case-2b header comment in scripts/check-spec-symbol-derivation.mjs. The script consults ALLOW[key] for existence only (the use site is if (ALLOW[key]) { matchedAllowKeys.add(key); continue; }), so the edit cannot change behaviour; the governance suite's rule on entries (key shape, reason.length above 80, numeric issue) still holds. Each changed sentence is true of main plus this diff: 17.4.0 as case 2b, spec main as case 2c (the header's 2c explicitly includes "precise but describe a DIFFERENT shape"), the five spec-only and five local-only keys, the burn-down owed at the first bump past 17.4.0 as a published-type change, and the tripwire as the release condition. The suites reading the script are green on the head through the Test shards.

Head check-runs. 43 on 22226a8e9: 40 success, 3 skipped (the coverage matrix placeholder, Test (coverage), dependabot). Spec Main Shape Gate success, Type Check success, Test and all eight shards success, Changeset Declaration, Changeset Claim Re-read, Line Citation Gate, Lint success. Vercel status success.

② Semver level

Empty frontmatter, releasing nothing: correct. packages/types publishes dist and the change is a test file under src/__tests__/ plus a repo script; check-changeset-presence counts test files under a released package's src/ and asks for a declaration, and the empty-frontmatter declaration is that gate's own sanctioned answer, in the same shape PR #10089 used for objectui#10080. Every sentence of the prose is true: the pin now compiles against both the installed 17.4.0 (erased) and spec main (typed by objectstack#20369); the test-time tripwire fails at the first bump past 17.4.0; test and ledger text only. Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Overwrite Report and Changeset Claim Re-read are all success on the head.

③ Boundary flags

  • Open question A versus B: A is sound. B's marker would couple this pin to a symbol #20369 did not move and go red on unrelated spec changes; and, as read above, no marker caused by this schema can distinguish a pre-bump re-erasure from 17.4.0 at compile time. A leaves a real hole before the bump on the consumer gate only; the producer's pin closes it at the source, and the version row closes it at the bump. Recorded, not a defect.
  • Dev's falsifications hold. The changeset falsification reproduces in the gate's own counting rule; the "still fail on re-erasure" falsification reproduces in this seat's marker search. objectui#4231 being a bump with no two-spec leg is consistent with the precedent set the card names.
  • Out-of-scope notes are sound. The owed burn-down replaces the published JoinedReportBlock and JoinedSpecReport.blocks (SpecReport aliases the spec's ReportParsed, so on a typed spec the intersection would type columns as both names and objects): Clause-②: yes (narrowing), landing WITH the bump, is the right reading. objectui#3162 is closed and its body still carries the 17.2.0 verdict; leaving a closed card's body alone and moving the in-tree row is correct. Nothing in the tree reads the local interface's five legacy members: the joined branch of DatasetReportRenderer reads dataset, rows, columns, values, runtimeFilter and order through readNames and readOrder.
  • PR body. No path:line address anywhere (scanned for file-colon-number and paren coordinates), no angle brackets, diagnostics quoted by code and text. Statements checked and true: 17.4.0 is npm latest (published 2026-09-09, before #20369); the gate runs type-check and no test; the two later commits touch only scripts/ and .changeset/; the ablation rows A5 (installed) and V reproduce here. The draft state, the missing ready flip and auto-merge are the seat's calls, not this record's.
  • Minor, non-blocking, for the bump slice to know: probes 3 and 4 are vacuous when the spec DROPS columns or both label keys (probe 2 catches it); the forcing at the bump is a test row plus prose; the version row's "every release after 17.4.0 carries #20369" assumes releases are cut from main, which is how this spec releases.

Implemented-by: claude/issue-10916-joined-report-block-pin
Reviewed-by: session_01DuWo5bdP9SdVebamn99GGk

VERDICT: PASS


Generated by Claude Code

huangyiirene pushed a commit that referenced this pull request Sep 28, 2026
…ocks

Picks up PR #10934 (objectui#10916), which clears the repo-wide Spec Main Shape Gate red. No conflict.

Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…bjectui#4356) (objectstack-ai#10930)

Fixes objectstack-ai#4356

Clause-②: yes — published runtime behaviour is removed (the bare-string
lift, and the exported `resetDashboardFilterWarnings` if it retires with
its memo), per the execution parameters in `5511028727`.

The claim's "if" resolved to **stays**: the memo survives (it now
dedupes the dropped-member warning, see "The audible-skip question"), so
`resetDashboardFilterWarnings` keeps its job and its export; the
changeset names no removed export.

Final head `a890654027a69911412efbcb146e326da8476256`. Commits on this
branch since the branch point `733fd5ac6`: `bc4ea976e` (the retirement),
`98623a24d` (rework round 1: W1–W3), `e0647486b` (patch round 2: W5),
the two merges of `origin/main` (`1daf7edf7` at `29c76e573`, `a1058f90e`
at `2eaf5be27`), and `a89065402` (W4, the dated note). Implemented in
session `session_012UwY3ahMixEFkfTUxMVkYm`.

## What changed

Phase 2 of objectui#4356 — the runtime bare-string
`globalFilters[].options` lift retires.

- `packages/core/src/utils/dashboard-filters.ts` — in
`normalizeFilterOptions`, the non-object arm no longer pushes `{ value:
String(o), label: String(o) }`. A member that is not an object (string,
number, boolean) yields NO option: a shorthand-only filter resolves with
no `options`, a mixed array keeps only its object members. The nullish /
value-less skips are unchanged. The Phase 1 deprecation warning
(`warnShorthandOptions`, whose text said "Still lifted here") is
deleted; a dev-mode, once-per-filter `warnDroppedShorthandOptions` names
the filter, the dropped members and the rewrite, and promises no lift.
The `DashboardFilterDef.options` JSDoc, the `normalizeFilterOptions`
docblock, the memo docblock and the `resetDashboardFilterWarnings`
docblock stop describing a lift. No lift, alias, fallback or migration
is added anywhere.
- `packages/core/src/utils/__tests__/dashboard-filters.test.ts` — every
lift-pinning test is flipped to pin the new contract (red-first, quoted
under Tests): `normalizes options: spec {value,label} objects AND
bare-string shorthand` became `normalizes spec {value,label} objects,
and DROPS a bare-string member rather than lifting it`; the `[objectstack-ai#4356]`
describe is `retired: dropped, not lifted` — a shorthand-only filter
yields no `options`; a mixed array keeps only its object members; a
number and a boolean are dropped too; warn ONCE across renders while
dropping on every render; a different filter warns separately; canonical
`{ value, label }` options and an `I18nLabel` map are untouched and
silent; `resetDashboardFilterWarnings` re-arms the memo. The stale Phase
1 header above the block is removed (W3).
- `content/docs/guide/dashboard-filters.md` — the "Deprecated: … still
lifted by the runtime" callout became false and now reads "Not
accepted": refused at publish, no longer lifted, a bare-string member
yields no option, rewrite `"X"` as `{ "value": "X", "label": "X" }`. No
fenced block changed.
- `packages/types/src/zod/complex.zod.ts` — the JSDoc of the exported
`GlobalFilterSchema` said the runtime "still LIFTS a stored bare-string
option on read"; that sentence ships in `@object-ui/types`' emitted
declarations and is corrected in place. Outside the claim's original
surface; accepted by the seat's addendum, see "Deviations".
- `packages/plugin-dashboard/README.md` (W1, ships in that package's
`files`) — the `globalFilters` example's `options` comment no longer
says a stored bare string "is still lifted at runtime, and warns"; it
says NOT accepted, the runtime no longer lifts it (objectui#4356), a
bare member yields no option, a dev-mode warning names it, rewrite each
X as `{ "value": X, "label": X }`.
- Three test docblocks state the retirement instead of the lift (W2):
`packages/types/src/__tests__/dashboard-global-filters-spec-binding.test.ts`,
`packages/types/src/__tests__/report-chart-query-spec-parity.test.ts`
(its header docblock only — nothing near objectui#10916's
`JoinedReportBlock` pin, which PR objectstack-ai#10934 rewrote and this branch has
merged),
`packages/plugin-dashboard/src/__tests__/DashboardRenderer.filters.test.tsx`.
A fourth (W5),
`packages/plugin-dashboard/src/__tests__/DashboardFilterBar.i18nLabel.test.tsx`,
no longer says the shorthand "is deprecated and now warns" nor names the
renamed lift pin; it names the current DROP pin. No assertion moved in
any of the four.
- `.changeset/retire-options-shorthand-lift-4356.md` — `@object-ui/core:
minor`, opening with the ruled wording: "bare-string
`globalFilters[].options` is no longer lifted; use `{ value, label }`
objects, the spec's form". Never `major` (AGENTS.md §9 version policy:
the break is stated in the body, the bump stays `minor`);
`check-changeset-no-major` exit 0.
- `.changeset/7759-dashboard-header-global-filters-spec.md` (W4) — a
dated note appended, nothing else touched. That pending entry says "The
runtime is unchanged too: `@object-ui/core` still converts a STORED
bare-string option into a pair when it reads the document, and logs a
deprecation warning. That conversion keeps its own retirement schedule
(objectstack#7917)." — a sentence this PR makes false in the same
release. The note (PR objectstack-ai#10891 form, dated 2026-09-29) says which
sentences no longer hold and what ships. Authorised by the maintainer's
ruling of 2026-09-29, option 「Allow the appended note (Recommended)」,
recorded on the card as `5883360525`; the option reads, verbatim: "You
allow developers to append dated correction notes to pending .changeset
files (appended only, no existing line or frontmatter edited). The
objectstack-ai#10930 developer adds the W4 note inside that PR, then it merges main,
gets a final review, and merges." Measured: 16 insertions, 0 deletions;
frontmatter md5 `ac1bfd0472c0d0b981d923c32d435f18` before and after;
whole-file md5 `94edd5855709afe137f9184c630f298c` →
`5c8f0508bed5ed6f5b76254bb74d622d`; 0 angle-bracket characters; `diff`
against `origin/main` shows only the appended block.

Untouched on purpose:
`examples/schema-catalog/test/plugin-dashboard-global-filters-spec.test.ts`
(the `GlobalFilterSchema` catalog guardrail from PR objectstack-ai#4601 — the positive
control; its docblock narrates history and spec facts only, and stays
true); the assertions of the two `packages/types` tests that pin the
SPEC's refusal of `options: ['EMEA']` (rejection pins, not lift pins);
`@objectstack/spec`; `content/docs/releases/**`; `skills/**` (no file
there becomes false — measured below).

## Rulings executed

- objectstack-ai/objectstack#7917, maintainer 2026-08-12, verbatim 「7917
②」 — the spec stays strict; the runtime bare-string lift retires.
- Maintainer, 2026-09-02, verbatim 「objectstack#7917 不考虑现有数据」 (director
release `5511028727` on objectui#4356) — Phase 2 on release cadence
alone; no live-tenant survey, no migration entry.
- Maintainer direct dispatch to the `domain:spec @ objectui` seat,
verbatim 「这个你也接管继续
objectstack-ai#4356」.
- Maintainer, 2026-09-29, 「Allow the appended note (Recommended)」 (card
comment `5883360525`) — authorises the W4 append in exactly the form
landed here.

## The audible-skip question — governing text found

The dispatch asked whether any governing text requires the post-removal
skip to be audible. What was read, verbatim:

- objectstack `AGENTS.md`, "Degradation log levels — `warn` vs `error`":
"**Functional degradation → `warn` / `info`.** A screen is missing, a
trigger is not armed, a capability is not enabled … The system is
*visibly* smaller than it should be, and the next person to use the
missing thing finds out." and "**prefer failing to falling back**, and
when you must fall back, say what was lost." — A `select` filter whose
members were all bare strings now renders with an empty option list
(`DashboardFilterBar`: `const authored = def.options?.length ?
def.options : (dynamicOptions ?? [])`): visibly smaller, functional,
`warn`-level — and "say what was lost" is the operative sentence.
- objectstack `docs/adr/0078-no-silently-inert-metadata.md`, §4
"Loud-not-silent at the runtime seam": "Where the runtime *skips* an
inert instance … it emits a **dev-mode diagnostic** at registration
rather than swallowing it silently. This is the only layer that is
**authoring-tool-agnostic**". The ADR's completeness gate (§1) is about
Zod-VALID instances and a bare string is Zod-invalid, but §4 is exactly
the case here: a STORED dashboard is the one document no author-time
gate re-reads.
- objectui `AGENTS.md`: no clause on audibility; #0.1 (contract-first,
no lenient fallback) is honoured — a diagnostic is not a fallback, and
nothing is lifted.
- The module's own convention, same file: `buildFilterCondition` —
"anything else → skipped with a console warning, never silently
downgraded"; `buildWidgetScopedFilter` skips an unknown default field
"with a console warning instead of emitting a query".

Conclusion: yes, at `warn`, dev-mode, deduped — the smallest audible
path consistent with the ruling. Implemented as
`warnDroppedShorthandOptions`: same `isDev()` gate and memo key as Phase
1 (no per-render flood), new sentence (DROPPED, "no longer lifted", the
rewrite), no lift. Consequence: the memo and therefore
`resetDashboardFilterWarnings` stay. The contract review (`5866198610`)
judged this RIGHT; dev-only rather than always-on is the same review's
answer for this PR.

## Measurements against the dispatch's mechanism assumptions

1. Lift live on `origin/main` `733fd5ac6` — confirmed: the `else` arm
pushed `{ value: String(o), label: String(o) }` and called
`warnShorthandOptions` for EVERY non-nullish primitive. Pinned now in
`drops EVERY non-object member — a number and a boolean, not only a
string`.
2. `resetDashboardFilterWarnings` — `git grep` repo-wide: importers are
`dashboard-filters.test.ts` only (plus a `packages/core/CHANGELOG.md`
paragraph); it clears only `warnedShorthandOptions`. The inference "with
the memo gone it has no job" is falsified by (4): the memo does not go.
It stays, with a docblock saying why.
3. Fixture recount, repo-wide over every file containing `globalFilters`
(tests included; positive control: the same matcher found the callout it
was meant to find): lift-pinning bare-string members existed only in
`dashboard-filters.test.ts` — six members across five tests (the
dispatch's list lacked the bare `'lost'` of the mixed-array test; the
director's "three" and the dispatch's "five" were both stale). All six
retired or flipped. The two `packages/types` occurrences pin the spec's
REFUSAL and keep their assertions.
4. Skip audibility — see the section above.
5. Teaching sites — swept at the final head with `git grep -n -i -E
'lift' -- ':!**/CHANGELOG.md' ':!content/docs/releases/**'
':!pnpm-lock.yaml' | grep -i -E 'shorthand|bare[- ]string|4356'` over
the whole tracked tree (a superset of every package's `files[]`,
`content/docs`, `skills/`, `examples/`): 20 hits on `bc4ea976e` with
`packages/plugin-dashboard/README.md`'s "still lifted at runtime" line
as the found positive control; 18 on `a89065402` — every hit either
states the retirement (this PR's changeset, source, docs and test text)
or is the unrelated `data`-array shorthand in `plugin-grid` /
`plugin-map`. The first-round sweep's `option|filter` post-filter is
what hid the README line (W1) and the `i18nLabel` comment (W5); the
rework sweep has no post-filter. When the sweep runs on a ref, the ref
prefix is stripped before the second grep — this branch's name contains
both "shorthand" and "lift". Three other sites author `options: ['…']`
on `select` FIELDS (form / object field schemas:
`content/docs/guide/expressions.md`,
`content/docs/guide/schema-playground.md`,
`skills/objectui/guides/project-setup.md`) — a different contract, out
of this card, untouched; the governed `skills/**` file does not become
false.
6. Release — `packages/core/package.json` is `17.6.0`;
`.changeset/config.json` has `@object-ui/core` in the `fixed` group; a
`minor` changeset makes the next release ≥ 17.7.0; release PR objectstack-ai#5400 is
open (not touched).
7. Pending changesets — swept on the merged tree (2326 pending entries)
with `git grep -n -i -E 'normalizeFilterOptions|bare[-
]string|shorthand|lift' -- '.changeset/*.md'` filtered to option /
`globalFilters` / 4356 / 7917 context: exactly ONE pending sentence this
PR makes false, the `7759` paragraph quoted above (W4). Its earlier
sentence "the bare-string option shorthand (`options: ['EMEA']`) is
refused with `invalid_type`" stays true. Every other hit is a different
contract (export-options bare array, object-map / gantt `data` arrays,
kanban `columns`, picklist / field `options`, widget shorthand). The
nine entries the `changeset-claim-re-read` comment `5865883972` lists
were re-read: none says anything about the options lift; all still true.

## Merges of `main`

- `1daf7edf7` — `origin/main` `29c76e573` (PR objectstack-ai#10934, objectui#10916).
No conflicts; `report-chart-query-spec-parity.test.ts` auto-merged
(disjoint hunks); both sides verified — my header-docblock correction
present, PR objectstack-ai#10934's pin text byte-for-byte as `main` has it.
- `a1058f90e` — `origin/main` `2eaf5be27` (70 commits, among them PR
objectstack-ai#10914's citation edits in `dashboard-filters.ts` and three
`complex.zod.ts` commits). No conflicts; after the merge the files
differing from `origin/main` are exactly this PR's ten, and `git diff
origin/main` on `complex.zod.ts` and on the parity test is exactly this
PR's docblock hunks.

No rebase, no amend, no force-push at any point; every push was a plain
`git push`.

## Tests

Red-first (at `bc4ea976e`, the new pins against the UNTOUCHED source,
`pnpm exec vitest run
packages/core/src/utils/__tests__/dashboard-filters.test.ts`, exit 1):

```text
× normalizes spec {value,label} objects, and DROPS a bare-string member rather than lifting it (objectui#4356)
× DROPS a bare string: a shorthand-only filter resolves with no `options`, and says so
× keeps ONLY the object members of a MIXED array, and names only the dropped ones
× drops EVERY non-object member — a number and a boolean, not only a string
× warns ONCE per offending filter across repeated renders, and drops on every one of them
AssertionError: expected [ …(2) ] to be undefined
AssertionError: expected [ Array(2) ] to deeply equal [ { value: 'won', label: 'Won' } ]
AssertionError: expected [ { value: '42', label: '42' }, …(2) ] to deeply equal [ { value: 'gold', label: 'Gold' } ]
 Test Files  1 failed (1)
      Tests  5 failed | 44 passed (49)
```

The three that stayed green on the old code are the ones the block
labels as non-discriminating (different-filter memo, canonical silence,
memo reset). Green after the source change, same command: `Tests 49
passed (49)`.

Final head `a89065402`, one `pnpm exec vitest run` from the repo root
under the shared verify lock over `packages/core/` plus the two
`packages/types` global-filter tests and the two `plugin-dashboard`
dashboard-filter tests this PR touched: `Test Files 188 passed (188)` ·
`Tests 3725 passed | 27 skipped (3752)`, exit 0. The changeset script
tests (`check-changeset-{presence,no-major,fixed,overwrite,claims}`,
`check-pending-changeset-literals`, `changeset-polarity-census`): `Test
Files 7 passed (7)` · `Tests 292 passed (292)`, exit 0.

Consumer sweep at `bc4ea976e` (`resolveDashboardFilterDefs` consumers
re-derived by `git grep`: `core`, `types`, `plugin-dashboard`,
`plugin-designer`, `app-shell`, `examples/schema-catalog`), exit 0:
`packages/core` 182 files / 3643 tests; `packages/plugin-dashboard` 147
/ 1346 (6 pre-existing skips); `packages/types` 3 / 58;
`packages/plugin-designer` (`DashboardDesignPage.presetAlias`) 1 / 4;
`packages/app-shell` (`DashboardWidgetInspector.test.tsx`) 1 / 19;
`examples/schema-catalog` (the guardrail) 1 / 9 — totals `335 passed
(335)` · `5073 passed | 6 skipped (5079)`. No consumer fixture asserted
the old lift. The code has not changed since that head (every later
commit is prose or a merge), and the final-head run above re-covers
`core`, `types` and the touched `plugin-dashboard` tests.

## Gates on the final head `a89065402` (hand-derived from root
`package.json` and `.github/workflows/`; exit codes captured before any
pipe)

| gate | exit | note |
|---|---|---|
| `pnpm --filter '@object-ui/core^...' build` then `pnpm --filter
@object-ui/core build` | 0 / 0 | dependency closure first, so the
type-checks read rebuilt declarations |
| `pnpm --filter @object-ui/core type-check` · `pnpm --filter
@object-ui/types type-check` | 0 / 0 | |
| `pnpm --filter @object-ui/core lint` · `pnpm --filter @object-ui/types
lint` | 0 / 0 | 0 errors; the warnings are pre-existing
`no-explicit-any` — on the touched files the `as any` counts equal the
base (2 / 10 / 0), unchanged since `bc4ea976e` |
| vitest `packages/core/` + the four touched dashboard-filter tests
(under the lock) | 0 | 188 files, 3725 passed, 27 skipped |
| vitest changeset script tests ×7 | 0 | 292 passed |
| `node scripts/check-changeset-presence.mjs` | 0 | 11 files changed vs
merge-base `2eaf5be27`, 7 published source files of 3 released packages,
1 changeset added |
| `node scripts/check-changeset-no-major.mjs` ·
`check-changeset-fixed.mjs` · `check-changeset-claims.mjs` ·
`check-changeset-overwrite.mjs` | 0 ×4 | claims and overwrite are
report-only; the W4 append is overwrite's documented "correcting a
declaration on purpose" case |
| `pnpm check:pending-changeset-literals` | 0 | "No test source names a
pending changeset" |
| `node scripts/check-control-bytes.mjs` | 0 | 9331 tracked text files |
| `node scripts/check-new-cross-file-line-citations.mjs` | 0 | 0 new
citations (the W4 note cites by quoted sentence) |
| `node scripts/check-governed-queue-guard.mjs --test` (every touched
path) | 0 | NOT GOVERNED |
| `pnpm check:doc-fences` · `check:doc-types` · `check:doc-example-ids`
· `check:spec-symbols` · `node scripts/check-doc-links.mjs` ·
`check:test-path-roots` · `check:vi-mock-specifiers` ·
`check:phantom-deps` | 0 ×8 | measured at `bc4ea976e` / `98623a24d`; the
files they read have not changed since |
| `pnpm check:doc-snippets` · `pnpm check:readme-exports` | 2 / 1 | NOT
MEASURED locally — both `PRECONDITION NOT MET` (unbuilt packages); CI's
Doc Snippet Type Check and README Exports are the readings. The docs
hunk changes no fenced block; the README hunk is a JSON comment with no
import binding |

Not run locally, CI-owned: repo-wide `pnpm lint`, `pnpm test`, `pnpm
type-check` (the public type face is unchanged — no export added or
removed, no signature changed; emitted declarations differ in JSDoc text
only), `pnpm check`, the i18n gates, `plugin-dashboard`'s own type-check
(comment-only hunks in two test files).

## Deviations

- `packages/types/src/zod/complex.zod.ts` was outside claim
`5865368364`'s original file surface; edited under the bounded
in-place-fix exemption (same defect class; mechanical; no other open PR
on the file; inside core's type-checked closure) and accepted by the
seat's addendum `5865980488`. The later addenda `5866223135`,
`5866346457`, `5866480991` added the W1–W5 files and the `7759`
changeset.
- The first-round teaching sweep under-reported (W1, W5) because of its
`option|filter` post-filter; corrected above.
- The W4 append was first refused by the auto-mode classifier ("Modify
Shared Resources", record `5866443287`), held and not re-routed; it
landed only after the maintainer's 2026-09-29 ruling.

## Acceptance notes

- `warnDroppedShorthandOptions` is dev-only, exactly as the Phase 1
warning was; the module's other skip warnings are not dev-gated.
Always-on is a one-line product decision the review left to the
maintainer; not this PR's.
- Field-level `select` schemas teaching `options: ['…']` are a different
contract and were not judged here — observation only, not filed.
- `packages/core/CHANGELOG.md` and
`packages/plugin-dashboard/CHANGELOG.md` narrate the lift as history;
published record, not re-addressed (the CHANGELOG clause under AGENTS.md
objectstack-ai#11: never re-address a paragraph in a published CHANGELOG).
- PR objectstack-ai#10914 (objectui#10803 batch 6) has landed on `main` and is merged
into this branch; no other open PR touches this PR's files.

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

---------

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

3 participants