Repository navigation
fix(lint)!: refuse a flow node no edge path from the start node reaches - #31287
Conversation
…ches Claude-Session: https://claude.ai/code/session_01FEkQ5oEQNYg7j3ueo5t6pV Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5oEQNYg7j3ueo5t6pV Co-Authored-By: Claude <noreply@anthropic.com>
…dge reaches Claude-Session: https://claude.ai/code/session_01FEkQ5oEQNYg7j3ueo5t6pV Co-Authored-By: Claude <noreply@anthropic.com>
…lint) Claude-Session: https://claude.ai/code/session_01FEkQ5oEQNYg7j3ueo5t6pV Co-Authored-By: Claude <noreply@anthropic.com>
A run starts at the start node and follows edges, so a top-level node no edge path reaches never executes, while the flow parses, registers and reports itself as configured. lintFlowPatterns now reports it as `flow-node-unreachable` (severity error), which gates os validate/build/lint and the flow publish doors; a draft save still stores, with the finding in its diagnostics. Every edge type counts as a path. boundary_event nodes and the nodes only they lead to are exempt (reached by attachment), and region bodies are not judged (single-entry DAGs). A flow with no start node or a dangling edge is left to the flow parse. It is lint rather than a FlowSchema rule because the save door parses every save, drafts included, and the Studio designer's node delete can leave a successor unwired. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5oEQNYg7j3ueo5t6pV
…boundaryConfig Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5oEQNYg7j3ueo5t6pV
… edge reaches The save door's flow lint now refuses a top-level node no edge path from the start node reaches, which is not the refusal these round trips are about. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5oEQNYg7j3ueo5t6pV
…tion test check:test-source-alias asks an edited test file that reaches the save door's dynamic lint import to import it at module top. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5oEQNYg7j3ueo5t6pV
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin bbb1341eddff0b0fcf4cd3ee59abd22da8ecf813 && git checkout bbb1341eddff0b0fcf4cd3ee59abd22da8ecf813
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ebc2cdc52207e0e755a910afeda65a39630a84fa 0a1def67c464136ffbf5eca8a88dfd9f3012be8d && git checkout -B drift-repro ebc2cdc52207e0e755a910afeda65a39630a84fa && git merge --no-ff 0a1def67c464136ffbf5eca8a88dfd9f3012be8d
node scripts/docs-audit/affected-docs.mjs --json ebc2cdc52207e0e755a910afeda65a39630a84fa
|
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Contract-review-claim: session_01FEkQ5mw7HFYn9o4LEGMQdK · head d3db24b · 2026-11-07T22:25Z |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Contract reviewServed-tier: Inputs read: card #31224 body and every comment (triage 6711648543, claim 6712039823, plan 6712129038, seat rulings 6712144718, os-dev-report 6713825906); PR #31287 body, file list and ① Derived judgmentsThe rule, against the engine it describes — right. Start-node cases — right, one wording nit. No Edges to or from missing nodes — right. The scan returns without judging when any edge endpoint is not a node id. That branch is reached only by a document the flow parse has already refused: ADR-0031 regions — right. Containers carry their body as a nested graph in
Which doors refuse, which keep. The rule sits in a Shipped and example flows — none refused. Every Tests — behaviour right, one type error (must-fix, ③). The six re-wired lint fixtures and the approval case each keep their own assertion (compared against ② Semver level
③ Boundary flags
④ Design judgmentThe narrowing, judged against the published spec on Implemented-by: VERDICT: FAIL |
…ts context
A second `start` node is unreached by construction (a run starts at the first
one), so its hint says to delete the extra start rather than wire an edge into
it; the os explain entry says the same. The registration test passes the
`{}` context `AuthoringRule.run` takes, which check:test-typecheck asks for.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5oEQNYg7j3ueo5t6pV
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Contract reviewServed-tier: Delta review on the narrowing face. This head is one commit on top of ① Derived judgmentsMust-fix landed — right. The registration case now calls Second-start cure — right, and it fires only where it should. The hint branch keys on Explanation and changeset sentences — right. Nothing else moved. The delta touches no door, no exemption, no corpus fixture, no ② Semver levelUnchanged from 6714019852: ③ Boundary flags
④ Design judgmentUnchanged from 6714019852 and strengthened by the delta. Against the published spec on No must-fix open on this head: the type error is fixed in the tree, both should-fixes are closed, and the delta introduces no new accept-set change, no new export and no new door. Implemented-by: VERDICT: PASS |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Test Core (8/8) red on This red is not this PR's. The diff touches No fix exists on |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Contract reviewServed-tier: Merge-only delta review on the narrowing face. ① Derived judgmentsThe net diff is the reviewed one — right. Nothing main brought in interacts with the rule — right. Main's side adds no flow node anywhere (no added line spells
The PR's own findings stand. ① of comments 6714019852 and 6714102343 — engine match, start-node cases including the second-start cure, dangling edges, ADR-0031 regions, ② Semver levelUnchanged: ③ Boundary flags
④ Design judgmentUnchanged from 6714102343. Against the published spec on No must-fix open on this head: the merge carries the reviewed hunks unchanged, brings in no flow or door change that interacts with the rule, and the declarations and level are as reviewed. Implemented-by: VERDICT: PASS |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK 维护者速读
席位核对Verdict: ACCEPT · head
Ruling-ref: the maintainer, 2026-11-07T16:00Z: 「你有 fable ,车道契约复审可以自己做」; 2026-11-07T16:59Z: 「你应该使用 pm-dispatch 技能审核和落地pr」; standing landing authorisation 2026-10-25T11:35Z: 「按照 pm-dispatch 规则复核过的都授权」. |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 42427976281 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK The queue build 42427976281 failed on Test Core (4/8). The failing test is The seat re-queued this PR once, with the head unchanged ( |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 42429367955 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Queue build 42429367955 failed, and the cause is the same root as before. The build's base #31233 is out of the queue again, so the seat has re-queued this PR on the same head ( |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 42430895501 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
domain:spec#1 · os-project-manager · session_01FEkQ5mw7HFYn9o4LEGMQdK Queue build 42430895501 failed, and the root is the same as before. Its base, #31233 left the queue and re-entered it on the head that carries that fix, so a stack behind it should no longer carry the break. The seat has re-queued this PR on the same head ( |
Requested by Leehom · project thread
Refs #31224
Clause-②: yes (narrowing)
Metadata-protocol: narrowed
Skills-docs: none —
skills/**andcontent/docs/**teach only wired top-level flows; the edgeless examples incontent/docs/automation/flows.mdxare region bodies, which this rule does not judge. The reasoning ships as theos explain flow-node-unreachableentry.维护者速读
@objectstack/lint的lintFlowPatterns新增一个 error 级发现flow-node-unreachable:流程顶层某个节点从 start 节点出发沿任何边都走不到,就报出该节点、它为何永远不会执行、以及修法(从会执行的节点连一条边进来,或删掉它)。另加os explain条目、测试和一个 lint minor changeset。protocol-save.ts:1429对每次保存(含草稿)都跑FlowSchema,而 Studio 设计器删除节点时edgesAfterNodeRemoval会让后继节点断开。放在 parse 会让这类草稿直接存不了;放在 lint,草稿照存(发现写进_diagnostics),发布被拒。os lint/validate/build和流程发布门会拒绝此前接受的流程。普查为零:objectstack 54 个流程 238 个节点、hotcrm 35 个流程 256 个节点,不可达均为 0(对照:分别有 184、221 个非 start 节点可达)。回滚即 revert。pnpm --filter @objectstack/lint exec vitest run src/lint-flow-patterns.test.ts,预期 255 个全部通过,其中新 describe 的 10 条为本卡新增。What changes
packages/lint/src/lint-flow-patterns.ts:FLOW_NODE_UNREACHABLE = 'flow-node-unreachable'andscanUnreachableNodes, called fromlintFlowPatternsonce per flow over the top-level graph. A breadth-first walk from the firststartnode over every top-level edge; one finding per node it does not reach,severity: 'error', atflow 'NAME' · node 'ID' (TYPE). The message names the start node and says why the node never runs; the hint is "Add an edge into it from a node that runs, or delete the node." A secondstartnode (unreached by construction: a run starts at the first one) gets its own hint: delete the extra start, and wire what follows it from a node that runs.boundary_eventnode and the nodes reached only from one (attached to its host byboundaryConfig.attachedToNodeId, not entered by an edge; its own contract owns its refusal).analyzeRegion/validateControlFlowalready hold, so every node in one is reached); a flow with nostartnode; a flow with an edge whose endpoint names no node (the flow parse refuses that edge first, and reporting its target here would prescribe the wrong cure).packages/lint/src/index.ts: exportsFLOW_NODE_UNREACHABLEbeside the other flow rule ids. This new export is whyClause-②readsyes (narrowing)rather than the claim'sno (narrowing), following report publish gate: a report whose runtimeFilter names a field the dataset's object lacks publishes 200 and renders "Dataset query failed", where the same key on a dashboard widget filter is refused 422 #29900's changeset (new rule id exports under a narrowing).packages/lint/src/rule-explanations.ts: theos explain flow-node-unreachableentry (what reaches a node, what is not judged, why lint and not parse, the gate reach)..changeset/31224-flow-node-unreachable.md:'@objectstack/lint': minor, BREAKING banner, ADR-0087not-required (no-migration-prescription): no key moves, and an unreached node never ran, so no rewrite keeps what the author meant.Why lint, not the flow parse. A first build put the rule in
FlowSchema.superRefine. It was taken back out (the revert commit on this branch leavespackages/specbyte-identical tomain) for this reason. The metadata save door safeParsesFlowSchemaon every save, drafts included (packages/metadata-protocol/src/protocol-save.ts:1429, 422INVALID_METADATA). The Studio designer's node delete (edgesAfterNodeRemoval,ui/packages/app-shell/src/views/metadata-admin/previews/flow-problems.ts:729) bridges the gap only when the deleted node had exactly one in-edge and one out-edge, and otherwise leaves the successor unwired. A parse rule would refuse that draft save outright.Doors the error reaches.
lintFlowPatternsis agatingauthoring rule withsurfaces: CLI_AND_RUNTIME,runtimeTypes: ['flow'](packages/lint/src/authoring-rules.ts), so:os lint,os validateandos buildfail on it._diagnostics(valid: false), and its publish refuses with the same words.No
packages/spec, metadata-protocol source, runtime orui/change.Tests
packages/lint/src/lint-flow-patterns.test.ts, new describe "a node no edge path from start reaches":where, message and hint pinned); a chain of two unreached nodes (one finding each); a second start node (its own hint); a cycle cut off from start.boundary_eventand the node only it leads to; no start node; a dangling edge; a loop region body.lintFlowPatternsauthoring rule isgating, carriesruntimeTypes: ['flow'], and serves the finding aserror.start. The "normal approval with no revise edge" case drops its unwiredapproval_revisenode. Each case keeps its own assertion.packages/metadata-protocol/src/protocol.draft-publish-verdict.test.ts: aflow-node-unreachablerow inCASES. The draft saves and readsvalid: false, publish refuses with the same words, and the package publish refuses and rolls back.packages/metadata-protocol/src/protocol.metadata-redaction.test.ts: four [security] closeout: two stored-credential positions outside #20552's projection still reach a served read unredacted (a flow http node's signingSecret; the /meta list read's raw fallback) #20590 round-trip fixtures saved flows with top-level nodes no edge reached, and the save door now refuses them for that. They carry the edges from their start node; two edge-id pins list the added edges; each case's own assertion is unchanged.scanUnreachableNodescall commented out, 4 of the new cases go red (the three refusals and the registration case); restored, green (254/254 at the time; 255 with the second-start case added since).startover every top-level edge:main6961ab1: 35 flows, 256 nodes, 0 unreachable; control: 221 reached.Acceptance notes
required: true) is not built: the seat ruled it an enforced presence contract. Item B lands as B1 in its own PR; item D also lands in its own PR. This PR is item C only, soRefs, notFixes.simulator/flow-sim-validate.ts:412) is unchanged; this rule is the gating one.Verification
Final head
3284c658(branch merged withorigin/main8f4e8fb9before the final runs).3284c658follows the contract review: the registration test passes the{}contextAuthoringRule.runtakes (theType Check · workspacered), and a second start node gets its own hint. After it,@objectstack/lintand@objectstack/metadata-protocolpnpm typecheck(incl.check:test-typecheck) exit 0, andlint-flow-patterns.test.ts+rule-explanations.test.tspass 263/263.dispatch-gates --commandsand run on8f731d6a. 61 exit 0.check:test-source-aliaswas red on the edited redaction test (the save door's lint import must load at collection). Thed3db24b1commit adds the module-topimport '@objectstack/lint', and the gate is then exit 0.--ranreconciles 62 accounted, 0 NOT-MEASURED, 0 unrun.check:dual-build-cjs-loadsran after the full build. The teardown self-test ran with the pinned fixture commit fetched.check-changeset-no-majorwith apull_requestevent carrying this body exits 0 (LEVEL AXIS:yes (narrowing)+narrowed, no package gradedpatch).pnpm build(119 tasks), thenpnpm --filter @objectstack/dogfood testunder the verify lock on8f731d6a: 435 files passed, 3 skipped; 3609 tests passed, 41 skipped; VERDICT command-exit 0. The laterd3db24b1commit is one test-file import.@objectstack/lint: 197 files, 8352 tests passed.@objectstack/metadata-protocol: 322 files. The 5 redaction round-trip reds this rule caused are wired in this PR, and the file then passes 67/67; the draft-publish verdict file passes 50/50.@objectstack/runtime, run on a working tree before the final merge (not re-run on the final head; dogfood covers it there): 557 of 558 files passed. The 1 red (standalone-stack-uncreatable-db-directory, a/proctiming case) passes 6/6 when rerun alone and touches no flow.CI runs the full set.
Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5oEQNYg7j3ueo5t6pV