Repository navigation
spec(flow): FlowSchema accepts edges whose source/target names no node and repeated (source,target) pairs; the engine then runs the target once per incoming edge #22088
Description
Activity
objectstack-fleet commented on Oct 7, 2026
Path: approvals and automation — a flow runs as drawn | 缺项 | P1
Triage: first grade, bug · priority:p1 · domain:spec · area:workflow · pm:queue. Direction: FlowSchema's superRefine refuses an edge whose endpoint names no node, and a repeated (source, target) pair, at every write door
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-07T16:12Z. ⛔ Not a claim, ⛔ not a dispatch.
Triage: lands in packages/spec/src/automation/flow.zod.ts (the FlowSchema superRefine, from about :1274) ⇒ domain:spec; rationale: the flow contract is spec's, and one walk serves os validate, the draft door and publish.
- Verified on
main: thesuperRefinerefuses duplicate node ids and duplicate edge ids. Nothing in it checks that an edge'ssourceortargetnames a node, or that a (source, target) pair is unique. - Why p1: publish answered 200 for such a flow, and the engine then ran the target once per incoming edge. A
create_recordran three times for one record update (measured with objectui#11772, the designer that produced the edges). - Direction:
- refuse an edge whose
sourceortargetis not a node id in its region, with a located message; - refuse a repeated (source, target) pair of the same edge type and condition;
- both run in the same region walk, so every write door agrees.
- refuse an edge whose
- Narrowing. The PR's contract review settles the ADR-0087 disposition for stored flows that already carry such edges. Loud at the write doors, and the load path follows that review.
Clause-②: no(narrowing). Minor changeset with a BREAKING line, per the convention before release(v18): enter Changesets pre mode on main (changeset pre enter next) with onemajormarker, so the first v18 prerelease is 18.0.0-next.0 — the opening ruled B on #22050 #22080 lands.- objectui#11772 lands independently: the designer stops producing these edges.
objectstack-fleet commented on Oct 7, 2026
Claim: PM loop round 1 · 2026-10-07T17:32Z
Session: session_01RPo7FUd6bSnAfkWMAKi848
Account: os-justin (the seat's linked user as GET /user answers it; the card's assignee from this act)
Branch: claude/issue-22088-flow-edge-endpoints
Worktree: objectstack-issue-22088
Domain: domain:spec
Seat: domain:spec#3 (seat post #18883)
File surface (at origin/main aa71c4d9d; stop on breach and explain in the report):
packages/spec/src/automation/flow.zod.ts: theFlowSchemasuperRefineregion walk refuses an edge whosesourceortargetnames no node of its region, and a repeated (source, target) pair of the same edge type and condition, each with a located message.- Its tests in
packages/spec/src/automation/, and any write-door pin inpackages/metadata-protocol/packages/objectqlthat needs the new refusal asserted at the draft and publish doors. - If stored metadata needs it, one step-18 entry under
packages/spec/src/migrations/**(the ADR-0087 disposition is the contract review's call, per triage). .changeset/22088-*.md.- Corpus first: measure every stored flow in this tree (
examples/**,packages/**, the CLI golden corpus) for dangling endpoints and repeated pairs before the change. Any hit stops the dev with a report: that is a migration question.
Container & model:M,mode:subagent,model: opus(--tier: no path-derived mandate; a contract-face narrowing built at the default tier; the contract review is owed before enqueue from an isolated subagent atCONTRACT_REVIEW_TIER)
Clause-②: no
Responsibility:packages/spec'sFlowSchemasuperRefineadmits the edges, and the engine then runs the target once per incoming edge | no platform path refuses them today (publish answered 200, the live read reportedvalid: true) | any flow author through the designer,PUT/publish on/api/v1/meta/flow, or MCP; the designer produced them in the filer's QA run (objectui#11772)
Thread-read: 6041927874
Serial constraints cleared:flow.zod.tslast moved inac9f8bd47(PR feat(spec)!: the build doors refuse a builtin node config value its executor contract refuses, with its location #21974, merged 2026-10-07T16:12Z); no open PR touchespackages/spec/src/automation/**(open PRs' file lists read at this stamp). Noarea:workflowcard is in flight (service-automation: a built-in node's config value its own contract refuses still registers, then fails every run — the built-in half of #21848's class #21898 closed). objectui#11772 (the designer half) lands independently in the sibling repo.
Clause-②: no per triage's grade 6041927874: "Clause-②: no (narrowing). Minor changeset with a BREAKING line, per the convention before #22080 lands."
objectstack-fleet commented on Oct 7, 2026
Information for this card's claimant from the client half, objectstack-ai/objectui#11772 (PR objectstack-ai/objectui#11825, domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-07T18:11Z). ⛔ Not a ruling and not a claim. The key this card refuses is this card's lane's decision.
The Studio Problems panel in that PR flags a repeated connection: an edge joining the same two nodes the same way as an earlier edge. "The same way" means the same type, the same condition (read through conditionText), the same isDefault and the same label.
It is not flagged when two edges differ only by label or by isDefault, because the engine treats them as different routes. Read on objectstack main bafb58bb: packages/services/service-automation/src/engine.ts, traverseNext (:11327), narrows out-edges to e.label === branchLabel when the node returned a branch, and falls back to e.isDefault for the default sentinel. So an approval whose unconditioned approve and reject edges both lead to one node runs that node once per outcome, not twice.
This card's triage wording refuses "a repeated (source, target) pair of the same edge type and condition". If the door's key leaves out label and isDefault, it would refuse that approval flow, and the designer would not flag it. The objectui#11772 dev recommends that both halves use one definition (type, condition, default flag, label), ideally one helper in the spec that both the door and the designer read. That is input for you; the PR lands on its own either way.
Generated by Claude Code
objectstack-fleet commented on Oct 7, 2026
os-dev-report
{
"issue": 22088,
"status": "done",
"branch": "claude/issue-22088-flow-edge-endpoints",
"pr": "#22119",
"session": "session_01RPo7FUd6bSnAfkWMAKi848 — mode:subagent, the PM's id; identity is the branch named by Claim 6043281172 (verified newest Claim)",
"premise_still_valid": true,
"summary": "FlowSchema's superRefine now refuses (1) an edge whose source/target names no node of the graph that declares it (top-level nodes for a top-level edge, the region body's nodes for a region edge), anchored at edges.N.source / edges.N.target (region path for region edges), naming the missing id and, when it lives in another graph, that graph; and (2) a repeated edge, anchored at edges.N on the later copy, naming the earlier one. Repeated = the engine's selection key in traverseNext: source, target, type, condition (dialect+source) and branch label; isDefault is not in the key. Same custom Zod issue as the id rules (422 INVALID_METADATA at the save door), no new code. Corpus at aa71c4d had 0 hits (35 example flows / 55 graphs / 131 edges, plus package-shipped flows by reading; CLI golden corpus has no flow), so no migration question was triggered; proposed ADR-0087 disposition is a registered D3 semantic entry flow-edge-unresolved-or-repeated-refused (step 18, no D2) for the contract review to settle. Changeset: @objectstack/spec minor + BREAKING banner (pre.json absent on origin/main).",
"tests": "All at HEAD a4ababd (remote head = a4ababd). spec src/automation: 31 files, 1035 passed. spec full --project local: 622 files, 18579 passed, 1 todo. Closure build (turbo, 26 tasks successful). Consumer fixtures, every test file mentioning edges: metadata-protocol 27 files 696 passed (includes the new save-door section 9: publish+draft dangling 422, publish+draft repeat 422, CONTROL stored); objectql 15 files 501 passed; rest 6 files 350 passed 8 skipped; lint 14 files 1411 passed; service-automation full suite 174 files 2116 passed. Typecheck: spec exit 0, metadata-protocol exit 0. Fixture triage: 1 spec fixture re-judged (node-id test whose shared start→n→end edges named no node of its own list; given its own edge), 0 consumer fixtures red. Ablation (committed state; spec tests import ./flow.zod from src, no dist on the path; scripts/ablation-replace.mjs anchor 1→0 each): endpoint rule disabled → 6 of 12 new cases red, 6 green; repeat rule disabled → 4 red, 8 green; baseline 12 green; restore proven by blob 6f05aaec == HEAD blob and empty git diff HEAD. Door-pin ablation NOT MEASURED (needs spec dist rebuild). Targeted eslint --no-inline-config --format json over the 6 touched .ts files: 6 files, 0 errors, 0 warnings; population from eslint.config.mjs (**/*.ts, none in NEVER_LINTED), no type-aware linting so untouched files cannot move; repo-wide pnpm lint declared to CI. Census positive control (planted dangling, cross-region and repeated edges) fired on every arm.",
"mcp_calls": "0",
"api_writes": "3 REST writes, each through the fleet-write relay (one repository_dispatch per stroke, executed as objectstack-fleet[bot]): POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft, #22119, body read back byte-identical); POST /repos//issues/22119/assignees (label-write --assign os-justin, read back matches); POST /repos//issues/22088/comments (this os-dev-report). git push is not REST.",
"open_questions": [
{
"question": "ADR-0087 disposition for stored flows already carrying such edges (triage left it to the contract review).",
"options": [
"A registered D3 semantic entry, no D2 (as shipped in the PR): a stored row with such an edge is refused at registerFlow (skipped with a warn, trigger not armed); the entry carries the manual remedy",
"B registered D3 plus a D2 conversion that drops EXACT repeated copies at load (mechanical, but it changes how many times the target runs)",
"C not-required"
],
"recommendation": "A, because it matches the step-18 precedent for flow parse narrowings (builtin config values, approval contract, decision branch expression), a dangling endpoint carries no recoverable intent, and dropping a copy changes run counts, which is the author's call"
},
{
"question": "The repeat key includes branch label, one field beyond triage's wording 'same edge type and condition'.",
"options": [
"A keep label in the key (as shipped): approve and reject branches into one node stay legal",
"B drop label: refuse any same-(source,target,type,condition) pair regardless of label"
],
"recommendation": "A, because decision/approval executors narrow out-edges to the selected label in traverseNext, so label-distinct edges are distinct traversals; B would refuse a legitimate approval shape"
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — same pair with different non-fault types (default beside back, or a conditional-typed edge without condition beside default) is accepted under triage's 'same edge type' though traverseNext runs the target twice (PR Acceptance notes)",
"carrier: 承接者:无 · noted, not filed — label-distinct edges out of a node that never selects a branch both run; a parse cannot know which node types select by label (ADR-0018 open namespace) (PR Acceptance notes)",
"carrier: 承接者:无 · noted, not filed — edge-id uniqueness is still judged on top-level edges[] only, not inside region bodies; pre-existing, untouched, reach not measured (PR Acceptance notes)"
],
"gates": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-dev-prereqs.mjs --self-test :: exit 0",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"node scripts/release-pending-publish.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
"pnpm --filter @objectstack/spec run check:liveness :: exit 0",
"pnpm --filter @objectstack/spec run check:llms-txt :: exit 0",
"pnpm --filter @objectstack/spec run check:migration-registry :: exit 0",
"pnpm --filter @objectstack/spec run check:objectui-pin-citations :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-refs :: exit 0",
"pnpm --filter @objectstack/spec run check:spec-changes :: exit 0",
"pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
"pnpm --filter @objectstack/spec run check:upgrade-guide :: exit 0",
"pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
"pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:dispatcher-error-vocabulary :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:durability-log-level :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:future-spec-major :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:merge-driver :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:pm-prior-rulings :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:spec-parsed-alias :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0",
"pnpm --filter @objectstack/spec run check:api-surface :: exit 0",
"pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0",
"pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0",
"pnpm --filter @objectstack/spec run check:docs :: exit 0",
"pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0",
"pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0",
"pnpm --filter @objectstack/spec run check:export-origins :: exit 0",
"pnpm --filter @objectstack/spec run check:exported-any :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm --filter @objectstack/spec check:generated :: exit 0",
"node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran-exit.txt :: exit 0 — '90 derived famil(ies) accounted for — 90 run, 0 NOT-MEASURED (a DERIVED zero — all 90 recorded an exit code and none of them is 3)'"
],
"files_changed": {
"packages/spec/src/automation/flow.zod.ts": "+140",
"packages/spec/src/automation/flow.test.ts": "+206 -7",
"packages/spec/src/automation/control-flow.zod.ts": "+5 -1 (comment)",
"packages/spec/src/migrations/entries/semantic/18.flow-edge-unresolved-or-repeated-refused.ts": "+76 (new)",
"packages/spec/src/migrations/registry.ts": "+88 (generated region + step-18 rationale fragment, order 86)",
"packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts": "+98",
".changeset/22088-flow-edge-unresolved-or-repeated-refused.md": "+39 (new)"
},
"line_counts": "7 files, +644 -8 (aa71c4d..a4ababd)",
"deviations": [
"PR body declares Clause-②: no (narrowing): the claim's Clause-②: no plus triage's own arm, so the body and the changeset carry one line; readClause2Line reads it as declared/no/narrowing",
"Repeat key adds branch label to triage's 'type and condition' (H3 evidence); flagged as open question 2",
"Two queued base spec builds timed out in the verify-lock queue (VERDICT queue-timeout exit 99, never acquired: NOT MEASURED); the base census therefore ran from spec SOURCE via tsx path mapping (examples are untouched by this diff); all readings above come from one later lock hold at a4ababd (held 26m04s, shared box)",
"The queued lock run's script file was replaced with the batched verification script before it acquired; the wrapper shell I had recorded was killed, the orphaned lock wrapper ran to completion (VERDICT command-exit 0)",
"origin/main not merged (8 commits ahead; git merge-tree --write-tree HEAD origin/main clean; registry.ts is not driver-routed)",
"Commit trailers are AGENTS.md's model-free pair, and the PR footer is AGENTS.md's session-URL form, over the harness reminder's model-named Co-Authored-By and its PR footer text",
"Worktree removed after the PR opened (node_modules deleted, git worktree remove exit 0)"
]
}
objectstack-fleet commented on Oct 7, 2026
✅ ACCEPT: PR #22119 at a4ababd1da. ⛔ Not enqueued yet: the contract review is owed and settles the stored-flow disposition, and CI is converging
domain:spec seat 3 (#18883) · os-justin · session session_01RPo7FUd6bSnAfkWMAKi848 · 2026-10-07T18:42Z · holder of claim 6043281172; the review of record for the report 6044439981.
Checklist (read on GitHub and the PR's own diff, not from the report):
- Form: draft, base
main, first lineFixes #22088, no other closing keyword;Clause-②: no (narrowing)starts a line. PR assigneeos-justin. - Scope: 7 files, +644 / −8, inside the claim's surface:
flow.zod.ts(+140) and its tests;- a comment-only line in
control-flow.zod.ts; - the save-door pins in
metadata-protocol; - one step-18 semantic entry with the generated
registry.ts; - one changeset.
- The source change: two blocks in
FlowSchema'ssuperRefine, overcollectFlowGraphs:- an endpoint that names no node of the edge's own graph is refused at
edges.N.source/.target(the region path for a region edge), naming the graph the id does live in; - a repeated edge is refused at
edges.N, naming the earlier copy. The key issource,target,type(default'default'), the condition's dialect and source, and branchlabel. - The same
customissue the id rules raise, so no new error code.
- an endpoint that names no node of the edge's own graph is refused at
- Triage's direction followed, one field beyond its wording: the repeat key adds
label, because decision and approval executors narrow their out-edges by label intraverseNext. Without it, approve and reject branches into one node would be refused. Accepted: it refuses exactly the shape that runs a target twice. - Corpus first (report): 35 example flows, 55 graphs, 131 edges, 0 hits; the planted positive controls fire on every arm.
- Fixture triage: one spec fixture re-judged (its shared edges named no node of its own list) and given its own edges; no consumer fixture went red.
Prose read sentence by sentence against the diff: the changeset's headline; Why; What is refused (both bullets and the door list); What is still accepted; the FROM → TO table; the one-line fix, including the conditioned-edge-into-a-missing-node exception; Who is affected; and The kit. Each matches the code. The refusal messages carry no tracker number.
For the contract review to rule (triage left it there):
- The stored-flow disposition. The PR ships a registered D3 semantic entry with no D2. A stored flow that carries such an edge is then refused at registration: at boot it is skipped with a warn, and its trigger is not armed. So a deployed automation that ran its target twice today stops running at all after the upgrade. The review weighs this against option B, a D2 that drops exact repeated copies at load.
- Residual same-pair shapes the key still admits: a
defaultedge beside abackedge, or a conditional-typed edge with no condition beside a default one, both run the target twice. The dev noted them (no reach measured), and they stay Acceptance notes unless the review rules otherwise.
Out of scope:
- Edge-id uniqueness is still judged only on the top-level
edges[]: Acceptance notes, pre-existing, no reach measured. - Label-distinct edges out of a node that never selects by label both run: Acceptance notes; a parse cannot know which node types select (ADR-0018's open namespace).
Landing, held until:
- a
## Contract reviewPASS atCONTRACT_REVIEW_TIERis on record for the landing head (dispatched to an isolated at-tier reviewer); - every check is green on that head.
Then check-governed-merges --pr 22119, and the relay's pr_ready + automerge_enable.
objectstack-fleet commented on Oct 7, 2026
Landed: PR #22119 → db4c45b8c3. FlowSchema refuses an edge whose endpoint names no node of its graph, and an edge that repeats an earlier one
domain:spec seat 3 (#18883) · os-justin · session session_01RPo7FUd6bSnAfkWMAKi848 · 2026-10-07T20:03Z · holder of claim 6043281172.
- Landed: PR feat(spec)!: FlowSchema refuses an edge whose endpoint names no node of its graph, and a repeated edge #22119 merged through the merge queue at 2026-10-07T20:02Z as
db4c45b8c3, one parent (1920cf3f8), an ancestor oforigin/main.Fixes #22088closed this cardcompleted. - Content check: the six hand-written files on
db4c45b8c3are blob-equal to the accepted heada4ababd1da(ACCEPT6044475647; contract review PASS6044726589on that head). The generatedregistry.tscarriesflow-edge-unresolved-or-repeated-refused. - What now holds: every door that parses
FlowSchemarefuses a dangling edge endpoint (per graph and region body) and a repeated edge. Those doors aredefineFlow,defineStack,os validate,os compile,registerFlowand the metadata save door in draft and publish. The repeat key is source, target, type, condition and branchlabel. The review ruled the stored-flow disposition A: a registered D3 entry with no D2, so a stored flow carrying such an edge is skipped at boot with a warn. It ships asfeat(spec)!:minorwith its BREAKING banner. - Not filed yet; each waits for a reach measurement, per the filing gate. The review's escalated candidates:
- same-pair edges of different non-fault types (for example
conditional(X)besidedefault(X)) still run the target twice; - edge-id uniqueness is not judged inside region bodies;
- the save door logs "persisted as submitted" before it refuses the flow.
- same-pair edges of different non-fault types (for example
pm:dispatchedremoved from this closed card in the same act. build: ascriptnode's undeclared config key passesobjectstack validate,compileandregisterFlow, then fails every run — the key half of #21898's class (subflowby reading) #21982 (area:workflow, the same step-18 chain), held behind this landing, is now claimable.
Filing gate ① — product defect with a named location and a reproduction. reach:
POST /api/v1/meta/flow/repro_edges/publish?package=com.example.repairsanswered 200 for a flow whose nodes are[start, end]and whose edges arestart→node_1,node_1→end; the live read then reports_diagnostics: {valid: true}. A second published flow with threestart→node_1edges executednode_1three times per trigger.Who acts on it: objectstack triage (spec / automation owner). ⛔ Not a claim. Found in a manual browser QA pass of Studio on 2026-10-07; filed one card per finding on the maintainer's word: 「你发现的问题全部提交 issue」, and on the one-card-per-finding question 「覆盖规则,逐条立卡」.
What happens
The flow contract accepts, saves and publishes:
sourceortargetnames no node in the flow;(source, target)and different ids.At runtime the engine follows each incoming edge, so the duplicated target ran once per edge: one record update → three
create_recordexecutions → three identical rows (run detail insys_automation_run.steps_json).Where it comes from (read in source)
FlowSchema'ssuperRefine(packages/spec/src/automation/flow.zod.ts) refuses duplicate node ids and duplicate edge ids, but nothing checks that an edge's endpoints exist or that a(source, target)pair is unique.Expected
Save/publish refuse an edge to a missing node, and refuse (or collapse) a repeated
(source, target)pair of the same edge type and condition, with a located message.Suggested direction (triage to rule)
Add both checks to the same
superRefineregion walk, soos validate, the draft door and publish agree. The objectui half — the designer that produced these edges — is filed on objectui.Environment
objectstack
879bd38c·examples/app-showcasebooted withobjectstack dev --ui --seed-adminon an isolated port and SQLite file · objectui179f6fe9(HEAD; the framework pin.objectui-shaisa58626c8) served by the console's Vite dev server, perf numbers from avite buildof the same commit · Chromium 141 at 1440×900 · signed in as the seeded platform adminadmin@objectos.aiunless stated.Duplicate check
FlowSchemaaccepts a flow whoseedges[]declares the same id twice — measured with a control, and it let a duplicate id ship on green CI #14964 (closed) — same FlowSchema edge-integrity family; that card covers duplicate edge IDS, this card covers dangling endpoints + repeated (source,target) pairsFlowSchemaaccepts a flow whose top-levelnodes[]declares the same id twice — measured with two lit controls; only region nodes are checked (analyzeRegion) #15713 (closed) — sibling node-id integrity fix; same familylintFlowPatternsthrows on a non-record member of a flow'sedgeslist — the sibling list #16751 did not cover #16910 (closed) — edges-list robustness; neighbouringDedupe words: flow edge target missing node · duplicate source target edges · flow published diagnostics valid · node executed per incoming edge
Filed by Claude Code (session
session_01D76mrPJrSSdaKRxR2rvrMG) from that QA pass.Generated by Claude Code