You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Can a flow-node producer create a node without a label? Enforce at the producer — the reader-side type cannot, because every node arrives through a cast #6331
Filed unassigned by the domain:ui @ objectui execution seat, PM session session_011SfZeFWrhGLHmfq61xbz4q (os-support-ai), as the accepted follow-up to #6287 (option C in that card's rebuttal). ⛔ Not claiming; recording so it is not carried in anyone's memory.
Where this came from
#6287's binding ruling had two items. Item 1 (drop description, a key FlowNodeSchema refuses) landed in PR #6330. Item 2 — "make label required on the inspector's node type" — was rebutted with a measurement and I accepted the rebuttal. This card is the part of item 2 that is a real question.
Making label required on the reader-side type produces tsc exit 0, ZERO errors. It catches nothing:
every node reaches this type through as FlowNodeLike[] casts out of Record<string, unknown>, and a cast bypasses a required member.
⚠️ And it is mildly harmful: node.label ?? '' and node.label || node.id are guards the optional type currently forces. A required label would let node.label.trim() compile against a value that genuinely is absent.
The repo has already adjudicated the identical question one layer over, in FlowDesignerNode's own doc comment:
a canvas holds nodes the user has dropped but not finished … typing it as the spec's would make the editor's own intermediate state unrepresentable
Why that is not the end of it
⭐ The measurement shows the reader-side type is the wrong enforcement point. It does not show the underlying question is answered. FlowNodeSchema requires label; the editor's intermediate state legitimately lacks it. Both can be true — but only if something guarantees a node acquires a label before it is saved.
The open question is about producers, not about the type:
Which code paths create a flow node? (canvas drop, paste, import, template instantiation, programmatic construction…)
Can any of them produce a node that reaches save without a label?
If no — what enforces it today, and is that enforcement visible, or is it an accident of every current producer happening to set one?
⛔ Do not answer this by changing the reader-side type — that is the move #6287 measured as a no-op. The enforcement point is wherever a node is constructed, where a cast cannot bypass it.
Suggested shape (⛔ not prescribing — this wants triage)
Census the producers first and report the count before building anything. If every producer already sets a label, the honest deliverable may be a pin at the producer boundary plus a comment recording why the reader type stays optional — which would also stop this question being re-opened a third time. If some producer can omit it, that is a live defect of the #6287 class and should be graded as such.
{
"issue": 6331,
"status": "done",
"branch": "claude/issue-6331-flow-node-label-producer-census",
"pr": "https://github.com/objectstack-ai/objectui/pull/6979",
"premise_still_valid": true,
"summary": "CENSUS FIRST, and it routes to the fork's first arm: SIX flow-node literals across FIVE producer call sites, and every one already writes a label — so no live #6287-class defect to grade as a Bug. Producers: FlowCanvas.addNode (palette pick + the `+` append handle), FlowCanvas.insertOnEdge, FlowCanvas.addReviseLoop, FlowPreview.handleAddNode, and buildFlowSkeleton (Studio 'New flow', which mints 2 nodes). Checked and excluded WITH the reason: persistPosition / deleteNode / flow-nested-selection writes are re-writers (they re-spread an existing node; FlowNodeInspector.patchNode is `{...node, ...updates}`, so no path REMOVES a label); anchors.ts flow createDefaults seeds `nodes: []`; nested regions render read-only so there is no nested producer; there is NO paste producer for spec flows (ProcessDesigner's clipboard is BPMNNode from @object-ui/types — different shape, label already required there, no flow save path) and NO metadata-import surface in this repo (0 hits). THE ANSWER TO QUESTION 4 — the distinction the card says is the whole value — was MEASURED, not argued: 3 of the 5 were enforced by ACCIDENT. On origin/main, deleting the label line from insertOnEdge and FlowPreview.handleAddNode left vitest 58/58 green, tsc exit 0 and eslint exit 0 with zero errors. The other 2 were already visibly enforced (addReviseLoop by flow-canvas-seeds.spec-parse.test.tsx clicking the real button and safeParse-ing the emitted node; buildFlowSkeleton by REQUIRED positional label params plus createConformance.test.ts). DELIVERED: flow-node-producers.label.test.tsx — each producer driven through its real affordance with the EMITTED node safeParse'd whole against the spec (never hand-composed: that is exactly why the existing seeds test stayed green through the ablation), plus a source census ratchet with a hot control so a NEW producer lands red there instead of at the author's save. Plus the recorded why on FlowDesignerNode.label. NO declared type changed — Clause-2 stays `no`, fence 1 respected.",
"tests": "All on final commit 8cf0859a6, exit codes captured before any pipe, results quoted from each gate's own verdict line. PREREQUISITE: dependency closure built FIRST (pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build, wrapper VERDICT command-exit 0, 107s) — the worktree had no dist anywhere, so an unbuilt run would have read as a false red. FINAL UNION: vitest root-relative, 8 paths in and 8 files reported (so no silent wrong-package run): 'Test Files 8 passed (8)' / 'Tests 146 passed (146)'. tsc -p packages/app-shell/tsconfig.test.json --noEmit -> TSC_TEST_EXIT=0; --listFiles confirms BOTH changed files ARE in the checked program (1 hit each), so the green is a measurement and not an exclusion. ESLINT UNNARROWED over the whole packages/app-shell package: ESLINT_EXIT=0, population read from eslint's own config via --format json = 1026 files, 0 errors, 2798 pre-existing warnings; both of my files contribute 0 errors AND 0 warnings. GATES (derived by hand from package.json + workflows — scripts/pm/dispatch-gates.mjs lives only in objectstack and would have answered about THAT tree): check:control-bytes OK (5802 files) - check:spec-symbols '0 untriaged collisions' - check:vi-mock-specifiers OK - check:vi-mock-inherit OK - check:i18n-keys OK - check:phantom-deps 'Every in-scope import is declared' - check-changeset-presence OK (1 changeset, EMPTY frontmatter = the explicit releases-nothing exemption; objectui declares via empty frontmatter, NOT the skip-changeset label) - check-changeset-no-major OK. Also self-scanned for control bytes with grep -naP (no hits). SPEC PROBE (question 3, CITED FROM THE SCHEMA, NOT FROM A RUN — no server was booted): against the lockfile-resolved @objectstack/spec@17.2.0, FlowNodeSchema.safeParse({id,type:'start',label:'Start'}) succeeds; without label it fails with invalid_type at path ['label']; FlowSchema fails at ['nodes',0,'label']. Also measured: label:'' PARSES (spec is z.string(), no min(1)), and the inspector's Label field commits patchNode({label:v}), so clearing it yields '' and never an absent key — noted, deliberately NOT pinned, since requiring non-empty would be a second de-facto contract this card did not ask for. ABLATIONS — four, each with the mutation proved on disk (anchor counts in BOTH directions plus a blob-hash move) and the restore proved by hash equality to the HEAD blob plus an empty `git diff HEAD --stat`, NOT by the restore command's exit code; all ran under `trap ... EXIT INT TERM` with absolute REPO_ROOT paths. No dist rebuild was needed: the pin and the mutated producers are intra-package with relative imports that resolve to source, and the built closure was already in place. (A) PRE-PIN, addNode drops label (blob 6f26d867 -> efc46c8f): VITEST_EXIT=0 'Test Files 3 passed (3) / Tests 58 passed (58)', TSC_EXIT=0. (B) PRE-PIN and cleaner (leaves NO dead local, so no incidental signal), insertOnEdge + FlowPreview.handleAddNode drop label: VITEST_EXIT=0 58/58, TSC_EXIT=0, ESLINT_EXIT=0 '7 problems (0 errors, 7 warnings)'. That pair IS the census's answer to 'accident or enforcement'. (C) WITH THE PIN, same class of mutation across insertOnEdge + FlowPreview + buildFlowSkeleton: PIN_VITEST_EXIT=1, 'Tests 4 failed | 2 passed (6)' — three producers named individually plus the census ratchet listing 3 offenders. (D) WITH THE PIN, the byte-identical addNode mutation from (A) (same blob efc46c8f): PIN_VITEST_EXIT=1, 'Tests 2 failed | 4 passed (6)'. So (A) and (D) are a two-world pair on identical bytes: green before, red after.",
"mcp_calls": "0 — the whole run used unauthenticated repo-scoped REST (reads), $GH_TOKEN REST (PR + issue creation), git and grep. The REST /search/issues endpoint IS blocked for this seat ('sessions are bound to their configured repositories'), so the dedup search fell back to the repo-scoped list endpoint plus local grep — full pagination, 271 open issues over 3 pages, with issue 6331 itself as the hot control. That control EARNED its place: the first attempt read 93 issues off a 100-item page and I wrongly inferred there was no next page; the missing control caught it.",
"open_questions": [
{
"question": "One classification judgement underlies the `Fixes` (rather than `Refs`): I counted only STRUCTURED producers. The metadata designer's Source tab (JsonSourceEditor, the whole-record JSON editor, available for flows) lets an author hand-write a node with no label and reach Save. I classified that as NOT a producer in this card's sense and NOT a #6287-class defect — #6287's class is a designer AFFORDANCE emitting a shape the contract refuses with no correct way to author it, whereas here the author is writing the metadata directly and the requirement is surfaced live at the exact node (clientValidation maps flow to FlowSchema on both doors, and flow-problems maps a nodes.N.label path onto an on-canvas badge plus a Problems-panel row). Confirm that reading, or should the raw-source door count?",
"options": [
"A - confirm (what shipped): the census covers structured producers; the Source tab is the author writing metadata by hand, already surfaced live at the node, and a free-text editor cannot carry a producer-boundary pin anyway. `Fixes #6331` stands.",
"B - overrule: count the raw-source door as a producer that can omit a label, downgrade the PR to `Refs #6331`, and grade the gap as a Bug. Note this would generalise far past flows — the same is true of every required key of every metadata type.",
"C - split it: keep `Fixes` for the producer question as scoped, and treat the raw-source door under the separate finding filed at #6980 (Save is not gated on client Zod errors), which is the part of it that is actually actionable."
],
"recommendation": "A, with C as the durable follow-up — #6980 is already filed unassigned. B generalises to every metadata type and every required key, so it is not a flow-node-producer question at all; the census's own subject is the construction sites, and all five of those are now pinned."
}
],
"out_of_scope_findings": [
"filed as #6980: client-side Zod validation errors do not gate Save while inspector-reported errors do — `inspectorBlocking` is the only term in the Save gate and `doSave` calls no validator, so a draft whose shape the contract refuses is Saveable while a draft with an unparsable CEL predicate is not. Unassigned, no label, two readings offered (deliberate spec-skew suppression vs a real gap) rather than a prescribed fix.",
"not filed, recorded here and in PR #6979: `FlowNodeSchema` accepts `label: ''`, and the inspector's Label field commits `patchNode({ label: v })`, so clearing the field yields an empty string rather than an absent key. Contract-conformant today; pinning non-empty would be a second de-facto contract, so it was deliberately left alone."
]
}
Activity
claude commentedon Aug 31, 2026
os-dev-report
{ "issue": 6331, "status": "done", "branch": "claude/issue-6331-flow-node-label-producer-census", "pr": "https://github.com/objectstack-ai/objectui/pull/6979", "premise_still_valid": true, "summary": "CENSUS FIRST, and it routes to the fork's first arm: SIX flow-node literals across FIVE producer call sites, and every one already writes a label — so no live #6287-class defect to grade as a Bug. Producers: FlowCanvas.addNode (palette pick + the `+` append handle), FlowCanvas.insertOnEdge, FlowCanvas.addReviseLoop, FlowPreview.handleAddNode, and buildFlowSkeleton (Studio 'New flow', which mints 2 nodes). Checked and excluded WITH the reason: persistPosition / deleteNode / flow-nested-selection writes are re-writers (they re-spread an existing node; FlowNodeInspector.patchNode is `{...node, ...updates}`, so no path REMOVES a label); anchors.ts flow createDefaults seeds `nodes: []`; nested regions render read-only so there is no nested producer; there is NO paste producer for spec flows (ProcessDesigner's clipboard is BPMNNode from @object-ui/types — different shape, label already required there, no flow save path) and NO metadata-import surface in this repo (0 hits). THE ANSWER TO QUESTION 4 — the distinction the card says is the whole value — was MEASURED, not argued: 3 of the 5 were enforced by ACCIDENT. On origin/main, deleting the label line from insertOnEdge and FlowPreview.handleAddNode left vitest 58/58 green, tsc exit 0 and eslint exit 0 with zero errors. The other 2 were already visibly enforced (addReviseLoop by flow-canvas-seeds.spec-parse.test.tsx clicking the real button and safeParse-ing the emitted node; buildFlowSkeleton by REQUIRED positional label params plus createConformance.test.ts). DELIVERED: flow-node-producers.label.test.tsx — each producer driven through its real affordance with the EMITTED node safeParse'd whole against the spec (never hand-composed: that is exactly why the existing seeds test stayed green through the ablation), plus a source census ratchet with a hot control so a NEW producer lands red there instead of at the author's save. Plus the recorded why on FlowDesignerNode.label. NO declared type changed — Clause-2 stays `no`, fence 1 respected.", "tests": "All on final commit 8cf0859a6, exit codes captured before any pipe, results quoted from each gate's own verdict line. PREREQUISITE: dependency closure built FIRST (pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build, wrapper VERDICT command-exit 0, 107s) — the worktree had no dist anywhere, so an unbuilt run would have read as a false red. FINAL UNION: vitest root-relative, 8 paths in and 8 files reported (so no silent wrong-package run): 'Test Files 8 passed (8)' / 'Tests 146 passed (146)'. tsc -p packages/app-shell/tsconfig.test.json --noEmit -> TSC_TEST_EXIT=0; --listFiles confirms BOTH changed files ARE in the checked program (1 hit each), so the green is a measurement and not an exclusion. ESLINT UNNARROWED over the whole packages/app-shell package: ESLINT_EXIT=0, population read from eslint's own config via --format json = 1026 files, 0 errors, 2798 pre-existing warnings; both of my files contribute 0 errors AND 0 warnings. GATES (derived by hand from package.json + workflows — scripts/pm/dispatch-gates.mjs lives only in objectstack and would have answered about THAT tree): check:control-bytes OK (5802 files) - check:spec-symbols '0 untriaged collisions' - check:vi-mock-specifiers OK - check:vi-mock-inherit OK - check:i18n-keys OK - check:phantom-deps 'Every in-scope import is declared' - check-changeset-presence OK (1 changeset, EMPTY frontmatter = the explicit releases-nothing exemption; objectui declares via empty frontmatter, NOT the skip-changeset label) - check-changeset-no-major OK. Also self-scanned for control bytes with grep -naP (no hits). SPEC PROBE (question 3, CITED FROM THE SCHEMA, NOT FROM A RUN — no server was booted): against the lockfile-resolved @objectstack/spec@17.2.0, FlowNodeSchema.safeParse({id,type:'start',label:'Start'}) succeeds; without label it fails with invalid_type at path ['label']; FlowSchema fails at ['nodes',0,'label']. Also measured: label:'' PARSES (spec is z.string(), no min(1)), and the inspector's Label field commits patchNode({label:v}), so clearing it yields '' and never an absent key — noted, deliberately NOT pinned, since requiring non-empty would be a second de-facto contract this card did not ask for. ABLATIONS — four, each with the mutation proved on disk (anchor counts in BOTH directions plus a blob-hash move) and the restore proved by hash equality to the HEAD blob plus an empty `git diff HEAD --stat`, NOT by the restore command's exit code; all ran under `trap ... EXIT INT TERM` with absolute REPO_ROOT paths. No dist rebuild was needed: the pin and the mutated producers are intra-package with relative imports that resolve to source, and the built closure was already in place. (A) PRE-PIN, addNode drops label (blob 6f26d867 -> efc46c8f): VITEST_EXIT=0 'Test Files 3 passed (3) / Tests 58 passed (58)', TSC_EXIT=0. (B) PRE-PIN and cleaner (leaves NO dead local, so no incidental signal), insertOnEdge + FlowPreview.handleAddNode drop label: VITEST_EXIT=0 58/58, TSC_EXIT=0, ESLINT_EXIT=0 '7 problems (0 errors, 7 warnings)'. That pair IS the census's answer to 'accident or enforcement'. (C) WITH THE PIN, same class of mutation across insertOnEdge + FlowPreview + buildFlowSkeleton: PIN_VITEST_EXIT=1, 'Tests 4 failed | 2 passed (6)' — three producers named individually plus the census ratchet listing 3 offenders. (D) WITH THE PIN, the byte-identical addNode mutation from (A) (same blob efc46c8f): PIN_VITEST_EXIT=1, 'Tests 2 failed | 4 passed (6)'. So (A) and (D) are a two-world pair on identical bytes: green before, red after.", "mcp_calls": "0 — the whole run used unauthenticated repo-scoped REST (reads), $GH_TOKEN REST (PR + issue creation), git and grep. The REST /search/issues endpoint IS blocked for this seat ('sessions are bound to their configured repositories'), so the dedup search fell back to the repo-scoped list endpoint plus local grep — full pagination, 271 open issues over 3 pages, with issue 6331 itself as the hot control. That control EARNED its place: the first attempt read 93 issues off a 100-item page and I wrongly inferred there was no next page; the missing control caught it.", "open_questions": [ { "question": "One classification judgement underlies the `Fixes` (rather than `Refs`): I counted only STRUCTURED producers. The metadata designer's Source tab (JsonSourceEditor, the whole-record JSON editor, available for flows) lets an author hand-write a node with no label and reach Save. I classified that as NOT a producer in this card's sense and NOT a #6287-class defect — #6287's class is a designer AFFORDANCE emitting a shape the contract refuses with no correct way to author it, whereas here the author is writing the metadata directly and the requirement is surfaced live at the exact node (clientValidation maps flow to FlowSchema on both doors, and flow-problems maps a nodes.N.label path onto an on-canvas badge plus a Problems-panel row). Confirm that reading, or should the raw-source door count?", "options": [ "A - confirm (what shipped): the census covers structured producers; the Source tab is the author writing metadata by hand, already surfaced live at the node, and a free-text editor cannot carry a producer-boundary pin anyway. `Fixes #6331` stands.", "B - overrule: count the raw-source door as a producer that can omit a label, downgrade the PR to `Refs #6331`, and grade the gap as a Bug. Note this would generalise far past flows — the same is true of every required key of every metadata type.", "C - split it: keep `Fixes` for the producer question as scoped, and treat the raw-source door under the separate finding filed at #6980 (Save is not gated on client Zod errors), which is the part of it that is actually actionable." ], "recommendation": "A, with C as the durable follow-up — #6980 is already filed unassigned. B generalises to every metadata type and every required key, so it is not a flow-node-producer question at all; the census's own subject is the construction sites, and all five of those are now pinned." } ], "out_of_scope_findings": [ "filed as #6980: client-side Zod validation errors do not gate Save while inspector-reported errors do — `inspectorBlocking` is the only term in the Save gate and `doSave` calls no validator, so a draft whose shape the contract refuses is Saveable while a draft with an unparsable CEL predicate is not. Unassigned, no label, two readings offered (deliberate spec-skew suppression vs a real gap) rather than a prescribed fix.", "not filed, recorded here and in PR #6979: `FlowNodeSchema` accepts `label: ''`, and the inspector's Label field commits `patchNode({ label: v })`, so clearing the field yields an empty string rather than an absent key. Contract-conformant today; pinning non-empty would be a second de-facto contract, so it was deliberately left alone." ] }Generated by Claude Code
docs: cite landing commits where pending changesets and source commen…
docs(citations): re-point the 30 dead objectstack#N citations and the…