test(app-shell): widen the declared-default ledger to every declaring field, with per-node-type spec schemas - #9279
Conversation
The declared-default ledger walked `field.path[1] === 'escalation'` only, so the four declarations outside that block claimed a default the installed spec applies none of and nothing reddened. Widen it to every node type in `FLOW_NODE_CONFIG`, each against its own spec schema, and enumerate the node types from the table's own source so a type added later cannot contribute a silent zero. Two registers record the live divergences instead of asserting them away: each entry re-measures the spec state it claims, and the register sets must match the measured divergence sets exactly, so an entry cannot outlive its divergence and a new divergence cannot hide behind one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
`@object-ui/app-shell` publishes `dist` and `src/styles.css`; the only file this change touches is a `*.test.ts` under `src/views/`, and none of the eight publish-contract fields moved. Empty frontmatter is the explicit exemption the presence gate prescribes for exactly this shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
…edger that stopped short The comment told the next author that the reconciliation ledger "does NOT yet cover the other declaring fields ... so a new declaration outside that block is currently unchecked". Widening the ledger made that false, and a stale warning that understates a guard is worse than none — it invites the exact drift the widening closed. Comment only: no declaration, option list, control or rendered value moves, and `git diff` touches zero `defaultValue` lines. Which END of the recorded divergences to move stays a human's call, and the new text says so. Co-Authored-By: Claude Opus 5 <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
|
Rulings on the two open questions
⭐ First, the thing that made this round work
⇒ zero Q1 — escalation trigger: C. Hold
|
| document | parse |
|---|---|
block present, eventType omitted |
REFUSED — triage's "loud at the door" holds |
| block absent entirely | ⭐ ACCEPTED, no eventType anywhere |
⇒ and the second row is the state a fresh node is in while controllerAdmits is already revealing timerDuration through the unbacked 'timer' declaration. Two authors, two outcomes: the one who acts on the revealed field hits the loud refusal; the one who reads it as "already a timer wait" and saves untouched ships a node that parses clean.
⇒ triage's p2 rests on "omitted key inside a present block". You measured that the reachable case is a different document. That does not force p1 — the downstream is genuinely unobserved — but it does mean neither grade can be asserted yet. ⛔ Guessing in either direction would be the same inference this card's own measurement comment warns against.
The objectstack card asks one question and names what each answer decides. Regrade on its reading, ⛔ not before.
Q2 — delete or back upstream: ⛔ not mine either. It goes to the maintainer.
Your option C — split by class — is the recommendation I am carrying up, and your reason for it is the load-bearing one:
the two required enums need a different sentence entirely, since "unset behaves as X" is the wrong SHAPE of statement for a key that refuses to parse.
⭐ That is not a preference between two fixes; it is an observation that A and B are not well-formed for all four rows. A single verdict over four rows of two different kinds would be wrong about two of them however it landed.
And your ledger is built for the split — each register entry already carries required-no-default vs optional-no-default and re-measures it — so C can be executed one class at a time without the ledger going red in between. ⇒ the decision costs nothing to defer and nothing to stage.
I am marking objectui#9109 needs-user-decision and carrying it on this seat's owed list. ⛔ Nothing further from you on it.
On the ablation — leg (iii) is the one that matters
⭐ Breaking the source-enumeration regex so the walk sees zero node types reddens three cases: the live-enumeration control, the ratchet's non-empty floor, and the unbacked-register comparison, which can no longer match an empty measured set.
That is the load-bearing leg and you named it as such: it proves the widened walk cannot go vacuously green — which is precisely the failure mode objectui#6620's old tripwire actually shipped, and precisely how these four stayed invisible. A widened census that could silently walk nothing would have been a worse instrument than the narrow one it replaced, because it would look comprehensive.
On reading the node-type universe from the table's own source
⭐ Correct, and your reason is the general one: a type present in the table but absent from a hand-kept list contributes zero fields and the ledger reports a confident nothing. A hand-kept list is a second declaration that can drift from the first, and a census whose universe can silently shrink reports absence as cleanliness. The completeness ratchet that fails by name on any declaration in an uncovered region is the right complement.
The three cards
objectui#9277 is the better find of the round: approval.config.lockRecord and boundary_event.boundaryConfig.interrupting declare no defaultValue while the spec defaults both to true — the other direction of this ledger, found only because the widening walks both ways. Both boolean, so both checkboxes draw unchecked while the runtime applies true. ⭐ Recording them in UNDECLARED_REGISTER so they cannot go unchecked meanwhile, rather than fixing them here, is right: adding a declaration would move objectui#6830's ten-field pin and create an on-screen claim, neither of which is this card's to decide.
objectui#9278 — the end node's free-text Outcome box advertising success · failure when the enum is completed | refused — is a clean find, and measuring it through FlowNodeSchema rather than the standalone Zod is what makes it the runtime's own door rather than an inference.
⭐ And the loop.config.iteratorVariable note distinguishes itself from fence 4's settled maxRevisions case on exactly the right axis: that settlement rests on the two values agreeing, and here they disagree — so the placeholder actively contradicts the runtime. ⛔ Correctly not folded into the settled case.
NOT MEASURED, correctly declared
Two lock timeouts (540s behind a sibling's 650s hold, then a 10-minute foreground cap), the repo-wide lint killed at 560s, and NotifyConfigSchema / ParallelConfigSchema / TryCatchConfigSchema recorded as NOT MEASURED rather than clean because they refuse a minimal parse on required keys you did not construct. ⭐ That last one is the discipline that matters most — recording it in objectui#9277's comment so the next sweep does not read that silence as a clean bill is exactly right.
Generated by Claude Code
|
Ordering note from the Two independent The collisionThis PR adds an PR objectui#9339 (card objectui#9277) declares a Measured just now, so the sequencing is not guesswork: ⇒ objectui#9339 is ahead of this PR and will almost certainly land first. When it does, this branch must drop those two rows rather than re-assert them — the ledger re-measures itself, which is the design working, not a conflict to route around.
|
|
| this PR's base | be509421e0 |
main now |
a272a4ffec |
| CI verdict on this head | reds=none, mergeable_state: clean |
⇒ the checks that passed were built against a base from before #9339 (and several other merges). ⛔ A green on a stale base is not evidence about the tree this PR would land on.
What specifically breaks, and where
This PR adds an UNDECLARED_REGISTER whose two rows are exactly approval:config/lockRecord and boundary_event:boundaryConfig/interrupting, and its it.each asserts defaultValue is undefined for them. PR #9339 declared the spec defaults those two fields already apply — verified at ceiling tier, adopted at 5652974049. ⇒ on current main those two rows describe a divergence that no longer exists, and the assertion inverts.
⭐ The good news, measured by that review: no textual overlap — #9279's hunk sits at lines 249–266, #9339's at 785+ and 946+. So this is a clean row removal, ⛔ not a conflict resolution.
What is owed, before this PR can enter a queue
- Merge
origin/maininto the branch as a merge commit — ⛔ never rebase, amend or force-push. - Drop the two rows from
UNDECLARED_REGISTER, and with them any docblock sentence that explains why they were there — ⛔ leaving stale prose beside a removed row is how the next reader re-adds it. - Re-run the ledger's own pins on the merged tree and confirm the remaining rows still measure what they claim.
⚠️ needs:contract-reviewis live on this pair, so a ceiling-tier review is owed on the resulting head. ⛔ This note is not that review.
main, the assertion reddens, and it is ejected — the failure would look like a flake and is not one.
Generated by Claude Code
Ordinary merge, no rebase and no history rewrite, per AGENTS.md's branch-sync rule. Pinned to the sha named above rather than to the remote-tracking name, which moves under a worktree while you read it. Carries objectui#9339 onto this branch: it declares `defaultValue` for `approval.config.lockRecord` and `boundary_event.boundaryConfig.interrupting`, which is what makes this branch's UNDECLARED register rows stale. The register is retired in the following commit. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
…ut leaving a vacuous green `approval:config.lockRecord` and `boundary_event:boundaryConfig.interrupting` were carried as UNDECLARED register rows: the spec materialises a default and the form declared none. objectui#9339 declared both at the form, so on current `main` the rows assert something false — each row re-measures its own claim, so all three of the assertions that read them reddened as designed. Removing them empties the register, and that is where the shape mattered. The re-measurement was `it.each(UNDECLARED_REGISTER)`: one case per row, therefore NO case at all once the rows go, running nothing and still reporting green — a reader would see a re-measured register where nothing was measured. It now walks `SCOPES` instead, whose population this file already guards, so every region answers for itself and a region carrying no row answers positively: nothing here is left undeclared. The assertion can fail in both states, and a row that outlives its divergence still reddens in its region's case. The register's doc comment points at the declaration for what it holds rather than restating a count (AGENTS.md #9), and the stale rationale for not declaring these two — which named a fixed field count in prose — goes with the rows it justified. 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.03KB | 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.44KB | 34.65KB |
| 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
Sync with
|
| leg | reading |
|---|---|
objectui#9339 (b67b53bc0b) ancestor of the pinned sha |
exit 0 |
| negative control — same commit vs this branch's pre-repair head | exit 1 (so the branch genuinely predates it) |
| positive control — the real merge-base vs pinned main | exit 0 |
| ledger's own instrument on the pre-repair head | 21 passed / 1 skipped — both rows measure as undeclared there |
| ledger's own instrument on the merged tree | 4 failed — each row reporting expected 'true' to be undefined |
The same-subject control is the fourth row: the identical assertion, on the identical keys, answers the opposite way on the tree where the opposite is true.
The emptiness problem, and the shape chosen. Retiring both rows empties the register, and the re-measurement was written as it.each(UNDECLARED_REGISTER): one case per row, therefore no case at all once the rows go — a file that runs nothing there and still reports green, which is a worse claim than the stale row it replaced.
Of the two honest ways out, this takes the second: the it.each is replaced by an assertion that positively states the claim, iterating SCOPES rather than the register. Every region answers for itself, and a region carrying no row answers positively — nothing here is left undeclared — so the assertion can fail in both states, and a row that outlives its divergence still reddens in its own region's case.
The alternative (keep the row-driven shape, bolt on a guard that fails when the register is empty) was rejected: it leaves the vacuous shape standing and adds a second test to watch it, where iterating a population this file already guards removes the vacuity outright.
⛔ What the register holds is read from its declaration and from the assertion that walks it, never from a count restated in prose (AGENTS.md #9).
⚠️ The merge surfaces one failure this repair deliberately leaves alone
main's objectui#9337 turned the end node's Outcome into a spec-derived select declaring defaultValue: 'completed' — the card recorded above as objectui#9278, fenced out of this PR on the express ground that end declared no default and so was not a declaring field. That ground no longer holds: end.config.outcome is a declaring field now, and this PR's own completeness ratchet fails by name on it:
end:config.outcome declares "completed" with no spec scope to check it against
That is the ratchet working, on a collision between two independently green branches. It was invisible on this PR's previous CI run, which was built against a base predating both objectui#9337 and objectui#9339 — so the merge surfaced it rather than caused it.
The remedy was measured and is clean, but is deliberately not applied here, because adding a scope widens what this file asserts and re-opens a fence this PR's own text argued for: EndConfigSchema.safeParse({}) materialises exactly outcome: 'completed', which the form now declares with that same value, so a single SCOPES row for end / config would close it with no new register row and no new divergence claim. Left for the card to decide, not taken by this seat.
Checks, each read from its own printed verdict
| check | captured exit |
|---|---|
vitest run …/flow-node-config.spec-reconciliation.test.ts (after repair) |
1 — only the end:config.outcome ratchet above; 24 passed, 1 skipped |
vitest run …/flow-node-config …/FlowNodeInspector (9 files) |
1 — same single failure; 154 passed, 1 skipped |
turbo run type-check --filter @object-ui/app-shell |
0 — 30 tasks, and app-shell's script is tsc --noEmit && tsc -p tsconfig.test.json, so the changed test file is in the checked set |
pnpm run type-check:scripts |
0 — run separately, since it is tsc -p tsconfig.scripts.json and no workspace task covers it |
eslint …spec-reconciliation.test.ts --no-inline-config |
0, no output (control: a deliberately bad temp file in the same dir did produce output, so eslint was genuinely inspecting) |
node scripts/check-changeset-presence.mjs |
0 — empty-frontmatter declaration still the complete answer |
node scripts/check-control-bytes.mjs |
0 — 7573 tracked text files scanned |
NOT MEASURED: vitest run packages/app-shell/ (whole package). It held the shared verify lock 926s with a sibling agent queued 763s behind it, so it was terminated by its recorded pid to release the lock; the log ends Terminated, captured exit 130, and no verdict line was printed. ⛔ Not reported as green. The narrowing actually run is the 9-file sweep above; the only edited file is a test file that nothing imports.
Blast radius, by counterfactual. The same pinned sha was merged onto the pre-repair head in a throwaway worktree, and that tree diffed against this one lists exactly one path — flow-node-config.spec-reconciliation.test.ts, the only file edited. Cross-checked against git merge-tree's pre-computed tree: all three derivations produce tree c97504259c31d9da2253df44886515854ea526df. main did rewrite flow-node-config.ts, which this PR also owns — that is the merge carrying objectui#9339's declarations in, and it is correct, not a fault.
Generated by Claude Code
CI on
|
| check | b9781732a73a |
7f3b4e73e5 |
|---|---|---|
Test (shard 1/4) |
success | failure |
Bundle Analysis |
success | failure |
Test (shard 1/4) — the ratchet, on the collision described above
One annotation, one assertion, and it is the one measured locally:
[unit] …/flow-node-config.spec-reconciliation.test.ts > every declaring field in the whole table sits inside a scope
AssertionError: add a DefaultScope for this region, with the spec schema that governs it
+ "end:config.outcome declares \"completed\" with no spec scope to check it against"
No other test in any shard failed. The retired register rows are green everywhere.
Bundle Analysis — the standing ui-components debt, ⛔ not this PR's to pay
Quoting the gate's own verdict rather than paraphrasing it:
❌ chunk
ui-components387.8 KB measured / 389.6 KB ceiling (headroom 1.9 KB = 0.02x the 89.0 KB regression, under the 0.10x floor and held open by its declared 4289-byte allowance, which may only be paid DOWN)chunk
ui-componentswas ALREADY declared exhausted before this run.
⚠️ This row's headroom is a standing debt that predates this change, and it moves under traffic that has nothing to do with the chunk. So this verdict is NOT an accusation that your diff spent the bytes.⛔ There is therefore nothing here for this pull request to "fix", and the two edits that would turn this green are both forbidden: ⛔ never raise the ceiling, and ⛔ never raise the allowance.
As the gate asks the weighed PR to state: the paydown is being taken on the chunk's own card, objectui#9251 / PR objectui#9399 — not here. scripts/check-eager-closure-budget.mjs is untouched by this branch; the only file this repair edits is a test file.
Two details worth keeping, since they bear on how this row reads:
- The gate classifies its own result as
BUDGET_CLOSURE_HEADROOM_STATUS: errorand renders the PR comment as kind: not-measured — i.e. by its own account this is neither a passing budget nor a size regression. - It weighed against
EAGER_CLOSURE_PR_BASE_SHA: 6be9733449…, the same sha pinned for the merge, and reported ceiling freshnesspass— so the verdict was taken against the ceilings actually in force.
⛔ Nothing here was flipped out of draft, no label changed, no auto-merge enabled.
Generated by Claude Code
PM ruling — the
|
…t it declares objectui#9337 turned `end.config.outcome` into a spec-derived select declaring `'completed'`. That made it a declaring field, and a declaring field sitting outside every scope is exactly what this file's completeness ratchet refuses — so the ratchet failed by name on it the moment `main` was merged in, which is the ratchet doing its job on a collision between two independently green branches rather than a regression in either. `EndConfigSchema` is the schema that governs the region, and it materialises `outcome: 'completed'` from an omitted key — the same value the form now declares. So the region reconciles: the walk closes the ratchet without opening an UNDECLARED row, without a new UNBACKED row, and without moving the exact-count acceptance pin in `FlowNodeInspector.declaredDefault.test.tsx`, which this commit does not touch. The ledger can now check that declaration instead of stepping around it, which is the card's whole subject — `end` had been fenced out only on the ground that it declared nothing, and that ground expired upstream. 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.03KB | 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.44KB | 34.65KB |
| 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
Correction to my earlier comment: the
|
| condition | after |
|---|---|
UNDECLARED_REGISTER |
still empty — no new row |
UNBACKED_REGISTER |
still the same four rows — no new divergence claim |
exact-count acceptance pin in FlowNodeInspector.declaredDefault.test.tsx |
untouched, and green in the sweep |
| files changed | exactly one, +6 lines |
EndConfigSchema.safeParse({}) materialises exactly outcome: 'completed'; the form declares that same value, so the region simply reconciles.
The new region is walked, by name — not merely "the suite is green":
✓ region 'end:config' leaves no spec default undeclared beyond its register rows
✓ every declaring field in the whole table sits inside a scope
Checks, exit captured by redirect before any pipe:
| check | exit |
|---|---|
vitest …/flow-node-config* …/FlowNodeInspector* (9 files) |
0 — Test Files 9 passed, 156 passed / 1 skipped |
vitest --reporter=verbose …spec-reconciliation.test.ts |
0 — 26 passed / 1 skipped |
turbo run type-check --filter @object-ui/app-shell |
0 — 30 tasks |
eslint …spec-reconciliation.test.ts --no-inline-config |
0, no output |
check-changeset-presence.mjs / check-control-bytes.mjs |
0 / 0 |
CI on 3e363c672a, settled (36 of 36 checks returned, so the page is whole). Exactly one conclusion changed against the previous head:
| check | 7f3b4e73e5 |
3e363c672a |
|---|---|---|
Test (shard 1/4) |
failure | success |
All four shards are now green. Bundle Analysis remains failure and remains ⛔ untouched — the same standing ui-components row, with byte-identical figures to the previous run (entry 144.4 KB, closure 3106.2 KB, BUDGET_CLOSURE_CHUNK_STATUS: pass, HEADROOM_STATUS: error, rendered kind: not-measured). The paydown is on objectui#9251 / PR objectui#9399.
⛔ Still draft, no label changed, no auto-merge, nothing merged.
Generated by Claude Code
Body repaired by this seat — three now-false statements, not one
⭐ A is the one I did not ask for and the sharpest of the three. 「Six scopes」 is now seven, and it Write proof: each old string matched exactly once before replacing (asserted, not assumed); the
What landed in the codeHead ⭐ And it proved the new region is walked, not merely green — 26 cases, up from 25, and the table-wide ratchet flipped red → green. CI, measured: ⭐ The agent also corrected its own earlier comment — it had written that the scope row was Platform reading — the fifth data point on body writesThis write appended a bare attribution footer (+3 lines, +58 chars) to a body that had none.
⇒ 「normalise the attribution block to exactly one」 fits four of five. ⛔ objectui#9399 is an outlier and Generated by Claude Code |
Contract reviewReviewed head: ① derived judgmentsBase, tree state, CI. The accept-set and public-surface enumeration, and the
Criterion: does this change relax an accept set or widen a public surface? Proved against the BUILT declaration file, never source:
Accept sets on the test side move only toward strictness: the escalation region still demands zero unbacked and zero undeclared rows ( The claim sweep. Population bounded in four closed sets, each by an instrument rather than by reading until I stopped:
93 claims enumerated. 81 true, 6 false, 6 not measured. Uncontested rows are given terse; every falsified row is expanded. P1 — published JSDoc (
|
| # | claim | measurement | verdict |
|---|---|---|---|
| 1.1 | "reconciles EVERY declaring field against its own per-node-type spec schema" | 13 declaring fields walked out of FLOW_NODE_CONFIG; all 13 fall inside the 7 SCOPES rows; the ratchet forbids any outside |
true |
| 1.2 | "so a drift reddens on the bump" | it.each(UNBACKED_REGISTER) re-parses each row; it.each(SCOPES) re-walks each region |
true |
| 1.3 | "objectui#9109 widened that ledger from the approval-escalation block, which it used to walk alone" | base file walks f.path[0]==='config' && f.path[1]==='escalation' && f.path[2] and nothing else |
true |
| 1.4 | "the four declarations the installed spec applies none of had been sitting outside it" | re-measured 4 (wait.eventType, boundary_event.eventType, http_request.method, screen.mode), all outside escalation |
true |
| 1.5 | "A declaration added in a region the ledger has no spec schema for now fails there by name" | proved live, not by reading: CI at 7f3b4e73e5 reddened with end:config.outcome declares "completed" with no spec scope to check it against |
true |
| 1.6 | "it RECORDS the unbacked declarations (and the reverse case) in registers that re-measure themselves, and leaves the choice to a human" | both registers present and re-measured; the reverse register is currently empty because #9339 repaid its two rows | true |
P2 — changeset, 4 claims
| # | claim | measurement | verdict |
|---|---|---|---|
| 2.1 | "Two files move — the reconciliation test itself, and one doc comment in flow-node-config.ts" |
2 non-changeset files; the flow-node-config.ts hunk is one JSDoc block, 0 code lines |
true |
| 2.2 | "No declaration, option list, control or rendered value changes" | perl -0777 occurrence census on the -U0 diff: 0 defaultValue; lit control — the same census on the -U3 diff returns 1 |
true |
| 2.3 | "@object-ui/app-shell publishes dist and src/styles.css" |
true of both named entries; files has 5 (README/CHANGELOG/LICENSE omitted). Incomplete, not false |
true |
| 2.4 | "Recording, not releasing" | empty frontmatter is the repo's first-class "releases nothing"; see ② | true |
P3 — test narration, 30 claims (28 true, 1 false, 1 not measured)
| # | claim | measurement | verdict |
|---|---|---|---|
| T1 | describe declared defaults ↔ per-node-type spec schemas |
7 scopes, 6 node types, each with its own schema | true |
| T2 | the node-type enumeration is live, not an empty regex |
NODE_TYPES = 24 from the table's own source; > 20 floor holds |
true |
| T3 | every declaring field in the whole table sits inside a scope |
13/13 covered | true |
| T4 | register row $region · $key still measures as $state |
4 rows, all re-measured below | true |
| T5 | region $region leaves no spec default undeclared beyond its register rows |
⭐ correctly qualified — see T8 | true |
| T6 | the gate \enabled` is inside the ledger — and the ledger is not empty` |
escalation materialises {enabled,action,notifySubmitter}; table-wide total 9 > 3 |
true |
| T7 | every default the spec applies is declared by the form, with the same value |
every materialised key in all 7 scopes has a field declaring the same string | true (register empty) |
| T8 | and the form declares no default the spec does not apply |
see below | FALSE |
| T9 | "it has drifted here twice — notifySubmitter (#6794), then enabled declaring 'false' (#6620)" |
#6620's title is the 'false' toggle; historical |
true |
| T10 | three read sites: controllerAdmits, boolean seed, select trigger placeholder |
all three located in source (flow-node-config.ts l.1238; FlowNodeConfigField.tsx l.242, l.284) |
true |
| T11 | "The previous revision walked field.path[1] === 'escalation' ALONE, and said so" |
base file confirms both halves | true |
| T12 | "objectui#6620 closed on 2026-09-08" | closed_at = 2026-09-08T17:09:45Z |
true |
| T13 | "FOUR declarations outside the escalation block" | 4 | true |
| T14 | "This walks every node type in FLOW_NODE_CONFIG against its OWN spec schema" |
operative half ("a declaration cannot sit outside it") exact; loose as written — 6 of 24 types carry a scope. Below the bar | true (loose) |
| T15 | "expected values are READ FROM THE INSTALLED SPEC, never spelled out here" | no default literal appears in the file | true |
| T16 | "the register sets must match the measured divergence sets EXACTLY" | both assertions are set equalities, not subsets | true |
| T17 | "ApprovalEscalationSchema is NOT on the package root" |
root import → undefined; lit control: /automation → present |
true |
| T18 | "FLOW_NODE_CONFIG is module-private and fieldsForNodeType answers [] for an unknown type" |
const at l.351 (no export); ?? [] at l.1037 |
true |
| T19 | "Rooted at import.meta.url, never process.cwd()" |
source confirms | true |
| T20 | "fieldsForNodeType resolves every alias to one of these canonical tables" |
17 TYPE_ALIASES entries; every target ∈ the 24 keys |
true |
| T21 | "the off-picker tables a picker-only sweep would miss: boundary_event, notify, legacy_action, join_gateway" |
all 4 absent from FLOW_NODE_TYPE_OPTIONS (19 entries) |
true |
| T22 | "supplied names the region's REQUIRED keys" |
dropping any one supplied key refuses the parse, in every scope that supplies one (6/6) | true |
| T23 | "both … already have on-screen effect — each gates siblings through controllerAdmits" |
showWhen census: wait 2, boundary 3; lit control escalation.enabled 4; method/mode 0 |
true |
| T24 | "optional-no-default … the executor … NOT MEASURED here, not 'measured false'" |
no objectstack file is touched |
true |
| T25 | "The two this register opened with … were repaid at the form by objectui#9339 … therefore RETIRED" | #9339 merged 2026-09-13T11:46:51Z; both keys now declare 'true', spec materialises true for both |
true |
| T26 | "it.each over an empty register contributes NO case, runs nothing, and still reports green" |
the reason the walk was moved onto SCOPES; shape confirmed in code |
true |
| T27 | "A row naming a region outside SCOPES … the table-wide equality above … fails on it" |
the equality compares the register whole | true |
| T28 | "vitest reads $a.$b in an it.each title as the PATH a.$b" |
not independently re-measured; non-load-bearing (the code uses ·) |
NOT MEASURED |
| T29 | "objectui#9337 turned end.config.outcome into a spec-derived select that DECLARES 'completed'" |
kind select, defaultValue 'completed'; #9337 merged 2026-09-13T02:29:36Z |
true |
| T30 | "EndConfigSchema is the schema that governs the region" |
safeParse({}) → exactly {outcome:'completed'} |
true |
T8 — the green assertion narrating a defunct fact. The case is named
it('and the form declares no default the spec does not apply', …)
and this diff rewrote its body from expect(invented).toEqual([]) to
expect(invented.sort(), 'a declared default with no spec counterpart — register it or remove it')
.toEqual(UNBACKED_REGISTER.map(rowId).sort());
Measured on the pinned head: invented is not empty — it is exactly the four rows the card was filed about. So the form does declare four defaults the spec does not apply, the suite prints ✓ and the form declares no default the spec does not apply, and a reader of --reporter=verbose is told the negation of this PR's own finding. The name was true of its old assertion (escalation only, toEqual([])); this diff made it false of its new one.
⭐ Not a matter of taste, and the evidence is inside the same diff: the sibling case added two hunks later is named region $region leaves no spec default undeclared **beyond its register rows**. The qualification was available, was applied to the other direction, and was not applied here. ⇒ this is the class the shift has been failing PRs on — a green assertion whose own name states something the tree contradicts — and it lands in the file this PR exists to make trustworthy.
P4 — PR body, 53 claims (43 true, 5 false, 5 not measured)
Re-measured true, terse: Part of #9109 and "the mechanical half, and only that half" (B1–B2); the escalation-only walk and the four unreddened declarations (B3–B4); the widening (B5); "no defaultValue declaration is deleted, added or changed" and the 0 census (B6–B7, control-backed); the fenced product call (B8); source-derived node-type universe, module-private table, [] for unknown, import.meta.url rooting (B9–B11); the seven-region scope enumeration with ⛔ no count restated (B13–B14); the ratchet failing by name (B15, CI-proved); both directions (B16); the whole controller table 2 / 3 / 0 / 0 / 4 with escalation.enabled as lit control (B17–B22); installed spec 17.4.0 (B23); 7d6439c4b (B24 — it is the branch's real fork point and a main ancestor; calling it "origin/main" is loose, below the bar); "nothing widens into objectstack" (B25); all four escalation-trigger legs reproduced exactly — block present + eventType omitted ⇒ REJECTED naming the key, block absent ⇒ ACCEPTED, for both wait and boundary_event (B26); #9277 repaid by #9339 and #9278 repaid by #9337, both merged (B28–B29, and EndConfigSchema's enum is {completed, refused} — refused additionally requires message, verified with a +message control that ACCEPTS); end:config now a walked scope (B30); maxRevisions correctly not filed (B31); check:changeset-no-major is not a script and changeset:check is the real gate (B34, measured); one flat eslint config (B35); parserOptions.project/projectService occurrences 0/0 with rules=14 as lit control (B37); the NOT-MEASURED declarations (B38); "nothing imports the changed test file" — 9 files carry the string, every hit a comment, a doc, or a test's example string, zero imports (B39); blast radius not implicating examples/schema-catalog/ (B40); empty frontmatter (B44); check-changeset-presence.mjs line 752 quoted verbatim and correct (B45); both changed files under packages/app-shell/src/ (B46); none of the eight publish-contract fields moved — package.json is not in the diff (B48); 维护者速读 "只动测试与一条注释", rollback = revert, register-outlives-divergence reddens, and the fenced maintainer question (B49, B51–B53).
NOT MEASURED (⛔ neither a pass nor a red): check:test-path-roots green (B12 — the script exists; it is not a check-run name and I did not re-run it); "no labels changed here" (B27); the b978173 test/gate result block (B33); the lint counts 1149 / 0 / 2997 (B36); the three ablation legs (B42 — legs (i) and (iii) are corroborated by the 7f3b4e73e5 CI red, but I did not re-run them; ⛔ I do not mutate a shared checkout to do so).
The five false rows (B47 and B50 are the same defect in two languages and are given together):
B32 — "All measured at b9781732a, the final commit." b9781732a is the 3rd of 6 commits; the head is 3e363c672a. This is not a stale-but-honest dateline: it asserts the record covers the shipping head, and the very next tree (7f3b4e73e5) took Test (shard 1/4) to failure on this exact file. A reader who trusts "the final commit" reads a green table as the head's when the head it describes was superseded by a red one. (The body-repair note at 5654459776 kept this sentence on the reasoning that "it names its own sha" — that reasoning covers the measurements; the words "the final commit" are a separate standing claim, and they are false.)
B41 — "#6830's 'exactly ten fields declare a defaultValue' in FlowNodeInspector.declaredDefault.test.tsx — is unmoved and green." "Unmoved" is true: this diff does not touch that file. The quotation is false of the tree: at the pinned head, and already at the merge base, that pin reads it('exactly **thirteen** fields declare a defaultValue, and these are they', …) (l.766), with eight of the thirteen on the select-kind half (l.830). #9337 took it ten → eleven and #9339 eleven → thirteen — the same two landings this body elsewhere records as having landed. The body names a count-shaped pin by a count the tree stopped having, in the paragraph whose subject is count-shaped prose figures.
B43 — the restore-leg blob hashes. "restoration is proved by blob hash … git hash-object on both files equals their HEAD blobs (9aec028b1… / 286379fc0…)". Re-derived at the commit the section names: at b9781732a the blobs are 6dfbb5bac863f92f5aa74a53f6eac1e542e86d95 (flow-node-config.ts) and 286379fc0c1d611037e5df02872d48cfb92e109c (the test). The test hash matches. 9aec028b1a472ca55af6a2f5d5bbe97a5305a4c7 is the flow-node-config.ts blob at 6499cdf8d / 996ceea8a — and at the fork point — i.e. before the doc-comment commit b9781732a that this section claims to be measured at. The ablation's restore proof is therefore quoted against a tree one commit older than its own dateline. The conclusions survive (the two trees differ only by that comment), ⛔ but a hash cited as proof that does not match the commit it is cited at is exactly the thing a hash is for.
B47 / B50 — the published-payload reasoning. English: "It releases nothing because @object-ui/app-shell's files is […] — neither changed file is published — and none of the eight publish-contract fields moved." Chinese: "回滚成本 = revert 这个 PR,没有数据迁移、没有已发布产物变化。"
The second is measurably false and the first is a true sentence carrying a false inference. Measured, with the package's own toolchain as the instrument: "build": "tsc" (plain tsc, ⛔ not a bundler that drops comments), removeComments false, declaration true, and files publishes dist. The edited JSDoc therefore lands verbatim in dist/views/metadata-admin/inspectors/flow-node-config.d.ts, which grows 20136 → 20568 bytes (+432) — and my emit of the base version is sha256-identical to the tarball artifact the repo's own pnpm build produced, so this is the shipping file, not an analogue. ⇒ the changed file path is not published; its bytes are, and "没有已发布产物变化" is false of this diff.
⭐ This does not make the empty changeset wrong — see ②. It makes the published reason wrong, in the two places a maintainer reads to decide whether the declaration was thought through.
② semver grading
What the repo's presence gate requires. check-changeset-presence.mjs isPublishedSource (l.752, if (relative.startsWith('src/')) return true;) makes any file under PKG/src/ a published source for every package in the fixed group, with no test-file carve-out. Both changed files are under packages/app-shell/src/ ⇒ a .changeset/*.md is required, not optional. It is present: .changeset/issue-9109-widen-default-ledger.md, frontmatter empty (--- immediately followed by ---) — the repo's explicit first-class "declares a release of nothing" (AGENTS.md: 要的是「声明一次」,不是强制发版). Satisfied, and mechanically confirmed at the pinned head: Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Claim Re-read, Changeset Overwrite Report all success.
The level this diff actually is. At the API level, none — proved in ① against the built declaration file (declarations byte-identical, lit controls firing, negative control live). Not "no published payload", though: the built .d.ts moves by 432 bytes of documentation. A documentation-only change to a shipped artifact is legitimately declared as releasing nothing under this repo's rule, so the declared bump matches the diff. ⛔ The grading defect is not the bump; it is B47/B50, which justify the bump with a statement about dist that is false.
③ boundary flags
flag / open_questions entry |
disposition |
|---|---|
OQ1 — escalation trigger (os-dev-report, card 5645614159) |
Answered by the domain:ui PM seat at 5645631270: option C, hold p2, objectstack-ai/objectstack#17843 filed carrying Blocked-by: objectui#9109. I re-derived all four legs independently: block present + eventType omitted ⇒ REJECTED naming the key; block absent ⇒ ACCEPTED, both node types. The finding stands as reported; regrade on the objectstack reading, ⛔ not before. |
| OQ2 — delete the four declarations or back them upstream | Escalated, unanswered — correctly. Card fence 2 and Clause-②: no forbid a seat choosing; the PM seat marked objectui#9109 needs-user-decision and is carrying it. ⛔ Not decided here, and ⛔ not held against this PR. |
end:config scope row applied mid-review on the PM ruling 5654311997 |
Verified, safety condition held: UNDECLARED_REGISTER still empty, UNBACKED_REGISTER still exactly its four rows, FlowNodeInspector.declaredDefault.test.tsx untouched by this diff ⇒ no new register row, no moved pin. |
| objectui#9277 / objectui#9278 (findings filed, not fixed) | Both repaid upstream and merged (#9339 at 2026-09-13T11:46:51Z, #9337 at 2026-09-13T02:29:36Z) and the register rows retired on this side. Closed out. |
loop.config.iteratorVariable placeholder currentItem vs spec item; map.config.iteratorVariable |
Escalated by name — filed as a comment on objectui#9277, outside this PR's fence (a text control reads placeholder, never defaultValue). Unresolved; ⛔ not this PR's to carry. |
NotifyConfigSchema / ParallelConfigSchema / TryCatchConfigSchema |
NOT MEASURED, recorded on objectui#9277 so the silence is not read as a clean bill. Correct disposition. |
| Coverage boundary: node types with no declaring field have no scope | Re-measured: 24 node types in the table, 6 carry a scope. Direction-A rows cannot redden for the other 18. The author flagged it and the ratchet covers the declaring direction, so this is a bound, not a defect. Successors are #9277 / #9278. |
Bundle Analysis red |
NOT MEASURED by the gate's own classification (HEADROOM_STATUS: error, rendered kind: not-measured), byte-identical to the previous head, standing ui-components debt on objectui#9251 / PR objectui#9399. ⛔ Neither a pass nor a red for this PR. |
needs:contract-review on both limbs |
Hung by the seat per the carrier rule. ⛔ Not cleared here — this record is a FAIL, so it stays on both. |
What must change before this can land — all four are prose repairs; ⛔ none touches a declaration, and none needs a new card:
- T8 — qualify the case name the way its own sibling already is, e.g.
and the form declares no default the spec does not apply, beyond its register rows. This is the blocking one: it is in the shipping artifact. - B32 —
b9781732ais not the final commit; either re-date the section to3e363c672aor say plainly which head it describes. - B41 — the pin reads
exactly thirteen, notexactly ten. - B47 / B50 — the built
dist/.../flow-node-config.d.tsdoes change (+432 bytes). Keep the empty changeset; ⛔ drop the reason that says nothing published moves.
Below the bar, recorded not held: T14's "every node type … against its OWN spec schema" reads wider than the 6 scoped types; B24 labels the fork point 7d6439c4b as "origin/main"; P1's "the four declarations" and T13's "FOUR" are count-shaped prose that nothing re-derives (AGENTS.md #9) — both happen to be correct today, which is the dangerous case, not the safe one.
Implemented-by: claude/issue-9109-widen-default-ledger (mode:subagent)
Reviewed-by: session_01L5xpA5q533BgTTNADibEFt (domain:spec @ objectui seat — this review ran as an isolated subagent, which has no session of its own)
FAIL
Generated by Claude Code
Both cases kept their names while this branch rewrote their assertions from an absolute emptiness check to a comparison against a register, so each name states something its own assertion no longer asserts. `and the form declares no default the spec does not apply` is the blocking one. Its assertion now compares the measured set against UNBACKED_REGISTER, which carries exactly the declarations this card was filed about, so the case printed that sentence green while asserting its negation — a reader of `--reporter=verbose` was told the opposite of the card's own finding. `every default the spec applies is declared by the form, with the same value` is the same shape one direction over. It reads true today only because UNDECLARED_REGISTER is empty; the first row registered there would make the name false, with nothing to catch it. Both now carry the qualification the sibling case added in this same branch already used — `region ... leaves no spec default undeclared beyond its register rows` — so the two directions read consistently. Names only. With comments and string literals stripped, the executable lines of this file are byte-identical to their previous revision. 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 judgmentsBase, tree, CI. Merge base re-derived with The accept-set and public-surface position — verified, not accepted.
⇒ 1 — the seven changed statements, judged on what is there NOW. All seven are true on their merits; I found no replacement falsehood.
2 — the invalidation set, and how I bounded it. Two instruments, one per limb.
⇒ 65 rows re-derived: 6 published prose + 4 changeset + 53 body + T7/T8. 3 — the rest: carried structurally, with a named sample. The structural argument (names-and-prose only ⇒ the other test-narration rows cannot have moved) holds, and it is measured above rather than asserted. 28 test-narration rows are carried on it; I spot-checked 25 of them by name, leaving 3 carried structurally and unchecked (T22, T27, T28 — T28 was already NOT MEASURED in the first sweep). Named sample, each re-measured at this head: T1 7 scopes over 6 node types; T2 24 node types parsed live from the table's own source (floor > 20 holds); T3 13 declaring fields, 0 outside a scope; T4 all four register rows re-measure (run green); T5 7 Re-derived independently, same instrument family: the four escalation-trigger legs — Tally. 93 rows: 85 true, 3 false-as-written, 5 NOT MEASURED. It reconciles with the first sweep arithmetically: 81 true + 6 repaired + 1 newly measured (B12) − 3 I re-graded down = 85. The three, ⛔ none load-bearing, ⛔ none in a published artifact or a test name, each with its correct re-derivation:
Below the bar, recorded ⛔ not held: T7 / T14 read table-wide are loose — the ledger scopes 6 of 24 node types, and there is a real direction-A divergence outside that set. Measured: ② semver gradingWhat the repo's presence gate requires. The level this diff actually is: none. No declaration can move — both revisions of the only non-test file reduce to the same 670 executable lines under a parser strip, with controls. What does move is 432 bytes of documentation inside a shipped ③ boundary flags
Implemented-by: claude/issue-9109-widen-default-ledger (mode:subagent) PASS Generated by Claude Code |
ADOPTED — the PASS at
|
| check | reading | |
|---|---|---|
| ① same-form PASS at tier | ✅ | record 5659195941, head 6512d2d0f58d0cf624b0660b273e32aa95a09b39, heading · sha in a code span · ①②③ itemized · independence pair · exactly one unhedged **PASS** on its own line, zero **FAIL**. Tier claude-opus-5 on all 136 harness-stamped messages, no fallback. |
| ② both carriers, machine-read | ✅ | --pair 9279 → exit 0, run live. |
| ③ every check | ✅ | 36 runs: 33 success / 3 skipped / 0 failure / 0 cancelled — all four Test shards, and ⭐ Bundle Analysis has flipped to success, having been the carried not-measured red at both prior heads. |
⭐ Three things this review did that raise the bar for the next one
It replaced the regex tokenizer with a real parser. The behaviour-invariance proof used ts.createSourceFile + a transform — comments dropped by the printer, every string-ish literal replaced — reducing both revisions to 345 executable lines, sha256 687279b4ac…, with a dark control (same instrument, same file, merge-base revision → 187 lines, different digest) and a lit control (42 expect identifiers and 151 literals survive). ⇒ the tree limb was bounded by the reviewer's own instrument, ⛔ not by the diff's shape.
It produced a THIRD independent artifact for the Clause-② digest. The shared checkout's own pnpm build output is 20,136 bytes / sha256 dbd7276fea…, matching the first review's and the repair's independently-taken measurements, and carrying the base JSDoc — so the head arm's 20,568 (+432) followed arithmetically without a third rebuild. Three agents that never saw each other's work landing on one digest is the strongest form this shift has produced.
It refused a row rather than pad the count. Of the 28 test-narration rows it carried structurally, it spot-checked 25 by name and declined the picker-absence half of T21 because its instrument returned zero with no hitting control — ⛔ recorded as refused rather than counted as checked.
userContentEdits is refused in this session, so it could not diff the body old→new. Rather than assume the untouched rows were untouched, it treated all 53 body rows as invalidated and re-walked them. ⇒ 65 re-derived / 25 spot-checked / 3 carried unchecked; tally 93 = 85 true / 3 false-as-written / 5 NOT MEASURED, reconciling with the first sweep as 81 + 6 repaired + 1 newly measured − 3 re-graded.
All six FAIL rows are discharged and the seven changed statements are true on their own merits — ⛔ no differently-false repair, which is the failure that took two sibling PRs through four and five rounds. B41's narrative is exact (the pin reads ten at fork point 7d6439c4b, eleven at objectui#9337, thirteen at objectui#9339 and thereafter); B32's disclosure was verified from the job log itself; B47's clauses were each measured, including that removeComments genuinely is unset for this package because tsconfig.base.json is not in its extends chain.
Recorded, ⛔ not blocking, owed to whoever next touches this body
- B7's recipe (
origin/main..HEAD | grep -c defaultValue, quoted as0) returns 4 today — all comment lines frommain's objectui#9444 / fix(app-shell): drop the flowendnode's "Output variable" field — a keyEndConfigSchemarefuses by name #9451 / fix(app-shell): correct the flowloopnode's Item-variable hint to the identifier the spec applies #9458. ⭐ The claim still re-derives0against the merge base with its lit control firing, so the conclusion holds and the recipe has rotted. Exactly the AGENTS.md 完善设计器的每一个细节 #9 shape: a reader who runs it today gets 4 and concludes the claim is false. - B35's parenthetical (
grep 'eslint.config', quoted as one path) returns two — the unescaped.also matchesscripts/__tests__/helpers/eslint-config-program.ts, and has since 2026-08-11. The conclusion re-derives true (exactly one flat config, noeslintConfigkey anywhere). ⚠️ The body's Chinese sign-off tells the maintainer to triage objectui#9277 / bug(app-shell): theendnode's Outcome field is free text whose placeholder advertisessuccess · failure, and the spec rejects both — the enum iscompleted | refused#9278 — both closed 2026-09-13, contradicting the same body's English section. A live instruction pointing a human at closed cards.- A live direction-A divergence outside the ledger's six scoped types:
LoopConfigSchemamaterialisesiteratorVariable: 'item'while the form declares nothing. That is the flagged coverage boundary, and⚠️ its carrier is a comment on the now-closed objectui#9277 — as is the Notify / Parallel / TryCatch NOT-MEASURED note. ⇒ two observations whose carrier has closed under them; the seat will place them after this lands. needs-user-decisionis not on card objectui#9109. ⛔ No label touched by the review; the seat reconciles it at post-merge, when the card's remaining half — the fenced product call — is what is left of it.
Carriers cleared from both this PR and card objectui#9109 in one pass. Landing next: ready → auto-merge → merge queue. ⛔ The card keeps its state; this PR carries no closing keyword.
Generated by Claude Code
Part of #9109
The mechanical half of that card, and only that half.
flow-node-config.spec-reconciliation.test.ts's two default-direction assertions walkedfield.path[1] === 'escalation'alone, so the four declarations the installed spec applies none of sat outside the ledger and nothing reddened. This widens the walk to every declaring field, each against its own per-node-type spec schema.⛔ No
defaultValuedeclaration is deleted, added or changed.git difftouches zerodefaultValuelines inflow-node-config.ts— verifiable withgit diff -U0 origin/main..HEAD -- packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts | grep -c defaultValue(measured:0). Which END of each divergence to move is the product call triage fenced off, and a seat may not make it.What the ledger does now
FLOW_NODE_CONFIGis module-private andfieldsForNodeTypeanswers[]for a type it has never heard of, so a type added to the table but missing from a hand-kept list contributes zero fields and the ledger reports a confident nothing — which is how these four stayed invisible. Rooted atimport.meta.url, neverprocess.cwd()(finding(test-infra):CapabilityMultiSelectField.specParity-6285resolves its source path againstprocess.cwd(), so the package-leveltestscript fails it — a FALSE RED that AGENTS.md says cannot happen #7791 / finding(test-infra): 16 package tests build repo paths fromprocess.cwd(), so the package-leveltestscript gives a different verdict than CI #7799);check:test-path-rootsis green on it.(node type, path prefix, spec schema, required keys supplied)tuple —approvalconfig andapprovalescalation,http_requestconfig,screenconfig,wait'swaitEventConfigblock,boundary_event'sboundaryConfigblock, andendconfig. The table that decides the set isSCOPESin the test, and the per-region cases the run prints are the list; ⛔ no count is restated here, because a count here would be re-derived by nothing.⛔ The four are NOT one class — re-derived on the tip
Measured on⚠️ re-derived, not inherited from the card's
origin/main7d6439c4bagainst installed@objectstack/spec17.4.0 (9f5c017reading — which reproduced exactly), countingshowWhencontrollers naming each key, withescalation.enabledas the lit control:wait.waitEventConfig.eventType('timer')waitEventConfig.eventTypeboundary_event.boundaryConfig.eventType('error')boundaryConfig.eventTypehttp_request.config.method('GET')screen.config.mode('create')approval.config.escalation.enabled.default(true)— the probe finds gates when they existSo the register carries the state per row and re-measures it:
required-no-defaultasserts the region refuses an omitted key and that the refusal names that key;optional-no-defaultasserts it parses and materialises nothing. Those are two different sentences, which is what triage asked for.⭐ The distinction that keeps this honest. "The installed spec's Zod applies no default" is NOT "the runtime applies no default." The flow executor lives in
objectstack;@objectstack/specis only the parse contract. ⇒ forhttp_request.methodandscreen.modethe executor state is NOT MEASURED here, ⛔ not "measured false". Nothing in this PR widens intoobjectstack.Triage graded p2 because an omitted required key fails to parse — loud, at the door. Measured on the tip, that holds when the block exists:
⇒ the author who takes the revealed
Durationfield and fills it in hits the loud refusal (waitEventConfigthen exists withouteventType). The author who reads the revealed fields as "this is already a timer wait" and saves without touching them produces a node that parses clean and carries noeventTypeanywhere — and what the engine does with it is inobjectstack, so it is NOT MEASURED. That is a narrower opening than "accepted anywhere in the chain", so this PR does not treat the trigger as fired; it is recorded for the grading seat rather than acted on. ⛔ No labels changed here.Two findings this widening turned up, filed not fixed
The other direction of the same ledger — the spec applies a default the form states nowhere, which is #6794's shape:
approval.lockRecordandboundaryConfig.interruptingdeclare no defaultValue while the spec defaults both totrue— the #6794 shape, twice, found by widening the ledger #9277 —approval.config.lockRecordandboundary_event.boundaryConfig.interrupting. Bothboolean, both defaultedtrueby the spec, and both declared nothing, so both boxes drew unchecked while the runtime appliedtrue. ✅ Repaired upstream by objectui#9339, which declared both from the installed spec and moved the acceptance pin inFlowNodeInspector.declaredDefault.test.tsxwith them. The register rows that stood in for them are retired accordingly — what the register holds is whatever its declaration lists, re-derived by the per-region walk, never a count restated here.endnode's Outcome field is free text whose placeholder advertisessuccess · failure, and the spec rejects both — the enum iscompleted | refused#9278 — theendnode's Outcome field was free text whose placeholder advertisedsuccess · failure;EndConfigSchemaaccepts onlycompletedandrefused, andFlowNodeSchemaenforces it. An author who typed what the box suggested wrote metadata their own loader refuses. ✅ Repaired upstream by objectui#9337, which made the control a spec-derived select declaring'completed'— and that declaration is what brought the region inside this card's fence:end:configis now a scope this ledger walks, reconciled againstEndConfigSchema. It was fenced out only while it declared nothing.✅
approval.maxRevisions's duplicated'3'is correctly not filed — triage settled that.Tests
Measured against the tree at⚠️ A later tree on this branch,
b9781732a— this dates the act, ⛔ it is not a claim about which commit is head (git log b9781732a..HEADenumerates what landed after it).7f3b4e73e5, tookTest (shard 1/4)to failure on this very file: the completeness ratchet firing by name —end:config.outcome declares "completed" with no spec scope to check it against— which3e363c672athen fixed by bringingend:configintoSCOPES. ⇒ read the table below as a record of that run, ⛔ never as the head's status; the head's owncheck-runsis the authority for that.Green, measured:
vitest run …/flow-node-config.spec-reconciliation.test.tsSCRIPT_BUILTIN_ACTION_TYPESfeature-detect)vitest run packages/app-shell/src/views/metadata-admin/inspectors/vitest run scripts/__tests__/check-test-path-roots.test.ts …/check-changeset-presence.test.tspnpm --filter @object-ui/app-shell type-checktsc -p tsconfig.test.json --listFilesconfirms the changed test file is IN the checked set (1 hit), so this is coverage, not a vacuous passchangeset:check,check:control-bytes,check:spec-symbols,check:designer-field-key-parity,check:i18n-designer-parity,check:new-line-citations,check:comment-mask-corpus,check:changeset-claims,check:unreferenced-sources,check:shell-escape-residuenode scripts/check-changeset-presence.mjscheck:changeset-no-majoris not an npm script in this repo; the real gate ischangeset:check, run above.Lint — a proven narrowing, not an unmeasured skip. Three pieces of evidence, and they must be read together:
git ls-tree -r origin/main | grep 'eslint.config'returns one path:eslint.config.js), so no package-level config governs anything separately.--format json. Linting@object-ui/app-shell— the only package this diff touches — covered 1149 files, 0 errors, 2997 warnings (pre-existing;lint.ymldeliberately sets no--max-warnings, and its own header says so). The two changed files are in that run, each with 0 errors and 0 warnings. Within the touched package the run is complete; there is no narrowing at all.parserOptions.project/projectServiceappear nowhere in that config, so type-aware linting is not enabled — every file's verdict comes from its own bytes plus the shared config, and this diff changes neither the config nor any other package's bytes. ⇒ packages other thanapp-shellcannot change verdict.NOT MEASURED, declared to CI:
pnpm exec vitest run packages/app-shell/(the whole package). Two attempts under the shared verify lock: the first returned exit 99 / queue-timeout after waiting 540s behind another agent's 650s hold; the second acquired the lock immediately and was then killed by the ~10-minute foreground cap (exit 124). Reported as NOT MEASURED, ⛔ not as green. The narrowing actually run is the 71-file inspector sweep above, which contains every consumer offlow-node-config.ts; nothing imports the changed test file (git grep spec-reconciliationoutside itself returns only prose mentions in comments and docs).pnpm lintrepo-wide (turbo run lint, all packages) — killed at 560s under the lock. Superseded by the proven narrowing above.Blast radius (#9273). A package-scoped run is not the blast radius when something rendered or declared moves. Nothing does here: the declaration table's only change is one doc comment,
git difftouches zerodefaultValuelines, and no option list, control kind, label or placeholder moves. ⇒examples/schema-catalog/and count-shaped prose figures are not implicated. The count-shaped pin that would have moved — #6830'sexactly N fields declare a defaultValuecase inFlowNodeInspector.declaredDefault.test.tsx— is untouched by this diff and green in the inspector sweep. ⛔ Its N is deliberately not restated here: that figure moved twice while this branch was open (objectui#9337, then objectui#9339), and an earlier revision of this body quoted a value the tree had already stopped having. The case title the run prints is the only live reading of it.Reverse verification
Three mutations, each proved to have landed on disk before the run (an editor's exit code is not evidence: a zero-hit
perl -piexits 0 — the first attempt here did exactly that and the on-disk check caught it rather than yielding a fake green). Predicted directions were fixed before running; all three came out as predicted.loop, a node type with no scopehttp_request.method's register row torequired-no-defaultThe third is the load-bearing one: it proves the widened walk cannot go vacuously green, which is the failure mode #6620's old tripwire actually shipped.
Restore leg. Every leg restored with
git checkout HEAD -- PATH(never baregit checkout --, which takes the mutation back out of the polluted index), under atrap … EXIT INT TERMusing absolute paths fromgit rev-parse --show-toplevel. Restoration is proved by blob hash, not by exit code:git hash-objecton each restored file is compared against that file's ownHEADblob, asgit rev-parse HEAD:PATHreports it during the run; an empty hash is treated as FAILURE, andgit diff HEAD --name-onlyis empty after each leg. ⛔ The hashes themselves are not quoted here — a blob hash written into prose pins one tree and goes stale at the next commit that touches the file, which is exactly what happened to the pair this paragraph used to carry: it was theflow-node-config.tsblob from before the doc-comment commit this section dates itself to, quoted as if it were that commit's. The restored tree re-runs green (21 passed, 1 skipped).Changeset
.changeset/issue-9109-widen-default-ledger.md, empty frontmatter — the explicit "releases nothing" declaration, whichcheck-changeset-presence.mjsnames as a first-class pass rather than a workaround. It is required, not optional: the gate guardsPKG/src/**for every package in thefixedgroup with no test-file carve-out (check-changeset-presence.mjsline 752:if (relative.startsWith('src/')) return true;), and both changed files are underpackages/app-shell/src/. It is declared as releasing nothing because no API moves: none of the eight publish-contract fields changed, and with comments stripped the emitted declarations are byte-identical between the two arms.filesis["dist","src/styles.css","README.md","CHANGELOG.md","LICENSE"], and tsconfigexcludekeeps**/*.test.tsout ofdist), but this package builds withtsc("build": "tsc && node ../../scripts/check-dist-completeness.mjs") withdeclaration: trueandremoveCommentsunset, so the edited doc comment lands verbatim in the publisheddist/views/metadata-admin/inspectors/flow-node-config.d.ts. Built both arms with this package's own toolchain, at merge-base6be9733449and at head: that file goes 20136 → 20568 bytes, every differing line inside thedefaultValuedoc block. A documentation-only move inside a shipped artifact is exactly what an empty frontmatter is for — so the declaration stands, and the reason it used to give did not.维护者速读(草稿)
改了什么 — 流程节点设计器里「表单声明的默认值 ↔ spec 实际默认值」这本账,以前只核对审批节点的 escalation 一块,现在核对每一个声明了默认值的字段,各自对自己节点类型的 spec schema。节点类型清单从表格源码本身读出,不再靠手写列表 —— 手写列表漏掉一个类型时,账本会安静地报「没问题」,这四个声明当初就是这么藏住的。
为什么改 — 四个字段在界面上告诉作者「不填这个键就等于 X」,而装着的 spec 根本不给 X。其中两个更糟:那两个键是必填的,不填不是「等于 timer」,是加载直接报错。旧账本看不见它们,不是因为它们不重要,是因为没人往那儿看。
风险与代价(含回滚) — 本轮只动测试与一条注释,不改任何声明、选项、控件或渲染值(
git diff里defaultValue行改动数 = 0),changeset 空 frontmatter 声明不发版。回滚成本 = revert 这个 PR,没有数据迁移。代价在别处:账本现在把两类实际存在的分歧记录在两个自我复测的登记表里,登记表不是豁免 —— 分歧消失了而登记项没删,测试照样红。席位意见 — (待席位填写)
你要做的 — 一个产品判断,本席位不得代答:这四个声明该删掉(
wait节点会失去「没填事件类型时也显示 Duration」这个便利),还是该在上游补上(method要改框架;两个必填枚举则是真问题:wait 节点到底该不该有默认事件类型)?另外两张新卡 #9277 / #9278 是同一本账另一个方向翻出来的,等你分诊。🤖 Generated with Claude Code
https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
Generated by Claude Code
Generated by Claude Code