Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,24 @@ from this repository at the same version.

### Fixed

- **Two functions can no longer share one `id`.** The id rule folds a name into a slug
and the collision suffix counted *names*, so two different names that slug to one
string were both left unsuffixed and sharing it: `__add__` and `add`, a Java `Builder`
constructor and its `builder()` factory, `~Widget` and `Widget`, `_M_x` and `_M_X`.
Two definitions on one line had nothing to separate them either, and two file paths
can fold to one stem — `service.cpp` beside `service.hpp`, or
`distutils/_msvccompiler.py` beside `distutils/msvccompiler.py`. Measured over
third-party code, that was **158 duplicated ids in the CPython standard library, 107
in PrimeVue, 106 in `/usr/include`, 4 in the TypeScript compiler** — ordinary code,
not a minifier curiosity. The damage is silent: `/code-flow.quality` computes
`unreached` by subtracting reached ids from catalogued ones, so reaching either
function marked both reached and a genuinely unreachable function simply never
appeared in the findings. `assign_ids` now counts derived ids rather than names, adds
a position suffix when a line holds two of them, and adds a `_f<rank>` suffix for the
ids two files both derived. All four counts above are now zero, and building
a whole map from tracer output over PrimeVue — the run that first tripped the
assertion — completes. The id rule in all four host templates says all of this, so a
map written by hand and a map written by a tracer still agree.
- **An empty `git ls-files` no longer means "this repository has no code".** Both file
listers returned null for the cases their docstrings named — not a git checkout, git
not installed — and the caller walked the tree for those. Neither treated git
Expand Down
49 changes: 45 additions & 4 deletions RELEASE-CHECK-2026-08-28.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ The two zeroes were checked rather than accepted:

The last part of step 6 — build a whole map from tracer output rather than by
re-reading — was run with `scripts/build-map.py` against PrimeVue. **It did not
finish; see below.**
finish; see below.** It was re-run after the fix and completes: 5,817 functions,
no duplicate ids.

## What this pass found

Expand Down Expand Up @@ -176,9 +177,49 @@ either one reports both as reached — so a genuinely unreachable function is
silently absent from the findings, which is the exact failure mode
`detectorsSkipped` and the "ran and found nothing" line exist to prevent.

**This is unresolved as of this pass.** It predates this release — the rule has
been this way since the tracers landed — and fixing class 3 means changing a
rule stated verbatim in four host templates.
**Fixed after this pass**, in the commit that follows it. `derive_id` is
untouched — the rule the four host templates state for an ordinary function is
the same string it always was — and `assign_ids` grew the two suffixes the three
mechanisms need:

- Class 1 disappears by counting **derived ids** instead of names before
applying `_l<line>`. Nothing else changes: the suffix is still the definition
line, still decided from the file's own contents.
- Class 2 gets `_<n>`, the definition's 1-based position among the same-id
definitions on its line, in source order — the only thing left to order them
by, since no record carries a column.
- Class 3 gets `_f<rank>`, the file's position among the paths that derived the
id, sorted. This is the one part of the rule that looks outside a single file,
because the collision is between two of them, and it is applied only to the
ids two files **both** derived — a `.hpp` beside its `.cpp` shares a stem, but
suffixing every function in both would rename most of a C++ catalog to fix a
handful of ids. Keying the rank off the id rather than off the stem also
closes the case where two files with *different* stems still meet on one id,
which a stem-keyed rank would have walked straight past.

Re-measured over the same corpora, from the same trace output:

| Corpus | Functions | Duplicated ids, before → after |
|---|---|---|
| CPython standard library | 14,720 | 158 → **0** |
| `/usr/include` | 12,765 | 106 → **0** |
| PrimeVue | 5,817 | 107 → **0** |
| TypeScript 5.6 compiler | 20,707 | 4 → **0** |
| Commons Lang 3 + Gson | 4,594 | 3 → **0** |

Each "before" is the pre-fix rule re-derived from the *same* trace output, so
the two columns are the two rules over one catalog rather than two runs that
might differ for another reason. Three of the corpora are not byte-identical to
the ones above — the Java corpus here is Commons Lang 3 and Gson without Guava,
JUnit or picocli, and the TypeScript compiler is 5.6.3 — so their totals are
smaller; the four that are identical reproduce their counts exactly.

2.6% of ids change in the standard library, 2.7% in `/usr/include`, 3.8% in
PrimeVue — the colliding ones and the groups they belong to, and nothing else.

The C fixture was rearranged in `1768daf` to dodge the `service.cpp` /
`service.hpp` case rather than cover it; that is reverted, so the suite now
proves the fix on the shape that first exposed it.

