Skip to content

fix(lead-conversion): stop the flow declaring the edge id e20 twice - #1571

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-1558-duplicate-edge-id
Sep 3, 2026
Merged

fix(lead-conversion): stop the flow declaring the edge id e20 twice#1571
os-sales merged 1 commit into
mainfrom
claude/issue-1558-duplicate-edge-id

Conversation

@os-sales

@os-sales os-sales commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1558

src/flows/lead-conversion.flow.ts used the edge id e20 twice. This is an id rename only — no edge's source, target, condition or label changed.

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:

the gap in the numbering is deliberate, so every surviving edge keeps the id it has always had

That keys on history, not on file position, and the two come apart here:

line edge origin
:506 send_notification → end the terminal edge of the original graph — has carried e20 all along
:444 get_lead → decision_duplicate introduced later, when the head of the graph was reordered

Three independent pieces of evidence agree that :506 is the incumbent:

  1. The file's own history note. The reorder that created decision_duplicate is described as reordering the head of the graph; the notification tail it did not touch already ended … → e19 → e20 → end.
  2. The sibling numbering. That same change's other new edges are e21e24, a contiguous block starting immediately above e20. That is only consistent with e20 already being occupied when they were chosen — the :444 edge is the mis-pick, not the incumbent.
  3. e18a / e18b. Suffixed ids exist precisely because e19 and e20 were already taken and the convention forbids renumbering — so the terminal e20 predates them.

So the terminal edge keeps e20, and :444 takes a fresh e27 — the next id above the highest in use (e26). e1 and e3 stay vacant, as retired ids do. git blame cannot 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 (id deliberately excluded, as the only field permitted to change), sorted, before and after:

BEFORE  5ae7911f63b02ff7454a2d5b0a0dd20a3308bb3f86d7439abca2bb4ca4d97b42
AFTER   5ae7911f63b02ff7454a2d5b0a0dd20a3308bb3f86d7439abca2bb4ca4d97b42

Identical SHA-256 across all 28 edges (24 nodes). diff reports zero differences.

The edit is confirmed on disk — blob 380599e64a8d3cee, with anchored counts on both the removed and injected text:

anchor before after
id: 'e20' 2 1
id: 'e27' 0 1
{ id: 'e20', source: 'get_lead' 1 0
{ id: 'e27', source: 'get_lead' 0 1
{ id: 'e20', source: 'send_notification' 1 1

Sweeps

  • This file, form-agnostic. Edge ids were read as runtime values, not by their source spelling, so a template literal or a constant would still be counted. e20 was the only duplicate — confirming the seat's result by a method that does not share its blind spot. The sweep demonstrably reads past the id: 'e<digits>' form: it picked up e18a, e18b, e4a, e4b and e_nouser.
  • Across the flow family. All 22 src/flows/*.flow.ts files, 26 flows, 168 edges. lead_conversion was the only flow with a duplicate id, so no other flow needs a card and the file surface stays as declared.
  • References to the moved id. Repo-wide, e20 occurs 3 times: the two lines above, plus 7e20966 in CHANGELOG.md — a commit-hash substring, not an edge id. Nothing in src/, test/, docs/ or content/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 on e16/e17 and e21/e22/e25 — which is why the collision was worth removing.

Verify

pnpm verify — the full chain, green on this commit's tree (9ef828a4, flow blob 4a8d3cee):

os-verify-lock: VERDICT command-exit 0 · held the lock 217s (3m37s)
  ✓ Validation passed        ✓ typecheck      ✓ lint
  ✓ i18n lint gate: 0 issues ✓ source hygiene clean
  ✓ source token ratchet clean   ✓ build
  Test Files  160 passed (160)
  Tests  3371 passed | 1 skipped (3372)

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. A patch entry 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 invalid type, 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.md sends 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

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
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
hotcrm Ignored Ignored Sep 3, 2026 1:23pm UTC

Request Review

@github-actions github-actions Bot added the backend Server-side behaviour — hooks, flows, actions label Sep 3, 2026
@os-sales
os-sales marked this pull request as ready for review September 3, 2026 13:29
@os-sales
os-sales added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit 0ed728e Sep 3, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Server-side behaviour — hooks, flows, actions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lead-conversion.flow.ts uses the edge id e20 twice — inert under the current traversal, and the file's own convention says ids are stable

2 participants