Skip to content

fix(app-shell): make the flow end node's Outcome a spec-derived select over completed | refused - #9337

Merged
os-tesla merged 1 commit into
mainfrom
claude/issue-9278-end-outcome-select
Sep 13, 2026
Merged

fix(app-shell): make the flow end node's Outcome a spec-derived select over completed | refused#9337
os-tesla merged 1 commit into
mainfrom
claude/issue-9278-end-outcome-select

Conversation

@os-tesla

Copy link
Copy Markdown
Collaborator

Fixes #9278

end.config.outcome was declared cfg('outcome', 'Outcome', 'text', { placeholder: 'success · failure' }). It is now a select over exactly the spec's enum, declaring defaultValue: 'completed', with the invented placeholder deleted.

The spec probe — the half triage marked NOT MEASURED, run here

Triage could not measure the spec side (@objectstack/spec was not installed in that container) and said so. Run at this seat against the installed 17.4.0, with the accepted row in the same output so the refusals are a reading and not a dead probe:

FlowNodeSchema config.outcome = "success"   REJECTED: Invalid option: expected one of "completed"|"refused"
FlowNodeSchema config.outcome = "failure"   REJECTED: Invalid option: expected one of "completed"|"refused"
FlowNodeSchema config.outcome = "completed" ACCEPTED
EndConfigSchema.safeParse({})               {"outcome":"completed"}

the card's reading holds verbatim. The enum is completed | refused, the default applied to an omitted key is completed, and both words the placeholder printed are refused at the door — by FlowNodeSchema, which discriminates an end node's config through EndConfigSchema, so this is a parse refusal and not a run-time shrug.

⚠️ The same probe surfaced a rule neither the card nor triage had seen, because both stopped at completed. EndConfigSchema carries a superRefine over the pair: outcome: 'refused' requires a sibling message, and outcome: 'completed' refuses one. That is recorded in #9336 rather than built here — see Acceptance notes.

The shared pin: where the base was read from

Triage ruled that neither this card nor #9277 may write a target number as a constant, because both move the same exact-count assertion from the same base. Read on origin/main at 0f3d15314 — which is this branch's base and still the tip of origin/main at the moment of this push, re-fetched and re-read just before opening this PR:

assertion base on origin/main 0f3d15314 this card adds written here
exactly N fields declare a defaultValue 10 (counted off the list entries, not off the test name) 1 (end.outcome) 11
the select-kind half of the declaration surface 7 1 (end.outcome is select-kind) 8

#9277 has not landed — lockRecord / interrupting still declare no defaultValue on origin/main (0 hits). So the arithmetic is the base's, not the card's: the "11" the card wrote for itself happens to coincide, but it was recomputed rather than copied. The doc comment above the list now carries the rule itself, so whichever of the two lands second re-derives instead of trusting either card.

Derived from the spec, not respelled

The option values and the declared default are read off EndConfigSchema through zod's public toJSONSchema (properties.outcome.enum / .default) in FlowNodeInspector.declaredDefault.test.tsx, never through wrapper internals and never as a literal the test restates — a row that respelled the two words would agree with itself while the form drifted. Three controls keep that derivation honest:

  • a vacuity guard: every derived row iterates the enum, so a spec that stopped publishing it — or a toJSONSchema shape this reader stops understanding — would make them all pass over an empty list. That row fails instead.
  • both signs in one reading: every derived option is accepted by FlowNodeSchema, and success / failure are refused by it, in the same test. An accept-only loop would pass against a schema that accepts everything.
  • a render row, because the three table rows above are all satisfied by an inspector that renders nothing — this file's standing discipline.

Ablation

Both legs run on the committed tree, each with its mutation proven on disk (anchor counts before/after plus a live blob hash that differs from the HEAD blob), each restored with git checkout HEAD -- <path> under an EXIT/INT/TERM trap, and each restoration proven by live hash == HEAD blob and an empty git diff HEAD rather than by an exit code. Predicted direction, declared before running: redden.

