Skip to content

fix(ids): declaration merging mints one id per facet; both facets survive in types{} (#177) - #190

Merged
rahlk merged 1 commit into
mainfrom
fix/issue-177-declaration-merging
Sep 7, 2026
Merged

fix(ids): declaration merging mints one id per facet; both facets survive in types{} (#177)#190
rahlk merged 1 commit into
mainfrom
fix/issue-177-declaration-merging

Conversation

@rahlk

@rahlk rahlk commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Closes #177. Spec: docs/design/specs/declaration-merging-ids.md (D1–D5, mirrored in .claude/SCHEMA_DECISIONS.md). Analyzer 1.5.0; Neo4j contract stays 2.0.0.

What was wrong

Every facet of one name has the same signature, and ids were minted from signatures alone, so a value and a type of one name (const TableOption = () => … + interface TableOption) shared an id and --emit neo4j MERGEd them onto one node with both labels and the last writer's kind. Worse, a type/type merge (class C + interface C) was "later kind wins" in types{} — the class vanished from analysis.json entirely.

What changes

  • Value facets keep the bare id. A type whose bare id is already minted by a callable or field in the same scope becomes <id>#type. Callables anchor call edges, body-node ids, @formal_in:N and parameters[i].id; a type's id is only referenced through extends_ids/implements_ids.
  • Type/type merges keep both facets. Builder order (class → interface → enum → alias → namespace): the first keeps the bare key and id; each later facet is keyed Name#<kind> in types{} and gets <id>#<kind> (#interface, #namespace, …).
  • Only on collision; signatures never move. A merge-free project is byte-identical (verified: sample-app -a 4 output on this branch is byte-identical to main). idBySig keeps the value facet for call-graph re-identification, callee backfill and homing; a new typeIdBySig gives heritage the type facet; a split is not an L1 collision.
  • Heritage resolution in the builder now picks the first type-like declaration of a merged symbol instead of declaration [0] (which was the value), so implements TableOption resolves to the interface facet.
  • Neo4j: nothing to change — distinct ids are distinct nodes; kind and labels agree on every node.

Tests

test/declaration-merging.test.ts: value+interface, alias+arrow, alias+const, class+interface, function+namespace — distinct ids per facet, both facets in types{}, implements#type facet, extends → bare class, no L1 collision, member ids under the bare class id, and the projection has no node with two kind labels. bun test: 345 pass.

No sibling precedent: python rebinds on a later def, Java forbids the construct.

…vive in types{} (#177)

Value facets keep the bare id; a colliding type facet is <id>#type; a later type facet of a
type/type merge is keyed Name#<kind> and gets <id>#<kind>. Signatures unchanged; heritage
resolves through the type facet; a split is not an L1 collision. Ids move only on collision.
@rahlk
rahlk merged commit 7a6f9e1 into main Sep 7, 2026
1 check passed
@rahlk
rahlk deleted the fix/issue-177-declaration-merging branch September 7, 2026 22:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Declaration merging mints one can:// id for a value and a type of the same name; the Neo4j MERGE collapses them into one node

1 participant