## Not covered by this pass

Expand Down
5 changes: 2 additions & 3 deletions templates/claude/code-flow.map.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,8 @@ By default, **also** produce a self-contained interactive HTML page next to the
Rules — follow these exactly, or the page will refuse to render:

- One node per function in the diagram. **Every `edge.from` and `edge.to` MUST match a node `id`.**
- `id` is derived from the node's own `file` and function name, so that the same function always gets the same `id` in every flow and downstream tools can join flow nodes against a function catalog. Derive it exactly like this: **(1)** take the repo-relative `file` path and drop the extension from its **last segment only** — the final `.` in the filename and everything after it, so `src/v2.1/handler.py` → `src/v2.1/handler`, and a filename with no dot loses nothing; **(2)** append `_` followed by the function's **unqualified** name — `authenticate`, never `User.authenticate` — which is the same name a function catalog records for it; **(3)** lowercase the whole string, replace every remaining character outside `[a-z0-9_]` (path separators, dots, dashes, spaces, anything else) with `_`, collapse each run of `_` into a single `_`, and trim any leading or trailing `_`. Example: `src/web/views.py` + `login_view` → `src_web_views_login_view`. If **the file itself** defines more than one function with that name — same-named methods on two classes, or an overload — append `_l` and the line number of the function's own definition keyword — the
`def`, `function`, `func` or `fn` line itself, never a decorator, annotation or
comment line above it: `src_jobs_worker_run_l31` and `src_jobs_worker_run_l88`. Decide that from the file's own contents, never from which nodes happen to be in this flow: an `id` must not change depending on what else you mapped.
- `id` is derived from the node's own `file` and function name, so that the same function always gets the same `id` in every flow and downstream tools can join flow nodes against a function catalog. Derive it exactly like this: **(1)** take the repo-relative `file` path and drop the extension from its **last segment only** — the final `.` in the filename and everything after it, so `src/v2.1/handler.py` → `src/v2.1/handler`, and a filename with no dot loses nothing; **(2)** append `_` followed by the function's **unqualified** name — `authenticate`, never `User.authenticate` — which is the same name a function catalog records for it; **(3)** lowercase the whole string, replace every remaining character outside `[a-z0-9_]` (path separators, dots, dashes, spaces, anything else) with `_`, collapse each run of `_` into a single `_`, and trim any leading or trailing `_`. Example: `src/web/views.py` + `login_view` → `src_web_views_login_view`. If **the file itself** derives that same id for more than one function — same-named methods on two classes, an overload, or *two different names that slug to one string*, which is what `__add__` and `add`, a `Builder` constructor and its `builder()` factory, or `~Widget` and `Widget` all do — append `_l` and the line number of each one's own definition keyword — the `def`, `function`, `func` or `fn` line itself, never a decorator, annotation or comment line above it: `src_jobs_worker_run_l31` and `src_jobs_worker_run_l88`. Suffix every one of them, including the first. If two of them are on the **same line**, the line cannot separate them, so append `_` and the position of each among that line's same-id definitions, counting from 1 in source order: `src_ui_panel_x_l7_1`, `src_ui_panel_x_l7_2`. Decide all of that from the file's own contents, never from which nodes happen to be in this flow: an `id` must not change depending on what else you mapped.
- Two **different files** can derive the same `id` — `src/service.cpp` and `src/service.hpp`, since the rule drops the extension, or `distutils/_msvccompiler.py` and `distutils/msvccompiler.py`, since the leading `_` becomes a separator and collapses into the one before it. Where that happens, and only for the ids both files actually derived, append `_f` and each file's position among those paths, sorted, counting from 1: `src_service_describe_f1` for `src/service.cpp` and `src_service_describe_f2` for `src/service.hpp`. Leave every other function in both files alone. This is the one part of the rule that looks outside a single file, because the collision is between two of them; it is also rare, so if you are applying it to more than a handful of ids, re-read the paths.
- `kind` on a **node** ∈ `entry` | `step` | `external` | `io` | `component` (default `step`). `entry` = where the flow starts; `external` = a third-party/library boundary; `io` = a DB/network/file side effect; `component` = a UI component rather than a plain function. This drives node color.
- **Exactly one** node MUST have `kind: entry`. If the flow has several plausible roots — two HTTP handlers, say — pick the one the user asked about, make that the `entry`, and mark the others `step`.
- `kind` on an **edge** ∈ `call` | `async` | `conditional` | `render` (default `call`). `render` is a parent component drawing a child. Set `"back": true` on any edge that closes a loop or recursion (points back to an ancestor) so it is drawn as a routed dashed curve.
Expand Down
3 changes: 2 additions & 1 deletion templates/copilot/code-flow.map.prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ By default, also produce a self-contained interactive HTML page next to the mark

Rules (follow exactly or the page refuses to render):
- One node per function. **Every `edge.from`/`edge.to` MUST match a node `id`.**
- `id` is derived from the node's own `file` and function name, so that the same function always gets the same `id` in every flow and downstream tools can join flow nodes against a function catalog. Derive it exactly like this: **(1)** take the repo-relative `file` path and drop the extension from its **last segment only** — the final `.` in the filename and everything after it, so `src/v2.1/handler.py` → `src/v2.1/handler`, and a filename with no dot loses nothing; **(2)** append `_` followed by the function's **unqualified** name — `authenticate`, never `User.authenticate` — which is the same name a function catalog records for it; **(3)** lowercase the whole string, replace every remaining character outside `[a-z0-9_]` (path separators, dots, dashes, spaces, anything else) with `_`, collapse each run of `_` into a single `_`, and trim any leading or trailing `_`. Example: `src/web/views.py` + `login_view` → `src_web_views_login_view`. If **the file itself** defines more than one function with that name — same-named methods on two classes, or an overload — append `_l` and the line number of the function's own definition keyword — the `def`, `function`, `func` or `fn` line itself, never a decorator, annotation or comment line above it: `src_jobs_worker_run_l31` and `src_jobs_worker_run_l88`. Decide that from the file's own contents, never from which nodes happen to be in this flow: an `id` must not change depending on what else you mapped.
- `id` is derived from the node's own `file` and function name, so that the same function always gets the same `id` in every flow and downstream tools can join flow nodes against a function catalog. Derive it exactly like this: **(1)** take the repo-relative `file` path and drop the extension from its **last segment only** — the final `.` in the filename and everything after it, so `src/v2.1/handler.py` → `src/v2.1/handler`, and a filename with no dot loses nothing; **(2)** append `_` followed by the function's **unqualified** name — `authenticate`, never `User.authenticate` — which is the same name a function catalog records for it; **(3)** lowercase the whole string, replace every remaining character outside `[a-z0-9_]` (path separators, dots, dashes, spaces, anything else) with `_`, collapse each run of `_` into a single `_`, and trim any leading or trailing `_`. Example: `src/web/views.py` + `login_view` → `src_web_views_login_view`. If **the file itself** derives that same id for more than one function — same-named methods on two classes, an overload, or *two different names that slug to one string*, which is what `__add__` and `add`, a `Builder` constructor and its `builder()` factory, or `~Widget` and `Widget` all do — append `_l` and the line number of each one's own definition keyword — the `def`, `function`, `func` or `fn` line itself, never a decorator, annotation or comment line above it: `src_jobs_worker_run_l31` and `src_jobs_worker_run_l88`. Suffix every one of them, including the first. If two of them are on the **same line**, the line cannot separate them, so append `_` and the position of each among that line's same-id definitions, counting from 1 in source order: `src_ui_panel_x_l7_1`, `src_ui_panel_x_l7_2`. Decide all of that from the file's own contents, never from which nodes happen to be in this flow: an `id` must not change depending on what else you mapped.
- Two **different files** can derive the same `id` — `src/service.cpp` and `src/service.hpp`, since the rule drops the extension, or `distutils/_msvccompiler.py` and `distutils/msvccompiler.py`, since the leading `_` becomes a separator and collapses into the one before it. Where that happens, and only for the ids both files actually derived, append `_f` and each file's position among those paths, sorted, counting from 1: `src_service_describe_f1` for `src/service.cpp` and `src_service_describe_f2` for `src/service.hpp`. Leave every other function in both files alone. This is the one part of the rule that looks outside a single file, because the collision is between two of them; it is also rare, so if you are applying it to more than a handful of ids, re-read the paths.
- Node `kind` ∈ `entry` | `step` | `external` | `io` | `component` (default `step`). `entry` = where the flow starts; `external` = a third-party/library boundary; `io` = a DB/network/file side effect; `component` = a UI component rather than a plain function. This drives node color. **Exactly one** node MUST have `kind: entry`; if the flow has several plausible roots, pick the one the user asked about and mark the rest `step`. Edge `kind` ∈ `call` | `async` | `conditional` | `render` (default `call`); `render` is a parent component drawing a child. Set `"back": true` on edges that close a loop/recursion.
- File paths use **forward slashes**, repo-relative; `meta.root` is the absolute root, forward slashes.
- `snippet` is optional (≤ ~40 lines). **Replace each `</` with `<\/` inside every snippet.** No trailing commas.
Expand Down
Loading