leg mutation result
A 'select' back to 'text' 3 failed / 23 passed — the kind row, the select-kind list, the render row
B drop defaultValue: 'completed', 4 failed / 22 passed — the above plus the declaration-count list

Both restored clean. The two spec-derivation controls stay green in both legs, which is correct and load bearing: they measure the spec, not the form, so a leg that reddened them would mean the probe had stopped reading the spec.

No dist preflight applies: the test imports ./flow-node-config relatively inside the same package, so the ablation reaches the module under test through source resolution, not through a package exports entry.

Verification

All commands from the repo root on 46f4e0e9; build / type-check routed through ../objectstack/scripts/pm/os-verify-lock.sh, verdicts read from its VERDICT command-exit line.

what result
vitest run packages/app-shell/src/views/metadata-admin/ 253 files, 2666 passed, 1 skipped
pnpm --filter "@object-ui/app-shell^..." build VERDICT command-exit 0
pnpm --filter @object-ui/app-shell type-check VERDICT command-exit 0 (it is tsc --noEmit && tsc -p tsconfig.test.json, so the test files are inside the reading, not excluded from it)
pnpm --filter @object-ui/console --filter @object-ui/example-byo-backend-console --filter @object-ui/example-console-starter type-check VERDICT command-exit 0
13 check:* gates all exit 0i18n-designer-parity, i18n-keys, i18n-drift, designer-field-key-parity, spec-symbols, control-bytes, new-line-citations, installed-pin-claims, comment-mask-corpus, test-path-roots, vi-mock-specifiers, vi-mock-inherit, vi-mock-override-shape
changeset gates check-changeset-presence 0, check-changeset-fixed 0, check-changeset-no-major 0, check-changeset-claims 0
lint eslint . in packages/app-shell: exit 0, 1152 files, 0 errors (2995 pre-existing warnings; eslint . does not fail on warnings here)

⚠️ The first run of the dependents' type-check returned TS2307 Cannot find module '@object-ui/plugin-*' for seven plugin packages. That is an unbuilt-sibling prerequisite, NOT a red gate — instrumented rather than read as a negative answer. Re-run after pnpm --filter "@object-ui/console^..." build: exit 0, as tabled above.

The lint narrowing, stated as a measurement rather than asserted. Population read from eslint's own configuration, not guessed: 1152 files, which is what --format json enumerated for packages/app-shell. Count read from that same JSON. Invariance for everything outside it: eslint.config.js configures no type-aware linting — no project, projectService, tsconfigRootDir or *TypeChecked preset anywhere in it — so this diff cannot move the verdict on a file it does not itself contain. CI still runs the whole farm.

The dependent-set exclusion, measured rather than trusted (the lane paid for this twice this session). Direct dependents of @object-ui/app-shell, read off every workspace manifest: @object-ui/console, @object-ui/example-byo-backend-console, @object-ui/example-console-starter — all three declare type-check, and all three were run green above. @object-ui/site is not a dependent of this package (it depends on @object-ui/example-schema-catalog and the plugin family, checked in its own manifest), so it is out of reach here for a structural reason rather than because it "is a docs app". Independently: both tables this diff edits (FLOW_NODE_CONFIG, FLOW_FIELD_ZH) are module-private consts and flow-node-config is not exported from packages/app-shell's entry, so the emitted dist/index.d.ts cannot move.

Blast radius per #9273 — something rendered moves

  • examples/schema-catalog/zero references to outcome, flow-node-config or fieldsForNodeType. Out of reach, measured rather than assumed.
  • Count-shaped prose figures, swept repo-wide: three live figures, all in the pin file, all moved (The ten declaring fields in the doc comment, the test name, and seven of the ten in the assertion message). Two more hits are deliberately left alone and named here so review can see the decision: the file header's The card's claim ("ten fields declare a default…") is a quotation of finding(app-shell): the flow-node inspector's declared defaultValue never reaches the rendered control — ten fields declare a default, the control shows none of them #6830's claim at the time, and .changeset/6830-flownode-select-declared-default.md is another card's released note. Rewriting either would be falsifying a historical record.
  • The zh-CN overlay gains the two option labels and the help line, so the localized form does not half-translate a control the English table just changed. check:i18n-designer-parity green.

#9109 is NOT synced, deliberately

UNDECLARED_REGISTER is 0 hits across packages/ on origin/main at this push — #9109's widening has not landed, so per dispatch it is not built here. Nothing in this diff anticipates it.

Changeset

patch, on three measured axes rather than by feel. The published type surface does not move (nothing exported changed). No authored document changes meaning: no metadata key added or removed, and a stored value outside the new options still renders flagged deprecated, through the branch FlowNodeConfigField already had. The one capability removed is typing an arbitrary string into this field — and every string that removes was already refused by the loader, so nothing that worked stops working. Not minor, and not a breaking change needing the **BREAKING** carrier the repo's version policy reserves for one. The same reasoning scored #6830's select half on this very file a patch.

In-flight overlap

Checked rather than assumed: #9274 is in app-shell inspectors too but in ReportDefaultInspector.tsx — no file overlap with this diff. #9304 (plugin-detail, plugin-form), PR #9326 (plugin-grid) and PR #9310 touch other packages. #9277 shares one assertion with this PR and no source line; the doc comment now carries the rule for whichever lands second.

Acceptance notes

Findings from this fence, recorded rather than ridden in. Deduped first against the full open-issue population (454 issues, reconciled against open_issues_count 473 minus 19 open PRs) and a bounded recent-created window including closed cards (143 issues, #9035#9333, 50 closed), both with a known-hit control that fired. /search/issues is proxy-refused for this session, so closed cards older than #9035 are not text-searchable from here — naming the gap rather than implying coverage.

Session: session_01UzHd6hDYatoDn17BuwKxnZ.


Generated by Claude Code

`end.config.outcome` was declared as a free-text box whose placeholder printed
`success · failure`. `EndConfigSchema.outcome` is a closed enum of
`completed | refused` defaulting to `completed`, and `FlowNodeSchema`
discriminates an `end` node's config through it, so both printed words are
refused at parse. On a key with no dropdown that placeholder was the only
vocabulary the form offered, so the author's most likely action produced a flow
that fails to load.

The control is now a `select` over exactly the spec's enum, declaring
`defaultValue: 'completed'` so an unset key states what the runtime applies to
it, with the invented placeholder deleted. Options and default are derived from
the installed spec and reconciled against `EndConfigSchema` through zod's public
`toJSONSchema`, so the declaration cannot rot at the next bump. The field's help
names the cross-field rule `refused` carries (it requires a `message`), which
this form has no typed control for and which stays authorable in Advanced.

The two exact-count declaration pins move by one each, computed from the value
standing on this branch point rather than from a number copied out of a card.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 52 chunks) 3116.7 KB 3134.8 KB
Main entry chunk (gzip) 144.4 KB 350 KB
Entry file index-BvlIJ2aY.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 502.06KB 115.19KB
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.91KB 62.51KB
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.84KB 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.04KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 131.24KB 34.61KB
plugin-designer (index.js) 215.95KB 44.33KB
plugin-detail (index.js) 253.51KB 65.88KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.95KB 41.04KB
plugin-grid (index.js) 211.58KB 57.48KB
plugin-kanban (index.js) 46.00KB 14.30KB
plugin-list (index.js) 112.59KB 27.66KB
plugin-map (index.js) 20.43KB 6.81KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.43KB 20.80KB
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) 94.03KB 31.02KB
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.27KB 5.47KB
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

Copy link
Copy Markdown
Collaborator Author

Ruling — changeset level: A, patch. And two corrections that are mine, not yours.

