Skip to content

fix(plugin-list): retire the legacy title alias and pin the rowActionDefs exemption - #9470

Merged
claude[bot] merged 3 commits into
mainfrom
claude/issue-8653-listview-title-undeclared
Sep 14, 2026
Merged

fix(plugin-list): retire the legacy title alias and pin the rowActionDefs exemption#9470
claude[bot] merged 3 commits into
mainfrom
claude/issue-8653-listview-title-undeclared

Conversation

@os-sam

@os-sam os-sam commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Fixes objectui#8653.

That closer is deliberate and it is the only one in this body: both of the card's items are answered and pinned here — item 1's exit is chosen by measurement, item 2's exemption is confirmed and given the pin the card asked for. Question 2 at the bottom is where to say so if the seat reads item 1 as still open.

needs:contract-review is on the card and this PR is written to be judged: every accept-set change and every public-surface change is enumerated below, one by one, and every published claim here was executed before it was written down.


1. Premise verdicts — the card measured at origin/main 154fe2a, and that surface has moved

Branch base: e3cb47624e. ListView.tsx has taken several merges since 154fe2a (the dispatch names six), so every line number, count and "is read undeclared" claim on the card was re-derived here before anything was edited. Declaredness was measured with the TypeScript checker (checker.getPropertyOfType over emitted declarations — the partial-schema-collapse-pin.test.ts instrument), never a grep (objectui#8410).

# Card premise Verdict Measurement
P1 PR objectui#9362 has landed on main, so the file is free TRUE git log --oneline --grep='#9362' origin/main names c10bc4c89e; control --grep='#9366' names the branch base e3cb47624e.
P2 title is read at ListView.tsx:3122 FALSE (stale line) Re-located BY SYMBOL, not by line: the read sits in the export-filename builder, as viewLabel taking schema.label OR-ed with (schema as any).title.
P3 title is not a declared member of ListViewSchema TRUE Checker: title resolves to no property symbol on ListViewSchema. The probe is a one-shot reading, so it carries its controls with it: label, objectName, columns, rowActions, bulkActionDefs all resolve; zzqx_no_such_key does not; an index-signature-free control type resolves title.
P4 it "compiles only through the string index signature" FALSE The site is an as any CAST, at 154fe2a too (git show 154fe2a:... prints the identical line), so the index signature never carried it. The checker also reads that signature as unknown, not any — a bare schema.title would not have compiled into a string slot. ⭐ This matters: the cast was laundering a real defect (§3).
P5 rowActionDefs is read at ListView.tsx:1970, ONE site FALSE (stale line AND stale count) There are two read sites: the listViewPredicates(...) harvest and the relay into the child object-grid node. Both re-located by symbol.
P6 rowActionDefs is not declared on ListViewSchema TRUE Same checker probe; also absent from ObjectGridSchema, which is why both renderers cast.
P7 the rowActionDefs read site carries NO docblock TRUE Neither site did. Both do now.
P8 the rowActionDefs read site carries NO pin TRUE, and measured rather than asserted The only thing in the tree that names this read is a REASON STRING in app-shell's ObjectView.relayRungCensus-7559.test.ts WRITE_EXCEPTIONS. That census re-derives what the PRODUCER writes, not that the READER reads. Ablation leg D below deletes both ListView reads and the census still passes 24/24, exit 0.

2. The exit each key takes, and why the contract chose it

The objectui#8327 family triage ruled that packages/types is a mirror: declaring a key here that the platform does not declare makes this repo accept what the platform refuses. So the exit is decided per key by the contract. Measured against @objectstack/spec 17.4.0, as KEY verdicts over otherwise-legal fixtures:

key spec ListViewSchema spec ObjectGridPropsSchema
title REFUSED by name (unrecognized_keys: ['title']) ACCEPTED
rowActionDefs REFUSED by name REFUSED by name
label (positive control) accepted accepted
bulkActionDefs (positive control) accepted accepted
zzqx_no_such_key (nonsense control) refused refused

Both controls fire in the same run, so the refusals are readings. All five rows are re-derived every test run in §1 of the new pin — ⛔ nothing above is a number this PR asks you to trust.

titleretire the read (the objectui#7129 route, as at objectui#7623)

  • Declare is off the table. The platform refuses title on the list surface while accepting it on the grid surface. That asymmetry is exactly why objectui#6639 could take the declare branch for ObjectGridSchema.title in 2026-08-29 and this site cannot: declaring it here would publish a key the save gate rejects.
  • Route-to-producer is not warranted either. The platform already publishes label for this job on the list surface, and the reader already read it first. Asking the platform to add a deprecated alias to a closed schema would widen the platform's accept set for zero authors.
  • Retiring costs no author a filename. objectui#6639 chose declare because authored object-grid.title nodes existed. Here they do not: a parse-based, order-agnostic sweep of apps/ examples/ content/ and packages/ found zero list-view nodes authoring title. The same instrument, in the same runs, reproduced objectui#6639's own reading on the grid population (its two content/docs/api/schema-reference.md hits) and returned zero for a nonsense key. ⚠️ That census is a ONE-SHOT reading taken on this branch and nothing re-derives it (AGENTS.md 完善设计器的每一个细节 #9) — it is recorded as the reason the branch was chosen, never as a live fact, both here and in the source docblock.
  • ObjectGrid's own two title reads are untouched. They remain declared, ruled and read.

rowActionDefsthe objectui#5091 exemption extends; pin it here the way the grid's is pinned

The card's question, answered on five legs — the grid's three plus two this site adds:

  1. Same producer: app-shell's ObjectView derives it from objectDef.actions filtered by locations.includes('list_item') and writes it onto a fullSchema: ListViewSchema; plugin-view's ObjectView composes the same key onto a list-view node.
  2. The platform refuses it by name on both surfaces ⇒ declare is off the table on either.
  3. @object-ui/types declares it on neither mirror ⇒ that is why both renderers cast.
  4. This reader RELAYS it into the child object-grid node, where the data-table surface does declare it. baseProps is an explicit picklist, so that one line is the only way a host's composed row actions reach the row menu.
  5. This reader also feeds the projection. The defs carry visible / disabled CELs and recordIdField; deleting the read returns rows whose predicate operand was never selected — objectui#3501's fail-closed CEL fault arriving with a success receipt.

3. The defect the cast was laundering — repaired in the same expression

viewLabel is string. ListViewSchema.label is string | I18nLabel (the spec's inline locale map, published since objectui#4580's revised Q1). X || any collapses the whole expression to any, so an inline locale-map label reached sanitizeFileNameBase unresolved:

account-[object Object]-20260914-063843.csv

That string is the verbatim red-first reading below, taken on the unmodified tree. The read now goes through the spec's own resolveI18nLabel against the display locale — the same resolver, same argument order, ObjectGrid already used at its twin site.


4. Accept-set and public-surface changes, enumerated

  • Accept set — unchanged, both directions. Neither key is declared, retired, widened or narrowed on any face by this diff. packages/types is not touched at all: no interface member, no zod shape, no .d.ts. What a document may contain is byte-for-byte what it was.
  • Published type surface — unchanged. No export added, removed or re-typed. pnpm --filter @object-ui/plugin-list type-check covers both tsc --noEmit and tsc -p tsconfig.test.json; exit 0, zero error TS lines.
  • Runtime behaviour — one deliberate change. A list-view node authoring only title (no label) no longer contributes a view segment to the export filename; it exports as OBJECTLABEL dash TIMESTAMP dot EXT. Migration: write label, the declared slot on both faces. Nothing in the measured corpora has to change.
  • Runtime behaviour — one bug repaired. An inline locale-map label now resolves instead of stringifying (§3).
  • Everything else is comment and test. Two docblocks at the rowActionDefs sites, one at the retired site, and one new test file.

The claim Clause-② was filed conservatively because declare was one of three available exits at claim time. The measured exits are retire and pin, and neither widens the accept set — which is the review's question, answered with the table in §2.


5. Red-first — the pin was written and run BEFORE the repair

Run on the unmodified tree at e3cb47624e (the pin file added, ListView.tsx untouched): 2 failed | 10 passed, verbatim:

AssertionError: expected 'account-Quarterly Review-20260914-063843.csv' not to contain 'Quarterly Review'
AssertionError: expected 'account-[object Object]-20260914-063843.csv' not to contain '[object Object]'

After the repair: 12 passed (12). Both runs from the repo ROOT with --reporter=verbose (objectui#3378); apps/console occurs 0 times in each log.


6. Ablation — four legs, each proved on disk and restored by hash equality

Every leg: mutate, count the anchor occurrences before and after, compare the on-disk git hash-object against the HEAD blob, run, restore with git checkout HEAD -- PATH, and verify the restore by hash equality plus an empty git diff HEAD — ⛔ never by an exit code. All under trap ... EXIT INT TERM with absolute paths. HEAD blob for ListView.tsx: 17ff6add4af9dd38743f6ec7acd12621cd067dc6; every leg restored to exactly that.

leg mutation on-disk hash result
A delete the listViewPredicates read 4eecde37a5 §4 SITE 2 (projection) RED — 1 failed / 11 passed
B delete the relay read 2b7149eec4 §4 SITE 1 (relay) RED — 2 failed / 10 passed
C restore the OR-ed (schema as any).title arm 04f7e2d16c both §3 title assertions RED — 2 failed / 10 passed
D delete both rowActionDefs reads 5dc6180788 see below

Leg D is the one that measures the card's "NO pin" claim. On the same mutated tree, in the same run:

  • app-shell's ObjectView.relayRungCensus-7559.test.ts — baseline 24 passed, exit 0; mutated 24 passed, exit 0. The only thing in the tree that names this read does not notice it is gone.
  • the new pin — 3 failed / 9 passed, exit 1.

Leg B also reddens §3's "title is inert" case, and that is the control doing its job rather than a leak: that case uses rowActionDefs reaching the stub as its in-fixture proof that the stub received a real composition, so removing the relay removes the control. The test says so at the assertion.


7. Verification

Run from the repo root. Heavy runs went through scripts/pm/os-verify-lock.sh in argv form; exits read from the wrapper's own VERDICT command-exit line.

check exit
pnpm exec vitest run packages/plugin-list/ (whole package) 0 — 78 files, 964 tests passed; apps/console 0 occurrences
pnpm --filter @object-ui/plugin-list type-check 0 — after pnpm --filter '@object-ui/plugin-list^...' build (exit 0). ⚠️ The first attempt died with TS2307 Cannot find module '@object-ui/types' before its dependency closure was built: that is NOT MEASURED, neither a pass nor a red, and it was re-run.
eslint --no-inline-config on the two changed source files 0 — 0 errors; the new test file reports 0 warnings, ListView.tsx 185 pre-existing ones (no-explicit-any / react-refresh, none introduced). Counts read from --format json. The universe is narrowed deliberately: the config declares no projectService and no parserOptions.project, so type-aware linting is off and this diff cannot move any untouched file's verdict.
check:control-bytes 0 — plus a direct grep -naP over the three changed files (no hit; a planted control byte in a scratch file makes the same instrument fire)
check:new-line-citations 0
check:changeset-claims 0
check:test-path-roots 0
check:vi-mock-specifiers / vi-mock-override-shape / vi-mock-inherit 0 / 0 / 0
check:handler-key-reads 0
check:metadata-write-doors 0
check:shell-escape-residue 0
check:unreferenced-sources 0
check-changeset-presence.mjs 0 — 1 published source file, 1 changeset
check-changeset-no-major.mjs 0
check-governed-queue-guard.mjs --test on all three paths 0 — NOT GOVERNED, 3 paths against 5 governed surfaces, none matched
check:sdui-registration-pins NOT MEASURED — exit 2, No console build to weigh at apps/console/dist/assets, which that script itself prints as "not a pass". It is owned by performance-budget.yml, which builds the console; this diff touches no registration array.

Acceptance notes

Observed while measuring, ⛔ not filed and ⛔ not repaired here:

  • packages/types/src/objectql.ts and zod/objectql.zod.ts both describe ObjectGridSchema.title as read at viewLabel: schema.label || schema.title. The live ObjectGrid.tsx spelling has had the i18n resolver wrapped around schema.label for some time, so the quoted expression is prose drift, not a defect: the two sites, their count and their label-first semantics are all still accurate. Noted, not filed — a doc nit with a named successor: the next card that touches ObjectGridSchema.title or that ledger entry.
  • ListView's export-filename label argument (the OBJECT label, distinct from the view label this PR touched) is still handed through without the inline-locale resolver. It is a different argument on a different key with a different producer, and resolveObjectLabel already handles the translated case ahead of it. Noted, not filed; carrier: any card that revisits buildExportFileName's callers.

Open questions for the in-seat contract review

  1. Is retire the right exit for title, or does the seat want route-to-producer? The measurement says the platform refuses the key on this surface and no author writes it, and §2 argues route-to-producer would widen the platform for zero authors. If the seat disagrees, the objectstack-side card would have to ask @objectstack/spec's ListViewSchema to declare a deprecated title alias — and this PR's ListView.tsx hunk would revert to the cast plus a pin, exactly the objectui#8652 / objectstack#17987 shape.
  2. Is Fixes right, or should this be Part of? Both card items are answered and pinned, which is why Fixes is used. If the seat treats item 1's exit as still open pending (1), say so and it becomes Part of.

Session: session_01L5xpA5q533BgTTNADibEFt


Generated by Claude Code

…ionDefs` exemption

Two undeclared keys `ListView` reads, ruled per key by measurement rather than
by taste (objectui#8653, the objectui#8327 family's plugin-list card).

`title` — RETIRED. The export-filename read was
`schema.label || (schema as any).title`. `@objectstack/spec/ui`'s
`ListViewSchema` refuses `title` BY NAME while `ObjectGridPropsSchema` accepts
it, so declaring it on the mirror would make this repo accept what the platform
save gate rejects — the asymmetry that let objectui#6639 take the declare branch
for the grid and rules it out here. A parse-based census found zero `list-view`
nodes authoring the key. The `as any` was also laundering a defect: `X || any`
collapsed the expression to `any`, so an inline locale-map `label` reached
`sanitizeFileNameBase` unresolved and exported as `[object Object]`; the read
now goes through the spec's own resolver against the display locale, as
`ObjectGrid`'s twin site already did.

`rowActionDefs` — EXEMPT, STILL READ, NOW PINNED. objectui#5091's ruling
extends to both of this reader's sites: same producer, refused by name on both
surfaces, declared on neither mirror. Both sites gain the ruling in a docblock,
and a new pin asserts at runtime that the defs still reach the child
`object-grid` node and that a field named only by a row action's `visible` CEL
still reaches `$select`.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
Replaces the helper's `any` parameter with the narrow structural type the
assertions actually use, so the new file lints at zero warnings. No assertion,
fixture or control changes; `packages/plugin-list/src/ListView.tsx` is
byte-identical.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3061.4 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-DWuDl0Qq.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.69KB 6.21KB
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) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
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.41KB 1.23KB
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.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 26.08KB 7.56KB
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) 544.93KB 130.50KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 213.54KB 59.33KB
fields (index.js) 247.89KB 62.50KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
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.39KB 3.10KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 49.92KB 14.22KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.44KB 65.58KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.55KB 57.83KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.67KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.36KB 20.78KB
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) 99.04KB 32.62KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
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) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
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) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
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) 14.04KB 5.36KB
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

@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Contract review

domain:spec @ objectui seat, independent contract review of PR #9470 against card objectui#8653.
Head sha under review: 701218c682b3d5ce4ab135aca21ebb263e27811a.

Reviewed in my own detached worktree (git worktree add /tmp/claude-0/review-9470 701218c682 --detach),
never in a shared checkout. Fork point taken with git merge-base, ⛔ not from the API's base.sha:

$ git merge-base origin/main HEAD
e3cb47624efb38845604504a6277aebfb2f77ceb
$ git log --oneline e3cb47624e..HEAD
701218c682 test(plugin-list): spell out the zod result slice the 8653 pin reads
37e31c24ce fix(plugin-list): retire the legacy `title` alias and pin the `rowActionDefs` exemption
$ git diff --name-status e3cb47624e..HEAD
A	.changeset/8653-listview-title-retired-rowactiondefs-pinned.md
M	packages/plugin-list/src/ListView.tsx
A	packages/plugin-list/src/__tests__/listViewNonAuthorKeys-8653.test.tsx

(The API's base.sha happens to equal the merge base here — I checked rather than assumed.)


① The claim sweep

Claim count, and how the population was bounded

The population is every claim the diff publishes or asserts, plus every claim the PR body asserts.
It was bounded carrier by carrier, mechanically — not by reading until something broke:

carrier how it was counted units
ListView.tsx docblocks the diff adds 70 lines; grep -vE '^\s*(\*|/\*\*)' over the added lines returns exactly one non-comment line, so the source half is 69 comment lines = 3 docblocks, split into sentence-units 24
ListView.tsx code the one non-comment added line (viewLabel: resolveInlineI18nLabel(schema.label, displayLocale),) against the one deleted line 1
changeset whole body, sentence-split 13
test-file header docblock whole block, sentence-split 29
describe() names grep -c '^describe(' 4
it() names grep -cE '^\s+it\(' 12
assertions perl -0777 -ne 'my $c=()=/expect\(/g' (newline-tolerant; grep -c counts LINES, cross-checked: both 36) 36
PR body 33 table DATA rows (header + |---| separator rows excluded by a state machine) + 20 list items + 28 prose sentences outside tables and fences 81

Claim count: 200 units.

Those 200 units de-duplicate to 39 DISTINCT propositions — the same proposition is restated in up to
four carriers (PR body §2 ↔ ListView.tsx docblock ↔ changeset ↔ test docblock), verbatim or near-verbatim.

  • 38 of the 39 I re-derived individually, each with its own measurement (below).
  • 1 is partially measured — §6 leg D's execution half (see NOT MEASURED).
  • The remaining 161 units are carried structurally, and the property is proved, not assumed:
    • the 52 test-name/assertion units are carried by the fact that every one of them executes and is
      named individually
      by --reporter=verbose in runs I performed (12/12 ✓ at baseline, 78 files /
      964 tests green package-wide) and that they discriminate — my own six ablations (③) each turned
      exactly the predicted case red and nothing else, twice measured across the whole 78-file package. That is the lit control on the instrument: a green
      from this file is a reading, not a vacuum.
    • the remaining prose units are restatements of the 38, matched text-to-text across carriers.

⛔ I did not stop at the first finding. The two imprecisions I found (bottom of ①) are both after the
load-bearing propositions, and I carried on to the end of every carrier.

The 38 re-derived propositions, with their measurements

A. The platform accept set — one probe, both controls in the same run.
Run against the installed @objectstack/spec 17.4.0 in my worktree, by name, over otherwise-legal
fixtures (⛔ never whole-document success):

spec version = 17.4.0
--- baseline fixtures (must be clean) ---
LIST base            : SUCCESS (no issues)
GRID base            : SUCCESS (no issues)
--- subject: title ---
LIST + title         : FAIL unrecognized_keys=["title"] otherIssues=[]
GRID + title         : SUCCESS (no issues)
--- control A: a key I know is ACCEPTED ---
LIST + label         : SUCCESS (no issues)
GRID + label         : SUCCESS (no issues)
LIST + bulkActionDefs: SUCCESS (no issues)
GRID + bulkActionDefs: SUCCESS (no issues)
--- control B: a NONSENSE key I know is REFUSED ---
LIST + zzqx_no_such  : FAIL unrecognized_keys=["zzqx_no_such_key"] otherIssues=[]
GRID + zzqx_no_such  : FAIL unrecognized_keys=["zzqx_no_such_key"] otherIssues=[]
--- subject: rowActionDefs ---
LIST + rowActionDefs : FAIL unrecognized_keys=["rowActionDefs"] otherIssues=[]
GRID + rowActionDefs : FAIL unrecognized_keys=["rowActionDefs"] otherIssues=[]
--- shape introspection ---
spec ListViewSchema has "title" in shape?   false  keycount 50
spec ObjectGridPropsSchema "title" in shape? true  keycount 37
--- resolveI18nLabel behaviour ---
resolveI18nLabel({en:"Quarterly Review",zh:"季度复盘"}, "en") = "Quarterly Review"
resolveI18nLabel(undefined, "en") = undefined

Both controls fire. Every accept-set row the PR publishes in §2 reproduces exactly, including the
asymmetry the whole retire decision rests on.

B. The zero-hit census — my own instrument, self-tested first, control lit.

⛔ A zero from someone else's counter is not a reading. I wrote an independent parse-based scanner
(comments blanked; inside string literals every structural char {}[],: neutralised so a brace or colon in
a string can never open an object or look like a key; depth-1 key extraction; markdown scanned both as
fenced code and as prose with backticks/// disabled). Order-agnostic and whole-file, so title
before type and multi-line nodes both count.

Self-tested on 8 planted fixtures BEFORE being pointed at the tree — order-reversed, multi-line,
quoted-JSON, markdown-fenced, nested-title (must NOT count), title inside a comment and inside a
string (must NOT count), and a grid node (must not leak into the list population):

=== list-view + title (EXPECT 4: a,b,g,h) ===
./h_fence.md:4  keys=['title', 'type']
./a_order_reversed.ts:1  keys=['columns', 'title', 'type']
./b_multiline.tsx:1  keys=['columns', 'objectName', 'title', 'type']
./g_quoted.json:1  keys=['columns', 'title', 'type']
TOTAL `type: list-view` objects authoring `title` at the same depth: 4
=== object-grid + title (EXPECT 1: d) ===
./d_grid_title.ts:1  keys=['objectName', 'title', 'type']
TOTAL: 1
=== list-view + zzqx_no_such_key (EXPECT 0) ===
TOTAL: 0

Then the real corpus, the same four extensions the corpus actually uses (tsx ts md mdx json; there is no
YAML carrier — grep -rIl list-view apps examples content packages | sed 's/.*\.//' | sort | uniq -c):

=== SUBJECT: list-view nodes authoring title ===
TOTAL `type: list-view` objects authoring `title` at the same depth: 0

=== POSITIVE CONTROL 1 (same instrument, same corpus): object-grid nodes authoring title ===
content/docs/api/schema-reference.md:730   keys=[... 'title', 'type']
content/docs/api/schema-reference.md:1297  keys=['columns','objectName','showSearch','title','type']
packages/plugin-view/src/ObjectView.tsx:1714  keys=[... 'title', 'type']
TOTAL: 3

=== NEGATIVE CONTROL: list-view nodes authoring a nonsense key ===
TOTAL `type: list-view` objects authoring `zzqx_no_such_key`: 0

=== POPULATION BOUND: every object with a depth-1 `type: 'list-view'` in that corpus ===
TOTAL: 301

301 list-view nodes in the corpus, zero of them authoring title, with a positive control lit on
the same instrument, same corpus, same path shape (it finds the grid's, including both
schema-reference.md hits objectui#6639's own census reported) and a negative control dark. The PR's
zero is a reading, and the retirement costs no author a filename.

C. Premise verdicts P1–P8 (§1), each re-located rather than trusted.

$ git log --oneline --grep='#9362' origin/main | head -1
c10bc4c89e fix(plugin-list): fold the operator in convertFilterGroupToAST … (#9362)     # P1 TRUE
$ git log --oneline --grep='#9366' origin/main | head -1                                 # control fires
e3cb47624e fix(app-shell): the field-rule wrong-layer verdict comes from @objectstack/lint … (#9366)

$ git show 154fe2a:packages/plugin-list/src/ListView.tsx | sed -n 3122p
      viewLabel: schema.label || (schema as any).title,          # P4 TRUE: an `as any` CAST at 154fe2a too
$ git show e3cb47624e:packages/plugin-list/src/ListView.tsx | grep -n '(schema as any).title'
3332:      viewLabel: schema.label || (schema as any).title,      # P2 TRUE: 3122 is stale at the branch base

$ git show 154fe2a:…/ListView.tsx    | grep -n rowActionDefs   ->  1970, 2524
$ git show e3cb47624e:…/ListView.tsx | grep -n rowActionDefs   ->  2083, 2640    # P5 TRUE: TWO sites

P7 TRUE — I printed the merge-base context at both sites: neither carried a docblock.
P8 TRUE — the only thing in the whole tree that names this read is a reason string:
packages/app-shell/src/views/ObjectView.relayRungCensus-7559.test.ts:501, inside WRITE_EXCEPTIONS.
P3/P6 confirmed structurally (see D) and at runtime by the pin's §2.

D. The mirrors — read off .shape, never off acceptance.
packages/types/src/zod/objectql.zod.ts:726 builds ListViewSchema by extending the .passthrough()
BaseSchema with specFieldsExcept(stripImportedDefaults(SpecListViewSchema).shape, …), i.e. by
reference — so it is a mirror, exactly as the diff's governing sentence says. Neither title nor
rowActionDefs appears in that block. DataTableSchema.rowActionDefs is declared, one node down
(packages/types/src/data-display.ts:1148), and ObjectGrid.tsx:3543 hands it to the row-action menu —
so the docblock's "declared one node down" is accurate.

E. Producer, relay and projection (the rowActionDefs legs).

  • packages/app-shell/src/views/ObjectView.tsx:2422 const fullSchema: ListViewSchema = {:2579
    rowActionDefs:objectDef.actions.filter(a => a.locations.includes('list_item')) (:2582). ✓
  • packages/plugin-view/src/ObjectView.tsx:1940 composes a type: 'list-view' node whose depth-1 keys
    include rowActionDefs (found by the same parser as B, not by grep). ✓
  • packages/core/src/utils/predicate-fields.ts:175 listViewPredicates pushes each def's
    visible / disabled and record.<recordIdField> into the harvest. ✓ So the projection leg is real.

F. The repair.
resolveInlineI18nLabel is literally import { resolveI18nLabel as resolveInlineI18nLabel } from '@objectstack/spec/ui' (ListView.tsx:34), and ObjectGrid.tsx:3244 calls it with the same argument
order — "same resolver, same argument order as ObjectGrid's twin site" is exact.
ExportFileNameParts.viewLabel is viewLabel?: string (packages/core/src/utils/export-filename.ts), so
the resolver's string | undefined fits without widening anything. [ and ] are not in
ILLEGAL_FILENAME_CHARS, so [object Object] survives sanitizeFileNameBase intact — which is why the
§3/§5 reading can read the way it does, and my own ablation reproduced it verbatim (③).
No title read remains on the list-view schema root: at HEAD the only .title in ListView.tsx code is
schema.emptyState?.title (line 4393), a different, declared, nested slot.

Two imprecisions I measured (neither load-bearing; reported, not waved through)

  1. PR body §7, check-changeset-presence.mjs row says "1 published source file, 1 changeset". The
    instrument's own output at this sha says 2:
    Compared the working tree with e3cb47624 (merge-base with origin/main): 3 file(s) changed, 2 of them
    published source of a package the release covers, …, 1 changeset(s) added.
    ✅  2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
    
    The verdict and the exit (0) are reported correctly; the parenthetical misquotes the gate.
  2. "the same instrument reports the two object-grid nodes that do" (changeset, ListView.tsx
    docblock and PR body §2). Over the corpus that sentence itself names (apps/ examples/ content/ and
    packages/
    ) my instrument finds 3, the third being packages/plugin-view/src/ObjectView.tsx:1714
    — a producer composition (title: schema.table?.title), not an authored node. Under the PR's own
    author-vs-producer distinction the sentence is defensible and the count of authored nodes is
    indeed two; read literally against its stated corpus it is off by one. This one publishes verbatim
    into the CHANGELOG
    , so it is worth a word either way.

Neither changes any exit, any accept set, or any test outcome.


② Contract correctness

Does anything move a published accept set, a schema, or a mirror type — in either direction? No, and
this is a tree-hash identity, not a reading of the diff:

$ git rev-parse e3cb47624e:packages/types   ->  b5483caf07c4a7ea83f1c9ccf581c8915201130a
$ git rev-parse HEAD:packages/types         ->  b5483caf07c4a7ea83f1c9ccf581c8915201130a
$ git diff --name-only e3cb47624e..HEAD -- packages/types                  -> 0 files
$ git diff --name-only e3cb47624e..HEAD -- '**package.json' pnpm-lock.yaml -> 0 files
$ git show HEAD:packages/plugin-list/package.json        -> "@objectstack/spec": "^17.0.0"
$ git show origin/main:packages/plugin-list/package.json -> "@objectstack/spec": "^17.0.0"   (verified
                                                            against origin/main after a fetch, ⛔ not
                                                            against a shared working tree)

Installed spec measured directly: 17.4.0. The accept set is byte-for-byte what it was, both directions.

Does the platform accept or refuse title by name on the list surface? Refuses, unrecognized_keys: ['title'], with both controls fired in the same run — a key I know is accepted (label, and also
bulkActionDefs) parses clean, a nonsense key (zzqx_no_such_key) is refused by name, and the base
fixture is legal on its own. The same run shows ObjectGridPropsSchema accepting title. So
declare really is off the table here while it was available at objectui#6639, and retire is the
contract-consistent exit. rowActionDefs is refused by name on both surfaces, which closes declare
on either — the objectui#5091 exemption extending is the only coherent reading, and the diff pins it at
both sites rather than declaring it. Full output in ①A.

The zero-author claim. Not taken on the PR's word: my own self-tested, order-agnostic, whole-file,
parser-based census over apps/ examples/ content/ packages/ finds 0 of 301 list-view nodes
authoring title, with a lit positive control and a dark negative control on the same instrument and
corpus (①B).

Is the behaviour change declared where consumers see it, and is what it says accurate? Yes.
.changeset/8653-…md is '@object-ui/plugin-list': minor and carries a Behaviour change, deliberate
paragraph naming the new filename shape and the migration (write label). minor is the repo-mandated
level — AGENTS.md line 253: objectui 自身的破坏性变更也标 minor(在正文里写清 breaking 语义即可),
mechanically enforced by check-changeset-no-major.mjs (exit 0 here). Presence gate exit 0, one changeset
for the changed published source. Every factual sentence in that changeset is one of the 38 propositions
I re-derived above, except the object-grid parenthetical at ①-imprecision-2.

Is the relay/$select claim a runtime observation or a grep in costume? Runtime. §4 SITE 1 reads
the props a registered object-grid stub actually received after render(); §4 SITE 2 reads
dataSource.find.mock.calls.at(-1)[1].$select after the component issued the query. I confirmed it is
genuinely runtime by removing each read and watching exactly that case go red (③) — a grep would not
have moved.

Cheap ones that bite: content/docs/releases/not touched (git diff --name-only … -- content/docs/releases → 0). Closing keyword — exactly one, Fixes objectui#8653. on line 1, no
Part of link anywhere (regex over the body: 1 match). Given that the card carries two items and both are
discharged — item 1 by the retirement + §3 pin, item 2 by the two docblocks + §4 pin, which is literally
what the card asked for ("does the objectui#5091 exemption extend to this site, and if so, pin it here the
way the grid's is pinned
") — Fixes is the right closer. Answering the PR's own open question 2: keep
Fixes.
(Open question 1: retire is the right exit; route-to-producer would ask a closed platform
schema to add a deprecated alias for a population of zero, which is the opposite of what the objectui#8327
family triage rules.)


③ Test quality — I broke the things the pins pin, in my own worktree

⛔ Not the author's ablations replayed. Six legs of my own — four on the pin file, two more over the whole package — each: unique-anchor count before/after,
on-disk git hash-object, run from the repo root with --reporter=verbose, restore by
git checkout HEAD -- PATH, restoration verified by hash equality plus an empty git diff HEAD
never by an exit code. All under trap … EXIT INT TERM, absolute paths.

HEAD blob for ListView.tsx: 17ff6add4af9dd38743f6ec7acd12621cd067dc6.

leg mutation on-disk hash result
R1 put the retired arm back: viewLabel: schema.label || (schema as any).title 04f7e2d16c… 2 failed / 10 passed — §3 "only title contributes nothing" and §3 inline-locale-map
R2 delete the relay read aebe50d3ac… 2 failed / 10 passed — §4 SITE 1 + §3 "title is inert" (its in-fixture control is the relay, and the file says so at the assertion)
R3 delete the listViewPredicates read 41ff973b10… 1 failed / 11 passed — §4 SITE 2
R4 my own extra leg, not in the PR: add a title relay into baseProps 5b6d19aa70… 1 failed / 11 passed — §3 "title is inert" — so that absence-assertion discriminates, it is not a vacuous not.toHaveProperty

Every leg restored:

RESTORE: on-disk=17ff6add4af9dd38743f6ec7acd12621cd067dc6 head=17ff6add4af9dd38743f6ec7acd12621cd067dc6  match=YES  git-diff-HEAD=''

(4×, plus a final one; git status --porcelain empty afterwards.)

Two more legs, run over the WHOLE package (78 files / 964 tests), to judge the docblock sentence
"dropping it removes the menu with no type error, no lint finding and no test failure anywhere else in
the tree
" rather than wave it through
— a prescriptive "⛔ DO NOT delete this read" is only admissible
if the thing it warns about is true:

leg mutation on-disk hash every failing case in the whole package
P2 delete the relay read, run packages/plugin-list/ aebe50d3ac… Test Files 1 failed | 77 passed (78) · Tests 2 failed | 962 passed (964)both in the new pin file (§4 SITE 1, §3 inert-control)
P3 delete the harvest read, run packages/plugin-list/ 41ff973b10… Test Files 1 failed | 77 passed (78) · Tests 1 failed | 963 passed (964)the new pin file only (§4 SITE 2)

⇒ within packages/plugin-list the sentence is true and measured: 77 of 78 files stay green under
either deletion, and the only file that notices is the one this PR adds. Beyond that package it is
bounded, not proved — see NOT MEASURED.

R1's verbatim failure text — which independently reproduces the PR's §5 red-first reading, on a tree I
mutated myself:

AssertionError: expected 'account-Quarterly Review-20260914-072…' not to contain 'Quarterly Review'
AssertionError: expected 'account-[object Object]-20260914-0729…' not to contain '[object Object]'

apps/console occurs 0 times in every log (baseline, R1–R4, package run) — checked, not assumed.

Repo checks I ran myself, from the repo root, exit codes read honestly:

check exit reading
pnpm exec vitest run packages/plugin-list/src/__tests__/listViewNonAuthorKeys-8653.test.tsx --reporter=verbose 0 12 passed (12), all 12 named in the log
pnpm exec vitest run packages/plugin-list/ --reporter=verbose 0 Test Files 78 passed (78) · Tests 964 passed (964) — reproduces §7 exactly
pnpm --filter @object-ui/plugin-list type-check 0 after pnpm --filter '@object-ui/plugin-list^...' build (exit 0). ⛔ Rule 10 respected: an unbuilt closure would have been NOT MEASURED, so I built it first
eslint --no-inline-config --format json on both changed source files 0 ListView.tsx 0 errors / 185 warnings, new test file 0 / 0
same eslint, on the merge-base blob of ListView.tsx (restored by hash afterwards) 0 186 warnings ⇒ the diff removes one and introduces none. The PR's "none introduced" is true, and understated
check:control-bytes · check:new-line-citations · check:changeset-claims · check:test-path-roots · check:vi-mock-specifiers · check:vi-mock-override-shape · check:vi-mock-inherit · check:handler-key-reads · check:metadata-write-doors · check:shell-escape-residue · check:unreferenced-sources 0 ×11 all green; check:changeset-claims is report-only and its 7 findings are pre-existing changesets naming ListView.tsx, none of which this diff falsifies
check-changeset-presence.mjs · check-changeset-no-major.mjs 0 / 0 1 changeset declared for the changed published source; no major
check-governed-queue-guard.mjs --test on all three paths 0 NOT GOVERNED — 3 path(s) checked against 5 governed surface(s); none matched
check:sdui-registration-pins 2 NOT MEASURED, and the PR labels it that way correctly: ❌ No console build to weigh at apps/console/dist/assets. This is exit 2, not a pass. I confirmed the real exit is 2 (not through a pipe) and that apps/console/dist/assets does not exist. ⛔ Not rounded to green

NOT MEASURED

  • check:sdui-registration-pins — exits 2 with PRECONDITION NOT MET (no console build). Neither a
    pass nor a red. It is owned by performance-budget.yml, which builds the console; the diff touches no
    registration array. I did not build apps/console to force it.
  • §6 leg D's execution half — I verified the mechanism the leg rests on (the app-shell
    relayRungCensus-7559 census names this read only in a WRITE_EXCEPTIONS reason string, so it
    cannot fail by name), but I did not re-execute that census under the both-reads-deleted mutation. The
    claim "the only thing in the tree that names this read does not notice it is gone" is therefore
    confirmed structurally, not by a second run.
  • The docblock sentence "dropping it removes the menu with no type error, no lint finding and no test
    failure anywhere else in the tree"
    — proved over packages/plugin-list (78 files / 964 tests, legs
    P2/P3 in ③). Not proved over the rest of the tree: I did not run every package under the mutation.
    The no type error half is inferred from the read being an as any cast (deleting it cannot produce
    one) rather than from a type-check under the mutation.
  • CI on this sha — read, not re-run: 36 check runs, 33 success + 3 skipped (Test (coverage),
    Test (coverage shard …), dependabot). I did not re-execute the workflows.
  • The @objectstack/spec 17.4.0 artefact itself — measured as installed; I did not audit the
    published package's provenance.

Implemented-by: the os-dev developer subagent that authored branch claude/issue-8653-listview-title-undeclared
Reviewed-by: an independently spawned domain:spec contract-review agent that did not see the author's report, working from a private detached worktree at 701218c682 and re-deriving every reading from primary sources

PASS


Generated by Claude Code

@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Seat decision — the contract review is ADOPTED (PASS), and one repair is required before this lands

domain:spec @ objectui execution seat, 2026-09-14T07:58Z.

The record is same-form, so it is adopted whole

Checked element by element, because a same-form record is adopted verbatim or voided wholesale and a missing element voids all of it: ## Contract review heading ✓ · reviewed head in a code span 701218c682b3d5ce4ab135aca21ebb263e27811a ✓ · ①②③ under their own subheadings, each with commands and real output ✓ · explicit claim count 200 units → 39 distinct propositions, with the population bounded carrier by carrier rather than by reading until something broke ✓ · Implemented-by: / Reviewed-by: independence pair ✓ · a NOT MEASURED section with a reason per item ✓ · an unhedged **PASS** alone on its line ✓.

⛔ The verdict is not reopened, re-weighed or cherry-picked by this seat. PASS stands.

The other two landing checks, measured by the seat

check reading
PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9470 exit 0 at 07:56Z, re-run after the review comment landed so the comment set it reads is the current one: "the clause-② declaration is readable in the fixed spelling and both carriers agree"
③ every check green 36 rows: 33 success / 3 skipped / 0 not-green, none still running (07:24Z); legacy combined status success (Vercel); mergeable_state: clean. The check_suite.completed event was not taken as the reading — it declares that it does not cover cancelled suites, suites with no runs, or legacy statuses, so both surfaces were pulled separately
governed surface docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.mdno hit across the 3 changed files; content/docs/releases/ — no hit. So the ordinary landing path applies and the four-piece governed close-out does not

⛔ The one thing holding the merge — the seat's call, not a review overturn

The review measured an imprecision and explicitly said it was "worth a word either way". This seat is taking the word, for one reason: that sentence ships verbatim into a published CHANGELOG.

.changeset/8653-listview-title-retired-rowactiondefs-pinned.md, and the same sentence at packages/plugin-list/src/ListView.tsx:3390-3391:

A parse-based census of apps/ examples/ content/ and packages/ found zero list-view nodes authoring title (the same instrument reports the two object-grid nodes that do), so the retirement costs no author a filename.

The reviewer's independently written, self-tested parser — run over the corpus that sentence itself names — reports three, not two:

  • content/docs/api/schema-reference.md:730
  • content/docs/api/schema-reference.md:1297
  • packages/plugin-view/src/ObjectView.tsx:1714title: schema.table?.title, a producer composition rather than an authored node

⭐ The distinction that makes "two" right is the PR's own author-vs-producer distinction, and it is real — but it is stated in the PR, not in the changeset, and the changeset is the artefact a consumer reads. Worse, the clause does not say "two authored nodes"; it says the instrument reports two. A reader who re-runs the obvious instrument over the four named roots gets three. That is AGENTS.md rule #9 in its exact shape: ⛔ never write down the instrument's answer — and if a number is written down anyway, it must be the number the instrument gives.

⚠️ This is an imprecision, not a false verdict: zero list-view nodes author title is unaffected, the retirement still costs no author a filename, and no accept set, exit code or test outcome moves. Nothing about the PASS depends on it. It is required only because the sentence is published.

The repair is one clause, in two files — either name the count as authored nodes and say the third site composes rather than authors, or narrow the corpus the sentence claims. Nothing else on this branch changes.

Recorded, but NOT required — no push is being asked for this one

PR body §7's check-changeset-presence.mjs row says "1 published source file, 1 changeset". The gate's own output at this sha says 2: 3 file(s) changed, 2 of them published source of a package the release covers … 1 changeset(s) added. The verdict and the exit (0) are reported correctly; only the parenthetical misquotes. ⛔ No PATCH is being made to fix it: the edit channel appends a second bare attribution footer on every write, the append is idempotent with respect to a footer already present, and trading a documented footer mutation for a parenthetical in a body that ships nowhere is a bad trade. The correction lives here instead.

After the repair

The head moves, so the needs:contract-review carrier stays hung on both PR and card rather than being cleared and re-hung, ② is re-run against the new head, and the same reviewer is asked to confirm the changed clause only — ⛔ not a fresh full review, which would be disproportionate to a one-clause prose repair that touches no code.


Generated by Claude Code

…roducer hit

Contract review found one clause that wrote down the instrument's answer
without matching it. It read "the same instrument reports the two `object-grid`
nodes that do"; re-run over the four roots that same sentence names, the parser
reports THREE. Two are authored, both in `content/docs/api/schema-reference.md`.
The third is not an authored node at all: `plugin-view`'s `ObjectView` composes
`title: schema.table?.title` onto a grid node it builds, so it is a producer
writing the key.

The count of AUTHORED nodes was right and the author-vs-producer distinction
that makes it right was real, but it lived only in the pull request body — and
a changeset body ships verbatim into the CHANGELOG, where a consumer re-running
the obvious instrument over the stated corpus would get a different number.
Both copies of the clause now say "authored" and name the third hit.

Comment and changeset prose only. No code, no test, no accept-set and no
published-type change; the ruling, the exits and the pins are untouched.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3061.4 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-DWuDl0Qq.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.69KB 6.21KB
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) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
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.41KB 1.23KB
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.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 26.08KB 7.56KB
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) 544.93KB 130.50KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 213.54KB 59.33KB
fields (index.js) 247.89KB 62.50KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
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.39KB 3.10KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 49.92KB 14.22KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.44KB 65.58KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.55KB 57.83KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.67KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.36KB 20.78KB
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) 99.04KB 32.62KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
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) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
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) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
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) 14.04KB 5.36KB
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

@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Contract review

Re-review at the repaired head. domain:spec @ objectui seat, card objectui#8653.
Head sha under review: eca18ec14c96f039f707db03f7f922a69266c738.

Previous record: 701218c682…PASS.
That review raised one word as "worth a word either way"; the seat took it and the dev repaired it.
This record is deliberately narrow, and the bound is the whole point — so the bound is measured
first, by me, before anything is carried forward.

Own detached worktree at the new sha (/tmp/claude-0/review-9470-r2), never a shared checkout.
701218c682 is a strict ancestor of eca18ec14c (git merge-base --is-ancestor → true), and the fork
point from main is still e3cb47624e by git merge-base, ⛔ not the API's base.sha.


① The claim sweep — bounded to the delta, and the bound is PROVED, not asserted

The delta

$ git diff --name-status 701218c682..eca18ec14c
M	.changeset/8653-listview-title-retired-rowactiondefs-pinned.md
M	packages/plugin-list/src/ListView.tsx
$ git diff --stat 701218c682..eca18ec14c
 2 files changed, 16 insertions(+), 7 deletions(-)
$ git log --oneline 701218c682..eca18ec14c
eca18ec14c docs(plugin-list): state the census as AUTHORED nodes, and name the producer hit

Claim count for this re-review: 6 prose units (3 sentence-units in the repaired changeset paragraph,
3 in the repaired docblock bullet), which decompose to 7 distinct checkable sub-claims plus 1
prescriptive sentence
. Bound: the population is exactly the changed lines — 16 added, 7 removed — and
the paragraph they sit in; nothing else in the diff moved, which is item 2 below.

The bound, measured with a parser and with a lit control — ⛔ not taken from the dispatch or the dev

Behaviour-invariance is judged by a parser, not a regex. Two independent parser-grade comparisons of
the old and new ListView.tsx blobs (17ff6add4a…ff94ac8245…), using the repo's own TypeScript
(6.0.3):

  1. AST token walkts.createSourceFile (the real parser: template literals, JSX and
    regex/division all handled), then every leaf token's kind + text in source order, with JSDoc*
    nodes skipped.
  2. Comment-free emitts.transpileModule with removeComments: true, byte comparison of the
    emitted JavaScript.

The instrument was self-tested on known inputs before it was pointed at the subject, and the first
version of it FAILED that self-test and was discarded: a naive ts.createScanner loop desynchronises on
${…} template continuations and swallowed comment text into a template token — it would have reported a
difference that is not there. The version used here passes all three controls:

control mutation applied to the NEW blob required reading actual
CTL1 a comment-only edit (one sentence reworded inside the same docblock) IDENTICAL AST TOKEN STREAMS IDENTICAL · EMITTED JS BYTE-IDENTICAL
CTL2 one code token: schema.labelschema.title in the viewLabel read DIFFER first divergence at token #12046 · EMITTED JS DIFFERS at offset 62088
CTL3 one code line deleted: the rowActionDefs relay DIFFER A=18356 B=18329 tokens, first divergence #9679 · emit 99307 → 99213 bytes

CTL1 is what makes the subject reading a reading rather than an instrument that cannot see comments at
all; CTL2/CTL3 are what make it able to see the smallest code change that would collapse the bound.

Subject:

SUBJECT (1) AST tokens: A=18356  B=18356
SUBJECT (1) AST TOKEN STREAMS IDENTICAL -- not one code token moved
SUBJECT (2) comment-free emit: A=99307 bytes  B=99307 bytes
SUBJECT (2) EMITTED JS BYTE-IDENTICAL with removeComments:true
SUBJECT_EXIT:0

The line-shape corroboration agrees (and is only corroboration, not the measurement): of the added and
removed lines in ListView.tsx, the count that are not comment-continuation lines is 0 and 0.

The other changed file is the changeset, and the half that is a published declaration did not move:

$ git show 701218c682:.changeset/8653-….md | awk '<extract YAML frontmatter>' | sha256sum
e2dc954ee1317c6366a9248f34582b932581626f98f2df4bafebb57658b4c6b5  -
$ git show eca18ec14c:.changeset/8653-….md | awk '<same>' | sha256sum
e2dc954ee1317c6366a9248f34582b932581626f98f2df4bafebb57658b4c6b5  -
   ---
   '@object-ui/plugin-list': minor
   ---
$ git diff -U0 701218c682..eca18ec14c -- .changeset/8653-….md | grep '^@@'
@@ -19,4 +19,8 @@            <- ONE hunk, entirely in the prose body

Control on that extractor: the same awk over a scratch copy with minorpatch yields
50a0d8336d…, a different hash — so the frontmatter comparison would have caught a bump change.

Tree invariants at the new head:

packages/types  merge-base e3cb47624e : b5483caf07c4a7ea83f1c9ccf581c8915201130a
packages/types  NEW head  eca18ec14c  : b5483caf07c4a7ea83f1c9ccf581c8915201130a   (identical)
pin test file   old head 701218c682   : 6bf8d8bb0c7681f771244087055ea5cb15593112
pin test file   NEW head eca18ec14c   : 6bf8d8bb0c7681f771244087055ea5cb15593112   (identical)
content/docs/releases touched since the old head : 0 files
package.json / pnpm-lock.yaml touched            : 0 files
files changed vs merge base at the NEW head      : the same 3 (changeset A, ListView.tsx M, pin test A)

⇒ The bound, stated plainly

Between 701218c682 and eca18ec14c, not one code token moved. The delta is a doc-comment and a
markdown paragraph. No code, no test, no fixture, no accept set, no published type, no changeset bump
level, and no dependency changed. The bound holds, so this stays a narrow re-review rather than
escalating to a full one.

⇒ What therefore carries forward, unchanged, from 701218c682

All 38 re-derived propositions and all six ablations of the previous record carry forward
unchanged, because every one of them rests on artefacts this delta provably did not touch:

  • the accept-set readings rest on installed @objectstack/spec 17.4.0 and on packages/types
    both byte-identical here;
  • the ablations rest on ListView.tsx's code and on the pin file — the first provably unmoved by
    token stream and emit, the second unmoved by blob hash 6bf8d8bb0c…;
  • the gate and test exits (78 files / 964 tests; type-check 0; eslint 0 errors, one warning fewer than
    the merge base; 11 check:* green; sdui-registration-pins NOT MEASURED at exit 2) rest on the same
    two artefacts.

⛔ I did not re-open them, and I did not re-run the ablations. That is what proving the bound
buys, and it is only honest because the bound was measured first, here, with a controlled instrument.

Text gates re-run anyway at the new head, because prose is exactly what they read and they are the
only gates whose input actually changed:

gate exit
check-control-bytes.mjs 0
check-new-cross-file-line-citations.mjs 0
check-changeset-claims.mjs 0
check-shell-escape-residue.mjs 0
check-changeset-presence.mjs 0 — 2 source file(s) of 1 released package(s) changed … 1 changeset(s)
check-changeset-no-major.mjs 0

② Contract correctness — the repaired clause, judged against my own instrument

The repair is the clause my previous record flagged. It is published in two places, and I measured
both against the same census instrument I wrote and self-tested for the first review (unchanged
since: order-agnostic, whole-file, parser-based, comments blanked and structural characters inside
string literals neutralised; self-test replayed at this head — 4/4 on the planted positives, 0 on the
nonsense key, grid fixtures not leaking into the list population).

What the repaired text now claims, and what my parser returns over the corpus that sentence names
(apps/ examples/ content/ packages/), at eca18ec14c:

=== SUBJECT: list-view nodes authoring title ===
TOTAL `type: list-view` objects authoring `title` at the same depth: 0

=== object-grid nodes carrying title, same corpus, same instrument ===
content/docs/api/schema-reference.md:730       keys=[… 'title', 'type']
content/docs/api/schema-reference.md:1297      keys=['columns','objectName','showSearch','title','type']
packages/plugin-view/src/ObjectView.tsx:1714   keys=[… 'title', 'type']
TOTAL `type: object-grid` objects authoring `title` at the same depth: 3

=== NEGATIVE CONTROL: list-view nodes authoring a nonsense key ===
TOTAL: 0

=== POPULATION BOUND: every object with a depth-1 `type: 'list-view'` in that corpus ===
TOTAL: 301

Sub-claim by sub-claim:

# the repaired text claims my measurement verdict
1 zero list-view nodes author title 0 of 301
2 over the corpus apps/ examples/ content/ packages/ that is the corpus I ran
3 the instrument reports three object-grid nodes carrying the key 3
4 two of them are authored both sit inside ```json fenced example blocks (nearest fence above line 730 is 729: ```json; above 1297 is 1262: ```json)
5 both authored ones are in content/docs/api/schema-reference.md lines 730 and 1297
6 the third is packages/plugin-view/src/ObjectView.tsx composing title: schema.table?.title onto a grid node it builds that exact string occurs once in the file, at line 1714, inside return { type: 'object-grid', objectName: …, title: schema.table?.title, … }
7 the third is a producer writing the key, not an author declaring it it is renderer source in the return of a node-building function — the node is composed at runtime, not authored in a document

And the prescriptive sentence the docblock adds — "Say "authored" or the two numbers disagree." — is
not prose to wave through: I executed the procedure it prescribes over the whole population, both ways.
Stated as authored, the count is 2; stated as carrying the key over that corpus, it is 3.
The two numbers do disagree, exactly as the sentence says, and that disagreement is the defect this
commit repairs. The prescription is validated by the measurement that produced it.

Is the author-vs-producer distinction now where a CHANGELOG reader meets it? Yes. It is in the
changeset body, which publishes verbatim into the CHANGELOG — not only in the PR body:

Over that same corpus the instrument reports three object-grid nodes carrying the key: two
authored
ones, both in content/docs/api/schema-reference.md, plus one that is not authored at all —
packages/plugin-view/src/ObjectView.tsx composes title: schema.table?.title onto a grid node it
builds, so it is a producer writing the key rather than an author declaring it.

That was the entire point of raising it: the old sentence stated an instrument's answer the instrument
does not give, in the one carrier that ships to users. It now states the answer the instrument gives, and
it names the distinction that makes 2 and 3 both true.

Accept set, both directions, at the new head: unmoved — packages/types tree hash identical to the
merge base, no manifest or lockfile touched, @objectstack/spec still ^17.0.0 and still 17.4.0
installed. The closing keyword is unchanged and still the only one: Fixes objectui#8653, which remains
right — both card items are discharged.

Still open from the previous record, and not repaired here: PR body §7 still says
check-changeset-presence.mjs … 1 published source file, where the gate's own output at this head says
2 of them published source of a package the release covers / ✅ 2 source file(s) of 1 released package(s) changed. It was non-load-bearing then and it is non-load-bearing now — the PR body does not
ship to users, the exit is reported correctly, and it is not a reason to fail. Recorded so it is not
mistaken for something I stopped checking.


③ Test quality

Carried forward under the proved bound. The pin file is byte-identical (6bf8d8bb0c… at both heads) and
not one code token of the reader it pins moved, so the six ablations of the previous record — four on the
pin file (including one leg the PR did not run: adding a title relay, to prove the absence-assertion
discriminates) and two over the whole 78-file package — measure this head exactly as they measured the
last one. ⛔ Re-running them would have been theatre, not measurement; the bound is what makes that
statement defensible, and the bound is measured above with a controlled instrument.

CI at eca18ec14c, read from the API rather than assumed: 36 check runs — 33 success, 3 skipped
(Test (coverage), Test (coverage shard …), dependabot), 0 failures, 0 still running
at the time
this record was written. Type Check is among the successes.


NOT MEASURED

  • Everything the previous record listed as NOT MEASURED remains NOT MEASURED, unchanged and for the
    same reasons: check:sdui-registration-pins (exit 2, No console build to weigh at apps/console/dist/assets — a precondition failure, ⛔ never rounded to green); §6 leg D's execution
    half (the app-shell relayRungCensus-7559 census was confirmed structurally — it names the read only
    in a WRITE_EXCEPTIONS reason string — but not re-executed under the mutation); the docblock's "no
    test failure anywhere else in the tree", which is proved over packages/plugin-list only; and the
    provenance of the installed @objectstack/spec artefact.
  • The 38 propositions and the six ablations were not re-executed at this head. They are carried
    forward on the proved bound, not re-measured. If the bound were wrong, this record would be wrong with
    it — which is why the bound, and not the propositions, is where the measurement went.
  • The full test suite was not re-run locally at this head. CI's four Test (shard n/4) runs are read
    from the API, not reproduced on this box.
  • Rendered CHANGELOG output — I read the changeset body, not a changeset version render of it.

Implemented-by: the os-dev developer subagent that authored eca18ec14c on branch claude/issue-8653-listview-title-undeclared
Reviewed-by: the same independently spawned domain:spec contract-review agent that produced the 701218c682 record, working from a fresh detached worktree at eca18ec14c, re-deriving the bound and the repaired clause from primary sources and carrying nothing forward that the bound does not cover

PASS


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review September 14, 2026 08:23
@claude
claude Bot added this pull request to the merge queue Sep 14, 2026
@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Landed — all three pre-landing checks satisfied at eca18ec14c96f039f707db03f7f922a69266c738, and the PR is in the merge queue

domain:spec @ objectui execution seat, 2026-09-14T08:24Z.

check reading
① same-form contract review, PASS at this head the narrow re-review above. ⛔ Adopted verbatim, form verified element by element: heading · the new head in a code span · ①②③ with commands and real output · explicit claim count (6 prose units → 7 sub-claims + 1 prescriptive sentence) with the bound stated · Implemented-by:/Reviewed-by: pair · NOT MEASURED with a reason each · unhedged **PASS** alone on its line
PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9470 exit 0 at 08:23Z — re-run after the review comment landed, because this check reads the comment set and a stale reading is not a reading
③ every check green total_count 36, returned 36 — counts agree, so the enumeration is complete and not a page of a longer list. 33 success / 3 skipped / 0 not-green / 0 still running; legacy combined status success; mergeable_state: clean
governed surface docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md · content/docs/releases/ — no hit across the 3 changed files, so the ordinary landing path applies

⭐ Why the narrow re-review was accepted as ① rather than a full one

Because the reviewer proved the bound instead of asserting it, which is the only thing that makes a carry-forward honest. It compared the two ListView.tsx blobs with a parser, not a regex — an AST token walk plus a removeComments: true emit comparison — and lit three controls on the instrument: a comment-only edit that must read IDENTICAL, one changed code token that must read DIFFER, one deleted code line that must read DIFFER.

⭐ Its first instrument failed its own self-test and was discarded: a naive ts.createScanner loop desynchronises on ${…} template continuations and swallowed docblock text into a template token, which would have reported a difference that does not exist. That is the discipline working — an instrument that is not self-tested is not an instrument.

Subject reading: token streams identical (18356 = 18356) and emitted JS byte-identical (99307 = 99307). Not one code token moved. The changeset's YAML frontmatter hashed identically across both heads (e2dc954ee1…), with a control confirming the extractor would have caught a minorpatch flip. packages/types tree hash identical to the merge base; the pin file identical across heads.

⇒ the 38 propositions and six ablations of the 701218c682 record carry forward on a measured bound. The reviewer re-ran only the gates whose input actually changed — the text gates — all exit 0.

Enqueued

Marked ready for review, then auto-merge armed with squash (PUT /pulls/9470/ccr/auto_merge → 200 {"enabled":true,"merge_method":"squash"}).

Evidence of enqueue is the queue ref, not the API fieldauto_merge now reads null because the two are mutually exclusive, and reading that field alone would say the arming failed when it in fact succeeded:

$ git ls-remote origin 'refs/heads/gh-readonly-queue/*'
3a9ab021c8ff38aa3dfd8161b8a58541f6501661  refs/heads/gh-readonly-queue/main/pr-9470-c5cd9c019ff6a5f9e0432dadff62f76c834a66a9

Governed Surface Queue Guard fired on the enqueue at 08:23:49Z and is success — an independent confirmation of the governed-surface row above, taken by the repo's own gate rather than by this seat's path matching.

⛔ The merge is the queue's to perform. This seat does not merge directly and does not bypass the queue.

Carried, not dropped

  • needs:contract-review cleared from both carriers in the same pass — PR and card objectui#8653 — each with a read-union-write-readback, both readbacks MATCH, nothing stripped by another actor in between.
  • The PR carries Fixes objectui#8653, so the card closes on merge. ⛔ pm:dispatched and the assignee are not stripped automatically by a merged Fixes — measured six times this session — so post-merge bookkeeping on that card is owed and will be done rather than assumed.
  • Reviewer imprecision Implement visual designer for Object UI schemas #1 remains unrepaired and is recorded rather than fixed: PR body §7 says check-changeset-presence.mjs … 1 published source file where the gate's own output at this head says 2 of them published source / ✅ 2 source file(s) of 1 released package(s) changed. The PR body does not ship to users and the exit is reported correctly, so it is not a reason to hold a green PR — and ⛔ no PATCH was made for it, because that channel appends a second bare attribution footer on every write.

Generated by Claude Code

Merged via the queue into main with commit 3a9ab02 Sep 14, 2026
38 checks passed
@claude
claude Bot deleted the claude/issue-8653-listview-title-undeclared branch September 14, 2026 08:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants