Skip to content

feat(ids): a module's can:// id names the module, not where it was checked out - #270

Open
rahlk wants to merge 1 commit into
mainfrom
feat/manifest-derived-module-prefix
Open

rahlk wants to merge 1 commit into
mainfrom
feat/manifest-derived-module-prefix

Conversation

@rahlk

@rahlk rahlk commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Spec: 2026-09-17-manifest-derived-module-prefix.md · Tracking: codellm-devkit/.github#92 · Spec review: codellm-devkit/.github#91

A module id gains a segment naming the module as its own manifest declares it, between the language segment and the path within that module:

canjv -i /path/to/daytrader-web-service --app-name daytrader

before  can://daytrader/java/src/main/java/org/apache/.../RunStatsDataBean.java
after   can://daytrader/java/daytrader-web-service/src/main/java/org/apache/.../RunStatsDataBean.java

No staging directory, no -f, and no pointing --input at a parent that sweeps in sibling repos.

The coordinate comes from the manifest rather than the directory name so it survives a rename, a relocated checkout, or a differently-laid-out CI workspace. The gate fixture demonstrates the difference: its directory is mvnw-working-test but its pom declares my-javaee-mvn, and the id carries the latter.

Two rules that are load-bearing, not incidental

The symbol_table key does not change. It stays the real --input-relative path. Uniqueness is the key's job and a path is unique by construction where an artifactId is not; with no path field on the module object, the key is also the only thing that still locates a file on disk. Consequence: the key is no longer the tail of the id. L3WalaOverlays recovered the application id by stripping the key off a module id, so it is now passed the real one and that derivation is deleted.

A contested coordinate is applied to neither claimant. The id is the join key for call_graph endpoints, so a coordinate claimed by two module directories would merge two distinct callables onto one node. Vendored duplicates are exactly where this bites — several services each building one shared internal library under the same module name. Verified:

[WARN] Module coordinate 'daytrader-core' is declared by 2 modules (svc-a/lib, svc-b/lib);
       ids for these keep their plain relative path, since a shared coordinate would collide them

can://dt/java/svc-a/lib/src/main/java/com/shared/Bean.java
can://dt/java/svc-b/lib/src/main/java/com/shared/Bean.java

Without that rule the change would regress "ids are distinct by construction" — the property it was chosen for.

A module with no readable coordinate keeps exactly the id it had before, so a loose source tree is unaffected: can://bare/java/src/main/java/com/foo/Bare.java.

Tests

16 new, written failing first: ModuleCoordinatesTest (7), ModulePrefixesTest (7), L1BuildContextTest (2). They pin the parent/dependency artifactId distinction, the DOCTYPE refusal, nearest-enclosing-manifest for reactor children, the --input search bound, the contested-coordinate fallback, and the key/id separation.

L3WalaOverlaysTest is deleted — its only test reflected into the derivation this PR removes. L2CallGraphGateTest's expected ids are updated; that fixture declares rootProject.name = 'call-graph-test', so it is also a live example of the documented app-name/coordinate redundancy (can://call-graph-test/java/call-graph-test/...), which the spec §5 accepts rather than special-cases.

Verification

Gate Result
./gradlew test 619 tests, 1 failed — the pre-existing CodeAnalyzerIntegrationTest Testcontainers/Docker initializationError
Schema conformance -a 1/2/3/4 all validate against analysis.v2.schema.json
Monotonicity json(-a 1) ⊆ (-a 2) ⊆ (-a 3) ⊆ (-a 4)
Determinism two -a 4 --eager runs byte-identical

Reviewer note: the warning is invisible by default

Log.toConsole gates every level behind isVerbose() (Log.java:128), and --verbose defaults to false, so without -v the analyzer prints no WARN and no ERROR at all. The contested-coordinate warning above only appears under -v.

That is pre-existing and wider than this PR — it also makes the README's "it degrades and says so on stderr" guidance false by default — but it means this PR's warning does not currently reach anyone who does not already pass -v. Worth fixing separately; flagging it rather than silently relying on a warning nobody sees.

Does not fix

#269 — a single analysis spanning source roots that declare the same fully-qualified type binds every caller to the last root, silently. This makes a bundled tree addressable, not correctly resolved.

…ecked out

A module id gains a segment naming the module as its own manifest declares
it -- Maven's <artifactId>, Gradle's rootProject.name -- between the
language segment and the path within that module:

    can://daytrader/java/daytrader-web-service/src/main/java/.../Bean.java

So pointing --input at one service repo names that service in the id, with
no staging directory and without pointing --input at a parent that sweeps
in sibling repos. The coordinate is read from the manifest rather than
taken from the directory name because a declared name survives a rename, a
relocated checkout and a differently-laid-out CI workspace, where a
path-derived segment would make every id a function of where the tree
happens to sit on disk.

The symbol_table key does NOT change. It stays the real --input-relative
path: uniqueness is the key's job, a path is unique by construction where a
coordinate is not, and with no path field on the module object the key is
also the only thing that still locates a file on disk. The key is therefore
no longer the tail of the id, and nothing may assume it is --
L3WalaOverlays recovered the application id by stripping the key off a
module id, so it is passed the real one instead.

The same non-uniqueness reaches the id, which is the join key for
call_graph endpoints. A coordinate claimed by two module directories is
applied to neither, with a warning naming the competitors; vendored
duplicates are exactly where this bites, and applying the prefix there
would merge two distinct callables onto one node. Without that rule the
change would regress "ids are distinct by construction", the property it
was chosen for.

A module with no readable coordinate keeps precisely the id it had before,
so a loose source tree is unaffected.

Spec: codellm-devkit/.github docs/design/specs/2026-09-17-manifest-derived-module-prefix.md
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.

1 participant