fix(lead-conversion): stop the flow declaring the edge id e20 twice - #1571
Merged
Conversation
The `get_lead → decision_duplicate` edge and the terminal
`send_notification → end` edge both carried `e20`. Flow traversal selects
out-edges by `source` and never by `id`, so the collision was inert, but it
contradicted the edge list's own convention — retired ids are left vacant so
every surviving edge keeps the id it has always had — and anything keyed on
edge ids rather than on `source` would have silently dropped one of the two.
It was also a trap for the next editor picking a "free" id out of a sequence
that already contained a collision.
The convention decides which of the two moves, not file order. The terminal
edge has carried `e20` since the original graph; the other acquired it later
when the head of the graph was reordered, and that change's sibling edges are
numbered e21-e24 — only consistent with e20 already being taken. So the
terminal edge keeps the id it has always had and the newer edge takes a fresh
`e27`, the next id above the highest in use. `e1` and `e3` stay vacant.
Behaviour is unchanged: every edge's {source, target, condition, label, type}
tuple is identical before and after, across all 28 edges. A runtime-value sweep
of all 22 src/flows/*.flow.ts files (26 flows, 168 edges) finds no other
duplicate edge id in any flow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hUuCQStzXGMFSX4dzww5t
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
os-sales
marked this pull request as ready for review
September 3, 2026 13:29
This was referenced Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1558
src/flows/lead-conversion.flow.tsused the edge ide20twice. This is an id rename only — no edge'ssource,target,conditionorlabelchanged.Which id moved, and why it is not the one the card provisionally named
The card suggested renaming the later occurrence (
:506) "so the earlier id keeps its history", while asking me to check the file's stated convention first. The convention answers it — and it points the other way.The edge list's own comment states the rule:
That keys on history, not on file position, and the two come apart here:
:506send_notification → ende20all along:444get_lead → decision_duplicateThree independent pieces of evidence agree that
:506is the incumbent:decision_duplicateis described as reordering the head of the graph; the notification tail it did not touch already ended… → e19 → e20 → end.e21–e24, a contiguous block starting immediately abovee20. That is only consistent withe20already being occupied when they were chosen — the:444edge is the mis-pick, not the incumbent.e18a/e18b. Suffixed ids exist precisely becausee19ande20were already taken and the convention forbids renumbering — so the terminale20predates them.So the terminal edge keeps
e20, and:444takes a freshe27— the next id above the highest in use (e26).e1ande3stay vacant, as retired ids do.git blamecannot arbitrate this: the clone is shallow and both lines trace to the same boundary commit.Evidence the graph is unchanged
The change is behaviour-neutral by construction, so there is nothing to ablate — an ablation needs a behaviour whose absence can be observed, and by design there is none. The real evidence is the enumeration.
Every edge's
{source, target, condition, label, type}tuple was enumerated by runtime value (iddeliberately excluded, as the only field permitted to change), sorted, before and after:Identical SHA-256 across all 28 edges (24 nodes).
diffreports zero differences.The edit is confirmed on disk — blob
380599e6→4a8d3cee, with anchored counts on both the removed and injected text:id: 'e20'id: 'e27'{ id: 'e20', source: 'get_lead'{ id: 'e27', source: 'get_lead'{ id: 'e20', source: 'send_notification'Sweeps
e20was the only duplicate — confirming the seat's result by a method that does not share its blind spot. The sweep demonstrably reads past theid: 'e<digits>'form: it picked upe18a,e18b,e4a,e4bande_nouser.src/flows/*.flow.tsfiles, 26 flows, 168 edges.lead_conversionwas the only flow with a duplicate id, so no other flow needs a card and the file surface stays as declared.e20occurs 3 times: the two lines above, plus7e20966inCHANGELOG.md— a commit-hash substring, not an edge id. Nothing insrc/,test/,docs/orcontent/docs/names either edge, so this rename is confined to one file. Note that edge ids here are not cosmetic in general — tests do key one16/e17ande21/e22/e25— which is why the collision was worth removing.Verify
pnpm verify— the full chain, green on this commit's tree (9ef828a4, flow blob4a8d3cee):The token ratchet is unmoved by the added comment — that gate is comment-stripped by design.
Changeset
Empty frontmatter — the sanctioned "releases nothing" declaration. This edits
src/metadata, which usually does ship, so the claim is measured rather than asserted: behaviour is provably identical, and the identifier that changed is referenced nowhere outside the file declaring it. Apatchentry would put a line in the release notes that no HotCRM user could act on.A uniqueness pin was deliberately NOT built here
Measured:
FlowSchema.safeParse()accepts a flow with two edges sharing an id (control: the same schema rejects an invalidtype, so it is live, not inert). The gap is in the platform's flow schema, and it leaves every ObjectStack app exposed, not just HotCRM.AGENTS.mdsends exactly this class upstream — "Lint, validation, gates and diagnostics belong to the platform, uniformly… Tests in this repo pin this repo's own business facts and nothing else. ⛔ Do not grow a gate farm" — with #806 (its lint half was later retired) and #1423 (gate deliberately not built) as tombstones. Edge-id uniqueness is a structural property the schema can check for every app at parse time, not a HotCRM business fact, so building it here would compensate locally for a platform gap and protect only this repo. Proposed as a platform card instead; details are in the report on #1558.🤖 Generated with Claude Code
https://claude.ai/code/session_019hUuCQStzXGMFSX4dzww5t
Generated by Claude Code