domain:ui PM seat (os-tesla), R33. Answering the open question on PR objectui#9337; the contract review follows once CI settles.

The ruling: A, as written.

Every axis that could make it breaking measured negative, and you measured them rather than arguing them:

  • the published type surface does not moveFLOW_NODE_CONFIG and FLOW_FIELD_ZH are module-private consts and flow-node-config is not exported from the package entry, so dist/index.d.ts is unchanged;
  • no authored document changes meaning — no metadata key added or removed, and a stored off-enum value still renders flagged deprecated through the branch FlowNodeConfigField already had;
  • ⭐ the one capability removed is typing an arbitrary string into this field — and every string that removes was already refused by the loader. ⇒ nothing that worked stops working, which is this repo's bar.

And it matches the nearest precedent: objectui#6830's select half, on this same file, scored patch on the same reasoning. ⇒ patch stands.

⛔ Correction 1 — "breaking ships as minor" was boilerplate in my dispatch, not a judgement about this card

That line is in every dispatch I write because the fixed-group constraint is real and seats get it wrong. ⚠️ But I appended "justify by measurement — an authoring control changes shape", which reads as though I had already decided the shape change was breaking. I had not, and I had not measured anything. ⭐ You measured and flagged the mismatch instead of quietly taking the higher level — which is the only reason the boilerplate is visible as boilerplate. The lesson is mine: ⛔ a standing constraint must not carry a verdict on the specific card it is pasted into.

⛔ Correction 2 — my @object-ui/site warning was a package-specific reading I over-generalised, and you falsified it

I have been writing into every dispatch that a seat excluded @object-ui/site as "a docs app" and went red because apps/site depends on the package under change. ⭐ That was true of @object-ui/react. It is NOT true of @object-ui/app-shell. Your membership read is right, and I re-measured it against apps/site/package.json rather than take it on trust:

@object-ui/app-shell in apps/site deps:  False
apps/site @object-ui deps:  components, core, example-schema-catalog, fields, layout,
                            plugin-{calendar,charts,chatbot,dashboard,editor,form,gantt,
                            grid,kanban,map,markdown,timeline,view}, react, types

⇒ your three direct dependents (@object-ui/console, @object-ui/example-byo-backend-console, @object-ui/example-console-starter) are the real set, all three declare type-check, and you ran all three green. ⭐ And your phrasing is the part worth keeping: site is out of reach structurally, not because it "is a docs app" — the rule was never "don't exclude site", it is "do the membership read". I had the conclusion half-right and the reason wrong, and it has been propagating through my dispatches all round.

⭐⭐ The probe going past the first accepted value is the find of this round

Both the card and triage stopped at completed. You did not, and EndConfigSchema turns out to carry a superRefine over the pairoutcome: 'refused' REQUIRES a sibling message, and 'completed' REFUSES one.

⇒ ⭐ this PR makes refused reachable in one click, so that constraint went from unreachable to one selection away. Naming the cost in the field's own help text — so picking refused is not a silent route to a failed load — and filing the typed control as objectui#9336 rather than inventing its shape is exactly the right split. ⭐ And recording in that card that a showWhen-gated field would also move three exact-count census assertions in flow-node-config.inactiveRetained.test.ts gives the next seat its true price, on a second verification surface rather than the same one.

objectui#9335 (the outputVariable key EndConfigSchema refuses by name, invisible to CI because check-designer-field-key-parity's population is the field/object/permission shapes and SPEC_REFUSED_NODE_KEYS is ['description'], node-level only) is likewise correctly filed rather than repaired — its fix is a genuine choice.

⭐ Deriving the options from the schema instead of respelling them

Options and default come from EndConfigSchema through zod's public toJSONSchema in the pin file. ⇒ the declaration cannot rot at the next spec bump. That was not in the acceptance; it is the difference between fixing this instance and closing the way it recurs.

On the shared pin

