fix(types): declare onNavigate and onAddComment on the detail arm (objectui#7804, plugin-detail slice) - #9343
Conversation
`BaseSchemaCore` ends `.passthrough()`, so a key the arm for `type: 'detail'`
does not declare is not refused - it stops being judged and the value is KEPT,
then reaches the renderer. `DetailView` is registered RAW for `'detail'`, so
both keys arrived by identity and RAN: `onNavigate` is called in the component's
own body (`handleBack` / `handleEdit` / the post-delete redirect), `onAddComment`
is forwarded as a prop into the comment composer that awaits it.
Measured on the unmodified arm before the code: an authored
`{ "action": "toast" }` parsed GREEN on both keys with the object surviving into
the parsed output, and the Back click then reported
`TypeError: schema.onNavigate is not a function`. `onBack`, already a named
refusal on the same arm, was refused on the same document and is the lit control.
Both are therefore RUNTIME SLOTS, measured per key rather than per prefix: a
named refusal on the JSON face, a callable twin on the TypeScript face. The two
`KNOWN_UNDECLARED_READS` rows they held are drained (39 -> 37).
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
✅ Console Performance Budget
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
Size Limits
|
… uniformly `it.each([...DECLARED, ['onBack']])` widened the row type to `string[] | readonly [key: string]`, which vitest cannot narrow into a callback signature (TS2345 under `tsc -p tsconfig.test.json`). Both tables are now `as const` tuples, so the key parameter types as the literal union and the `.shape` index needs no cast. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
✅ Console Performance Budget
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
Size Limits
|
…ed figures The sibling slice of objectui#7804 landed (the `plugin-kanban` arm, PR #9338), so both numbers this branch carried were derived against a base that has moved. Neither is re-stated by hand here: each is read back off the merged tree. `KnownDrift` header bullet — the one textual conflict. Both sides rewrote the head of the same bullet, so both one-sided resolutions were reproduced against the merged tree first and both are RED, which excludes picking a side: ours (40 entries / 63 keys) -> 2 failed: entry count 40, want 41 key total 63, want 65 theirs(41 entries / 63 keys) -> 1 failed: key total 63, want 65 The merged tree derives 41 entries / 65 keys (`ledgerEntryKeys` and `ledgerEntryMembers` over this file's own AST, via the two pins objectui#7733 and objectui#8222). The bullet now leads with those and narrates both slices in order, newest first: the `plugin-detail` keys onto an existing entry, then the `plugin-kanban` entry, then the objectui#8802 retirement below it. The restatement `N of the registered pairs carry TYPE drift TODAY` tracks the ENTRY count, not the key total. This slice adds two keys to an entry that already existed, so it does not move: main's 41 auto-merged and is correct. `KNOWN_UNDECLARED_READS` merged cleanly — the two slices drained disjoint rows. The gate of record now prints `35 exempted by ledger`, not the 37 this branch was written against. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
… detail slice drained it A semantic collision the merge could not see: PR #9338's `suite 4` CONTROL names one surviving `KNOWN_UNDECLARED_READS` row to prove the leg reads a SHRINKING map rather than an empty one, and the row it named was `detail::DetailSchema.onNavigate` — exactly the row this branch drains. Both PRs were green alone; the control only reddens once both are in one tree. Re-derived against the merged ledger rather than weakened: the witness moves to `button::ButtonSchema.onSuccess`, one of the 35 rows that survive both slices and belongs to neither. The assertion keeps both halves it had — a non-empty map AND a named row — because the length check alone passes on a map holding a single stale row, which is the reading the control exists to refuse. The comment now records that the witness is re-derived on each landing, so the next slice of objectui#7804 to drain it knows to move it rather than drop it. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
✅ Console Performance Budget
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
Size Limits
|
Contract reviewHead judged: ⛔ First: this PR's seat falsified an instruction I put in its dispatch, and it was rightMy dispatch told it to lead the changeset with
⇒ The instruction is withdrawn, retracted to the two live seats carrying it, and removed from my dispatch template. What survives: if something breaking ships, say so in the body, because a reader needs it. ⛔ Not because a scanner does.
① Derived judgments
1 & 2 — same mechanism the sibling slice landed on: 3 — the gate of record's own printed line on the merged tree: 4 — both one-sided resolutions reproduced RED first, so picking a side was mechanically excluded before a number was written: 5 — this is the judgement I would most easily have got wrong. That restatement tracks the entry count, not the key total, and this slice adds two keys to an entry that already existed — so it should stay at ⭐ The third moved thing, which neither the dispatch nor I predictedPR objectui#9338's suite-4 control asserted The witness was re-derived to ② Semver
③ Boundary flags
Independence⛔ For this lane a clause-② review is default-tier self-review plus the gates, not an independence-qualified ruling. VerdictPASS on the work. ⛔ But this PR cannot be armed yet, and the reason is mine, not its seat's. Landing pre-check ③ is Both fixes now need the maintainer: objectui#9352 (skills guard, governed surface, needs an APPROVED review from Generated by Claude Code |
Carrier provenance — the SPLIT carrier is healed; card objectui#7804 now carries the gate too⛔ Not a new gate and not a re-grading.
What is owed
⛔ Until then this PR is not armable, and ⛔ nothing here is a verdict on the code: no diff was reviewed to write this comment. Posted by the Generated by Claude Code |
Contract reviewReviewed head: Ceiling tier: objectui#7804 declares ① Derived judgments — the accept set that moves, and its public face1. Direction, measured on both trees with one instrument (a JSON face: ACCEPTED-and-KEPT → REFUSED BY NAME. 2. Q1 — does the protocol declare these keys on this surface? No. Measured on the INSTALLED
⇒ The consequence is the opposite of the dispatch's remedy: a strict spec document REFUSES an unknown key; before this PR objectui's 3. Q2 — are they READ? Yes, both, with a control that fires.
4. Q3 — does the declaration describe what the channel delivers?
5. Slice containment. 8 files: the two 6. Verification, own timestamps, in my own worktrees ( HEAD MERGED
Unlocked, single files, head 12:47Z / merged 12:48Z: the PR's probe + both ② Semver, graded against the changeset
③ Boundary flags and
|
ADOPTED — the PASS at
|
| ① same-form PASS at tier | ✅ | 5653807611, head 980126de0f3d, ceiling tier, ## Contract review heading · sha in a code span · itemized · independence pair · unhedged PASS |
② --pair exit 0 |
✅ | re-run live at 14:1xZ, not quoted from the review: PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9343 ⇒ EXIT=0, "the clause-② declaration is readable in the fixed spelling and both carriers agree" |
| ③ every check green | two reds, both base-inherited: Doc Snippet Type Check (05:31Z) and Skill Example Check (05:32Z), whose fixes landed at 06:11:55Z and 08:42:27Z. This diff touches none of those files. Relaxed for inherited reds under the maintainer's standing authorization; ⛔ the relaxation does not cover a PR's own red |
Carriers cleared in two writes 2 seconds apart — PR objectui#9343 at 14:16:54Z and card
objectui#7804 at 14:16:56Z. ⭐ Two removals close together is a clearance; a single removal would
be a strip. objectui#7804's assignee, missing while pm:dispatched, was restored in the same write.
⛔ Why this is not being armed yet
The review flagged one thing that ships to users: the changeset's line 27 states a "41-package fixed
group" while config.json enumerates 40. The verdict does not depend on it and the reviewer
correctly left the call here. This seat's call is that a false statement heading for a published
CHANGELOG is not something to land and fix afterwards, so a one-line repair goes out first — and it
can carry the base merge that clears ③'s two stale reds in the same push.
⇒ when that lands the head moves, needs:contract-review is re-hung on both carriers, and a
light re-review scoped to what moved settles it. ⛔ Not a re-run of this review.
Owed, and ⛔ not silently absorbed into this PR
The review produced a new finding outside this slice (its item ③.1): the detail-view arm still
answers accepted=true kept=… for onNavigate / onAddComment at this head, while the detail arm
now refuses them — opposite behaviour on the same two keys, and the gate cannot see it because the
wrapper hands it a derived bound object. views.zod.ts declares only onBack. That needs its own
home (parent card or the objectui#9344 family) and ⛔ is not being folded into this PR.
⭐ Worth recording for the board, because it inverts the dispatch's own premise: the reviewer measured
that the accept set narrows on the detail arm — accepted=true kept={"action":"toast"} at base
5a41ce733e → accepted=false issues=custom@key at head, with onBack refused on both trees as the
lit control. The PR was dispatched on the premise that it widens "by construction". It does not, and
narrowing toward the protocol is why the "file a spec card first" route does not trigger.
Generated by Claude Code
…t a count The sentence explaining why this ships as `minor` stated a package count for the `fixed` group. The count did not match what `.changeset/config.json` enumerates, and a changeset body publishes VERBATIM into the CHANGELOG, so the wrong figure was headed for a release note. Repaired per AGENTS.md #9 rather than by correcting the digits: the sentence now points at the instrument that re-derives the group — the `fixed` array `.changeset/config.json` enumerates — and carries no figure at all. A count written down here is derived once and never again; the next member added to or dropped from the group falsifies it silently, and a reader who spot-checks a still-correct figure confirms it and is wrong the moment the group moves. The sentence keeps its job: the reader still learns this package ships inside a fixed group, that any `major` would carry every member with it, and that `scripts/check-changeset-no-major.mjs` is what makes `major` unavailable. Why no gate caught it: `scripts/check-changeset-claims.mjs` is report-only and covers WENT FALSE only. This was BORN FALSE — a cardinal in prose, which that gate's own header records as the shape it refuses to judge. Frontmatter and every other line of the changeset are untouched. Refs: objectui#7804 Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
…ted reds Both reds on this PR were inherited from the base, not produced by this diff: `Doc Snippet Type Check` and `Skill Example Check`. Their repairs landed on main after this branch last synced, so the PR was running both gates against a base that had already moved. Merged at a PINNED sha rather than at a remote-tracking name: this container runs several agents against one shared `.git`, so `origin/main` advances under a worktree between two reads. The sha merged here is da45e6b. No conflicts, and no resolution was performed: `git merge-tree --write-tree` predicted a clean merge against this branch's head before the merge ran, and the merge reported none. Three files auto-merged, all of them base-side moves this branch does not own. This branch's own diff touches neither gate's inputs; the merge is a base refresh only. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
|
| Step | Outcome |
|---|---|
| Build packages | success |
| Check console performance budget | failure |
Which half objected:
| Eager-closure half | Verdict |
|---|---|
| Aggregate closure ceiling | ✅ pass |
| Per-chunk ceilings | ✅ pass |
| Ceiling sensitivity (headroom) | |
| Ceiling freshness (checkout vs. base branch) | ✅ pass |
⚠️ A broken gauge half is a verdict about the ceiling, not about the bundle: that line has drifted out of range of the regression it exists to catch, or the report behind it cannot be trusted. It does not say anything grew. TheCheck console performance budgetstep log carries the ceiling and the number it was compared against.
Reason: The entry chunk measured 144.4 KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either.
See the workflow run for details.
📦 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) | 502.02KB | 115.16KB |
| core (index.js) | 8.52KB | 3.41KB |
| create-plugin (index.js) | 27.94KB | 9.51KB |
| data-objectstack (index.js) | 211.58KB | 58.68KB |
| 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.25KB | 13.99KB |
| plugin-charts (index.js) | 71.34KB | 19.90KB |
| plugin-chatbot (index.js) | 195.34KB | 46.51KB |
| plugin-dashboard (index.js) | 131.22KB | 34.59KB |
| plugin-designer (index.js) | 215.94KB | 44.33KB |
| plugin-detail (index.js) | 253.46KB | 65.85KB |
| plugin-editor (index.js) | 2.23KB | 1.05KB |
| plugin-form (index.js) | 136.77KB | 34.17KB |
| plugin-gantt (index.js) | 166.95KB | 41.04KB |
| plugin-grid (index.js) | 211.66KB | 57.50KB |
| plugin-kanban (index.js) | 46.00KB | 14.30KB |
| plugin-list (index.js) | 112.58KB | 27.65KB |
| plugin-map (index.js) | 20.64KB | 6.86KB |
| 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) | 9.55KB | 3.32KB |
| plugin-view (index.js) | 84.42KB | 20.79KB |
| 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) | 96.00KB | 31.71KB |
| 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
✅ Console Performance Budget
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
Size Limits
|
… instrument The sibling of the narration repair one commit back, and the same defect class: the docblock hard-coded a row count that nothing re-derives, and the figure had already been overtaken by the population it described. Seat ruling on the record: the count is dropped in favour of the instrument, ⛔ not restated as the current one. Restating it would have been a NEW false claim rather than a repair -- the rows live on this tree are not the same SET the slice's finding named. Measured across four refs: at a686403 the ledger held 39 rows with no cast-spelled rows and no cast-piercing in the gate; at this head it holds 37, two of them `onTabChange` rows that arrived with objectui#9392 and two `detail` rows drained by this branch. Same cardinality family, different set. This file already reads the ledger live -- it imports `KNOWN_UNDECLARED_READS` from the gate of record -- so the docblock now points at that import and at the gate's printed line and `--list`, per AGENTS.md #9. ⛔ Narration only, in one docblock sentence. Proved rather than asserted: the file's non-comment lines are byte-identical before and after (209 lines, sha256 d7e9bae66... both sides), so no assertion, import or executable line moved. The changeset, the declarations, the zod arms, the labels and the draft state are untouched. Measured at this working tree: - `node scripts/check-handler-key-read-sites.mjs` exit 0, and its `--list` output is byte-identical to the previous head's; - the four touched test files: 4 files / 130 tests passed, exit 0, per-file split unchanged (71 / 32 / 14 / 13); - eslint on the edited file exit 0; `check:comment-mask-corpus`, `check:control-bytes` and `check:new-line-citations` all exit 0. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
✅ Console Performance Budget
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
Size Limits
|
Contract reviewReviewed head: ① derived judgmentsReviewed from scratch in my own Accept-set moves on the published mirror ( 1. Direction: narrowing — a document that was ACCEPTED and KEPT is now REFUSED BY NAME. 2. Public-surface moves, read on the BUILT declaration — two 3. 4. Non-published changes — six 5. Ledger walk re-derived on three installed worktrees, by two instruments each (the gate's 6. 7. 8. 9. 10. 11.
objectui#9338's contract review passed with that repair in it, and its own reasoning ⇒ Repair owed before landing: the sibling note's shape in ② semver gradingThe diff is breaking — major-class by behaviour. It removes documents from the accept Declared: The grading is correct for this repo, and the changeset states the reason in its own ③ boundary flagsThe earlier FAIL at
The two known-bad figures, re-measured rather than inherited.
Every flag the implementer raised, answered or escalated by name.
Raised here, not by the implementer.
Implemented-by: claude/issue-7804-detail-arm (mode:subagent) FAIL Generated by Claude Code |
`content/docs/api/schema-reference.md` is the author-facing reference for the `type: "detail"` face, and this branch moves `onNavigate` and `onAddComment` on that face from ACCEPTED AND KEPT to REFUSED BY NAME. The reference said nothing about either key, so it was left teaching the old acceptance, and nothing mechanical catches that: `check:doc-types` judges registered component TYPES, not accept sets, and is exit 0 either way. Same shape and role as the note objectui#9338 — the preceding slice of this same card, under the same batch #69 ruling — put under `### ObjectKanbanSchema` in this same file, but measured against THIS arm rather than transposed: - three keys are refused here, not two: `onBack` since objectui#7344, plus `onNavigate` and `onAddComment` from this change; - the refusal message is quoted from what `handlerKeyRefusal` actually emits, read off the built mirror (`packages/types/dist/zod/index.zod.js`); - the two keys reach the renderer on DIFFERENT channels, so the "used to do" clause names both: `schema.onNavigate` is called in `DetailView`'s own body (`handleBack`, `handleEdit`, the post-delete redirect), while `onAddComment` is forwarded as a prop into the comment composer, which renders BECAUSE the key is truthy and then awaits it on send; - the still-undeclared third key differs from the kanban board's. `onCardMove` is accepted and DROPPED; `detail`'s `onTabChange` is accepted, KEPT, read through a cast and handed to the tab strip's `onValueChange` — so it still reaches a call site expecting a function. The note says so rather than forcing the parallel. Docs only: no schema, no renderer, no test and no changeset change. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
…kanban docblock Merged at a PINNED sha, not at a remote-tracking name: several agents share one `.git` in this container, so `origin/main` advances between two reads. The sha merged here is 55f39ee; the merge-base was da45e6b, 223 files back. Four files changed on both sides. THREE auto-merged because the two sides edited different anchors in each: - `scripts/check-handler-key-read-sites.mjs` — this branch drains the two `detail::DetailSchema` ledger rows; main drained `object-kanban::ObjectKanbanSchema.onCardMove` with objectui#9342 and rewrote the comment above it. Both drains are in the merged ledger, which now holds 36 rows where the merge-base held 39. - `content/docs/api/schema-reference.md` — this branch adds the `DetailSchema` note; main rewrote the `object-kanban` note one section down. - `packages/types/src/__tests__/zod-mirror-parity.test.ts` — this branch widens the `crud.zod.ts#DetailSchema` entry and its header bullet; main rewrote two docblocks around the kanban entry. ONE conflicted: `packages/plugin-kanban/src/__tests__/handlerKeyDispositionsMeasured-7804.test.tsx`. Both sides rewrote the same opening sentence of the file docblock, for orthogonal reasons, so the resolution is their UNION and neither side's content is dropped: - ours replaced the hard-coded row count with a pointer at the instrument that re-derives it (AGENTS.md #9); - main's objectui#9342 changed "Two of the three" to "All three" and named the card that moved the blocking read. The union keeps main's fact and this branch's deferral. The count this branch removed is NOT reintroduced, and the clause claiming the file reads the ledger live was re-checked against the surviving import. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
…created
The `### DetailSchema` note contrasted `onTabChange` with "the `object-kanban`
board's third key" to make one point: an authored `onTabChange` is KEPT and
reaches a call site, where the board's loose key was accepted and dropped.
Merging origin/main moved the comparand. objectui#9342 gave `onCardMove` a
tombstone on both faces, so the board's third key is no longer accepted-and-
dropped — it is refused by name. Re-measured on the BUILT mirror
(`packages/types/dist/zod/index.zod.js`) after a scoped build:
ObjectKanbanSchema + onCardMove -> REFUSED
"Card move handler — `onCardMove` is RETIRED (objectui#6124, ADR-0049)"
DetailSchema + onTabChange -> ACCEPTED, key kept in the parsed output
DetailSchema + onNavigate -> REFUSED ("SPA navigation callback …")
DetailSchema + onAddComment -> REFUSED ("New comment callback …")
DetailSchema + onBack -> REFUSED ("Custom back action …")
Controls for that reading, so the refusals are not a blanket rejection and the
acceptances are not a dead probe: the bare `{ type: 'detail' }` document parses
green, and an invented `zzNotAHandler` key is ACCEPTED and kept on both arms.
The contrast the sentence was making still holds and is now stated against what
the arm actually does. Every other clause in the note was re-checked and none
of them moved: the three refusal messages, their lead labels, the objectui#6124
sentence and the node-type closing are verbatim from the probe above, and the
read sites it names are still `DetailView.tsx` 699-704, 715-720, 784-785 for
`onNavigate`, 1753 for the `onAddComment` prop, and the cast read at 1696 that
reaches `<Tabs onValueChange>` at 1765. main did not touch `DetailView.tsx`.
No count is written down here: `onTabChange` remains a live
`KNOWN_UNDECLARED_READS` row, and `check-handler-key-read-sites.mjs --list`
stays the instrument that enumerates it.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
✅ Console Performance Budget
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
Size Limits
|
Contract reviewReviewed head: ① derived judgmentsReviewed from scratch in my own Accept-set move on the published mirror — two keys, measured on the BUILT mirror. Probed The refusals are key-specific and the acceptances are not a dead probe. On the base tree ( Public surface, read on the BUILT declaration rather than the source. Clause-② — verified, not accepted. The declaration lives on the card's claim comments, not in the PR body: objectui#7804 Arm membership, derived. The built THE CLAIM SWEEP — 87 claims, and how the population was boundedBounding rule, stated so it can be checked. Every assertion of fact carried by (i) the prose-bearing artifacts of A —
B — C — D — E — test-file narration, 13 claims (12 TRUE, 1 FALSE)
F — G — the PR body, 38 claims (29 TRUE, 3 NOT MEASURED, 6 FALSE) Re-derived and TRUE (each measured here, none inherited): the gate's printed line The ablation reproduced EXACTLY, run from the committed state in my own worktree under All three mutated failures are named on NOT MEASURED, 3, each with its reason: the pre-merge FALSE at this head, 6:
All six sit under the body's own header claim, at the top of Verification: "Every figure below was re-derived AFTER the merge commit and after the documentation repair that follows it; none is carried forward from the pre-merge round." The Ablation section is carved out of that claim explicitly and correctly; the Acceptance notes are not, and G-a to G-e are pre-merge readings standing under it. ⭐ Settled, since the dispatch asked. The ② semver gradingThe changeset is What the diff actually is: breaking. A document authoring
⇒ grading: the declaration matches what the repo's own policy permits, and the body does not overstate it. ⛔ Not a defect. ③ boundary flagsThe two earlier FAILs, the merge resolution and the post-merge narration repair — each judged at THIS head.
Every implementer flag and
Below the bar, for the record, ⛔ not grounds:
Implemented-by: claude/issue-7804-detail-arm (mode:subagent) FAIL Generated by Claude Code |
… carrier `detail-handler-slots-7804.test.tsx` routed the key's objectui#6124 disposition to "objectui#9344's item ②". That card is no longer open — the API reads `state_reason: completed`, dated 2026-09-13T12:55:04Z — and its item 2 was never carried anywhere, so the comment sent the reader to a card that cannot answer it. The same file already said, nine lines above, that the row is "attributed to objectui#7804"; the two halves of one comment block disagreed, and the published note in `content/docs/api/schema-reference.md` plus the ledger rows in `check-handler-key-read-sites.mjs` both name objectui#7804. The sentence now says what is true: the disposition is OPEN, is decided neither here nor anywhere else yet, and sits on objectui#7804 — the card the gate script itself names for this row. ⛔ No new card is invented, and ⛔ the gap is not described as decided. Comment-only. The assertion `expect(castReads).toEqual(['onTabChange'])` is untouched, as is every other line of the suite. Measured at this head, from the repository root: - gate of record: 60 reachable / 60 judged / 36 exempted, exit 0 - `--list`: 61 lines, sha256 prefix 0b586b1c9e568ffc - the four touched test files: 4 files / 130 tests passed, exit 0 - `check:control-bytes`: 7625 tracked text files, exit 0 Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
✅ Console Performance Budget
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
Size Limits
|
Contract reviewReviewed head: ① derived judgmentsReviewed from scratch in my own
The accept set and the public surfaceThe move is on the published mirror, and I read the built artifacts, not the sources.
The The base accept set, reconstructed on the same published mirror by removing exactly the two ⇒ ACCEPTED-and-KEPT → REFUSED BY NAME. A narrowing move on a published mirror, so Public surface, read on the built declaration — Both signatures are byte-identical to
The declaration is load-bearing — ablation re-run, from the COMMITTED state, under
THE CLAIM SWEEPHow I bounded the population. Every claim this diff publishes into the repository — read ⛔ I inherited none of the previous sweep's 77 "true" rows. 174 rows I re-derived myself at this Excluded from the population and named: (a) A —
B — the
C —
D —
E —
F —
G —
H —
I —
J — the PR body, walked top to bottom (82)
The two false rowsD1 — "three different dispositions out of three keys" is false, and it ships in a source file. Two artifacts assert it:
Newline-tolerant census ( Measured, two independent instruments, with a control that varies only the claim: The control returns 1 where the truth is 1 and 2 where the truth is 2, so the instrument tracks the ⛔ This is not a vocabulary quibble imported from outside the diff. The PR body's own table separates What the kanban slice actually got is three different CHANNELS and two dispositions, which is a D2 — "All readings at final head The PR head is ⭐ The consequence is nil and I want that on the record: I re-derived every figure in that section at ② semver gradingChangeset of record: What this diff actually is: MAJOR. Both faces narrow, measured on built artifacts:
Why Coverage. The other two packages this diff touches ( ⇒ ② is graded correctly and declared correctly, and is ⛔ not a ground for the verdict below. ③ boundary flagsThe three FAILs and the four repair rounds — each named defect, at THIS head, measured.
Every flag the implementer raised, answered by name.
Real, but below the bar — recorded, ⛔ not grounds for the verdict.
The verdict. Two measurably false statements at this head, one of them published into a source Implemented-by: claude/issue-7804-detail-arm (mode:subagent) FAIL Generated by Claude Code |
…e kanban cross-reference The docblock claimed the sibling kanban slice got "three DIFFERENT dispositions out of three keys". It cannot have: HandlerKeyDisposition has exactly two members, so three keys can never carry three distinct dispositions. What that slice measured is three different CHANNELS carrying two dispositions - two runtime slots and one tombstone - which is what its own objectql.zod.ts docblock already says in as many words. The corrected reading is the stronger argument for per-key measurement, not a climb-down: it is exactly the case where counting keys and counting dispositions come apart, so neither can be read off the other. Comment-only. The gate of record and its --list digest are unmoved. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
✅ Console Performance Budget
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
Size Limits
|
Contract reviewReviewed head: ① derived judgmentsScope of this round, stated first. The repair since the fourth sweep is one commit, The structural argument — measured, and it holds
⇒ every claim whose subject is any file other than that one, or any runtime behaviour, has a byte-identical subject at Repair ① — the disposition count, judged on what is there NOWMeasured at this head, ⛔ not checked against what the last review said was wrong.
⇒ the repaired statement is true, and its agreement claim is true verbatim. It is not a differently-false replacement: the statement it replaces was false by arithmetic against a closed two-valued type; this one is a claim about routes, which no type bounds, and every countable part of it reproduces.
Repair ② — the provenance paragraph, judged on its own meritsDoes it still assert anything a narration-only push would falsify? — NO. I walked every proposition. The paragraph opens by disclaiming that it asserts a tip; each of its three bullets is a past-tense act at a named sha ("Taken at", "Independently re-derived at", "Re-measured at"), and an act performed at a commit stays true wherever the branch moves next. Census on the stored body: Its one falsifiable proposition is the invariant, "Every commit after the last sha named above is narration-only … and moves no figure in this section." A narration-only push keeps it true, which is exactly the property asked for. A figure-moving push would falsify it — and the paragraph says so in-band and makes the staleness mechanically detectable from the commit list. That converts silent staleness into a declared, checkable failure mode; it is the requested shape, not an evasion. Does it over-claim? — NO, and I checked the disclaimer rather than the claim. Bullet 3 names exactly three things re-measured at
And the disclaimer really does cover what was not re-run: the section's remaining figures (the 604/13001 union, type-check
The invalidation set — how I bounded it, and what I re-derivedBounded by the structural proof above: only two subjects moved, so the invalidation set is the 16 group-H rows (the test file whose prose changed) plus the group-J body rows that were rewritten or are head-dependent. Everything else has a byte-identical subject. Re-derived at this head, with my own instruments:
⭐ These six double as a lit control on the structural argument itself: if it were unsound, one of them would have moved. None did. The accept set and the public surface, re-derived rather than inherited. On the built JSON mirror
The counts
44 + 7 + 2 + 130 = 183. ⭐ I judge the structural argument sound, and the six probes above are the measurement that says so rather than an assurance that it must be. ② semver gradingChangeset of record: What this diff actually is: MAJOR. Both published faces narrow, measured on built artifacts above — the JSON mirror moves a document from accepted-and-kept to refused-by-name, and the TypeScript face moves both members from Why ③ boundary flagsThe nine defects named across the four FAILs — at THIS head, measured.
Every flag the implementer raised, answered by name. The final report carries
Real, but below the bar — ⛔ not grounds for the verdict.
Implemented-by: claude/issue-7804-detail-arm (mode:subagent) PASS Generated by Claude Code |
ADOPTED — the PASS at
|
| check | reading | |
|---|---|---|
| ① same-form PASS at tier | ✅ | record 5658634767, head 8da6a093c7d904bd96222487fa4dcfb454647ccd, heading · sha in a code span · ①②③ itemized · independence pair · exactly one unhedged **PASS**, zero **FAIL**. Tier claude-opus-5 on all 167 harness-stamped messages, no fallback. |
| ② both carriers, machine-read | ✅ | --pair 9343 → exit 0, run live: "both carriers agree". |
| ③ every check | ✅ | 36 runs, all complete: 33 success / 3 skipped / 0 failure / 0 cancelled. Settles the previous round's 1 in_progress note. |
⭐ Why this round was scoped down, and why that is not a shortcut
The fourth review swept 183 claims (178 true, 3 false, 2 NOT MEASURED). The repair after it was one commit, one file, +14/−2, comment-only — so this round was asked for a bounded review rather than a re-sweep, on the explicit condition that it prove the bound and sweep if the proof failed.
It proved it. git diff --name-only cb66ea5a37 8da6a093c7 names one file; stripping all comments leaves content byte-identical on both sides (sha256 prefix d4b73ee5e6…, 126 lines). ⭐ And it put a lit control on the stripper itself — the same filter across the branch's arm commit 23f839fed8 moves the hash 50a02812… → 8755acde… and 114 → 116 lines. ⇒ the zero is a real absence of change and not a blind instrument.
| rows | |
|---|---|
| re-derived | 44 |
| spot-checked (named: 76, 81, 106, 111, 115, 147, 154) | 7 |
| NOT MEASURED (146, 172, carried) | 2 |
| carried on the structural argument | 130 |
| 183 |
⭐ It also re-derived 6 probes inside the carried set — passthrough, the 40-member fixed group, the 2,512-file migration census, the gate-script deletions, the mirrors ledger, the parity header — as a lit control on the structural argument itself. None moved.
The two repairs, judged on their own merits
① The disposition count is TRUE. HandlerKeyDisposition enumerates to exactly 2 members; the kanban arm reads runtime-slot / retired / runtime-slot = 2 distinct, against the detail arm's 1 as control; and the claimed agreement is literally true — objectql.zod.ts:1512-1517 says "Two are RUNTIME SLOTS … the third, onCardMove, is a TOMBSTONE". ⭐ Not a differently-false replacement, and the reviewer said why: the old claim was impossible by arithmetic against a closed two-valued type, while the new one is about routes, which nothing bounds.
② The provenance paragraph is TRUE and does not over-claim. No proposition of the form "the tip is X" survives: All readings at (final )?head → 0, with the pre-repair copy firing 2 as a control that shares no suspect part of the instrument. A narration-only push falsifies nothing; the single falsifiable proposition — the invariant — is falsifiable only by a figure-moving push, is declared in-band, and is mechanically detectable from the PR's own commit list. Its three re-measured items all reproduced here (gate 60/60/36; --list 61 lines / 0b586b1c9e568ffc; four files 4 passed / 130 passed under --reporter=verbose with apps/console 0 times).
⭐ And the reviewer tested the paragraph's attribution, not just its shape: the fourth sweep's 3 false and 2 NOT MEASURED rows all fall outside the Verification section, so "every figure in this section holds there" is accurate rather than lucky.
② semver: the diff is substantively major — the published accept set narrows on both faces — and major is mechanically unavailable (one 40-package fixed group containing @object-ui/types, enforced by check-changeset-no-major.mjs inside changeset:check). .changeset/7804-detail-handler-slots-declared.md declares minor and states that reason. Correct for this repo.
Clause-②: yes verified against the tree, ⛔ not accepted from the declaration.
Recorded, ⛔ not blocking
- "three different channels" counts the dead route (
onCardMove, which reaches nothing) as a third channel. Imprecise, not false: the repo binds no closed vocabulary to "channel", the three per-key readings genuinely differ, and the sentence self-corrects by naming the third a TOMBSTONE. - The Files bullet (body line 86) still names only
083a7f1d68for the detail test file — act-bound about that commit and true of it ⇒ incomplete, not false. The repair said outright that its ② shape does not reach that bullet rather than claiming it did. - Body line 105's "⛔ Not edited" is true as scoped to its Merge section; same family.
- The doc note's
handleEdit … onNavigate(url, { replace })—handleEditpasses no options at:717/:720; the parameter is optional ⇒ imprecise, unchanged since the fourth sweep recorded it. - ⭐ A bookkeeping slip in the fourth review, ⛔ not in this PR: its group-J heading reads
(82)where the rows run 92-183, i.e. 92. The stated 183 reconciles with 92, so the heading is the typo — recorded so a later reader does not try to make 82 add up. - The gate script's objectui#9344 routing prose was verified pre-existing rather than accepted as such (1 occurrence at the merge-base and at HEAD; this PR's diff there is
0 insertions / 2 deletions) ⇒ genuinely objectui#9456's.
Carriers cleared from both this PR and card objectui#7804 in one pass. Landing next: ready → auto-merge → merge queue. ⛔ Card objectui#7804 stays open — this PR carries no closing keyword, by the batch #69 ruling.
Generated by Claude Code
Part of objectui#7804 — the
plugin-detailslice. The parent card stays OPEN and stays the parent; PRs land per package (director seat, decision batch #69, 2026-09-07, maintainer verbatim 「其他同意」). ⛔ No closing keyword anywhere in this body.What lands
DetailSchema— the zod arm thattype: 'detail'selects — now declares the two handler keys its registered renderer reads, as objectui#6124 RUNTIME SLOTS on both faces: a named refusal on the JSON face, a callable twin on the TypeScript face.onNavigateruntime-slotDetailView's own body and CALLED there —handleBack,handleEdit, the post-delete redirectonAddCommentruntime-slotDetailViewnever calls it: it is FORWARDED as a React prop into the comment composer, whose submit handler awaits it, behind aschema.commentsgate the same passthrough keeps alive⭐ Per key, not per prefix. The two share a registration and a document and still reach the renderer by two different routes; the sibling slice on
object-kanbantook three keys sharing one prefix and measured three different channels carrying only two dispositions — two runtime slots and one tombstone, which is what that slice's ownobjectql.zod.tsdocblock says in as many words.HandlerKeyDispositionhas exactly two members, so three keys can never carry three distinct dispositions, while the routes by which a key reaches a renderer are bounded by nothing. ⇒ the two counts come apart there and neither can be read off the other, which is a sharper case for per-key measurement than three-out-of-three would have been — so the per-key measurement is the deliverable, not a formality.The mechanism
BaseSchemaCoreends.passthrough()⇒ a key an arm does not declare is not refused, it stops being judged and the value is KEPT, then reaches the renderer.ComponentRegistry.register('detail', DetailView)registers the component RAW (unlike'detail-view', which goes through a data-source wrapper), so nothing is interposed and the authored value arrives by identity.Both keys were declared on NEITHER face:
BaseSchema'sany-valued index signature typed them on the TypeScript side and.passthrough()kept them on the zod side, while the renderer read and ran them.Base reading, written BEFORE the code and run on the unmodified tree (
a686403b3)Parse face, all three keys on the same arm:
Driven face, through the real
SchemaRenderer(happy-dom, React 19):Red-first run on that tree: 13 tests, 8 passed / 5 failed. The 5 failures are exactly the landing assertions (both members absent from the shape, both authored objects accepted, the derivation reporting both undeclared). The 8 that passed are the reachability legs, the hazard leg and the
onBackcontrol — i.e. the instrument was already working before the change moved anything.expect(...).toThrow()around the click. React 19 does not rethrow a handler error out of the dispatch — it REPORTS it and the click returns normally — so that spelling would have been a green assertion about a hazard that never fired. The leg now reads what React actually reports, and carries a silent-document control beside it. The miss is written into the test's own docblock.MERGED WITH
main— the sibling slice landed, and both derived figures movedobjectui#9338 (the
plugin-kanbanslice of the same card) merged, takingmainto5a41ce733.mainwas merged into this branch as a merge commit (8421c1f39) — no rebase, no amend, no force-push. Every figure below is re-read off the merged tree; none is stepped by hand.One textual conflict, in
zod-mirror-parity.test.ts: both slices rewrote the head of the sameKnownDriftbullet. Both one-sided resolutions were reproduced against the merged tree FIRST, so that picking a side is mechanically excluded rather than merely unattractive — both are RED:The derivation is the file's own AST instruments (
ledgerEntryKeys,ledgerEntryMembers) under the two pins objectui#7733 and objectui#8222 — not arithmetic on the figures in either branch. The bullet now leads with41 entries / 65 keysand narrates both slices newest-first: theplugin-detailkeys onto an existing entry, then theplugin-kanbannew entry, then the objectui#8802 retirement already below it.⭐ Three figures in that header are machine-read, not one. Besides the entry count, the KEY total is read by objectui#8222's block and the restatement
N of the registered pairs carry TYPE drift TODAYis read by objectui#7733's. The restatement tracks the entry count, not the key total — this slice adds two keys to an entry that already existed, so it does not move:main's41auto-merged and is correct untouched. That was verified, not assumed: in theoursrun above the restatement read 41 and PASSED while the entry count failed.suite 4CONTROL names one surviving ledger row to prove it reads a SHRINKING map rather than an empty one — and the row it named wasdetail::DetailSchema.onNavigate, exactly the row this branch drains. Both PRs were green alone; the control only reddens once both are in one tree. Re-derived, never weakened: the witness moves tobutton::ButtonSchema.onSuccess, a row the gate still exempts at this head and one belonging to neither slice. ⛔ How many such rows there are is not written down here —check:handler-key-reads --listenumerates them. The assertion keeps both halves it had — a non-empty map AND a named row — because the length check alone passes on a map holding a single stale row, which is the reading the control exists to refuse.After
Gate of record, its own printed line, re-read at head⚠️ The fork point MOVED with the merge recorded below:
e3c3589d1f. Fork point taken withgit merge-baseand ⛔ never frombase.sha(which is the base BRANCH TIP).git merge-base origin/main HEADis now55f39ee90f—origin/mainitself, because this branch has merged it. The ledger walk below still starts at the PRE-merge fork pointda45e6be7a, which is what it walks from and is no longer the merge-base.The two
KNOWN_UNDECLARED_READSrows this slice owns are drained, enumerated not counted. Re-derived at this head, the ledger walks 39 rows at the pre-merge fork point (da45e6be7a) → 38 onmain(tip55f39ee90f) → 36 on this tree. Two different cards drain it on those two legs: objectui#9342 tookobject-kanban::ObjectKanbanSchema.onCardMoveonmain, and this slice takesonNavigate+onAddCommenthere. ⛔ The other 36 are untouched by this branch. Three independent instruments agree on that 36: the gate's own printed line above,KNOWN_UNDECLARED_READS.sizeread off the module, and the[UNDECLARED]rows--listprints (counted by enumeration — ⛔ notgrep -c, which counts lines).59 reachable / 35 exemptedand the walk39 → 37 → 35, both read at head980126de0against the then-merge-basea686403b3. Two commits later both had moved, and not in the direction the old arithmetic assumed: the merge atcb5e80cce6brought objectui#9392 in, which taught the gate to read through a cast and so ADDED twoonTabChangerows to the ledger —main's figure rose to 39 rather than falling to 37. Nothing about this slice's own two rows changed.61 reachable / 61 judged / 37 exemptedand the walk39 → 39 → 37, read at head8a5fdc8f6a. The merge recorded below brought objectui#9342 in, which movedKanbanRenderer'sschema.onCardMoveread onto an explicit React prop: that retires one read site (61 reachable → 60) and drains one ledger row (37 → 36) without this branch touching either. ⭐ Both times the figure moved because the POPULATION moved underneath a written-down number, which is the failure mode AGENTS.md #9 names — the numbers here are re-derived at the head this body cites and nowhere kept as a constant.Contract carrier — it applies, and this is the direction
The accept set moves on a published mirror: a document authoring either key was ACCEPTED and KEPT before and is now REFUSED BY NAME (issue
code: 'custom'at the key's own path). That is a move in the narrowing direction, which is still a move ⇒needs:contract-reviewon both limbs; the parent card already carries it and this PR carries it too. ⛔ Neither limb is cleared here — that is the PM's.minor, notmajor: the 40-packagefixedgroup makesmajorunavailable (scripts/check-changeset-no-major.mjs, run insidechangeset:check), so the breaking accept-set move ships asminorwith the reasoning in the changeset.Files
packages/types/src/zod/crud.zod.ts— the twohandlerKeyRefusal(..., 'runtime-slot', ...)arms, with the measurement beside each. Untouched by the merge (blob identical before and after), which is why the ablation below still cites it verbatim.packages/types/src/crud.ts— the callable TypeScript twins, signatures taken from the call sites (onNavigate(url, options),onAddComment(text)), matching whatviews.tsalready declares for the same component under its other registration.packages/types/src/__tests__/handler-keys-string-any-mirrors-7344.test.ts— the ledger that owns thecrud.zod.ts#DetailSchemapair: 2 rows added (8 sites to 10, 4 runtime slots to 6).packages/types/src/__tests__/zod-mirror-parity.test.ts— theKnownDriftentry grows to three keys; the header bullet is the merge's one conflict, resolved to the derived41 entries / 65 keys.packages/plugin-detail/src/__tests__/detail-handler-slots-7804.test.tsx— new; the driven measurement, the hazard, the refusal, and a derivation off the read site. A follow-up commit (083a7f1d68) repairs NARRATION only in this file: suite 4's last leg was a green assertion narrating the pre-objectui#9392 cast blindness, and the docblock hard-coded a ledger size. The leg's name and comment now describe the tree they ship in and the size is deferred to the instrument; the assertion itself, the declarations and the changeset are untouched.packages/plugin-kanban/src/__tests__/handlerKeyDispositionsMeasured-7804.test.tsx— two independent changes, both narration: (1) merge fallout — thesuite 4control witness re-derived tobutton::ButtonSchema.onSuccess, per the collision described above, with the assertion keeping both halves it had; and (2) a later commit (bcbd2793cb) repairs the docblock under AGENTS.md 完善设计器的每一个细节 #9 — it hard-coded theKNOWN_UNDECLARED_READSrow count, and now defers that figure to the instrument instead. ⛔ The count is not restated here either:check:handler-key-readsprints it and its--listenumerates the rows. No assertion, declaration or changeset in this file is touched by either change. (3) the merge below conflicted here and the resolution is a UNION — see the Merge section.scripts/check-handler-key-read-sites.mjs— the two ledger rows removed. The merge adds objectui#9342's removal of a third,object-kanban::ObjectKanbanSchema.onCardMove; both drains are present and neither side's is lost.content/docs/api/schema-reference.md— the### DetailSchemanote. The merge invalidated ONE clause in it and the repair is a commit of its own: the note contrastedonTabChangeagainst theobject-kanbanboard's third key as accepted-and-dropped, and objectui#9342 made that key a tombstone refused by name. Re-measured on the BUILT mirror and re-stated; every other clause in the note was re-probed and none moved.KeepsFunctionhelper answerstruefor an absent member, becauseBaseSchema's index signature types itanyand[any] extends [never]is false. The four keys already in that list came fromstring/any, so the helper could fail on them; on these two it could not. ADeclaresExactlyassertion (spelled withEqual, like the file's ownRetiredIsNever) was added beside it, with two synthetic controls proving it fails on an absent member and on a wrong signature.Merge —
origin/mainat55f39ee90f, and what it invalidatedgit merge, ⛔ never a rebase and ⛔ never a force-push, so every existing checkout of this branch stays valid. Merge commit35146cc663, parents8a5fdc8f6aand55f39ee90f; the fork point wasda45e6be7a, 223 files back on themainside and 9 on this one.Four files changed on both sides. Three auto-merged, because each pair of edits sat at a different anchor:
scripts/check-handler-key-read-sites.mjs(this branch drains twodetail::rows, objectui#9342 drainedonCardMoveand rewrote the comment above it),content/docs/api/schema-reference.md(this branch'sDetailSchemanote, objectui#9342's rewrite of theobject-kanbannote one section down) andpackages/types/src/__tests__/zod-mirror-parity.test.ts(this branch widens thecrud.zod.ts#DetailSchemaentry,mainrewrote two docblocks around the kanban entry).One conflicted:
packages/plugin-kanban/src/__tests__/handlerKeyDispositionsMeasured-7804.test.tsx. Both sides rewrote the SAME opening sentence of the file docblock for orthogonal reasons — ours replaced a hard-coded row count with a pointer at the instrument that re-derives it (AGENTS.md #9);main's objectui#9342 changed "Two of the three" to "All three" and named the card that moved the blocking read. Resolved as their union:main's fact, this branch's deferral, neither side's content dropped. ⛔ The row count this branch removed is NOT reintroduced, and the clause claiming the file reads the ledger live was re-checked against the surviving import rather than assumed.What the merge invalidated, and how each was repaired or confirmed:
61 reachable / 61 judged / 37 exemptedto60 / 60 / 36; the body's copy is updated above.--list— moved, one row out and none in; captures and hashes quoted in Verification.detail-handler-slots-7804.test.tsx— still true.detail::DetailSchema.onTabChangeis still a live ledger row, still read atDetailView.tsx:1696behind the cast, and the file writes no count down. ⛔ Not edited.### DetailSchemanote — one clause invalidated, repaired in its own commite3c3589d1f.KnownDriftfigures41 entries / 65 keys— unmoved. Two independent derivations (a line-oriented parse and a TypeScript AST walk) agree at this head, and both read63keys onmainand on the fork point, so the instrument responds to the thing being measured rather than returning a constant.The CI-blocking hypothesis — CONFIRMED, with one correction to its mechanism. Before:⚠️ The correction: ⚠️ Correction — not all to the same list, as this clause used to read. 24 workflow files carry a
mergeable: false/dirty,merge_commit_sha: null, and 0 check runs at head8a5fdc8f6awhile the previous headbcbd2793cbcarried 36. After this push:mergeable: true/clean,merge_commit_sha: b18d013b48, and 36 check runs at heade3c3589d1f.refs/pull/9343/mergewas not absent during the conflict — it lingered, frozen, pointing at a merge of the PREVIOUS head into an OLDERmain, so a probe that only asks whether that ref exists would have wrongly refuted the hypothesis. The honest signals aremerge_commit_sha: nulland the zero check-run count. A non-conflicting PR read as the control: its merge ref names its CURRENT head and the CURRENTmaintip. Everypushtrigger in this repo's workflow files is branch-limited, sopull_requestis the only leg a feature branch has, and it needs that merge commit.pushtrigger: 18 are limited to[main, develop]and 6 tomainalone (five spelled[main], one as a block list,changeset-release.yml). None reaches a feature branch, so the conclusion this clause is used for stands; the sentence as written did not.Verification
Provenance — bound to the act, not to the tip.⚠️ This paragraph deliberately does not assert what the branch tip is. A line of the form "all readings at head X" goes false the instant X moves, and every repair round on this PR moves it — three of this PR's four FAILs have been provenance, or figures a later push invalidated. So each sha below names where a reading was taken, which no later push can change.
e3c3589d1f— the figures in this section (merge-base55f39ee90f, taken withgit merge-baseand ⛔ never frombase.sha, which is the base BRANCH TIP), working tree clean at the time of each run:git status --porcelainreported 0 paths before the gate sweep. Every figure was derived AFTER the merge commit and after the documentation repair that follows it; none is carried forward from the pre-merge round.cb66ea5a37by the fourth contract review (5658363534), which reports that every figure in this section holds there.8da6a093c7, the commit that repaired the docblock sentence above: the gate of record reproduced unchanged (60reachable /60judged /36exempted, exit 0), its--listreproduced unchanged (61 lines,sha256prefix0b586b1c9e568ffc), and the four touched test files ran green from the repository ROOT (4 passed (4)/130 passed (130), withapps/consoleabsent from the log). ⛔ The remaining figures in this section were not re-run at that commit, and this bullet does not claim they were — that commit is comment-only.The invariant, and the instrument that checks it. Every commit after the last sha named above is narration-only — prose, code comments, and this body — and moves no figure in this section. The discipline this PR follows is that a push which moves any figure re-derives the section and rewrites this paragraph last, so the paragraph can never be older than the figures it introduces. ⇒ ⭐ A reader is not asked to trust a tip — read this PR's own commit list. If every commit above
8da6a093c7is narration-only, this section is current; if one of them is not and this paragraph still stops at8da6a093c7, the discipline was broken and the section is stale. That check is mechanical, it needs nothing but the commit list this page already renders, and it stays valid across any number of further pushes.Tests —
pnpm exec vitest run packages/types/ packages/plugin-detail/ packages/plugin-kanban/ scripts/ examples/schema-catalog/, through the shared verify lock:examples/schema-catalog/is in the union deliberately: an authored key's acceptance moved, so a package-scoped run is not the blast radius (objectui#9273).packages/plugin-kanban/joined the union at the merge, because that is where the collision landed.Type-check — the dependent-set membership read was re-done on the post-merge tree, and it reproduces unchanged: 37 workspace manifests name
@object-ui/types, and all 37 declare atype-checkscript (all spelledtype-check, hyphenated),@object-ui/siteamong them.pnpm --filter '...@object-ui/types' type-checkunder the lock reportsScope: 42 of 47 workspace projects, 41 of which run atype-checktask — all 41 green, 0 errors,VERDICT command-exit 0.apps/sitefailedTS2307 Cannot find module '@object-ui/example-schema-catalog'becauseexamples/schema-catalogis not in the...@object-ui/typesDEPENDENTS closure and so is never built by that filter;examples/schema-catalog/distwas absent. A fullpnpm build(43/43) then made the same command green with 0 errors, which is the control separating a closure gap from a finding.Build —
pnpm build --concurrency=2under the lock:Tasks: 43 successful, 43 total,VERDICT command-exit 0.Gate of record —
check:handler-key-readsexit 0, re-run at heade3c3589d1f; line quoted above (36 exempted by ledger). Its--listwas captured before and after the merge and it moved:sha256prefixc6223378723c6bc1/ 62 lines before,0b586b1c9e568ffc/ 61 lines after. Normalising away line numbers, exactly one row left and none arrived —'object-kanban' ObjectKanbanSchema.onCardMove, retired by objectui#9342 — and every other difference is a line-number shift in a row whose text is unchanged.Other gates re-run on the merged tree, all exit 0:
check:control-bytes(7625 tracked text files, 0 findings) ·check:new-line-citations(0 new citations added by this branch) ·check:changeset-claims·check:spec-symbols·check:doc-types·check:test-path-roots·check:readme-exports·check:dist-completeness·check:unreferenced-sources·check:governed-queue-guard(NOT GOVERNED — 9 path(s) checked against 5 governed surface(s); none matched, the 9 being this PR's own files against the NEW merge-base, ⛔ not the 228 the merge carries) · changeset presence (6 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)) ·check:changeset-no-major.check:*families (56 exist in this repo) — CI owns the farm, and this seat runs the targeted set.check:sdui-registration-pins— exit 2, its own printed line isNo console build to weigh at apps/console/dist/assets ... This is exit 2, not a pass; it pins that registrations survive BUNDLING and this diff moves no registration. Remote CI convergence and the ready/queue flip — the PM's, per the dispatch.pnpm testin full — the repo-level scan, CI's run.pnpm --filter '...@object-ui/types' buildselects the DEPENDENTS of@object-ui/types, which excludes packages those dependents themselves depend on:@object-ui/react-runtime(depssucraseonly) and@object-ui/authname@object-ui/typesnowhere, so they were never built and@object-ui/components/@object-ui/app-shellcould not resolve their declarations. A fullpnpm buildis green at 43/43, which is the control proving the two failures were filter-closure gaps rather than findings.Ablation — the declaration is load-bearing, proved on disk
No
disthop is involved: the root vitest config aliases@object-ui/types/zodtopackages/types/src/zod/index.zod.ts, so the mutated file IS the module under test. The merge did not touchcrud.zod.ts— its blob is557c86b6ba292441f4f91e3170dac6964da3924dat the pre-merge head and at980126de0alike — so this leg stands as run.Leg run from the COMMITTED state, under
trap ... EXIT INT TERMwith absolute paths:All three mutated failures are NAMED on
onNavigate; everyonAddCommentleg stayed green — per key, not a blanket. And the gate of record turned red with its own printed line:Restored by
git checkout HEAD -- THE_PATH(spelled as a word: GitHub's body sanitizer eats angle-bracket-shaped fragments even inside code spans), and the restoration is proved by the blob hash returning and an EMPTYgit diff HEAD, never by an exit code.Acceptance notes
as anycast, so objectui#7804's finding was a floor rather than a total. Measured then with a firing control: 2 live cast-hidden handler reads in production sources,(schema as any).onTabChangeinDetailView.tsx(detail/detail-view) and(schema as any)?.onTabChangeincontainers.tsx(tabs); the control — the same anchor without theon-prefix — returned 33 files. That blindness is gone here. objectui#9392 landed onmainand arrived with the merge atcb5e80cce6:check-handler-key-read-sites.mjsnow unwrapsas/!/satisfies/ parens before it names the receiver, so BOTH cast reads are judged and BOTH are ledger rows attributed to objectui#7804.state_reason: completed, dated2026-09-13T12:55:04Z, and its item ② was never carried anywhere. The per-key disposition of those twoonTabChangerows is still OPEN, still lands in the zod arms and ⛔ still not in this PR — the ledger rows carry it on objectui#7804, which is the cardcheck-handler-key-read-sites.mjsitself names at:203-204, and whichcontent/docs/api/schema-reference.mdalready pointed at. ⛔ No new card is invented here. Dedup ran over all 5 pages of the repo-scoped open-issue list (453 open issues) with objectui#7804 as the known-hit control; the three near neighbours (objectui#8327, objectui#8649, objectui#6152) are each shown non-overlapping in that card's body.scripts/or.github/workflows/scans for a leading**BREAKING**(the only two files mentioning the word use it in unrelated prose), and.changeset/config.jsonuses the stock@changesets/cli/changelog, which publishes bodies verbatim. Across the pending changeset population the all-caps**BREAKINGspelling is a minority and the title-case**Breakingspelling — this changeset's — is the commoner of the two. ⛔ No cardinal is written here, per AGENTS.md 完善设计器的每一个细节 #9: the population is LIVE, no gate re-derives it, and it does not have a single right answer — at this headls .changeset/*.mdcounts 1540, one of which is the changesets tool's ownREADME.md, whilecheck-changeset-claims.mjsreports 1538 because it also excludes the changeset this branch adds. The clause's old1489/31were pre-merge readings. Instruments, so the reading can be retaken instead of trusted:ls .changeset/*.mdfor the denominator, and for either spellingperl -0777 -ne 'print "$ARGV\n" if /\*\*BREAKING/' .changeset/*.md | wc -l(⛔grep -ccounts LINES, not files). first-occurrence line numbers run from 5 to 51,main's own7804-object-kanban-handler-keys-judged.mdcarrying it at line 21. This changeset was therefore left as authored. Successor: whoever wants a real carrier convention has to build the gate first.check:changeset-claims(report-only) flags the pending changesets whose bodies name a file this change touches.16) was a pre-merge reading; at this head the gate reports 26. And its file list named three files when the findings resolve to four — it omitted the largest group. Measured at this head, by distinct changeset:content/docs/api/schema-reference.md12 (packages/types/src/__tests__/zod-mirror-parity.test.ts8,packages/types/src/crud.ts7,packages/types/src/zod/crud.zod.ts3 (26 distinct changesets, 31 name-resolutions, because some bodies name more than one). The population is live — re-derive withnode scripts/check-changeset-claims.mjs, ⛔ do not trust the cardinals above. Every flagged body predates this branch and none is falsified by it, and that is now measured rather than asserted: this PR's only edit toschema-reference.mdis purely additive —git diff --statagainst the merge-base reports1 file changed, 2 insertions(+), 0 deletions — so it removes and rewrites nothing those 12 bodies describe. Successor: the seat that next edits each of those changesets' own packages.commentsis not declared by this change.DetailViewgates theonAddCommentforward onschema.comments, kept by the same passthrough — but it is not a handler key, declaring it is an accept-set decision of its own, and objectui#7804's rows are the handler keys. Successor: whoever rules the'detail'arm's non-handler drift below.'detail'arm declares far fewer keys thanDetailViewreads —sections,fields,objectName,backUrl,editUrl,comments,activities,summaryFields,autoTabs, all kept by the passthrough.dataas a tenth member, and that was wrong.datais a DECLARED member ofBaseSchemaCore(packages/types/src/zod/base.zod.ts:330,z.any().optional()), whichDetailSchema = BaseSchema.extend({ … })unions into the arm — sodatais judged, ⛔ not kept. The nine above are measured kept;datais measured declared. Not a handler-key question and outside every row of objectui#7804; the carrier for it is objectui#7804's own'detail'arm, which stays open as the parent.KeepsFunctiontype helper cannot fail on a member that was ABSENT rather than wrongly typed (an absent member readsany, and[any] extends [never]is false). Fixed in place for this pair with aDeclaresExactlyassertion and two synthetic controls; the class successor is whoever next adds a previously-UNDECLARED key to that ledger.Session that produced this change, written as prose so it survives a body edit:
https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZGenerated by Claude Code
Generated by Claude Code