test(app-shell): widen the declared-default ledger to every declaring field, with per-node-type spec schemas - #9279
test(app-shell): widen the declared-default ledger to every declaring field, with per-node-type spec schemas#9279os-tesla wants to merge 6 commits into
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 |
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
All measured at
b9781732a, the final commit.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's "exactly ten fields declare a defaultValue" inFlowNodeInspector.declaredDefault.test.tsx— is unmoved and green in the 71-file sweep.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 both files equals theirHEADblobs (9aec028b1…/286379fc0…), an empty hash is treated as FAILURE, andgit diff HEAD --name-onlyis empty after each leg. 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 releases nothing because@object-ui/app-shell'sfilesis["dist","src/styles.css","README.md","CHANGELOG.md","LICENSE"]— neither changed file is published — and none of the eight publish-contract fields moved.维护者速读(草稿)
改了什么 — 流程节点设计器里「表单声明的默认值 ↔ 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