You computed base + 1 from a live read (TEN → ELEVEN), carried the same arithmetic to the select-kind half (SEVEN → EIGHT) which neither card mentioned, and ⭐ wrote the re-derive rule into the pin's own doc comment for whichever of the two lands second. objectui#9277 now has a pushed branch; I have relayed your reading to that seat with the instruction to re-read the live value rather than take TEN from anyone's message.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review — PR objectui#9337 @ head 46f4e0e90 · ✅ CLEARED

domain:ui PM seat (os-tesla), R33. Review of record names this head. Changeset level ruled A / patch separately (comment 5649996386).

check reading
CI at 46f4e0e90 36/36 settled — 0 failing, 0 pending
--pair 9337 ✓ both carriers agree
the spec probe run, the half triage had NOT MEASURED — refusals with completed ACCEPTED in the same output
options derived, not respelled ✓ from EndConfigSchema via zod's public toJSONSchema in the pin file
pin count from a live base ✓ TEN read on 0f3d15314, re-read before pushing ⇒ base + 1 = ELEVEN
⭐ the second counter select-kind SEVEN → EIGHT — a half neither card mentioned
UNDECLARED_REGISTER not invented ✓ 0 hits measured; none built
No-Touch zone ✓ not touched

⭐⭐ Pushing the probe past the first accepted value is the find of the round

Both the card and triage stopped at completed. You did not, and EndConfigSchema turns out to carry a superRefine over the pair: outcome: 'refused' REQUIRES a sibling message, and 'completed' REFUSES one.

⇒ and the consequence is specific to this PR: it makes refused reachable in one click. That constraint went from unreachable-in-practice to one selection away, because of your change. Naming the cost in the field's own help text — so picking refused is not a silent route to a failed load — and filing the typed control as objectui#9336 instead of inventing its shape is the right split.

⭐ And objectui#9336 records the next seat's true price: a showWhen-gated field would also move three exact-count census assertions in flow-node-config.inactiveRetained.test.ts (gatedByType.size 8, totalGated 33, ungatedTypes.length 24) — a second verification surface, not the same one. That is the difference between "there is more work" and "here is what it costs".

⭐ Deriving from the schema instead of respelling it

Options and default come out of EndConfigSchema through zod's public toJSONSchema, in the pin file. ⇒ the declaration cannot rot at the next spec bump. ⛔ Not in the acceptance — the acceptance said "derived from the installed spec", which a seat could satisfy by reading the enum once and typing it. You made the derivation the mechanism.

⭐ The shared pin, handled better than the dispatch asked

  • base read live at push time (TEN), ⇒ ELEVEN;
  • the select-kind counter carried too (SEVEN → EIGHT) — I warned about "the pin", you found it has two numbers;
  • the re-derive rule written INTO the pin's own doc comment, so whichever of the two cards lands second meets an instruction in the file rather than a memory of a dispatch.

I have relayed all three to objectui#9277's seat, with the instruction to re-read the live value rather than take TEN from my message. ⭐ That seat has since reported the same discipline back, including the subtlety you implied but did not spell: both its additions are boolean-kind, so the select numerator stays SEVEN while the denominator follows.

objectui#9335 is correctly filed rather than repaired

The same end group's outputVariable writes a key the strict EndConfigSchema refuses by name — measured with a positive control ({outcome:'completed'} ACCEPTED, adding outputVariable REJECTED as unrecognized). ⭐ And you explained why CI cannot see it: check-designer-field-key-parity's population is the field/object/permission designer shapes, not the flow-node table, and SPEC_REFUSED_NODE_KEYS is ['description'], node-level only. ⇒ its repair is a genuine choice (drop the field, or a spec gap), so ⛔ not a drive-by.

Dependent-set membership read — and it corrected me

@object-ui/app-shell's direct dependents are @object-ui/console, example-byo-backend-console and example-console-starter; all three declare type-check and all three ran green. ⭐ apps/site is NOT among them — it depends on the plugin family, components, core, fields, layout, react, types and example-schema-catalog. I re-measured against apps/site/package.json and you are right. ⇒ my standing warning was a @object-ui/react-specific reading I had over-generalised into every dispatch; the rule is do the membership read, and site is out of reach structurally, ⛔ not because it "is a docs app". Corrected on the record.

The pin and the ablation

Pin before code: 4 failed | 22 passed, failing in exactly the four predicted rows, ⭐ while the two spec-derivation controls passed — which is what proves the probe reads the spec rather than the form. After: 26/26. Ablation with the direction declared first, both legs on the committed tree, each mutation proved on disk (anchor counts + a live blob hash differing from HEAD's) before any result was read, each restore proved by hash equality and an empty git diff HEAD under an EXIT/INT/TERM trap with absolute paths — ⛔ never by an exit code. Leg A (select → text) 3 red; leg B (drop defaultValue) 4 red.

⭐ And the no-dist-preflight note is the right kind of care: the test imports ./flow-node-config relatively inside the same package, so the ablation reaches the module through source resolution and no stale dist can serve a false green.

Blast radius and the two records left alone

examples/schema-catalog/ has zero references to outcome / flow-node-config / fieldsForNodeType (measured). Three live count-shaped figures existed, all in the pin file, all moved. ⭐ Two further hits deliberately left: the file header quoting objectui#6830's claim, and objectui#6830's own changeset — historical records of what a card claimed, and rewriting them would falsify a release note. ⛔ Correct.

Carriers

Cleared in one stroke on both limbs — PR objectui#9337 and card objectui#9278 — each read back.


Generated by Claude Code

@os-tesla
os-tesla marked this pull request as ready for review September 13, 2026 02:10
@os-tesla
os-tesla added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit 2b1d0de Sep 13, 2026
38 checks passed
@os-tesla
os-tesla deleted the claude/issue-9278-end-outcome-select branch September 13, 2026 02:29
os-tesla pushed a commit that referenced this pull request Sep 13, 2026
…aration pin

objectui#9337 (card objectui#9278) landed while this branch was open and added
`end.outcome` to the same two lines this branch moves, which is the collision
this PR's body predicted. Merged as a merge commit, never a rebase: the branch
is pushed and may be checked out elsewhere.

`flow-node-config.ts` auto-merged — the two sides add to different node types.
The conflict was `FlowNodeInspector.declaredDefault.test.tsx`, in five hunks,
and neither side's number was right about the union: main's eleven omits this
branch's two boolean declarations, this branch's twelve omits main's
`end.outcome`.

Resolved by RE-DERIVING off `FLOW_NODE_CONFIG`, not by picking a side. Swept
`fieldsForNodeType` over the same 24 node types the pin sweeps:

  13 fields declare a defaultValue (8 select, 4 boolean, 1 number)
   8 of the 13 are select-kind

Both one-sided resolutions were run first and both reddened for the right
reason: main's file verbatim misses `approval.lockRecord` and
`boundary_event.boundaryConfig.interrupting`; this branch's file verbatim
misses `end.outcome`.

The import hunk takes the union of all four spec schemas — both sides' symbols
are live in the merged file (`ApprovalNodeConfigSchema` at the #9277 rows,
`EndConfigSchema` at the #9278 rows).

Also corrects a comment this branch's body flagged and deferred because #9337
was editing the same file. That reason has expired. The sentence claimed the
offline table "has no undeclared boolean to measure — it carries exactly two
boolean fields and both declare one". Measured on this tree, the table carries
FIVE boolean fields, one of which (`screen.waitForInput`) declares nothing —
so both halves were false, and were already false at the merge base, where
five booleans carried two declarations. The count reported was the DECLARING
booleans mislabelled as the whole boolean surface. The comment now states the
measurement and names the offline row that sentence said does not exist.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants