@descryy/adapter-java 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/extract.js CHANGED
@@ -1,31 +1,22 @@
1
1
  /**
2
2
  * Java units to Canonical IR.
3
3
  *
4
- * ## Why a breadth adapter reaches further in Java than in most languages
5
- *
6
4
  * Every import is explicit and fully qualified, every field and parameter
7
- * carries a written type, and a public type's simple name is its file's name.
8
- * None of that needs a type checker to read, so `repo.findById(id)` resolves
9
- * from the declaration `private final OwnerRepository repo` alone — a call
10
- * through a receiver, which in TypeScript needed R3.
11
- *
12
- * That is read as **R2 evidence, and never R3**. R2 is a reference resolved to a
13
- * specific definition, which is exactly what happens here — the level names the
14
- * evidence, not the machinery, and Java hands over the same evidence an LSP
15
- * would without one being run. R3 is a *checked shape*, and a written annotation
16
- * is not one: it can be shadowed, it can be generic, and nothing here verifies
17
- * which overload the compiler would select. Claiming R3 for a name-level fact is
18
- * the confusion DEC-058 was written about, where 83% of an adapter's edges
19
- * claimed a level they had not earned.
20
- *
21
- * `IMPORTS` stays at R1. An import is module resolution and nothing more.
5
+ * carries a written type, so `repo.findById(id)` resolves from the declaration
6
+ * `private final OwnerRepository repo` alone — a call through a receiver,
7
+ * which in TypeScript needed R3.
22
8
  *
23
- * ## What is deliberately not resolved
9
+ * That is **R2 evidence, never R3**: a reference resolved to a specific
10
+ * definition. R3 is a *checked shape*, and a written annotation is not one —
11
+ * it can be shadowed, be generic, and nothing here verifies overload
12
+ * selection. Claiming R3 for a name-level fact is the confusion DEC-058 was
13
+ * written about (83% of an adapter's edges once claimed an unearned level).
14
+ * `IMPORTS` stays at R1 — module resolution, nothing more.
24
15
  *
25
- * `var` declarations, chained calls whose receiver is another call, generic type
26
- * variables, and any name reachable only through a wildcard import that matches
27
- * more than one analysed type. Each goes to the ledger with its reason. A
28
- * missing edge is a disclosed gap; a guessed one corrupts every layer above.
16
+ * Deliberately unresolved: `var` declarations, chained calls whose receiver is
17
+ * another call, generic type variables, and names reachable only through an
18
+ * ambiguous wildcard import. Each goes to the ledger with its reason — a
19
+ * missing edge is a disclosed gap, a guessed one corrupts every layer above.
29
20
  */
30
21
  import { edgeId, endpointQsp, nodeId, normaliseEndpointPath, symbolQsp, testCaseQsp, } from "@descryy/ir";
31
22
  import { jpaModels } from "./jpa.js";
@@ -38,14 +29,11 @@ const CONFIDENCE = { 0: 0.5, 1: 0.7, 2: 0.9, 3: 0.95 };
38
29
  * so it cannot collide with one. */
39
30
  export const DEFAULT_PACKAGE = "(default)";
40
31
  const REASONS = {
41
- // The final sentence used to read "A scope boundary, not an analysis gap."
42
- // Measured on `okhttp`: of the 458 `outside` entries naming a qualified type,
43
- // **238 are declared in a `.kt` file in the same repository** — `OkHttpClient`,
44
- // `Response`, `RequestBody`. They are not a scope boundary; they are a
45
- // language this run cannot read, which is `CLAUDE.md` rule 6's *fourth*
46
- // category. The adapter cannot tell the two apart — deciding needs a
47
- // repo-wide, cross-adapter declaration index it is not given — so it must
48
- // stop claiming it can. `bench/cross-language-census.mjs`.
32
+ // Measured on `okhttp`: of 458 `outside` entries naming a qualified type,
33
+ // 238 are declared in a `.kt` file in the same repo — not a scope boundary
34
+ // but an unreadable-language case (rule 6's fourth category). The adapter
35
+ // can't tell the two apart without a cross-adapter declaration index it
36
+ // isn't given. `bench/cross-language-census.mjs`.
49
37
  outside: "names a type this run did not analyse. Either a scope boundary — the JDK, a dependency " +
50
38
  "on the classpath, a source root outside the analysed set — or a declaration this run " +
51
39
  "could not read, in another language in this same repository. This adapter sees only " +
@@ -69,10 +57,10 @@ const REASONS = {
69
57
  /**
70
58
  * The qualified symbol path for a Java symbol.
71
59
  *
72
- * The Java package *is* the namespace, so no anchor has to be inferred the way a
73
- * TypeScript package root does — DEC-047's entire problem does not exist here,
74
- * and neither does DEC-077's duplicate-anchor collision. The nesting path is
75
- * everything after the package: `Outer.Inner.method`.
60
+ * The Java package *is* the namespace, so no anchor has to be inferred the way
61
+ * a TypeScript package root does — DEC-047's and DEC-077's problems don't
62
+ * exist here. The nesting path is everything after the package:
63
+ * `Outer.Inner.method`.
76
64
  */
77
65
  function qspFor(fqn, unit) {
78
66
  const nested = unit.packageName !== "" && fqn.startsWith(`${unit.packageName}.`)
@@ -83,13 +71,11 @@ function qspFor(fqn, unit) {
83
71
  /**
84
72
  * Whether a unit's identity anchor names a test source set.
85
73
  *
86
- * `identityPackage` is `module/sourceSet/package` (`roots.ts`), so the source
87
- * set is the second segment when there are three. A unit with no `src/` split
88
- * has two segments and no source set, and returns false rather than guessing.
89
- *
90
- * `test`, `testFixtures` and `integrationTest` are the names Maven and Gradle
91
- * declare, not a convention read off a path — the distinction `roots.ts`'s
92
- * header exists for.
74
+ * `identityPackage` is `module/sourceSet/package` (`roots.ts`) — the source
75
+ * set is the second segment when there are three; a unit with no `src/` split
76
+ * has two segments and returns false rather than guessing. `test`,
77
+ * `testFixtures`, `integrationTest` are names Maven/Gradle declare, not a
78
+ * path convention.
93
79
  */
94
80
  function isTestSourceSet(identityPackage) {
95
81
  const segments = identityPackage.split("/");
@@ -97,10 +83,8 @@ function isTestSourceSet(identityPackage) {
97
83
  return false;
98
84
  return /^(test|.*[Tt]est.*)$/.test(segments[1] ?? "");
99
85
  }
100
- /**
101
- * Choose among the candidates a scope produced, preferring the analysed unit's
102
- * own identity package. Two survivors is genuinely ambiguous and is refused.
103
- */
86
+ /** Choose among a scope's candidates, preferring the unit's own identity
87
+ * package. Two survivors is genuinely ambiguous and is refused. */
104
88
  function pick(from, candidates) {
105
89
  if (candidates === undefined || candidates.length === 0)
106
90
  return { reason: REASONS.outside };
@@ -114,29 +98,23 @@ function pick(from, candidates) {
114
98
  /**
115
99
  * The scopes a written type name is searched in, in the order Java searches them.
116
100
  *
117
- * **Shared rather than written twice, and that is the point of pulling it out.**
118
- * Two consumers now resolve a supertype name: the `INHERITS`/`IMPLEMENTS` edges,
119
- * and JPA's `@MappedSuperclass` walk. A second copy of these rules would drift,
120
- * and the failure it drifts into is not symmetric — an edge resolved by one rule
121
- * and a column by another puts a **column on the wrong table**, which is a wrong
122
- * answer rather than a missing one.
101
+ * Shared rather than duplicated: both the `INHERITS`/`IMPLEMENTS` edges and
102
+ * JPA's `@MappedSuperclass` walk resolve a supertype name this way, and a
103
+ * second copy would drift — putting a column on the wrong table, not just a
104
+ * missing edge.
123
105
  *
124
- * `decides` marks a scope that settles the question even when it fails to
125
- * resolve. An explicit single-type import is unambiguous by language rule, so a
126
- * name it fails to resolve is not one a later scope may claim. Only the
127
- * same-unit scope falls through, because a file may declare several types and
128
- * only one of them can be the match.
106
+ * `decides` marks a scope that settles the question even on failure to
107
+ * resolve — an explicit single-type import is unambiguous by language rule,
108
+ * so a name it fails to resolve is not one a later scope may claim. Only the
109
+ * same-unit scope falls through, since a file may declare several types.
129
110
  */
130
111
  function scopesFor(index, unit, written) {
131
- // A dotted name is one of two things and they are not distinguishable by
132
- // spelling: `com.example.Order` is already fully qualified and needs no scope,
133
- // `DiskLruCache.Snapshot` is a nested type named through its enclosing type
134
- // and needs the *enclosing* name resolved first. Try the whole path as
135
- // written, then resolve the head and re-attach the tail.
136
- //
137
- // Only the whole path used to be tried, so every reference to a repo-declared
138
- // nested type was lost — 28 across the four corpora, `mall`'s `Criteria` ×19.
139
- // `decides: true` on that single scope is what made it terminal.
112
+ // A dotted name is either already fully qualified (`com.example.Order`, no
113
+ // scope needed) or a nested type named through its enclosing type
114
+ // (`DiskLruCache.Snapshot`, enclosing name resolved first) — not
115
+ // distinguishable by spelling, so try the whole path, then head+tail.
116
+ // Only the whole path used to be tried, dropping 28 nested-type references
117
+ // across the corpora (`mall`'s `Criteria` x19).
140
118
  if (written.includes(".")) {
141
119
  const asWritten = index.get(written);
142
120
  if (asWritten !== undefined)
@@ -144,9 +122,8 @@ function scopesFor(index, unit, written) {
144
122
  const cut = written.lastIndexOf(".");
145
123
  const head = written.slice(0, cut);
146
124
  const tail = written.slice(cut + 1);
147
- // The head is itself resolved by the normal rules — it may be an import, a
148
- // same-package type, or another nested name — so this recurses rather than
149
- // reimplementing the scope order a second time and letting the two drift.
125
+ // The head resolves by the normal rules (import, same-package, nested) —
126
+ // recurse rather than reimplementing the scope order and letting it drift.
150
127
  const ordered = [];
151
128
  for (const scope of scopesFor(index, unit, head)) {
152
129
  for (const candidate of scope.candidates) {
@@ -159,8 +136,8 @@ function scopesFor(index, unit, written) {
159
136
  ordered.push({ candidates: nested, decides: true });
160
137
  }
161
138
  }
162
- // Nothing found is a refusal, not a fallback to the bare tail. Resolving
163
- // `A.B` as `B` is exactly the dropped qualifier this branch exists to stop.
139
+ // A refusal, not a fallback to the bare tail — resolving `A.B` as `B`
140
+ // is the dropped qualifier this branch exists to stop.
164
141
  return ordered.length > 0 ? ordered : [{ candidates: [], decides: true }];
165
142
  }
166
143
  const ordered = [];
@@ -182,9 +159,9 @@ function scopesFor(index, unit, written) {
182
159
  ordered.push({ candidates: inPackage, decides: true });
183
160
  return ordered;
184
161
  }
185
- // 4. Wildcard imports. Accepted only when exactly one analysed type matches:
186
- // two candidates is genuinely ambiguous in Java too, and picking one would
187
- // be a coin toss recorded as a fact.
162
+ // 4. Wildcard imports. Accepted only when exactly one analysed type matches —
163
+ // two candidates is genuinely ambiguous, and picking one is a coin toss
164
+ // recorded as fact.
188
165
  const candidates = unit.wildcardImports.flatMap((wildcard) => [...(index.get(`${wildcard}.${written}`) ?? [])]);
189
166
  if (candidates.length > 0)
190
167
  ordered.push({ candidates, decides: true });
@@ -200,13 +177,10 @@ export function extract(input) {
200
177
  const clientCallSites = [];
201
178
  /**
202
179
  * Returns `false` when the edge's evidence outranks the level this run
203
- * reached, so the caller can disclose the reference instead of dropping it.
204
- *
205
- * An edge may not claim a level the run did not reach — the Normaliser rejects
206
- * it, and rightly: a run capped at R0 that emits R2 edges is claiming
207
- * resolution it did not perform. Capping the number instead would be worse,
208
- * because the edge would survive wearing a level its evidence never earned,
209
- * which is DEC-058's defect exactly.
180
+ * reached, so the caller can disclose it instead of dropping it silently.
181
+ * An edge may not claim an unreached level — the Normaliser rejects it, and
182
+ * capping the number instead would let the edge survive wearing a level its
183
+ * evidence never earned (DEC-058's defect exactly).
210
184
  */
211
185
  const push = (edge, level) => {
212
186
  if (level > input.reached)
@@ -241,18 +215,14 @@ export function extract(input) {
241
215
  ...(attrs === undefined ? {} : { attrs }),
242
216
  });
243
217
  };
244
- // -------------------------------------------------------------------------
245
- // Nodes
246
- // -------------------------------------------------------------------------
247
218
  /**
248
219
  * Fully-qualified Java name -> every analysed type that declares it.
249
220
  *
250
- * A list, not a single entry, and that is the whole lesson of the airbyte run:
251
- * `io.airbyte.integrations.destination.redis.RedisDataFactory` is declared
252
- * twice, in two source sets that never share a classpath. A `Map<string,
253
- * Declared>` here silently keeps the last one and resolves every reference in
254
- * both source sets to it — a wrong edge that no recall measurement can see,
255
- * because the name was found.
221
+ * A list, not a single entry: the airbyte run declares
222
+ * `...RedisDataFactory` twice in two source sets that never share a
223
+ * classpath. A single-entry map would silently keep the last one and
224
+ * resolve both source sets to it — a wrong edge no recall measurement
225
+ * can catch, since the name was found.
256
226
  */
257
227
  const byFqn = new Map();
258
228
  const byMember = new Map();
@@ -260,16 +230,13 @@ export function extract(input) {
260
230
  /** Simple name -> every analysed fully-qualified name that ends with it. */
261
231
  const bySimpleName = new Map();
262
232
  /**
263
- * Every declared type, indexed before any node id exists — and it has to be.
264
- *
265
- * A JPA entity is a `MODEL` rather than a `CLASS`, and the id hashes the kind
266
- * (DEC-004), so the kind must be settled before the id is minted. But settling
267
- * it now needs `@MappedSuperclass` resolution *across* compilation units, and
268
- * that needs an index. `byFqn` cannot be it, because `byFqn` holds ids.
269
- *
270
- * So the types are indexed twice, once without ids for the entity decision and
271
- * once with them for the edges. The alternative was minting a `CLASS` id and
272
- * rewriting it, which is the exact thing DEC-004 forbids.
233
+ * Every declared type, indexed before any node id exists. A JPA entity is a
234
+ * `MODEL` not a `CLASS`, and the id hashes the kind (DEC-004) — so the kind
235
+ * must be settled before minting, but that needs `@MappedSuperclass`
236
+ * resolution across compilation units, which needs an index `byFqn` can't
237
+ * be (it holds ids). So types are indexed twice: once without ids for the
238
+ * entity decision, once with them for edges — the alternative, minting then
239
+ * rewriting a `CLASS` id, is exactly what DEC-004 forbids.
273
240
  */
274
241
  const typesByFqn = new Map();
275
242
  for (const unit of input.units) {
@@ -287,9 +254,8 @@ export function extract(input) {
287
254
  const found = pick(unit, scope.candidates);
288
255
  if ("declared" in found)
289
256
  return found.declared;
290
- // An ambiguous supertype is refused rather than guessed: two candidates
291
- // means two different tables, and picking one puts real columns on the
292
- // wrong one.
257
+ // Ambiguous supertype refused rather than guessed — two candidates means
258
+ // two different tables, and picking one puts real columns on the wrong one.
293
259
  if (scope.decides)
294
260
  return undefined;
295
261
  }
@@ -297,12 +263,10 @@ export function extract(input) {
297
263
  };
298
264
  /**
299
265
  * The entity map for the whole batch, resolved once before any id is minted.
300
- *
301
- * Keyed by `identityPackage::fqn` rather than by `fqn` alone, because a
302
- * fully-qualified Java name is **not** unique across source sets — the airbyte
303
- * duplicate that this file already records for `byFqn`. Keying on the fqn here
304
- * would let a `@Entity` in one source set turn its namesake in another into a
305
- * MODEL it never was.
266
+ * Keyed by `identityPackage::fqn`, not `fqn` alone — a fqn isn't unique
267
+ * across source sets (the same airbyte duplicate `byFqn` records), and
268
+ * keying on fqn alone would let an `@Entity` in one source set turn its
269
+ * namesake in another into a MODEL it never was.
306
270
  */
307
271
  const entitiesByType = new Map();
308
272
  for (const unit of input.units) {
@@ -312,19 +276,14 @@ export function extract(input) {
312
276
  }
313
277
  for (const unit of input.units) {
314
278
  const pkg = unit.identityPackage;
315
- // The compilation unit is named by its *primary* type, which Java requires
316
- // to match the file name. That is a symbol path, not a file path — DEC-004's
317
- // rule is satisfied, and a moved file keeps its identity.
318
- //
319
- // `types[0]` was not that type. Nested declarations are appended before the
320
- // type enclosing them, so a file whose outer class holds a private interface
321
- // took its identity from the interface: `TeradataJdbcSourceAcceptanceTest.java`
322
- // became the module `SqlConsumer`, a nested private interface on line 123.
323
- // Two such files in one package would then share a module id.
324
- //
325
- // The stem is preferred because the language guarantees it names the public
326
- // type; a top-level declaration is the fallback for a file with none, and the
327
- // stem stands alone only when the file declares no top-level type at all.
279
+ // Named by its *primary* type, which Java requires to match the file name
280
+ // — a symbol path, not a file path, so a moved file keeps its identity
281
+ // (DEC-004). `types[0]` was not that type: nested declarations are
282
+ // appended before their enclosing type, so a file whose outer class held
283
+ // a private interface took its identity from the interface instead
284
+ // (`TeradataJdbcSourceAcceptanceTest.java` became module `SqlConsumer`).
285
+ // The stem is preferred since the language guarantees it names the public
286
+ // type; a top-level declaration is the fallback when the file has none.
328
287
  const stem = unit.file.slice(unit.file.lastIndexOf("/") + 1).replace(/\.java$/, "");
329
288
  const topLevel = unit.types.filter((each) => unit.packageName === ""
330
289
  ? !each.fqn.includes(".")
@@ -337,12 +296,11 @@ export function extract(input) {
337
296
  nodes.push({
338
297
  id: moduleId,
339
298
  type: "MODULE",
340
- // The compilation unit is *named* with its extension and *identified*
341
- // without one. Java requires a public type's simple name to equal its file
342
- // name, so a bare `Status` is the name of both the module and the class it
343
- // holds — and a corpus role bound on `(file, name)` then matches two nodes
344
- // and correctly refuses to guess between them. `Status.java` is what a Java
345
- // developer calls the unit and can never collide with a type.
299
+ // Named with its extension, identified without one — Java requires a
300
+ // public type's simple name to equal its file name, so a bare `Status`
301
+ // names both the module and its class, and a `(file, name)`-bound corpus
302
+ // role correctly refuses to guess between them. `Status.java` can never
303
+ // collide with a type name.
346
304
  name: `${unitName}.java`,
347
305
  file: unit.file,
348
306
  range: { startLine: 1, endLine: 1 },
@@ -351,11 +309,9 @@ export function extract(input) {
351
309
  resolution: 0,
352
310
  attrs: unit.hasError ? { parsedWithErrors: true } : {},
353
311
  });
354
- // A JPA entity is a MODEL rather than a CLASS, and the node id hashes the
355
- // kind (DEC-004) — so the kind has to be decided BEFORE the id is minted,
356
- // not patched onto the node afterwards. Everything downstream reads the id
357
- // out of `byFqn`, so one derivation keeps them in step. Resolved for the
358
- // whole batch above, because inherited mappings cross compilation units.
312
+ // Kind decided BEFORE the id is minted (DEC-004), not patched on after —
313
+ // everything downstream reads the id out of `byFqn`. Resolved for the
314
+ // whole batch above, since inherited mappings cross compilation units.
359
315
  for (const type of unit.types) {
360
316
  const model = entitiesByType.get(`${unit.identityPackage}::${type.fqn}`);
361
317
  const kind = model === undefined ? "CLASS" : "MODEL";
@@ -375,10 +331,10 @@ export function extract(input) {
375
331
  attrs["orm"] = "jpa";
376
332
  if (model.table !== null)
377
333
  attrs["table"] = model.table;
378
- // **Not gated on `input.reached`, and the exception is the point.** A
379
- // mapped column is a declaration the framework itself enforces, not an
380
- // inference, so its resolution is the evidence's rather than the run's
381
- // (DEC-058) — the same reasoning adapter-python records for SQLAlchemy.
334
+ // Not gated on `input.reached`, deliberately — a mapped column is a
335
+ // declaration the framework enforces, not an inference, so its
336
+ // resolution is the evidence's rather than the run's (DEC-058), same
337
+ // as adapter-python's SQLAlchemy reasoning.
382
338
  if (model.fields.length > 0)
383
339
  attrs["fields"] = model.fields;
384
340
  if (model.undecided.length > 0)
@@ -396,18 +352,13 @@ export function extract(input) {
396
352
  attrs,
397
353
  });
398
354
  /**
399
- * Overloads are one node, and the count is disclosed rather than hidden.
400
- *
401
- * A qualified symbol path carries no parameter types (DEC-011), so
402
- * `getPet(String)` and `getPet(Integer)` name one symbol — and on
403
- * `spring-petclinic` that produced three nodes sharing one id before this
404
- * existed. Encoding parameters into the path would fix the collision by
405
- * making Java's identity rule different from every other language's, which
406
- * is the leak the IR boundary exists to prevent.
407
- *
408
- * What is lost is real and is stated: an edge into an overload set does not
409
- * say which overload. That is a name-level fact, and DEC-058's rule caps it
410
- * at the level its evidence earned.
355
+ * Overloads are one node, count disclosed rather than hidden. A
356
+ * qualified symbol path carries no parameter types (DEC-011), so
357
+ * `getPet(String)`/`getPet(Integer)` name one symbol — encoding
358
+ * parameters into the path would fix the collision by making Java's
359
+ * identity rule leak language specifics above the IR boundary. What's
360
+ * lost: an edge into an overload set doesn't say which overload — a
361
+ * name-level fact, capped by DEC-058's rule at the level it earned.
411
362
  */
412
363
  const overloads = new Map();
413
364
  for (const method of type.methods) {
@@ -419,9 +370,8 @@ export function extract(input) {
419
370
  }
420
371
  for (const [name, group] of overloads) {
421
372
  const first = group[0];
422
- // A `@Test` method is a test case, not a function that happens to be
423
- // tested. Emitting both would put two nodes on one declaration and let a
424
- // coverage query count it twice.
373
+ // A `@Test` method is a test case, not a tested function — emitting
374
+ // both would double the node and let a coverage query count it twice.
425
375
  const isCase = group.some((each) => each.isTest);
426
376
  const memberId = isCase
427
377
  ? nodeId(scope, "TEST_CASE", testCaseQsp(unit.identityPackage, [type.simpleName], name), LANGUAGE)
@@ -431,27 +381,19 @@ export function extract(input) {
431
381
  method: first,
432
382
  ownerFqn: type.fqn,
433
383
  });
434
- // DEC-248: a CALLS edge out of this node can originate from *any*
435
- // overload's body — `emitCall` below resolves a ref against its own
436
- // `method` (the specific overload), not `member.method`, precisely so a
437
- // ref found inside a later overload's body is attributed correctly to
438
- // the shared symbol. A range pinned to `first`'s lines alone silently
439
- // excludes every overload but the first, so an edge attributed to this
440
- // node can point outside its own declared range — `spring-petclinic`'s
441
- // `Owner.getPet(String)` was credited with a call that only exists in
442
- // the neighbouring `getPet(Integer)` overload. The range now spans every
443
- // overload in the group, so it always contains whichever one is the
444
- // true source of an edge attributed to this node.
384
+ // DEC-248: a CALLS edge can originate from *any* overload's body —
385
+ // `emitCall` resolves against the specific overload, not
386
+ // `member.method` — so the range must span every overload, not just
387
+ // `first`'s lines, or an edge can point outside its own node's range
388
+ // (`spring-petclinic`'s `Owner.getPet(String)` was once credited with
389
+ // a call only in the neighbouring `getPet(Integer)`).
445
390
  const startLine = Math.min(...group.map((each) => each.startLine));
446
391
  const endLine = Math.max(...group.map((each) => each.endLine));
447
- // Widening is only sound when the group's own declarations are what
448
- // fill the span. Java does not require overloads — constructors above
449
- // all — to sit next to each other, so the widened range can swallow an
450
- // unrelated field or method declared in the gap between two of them.
451
- // `language-problems.md` §3's row 5 named this a latent risk on top of
452
- // the widening fix above; disclosed here rather than silently letting
453
- // a change to that unrelated member attribute to this node in diff
454
- // scoping.
392
+ // Widening is sound only when the group's own declarations fill the
393
+ // span — Java doesn't require overloads to sit adjacent, so the
394
+ // widened range can swallow an unrelated member in the gap
395
+ // (`language-problems.md` §3 row 5). Disclosed rather than letting a
396
+ // change to that member misattribute in diff scoping.
455
397
  const groupMembers = new Set(group);
456
398
  const includesOtherMember = group.length > 1 &&
457
399
  (type.methods.some((each) => !groupMembers.has(each) && each.startLine > startLine && each.startLine < endLine) ||
@@ -473,20 +415,14 @@ export function extract(input) {
473
415
  }
474
416
  }
475
417
  }
476
- // -------------------------------------------------------------------------
477
- // Name resolution
478
- // -------------------------------------------------------------------------
479
418
  /**
480
- * Pick the declaration of a fully-qualified name that the referring unit can
481
- * actually see.
419
+ * Resolve a simple or qualified type name as written inside one unit.
482
420
  *
483
- * Java's own rule is the classpath, which this adapter does not read. The
484
- * approximation is the strictest one that cannot be wrong in the common case:
485
- * a declaration in the same module and source set wins outright, one candidate
486
- * overall is taken, and a genuine tie between two scopes is refused rather
487
- * than broken arbitrarily.
421
+ * Java's own rule is the classpath, which this adapter doesn't read. The
422
+ * approximation: a declaration in the same module/source set wins outright,
423
+ * one candidate overall is taken, and a genuine tie is refused rather than
424
+ * broken arbitrarily.
488
425
  */
489
- /** Resolve a simple or qualified type name as written inside one unit. */
490
426
  const resolveType = (unit, written) => {
491
427
  for (const scope of scopesFor(byFqn, unit, written)) {
492
428
  const found = pick(unit, scope.candidates);
@@ -500,22 +436,17 @@ export function extract(input) {
500
436
  /**
501
437
  * The declared type of a field, following the scopes Java says are in scope.
502
438
  *
503
- * Two of them were missing, and both were found by the first recall
504
- * measurement this project has taken rather than by any precision sample —
505
- * a missing edge leaves nothing behind for a precision draw to land on.
506
- *
507
- * `viaThis` distinguishes `this.pet` from a bare `pet`. `this` names the
508
- * instance, so it reaches the type's own fields and inherited ones and stops;
509
- * a bare name additionally sees the *lexically enclosing* class's fields,
510
- * which is how a `@Nested` test class reads the fields of the class holding it.
511
- * Collapsing the two would resolve `this.x` against an outer class it cannot
512
- * legally reach.
439
+ * `viaThis` distinguishes `this.pet` from a bare `pet`: `this` reaches only
440
+ * the type's own and inherited fields; a bare name additionally sees the
441
+ * *lexically enclosing* class's fields (how a `@Nested` test class reads its
442
+ * holder's fields). Collapsing the two would resolve `this.x` against an
443
+ * outer class it can't legally reach.
513
444
  */
514
445
  const fieldTypeOf = (owner, name, viaThis) => {
515
446
  const search = [owner];
516
447
  const seen = new Set();
517
448
  if (!viaThis) {
518
- // Lexically enclosing types, innermost first: `pkg.Outer.Inner` -> `pkg.Outer`.
449
+ // Enclosing types, innermost first: `pkg.Outer.Inner` -> `pkg.Outer`.
519
450
  let fqn = owner.type.fqn;
520
451
  for (;;) {
521
452
  const cut = fqn.lastIndexOf(".");
@@ -567,14 +498,11 @@ export function extract(input) {
567
498
  }
568
499
  return undefined;
569
500
  };
570
- // -------------------------------------------------------------------------
571
- // Edges
572
- // -------------------------------------------------------------------------
573
501
  for (const unit of input.units) {
574
502
  const moduleId = moduleIdOf.get(unit.file);
575
503
  if (moduleId === undefined)
576
504
  continue;
577
- // IMPORTS — module to module. A wildcard import names a package and not a
505
+ // IMPORTS — module to module. A wildcard import names a package, not a
578
506
  // compilation unit, so it has nothing to point at and is disclosed instead.
579
507
  for (const each of unit.importLines) {
580
508
  if (each.wildcard) {
@@ -616,20 +544,19 @@ export function extract(input) {
616
544
  const member = byMember.get(`${unit.identityPackage}::${method.fqn}`);
617
545
  if (member === undefined)
618
546
  continue;
619
- // The caller's half of the HTTP boundary. Collected here, where the
620
- // enclosing FUNCTION node and the receiver scopes are both in hand, and
621
- // emitted with the routes below so both halves mint one `API_ENDPOINT`.
547
+ // The caller's half of the HTTP boundary, collected here (enclosing
548
+ // FUNCTION node and receiver scopes both in hand) and emitted with
549
+ // the routes below so both halves mint one `API_ENDPOINT`.
622
550
  {
623
551
  const read = clientCalls(method, member.id, (name) => writtenReceiverType(method, declared, name),
624
- // `identityPackage` is `module/sourceSet/package`, so the second
625
- // segment is the source set the build declared — read from the
626
- // anchor rather than pattern-matched off the file path.
552
+ // Source set read from the identity-package anchor, not pattern-
553
+ // matched off the file path.
627
554
  isTestSourceSet(unit.identityPackage));
628
555
  clientCallSites.push(...read.calls);
629
556
  for (const refusal of read.refusals) {
630
557
  ledger(refusal.fromId, "USES_API", refusal.raw, { file: unit.file, line: refusal.line }, refusal.reason,
631
- // Unset `refusalClass` stays legal and means unclassified — DEC-242 — so
632
- // `attrs` is omitted entirely rather than sent with an `undefined` field.
558
+ // Unset `refusalClass` means unclassified (DEC-242) — `attrs`
559
+ // omitted entirely rather than sent with an `undefined` field.
633
560
  refusal.refusalClass === undefined
634
561
  ? undefined
635
562
  : {
@@ -646,20 +573,18 @@ export function extract(input) {
646
573
  }
647
574
  if (ref.kind === "member") {
648
575
  // A dotted access whose receiver is a declared variable is a field
649
- // read, not a type reference. Nothing is emitted and nothing is
650
- // filed: the ledger records references that *could* have been edges,
651
- // and this one was never a candidate.
576
+ // read, not a type reference — never a candidate edge, so nothing
577
+ // is emitted or filed.
652
578
  if (method.locals.has(ref.name) || type.fields.has(ref.name))
653
579
  continue;
654
580
  emitTypeRef(member.id, unit, ref);
655
581
  continue;
656
582
  }
657
- // `method`, not `member.method`. An overload set shares one node, and
658
- // the node carries the *first* overload — so resolving a receiver
659
- // against `member.method.locals` would type overload A's variables
660
- // with overload B's declarations. Same name, different parameter,
661
- // different type: a wrong edge, and one no recall measurement could
662
- // ever see.
583
+ // `method`, not `member.method` — an overload set's node carries
584
+ // only the *first* overload, so resolving against
585
+ // `member.method.locals` would type overload A's variables with
586
+ // overload B's declarations: a wrong edge no recall measurement
587
+ // could see.
663
588
  emitCall(member, method, declared, unit, ref);
664
589
  }
665
590
  }
@@ -676,10 +601,10 @@ export function extract(input) {
676
601
  ledger(from, "USES_TYPE", ref.name, file(unit, ref), resolved.reason);
677
602
  }
678
603
  function emitCall(member, method, owner, unit, ref) {
679
- // A coverage relation is a claim about a target. An unresolved reference has
680
- // no target to make it about, so it is filed as the call it plainly is —
681
- // the TypeScript adapter learned this by putting 21,223 assertion calls into
682
- // its own test-coverage denominator.
604
+ // A coverage relation is a claim about a target; an unresolved reference
605
+ // has none, so it's filed as the call it plainly is — the TypeScript
606
+ // adapter learned this the hard way (21,223 assertion calls once counted
607
+ // in its test-coverage denominator).
683
608
  const resolvedType = method.isTest ? "TESTS" : "CALLS";
684
609
  const target = receiverTarget(method, owner, unit, ref);
685
610
  if ("reason" in target) {
@@ -688,9 +613,8 @@ export function extract(input) {
688
613
  }
689
614
  let found = findMember(target.declared, ref.name);
690
615
  // An unqualified name may be bound by a static import rather than declared
691
- // on the enclosing type. The language guarantees that binding, so following
692
- // it is resolution and not inference — and without it every JUnit assertion
693
- // and every statically-imported helper is unresolvable.
616
+ // on the enclosing type — the language guarantees that binding, so
617
+ // following it is resolution, not inference.
694
618
  if (found === undefined && ref.receiver === undefined) {
695
619
  const owningType = unit.staticMemberImports.get(ref.name);
696
620
  if (owningType !== undefined) {
@@ -703,10 +627,9 @@ export function extract(input) {
703
627
  ledger(member.id, "CALLS", ref.raw, file(unit, ref), REASONS.unmodelled);
704
628
  return;
705
629
  }
706
- // The evidence is a reference resolved to a specific definition, which is
707
- // what R2 names — reached here through the language's own declarations
708
- // rather than through an LSP. DEC-058's rule is that the level describes the
709
- // evidence, not the machinery that produced it.
630
+ // A reference resolved to a specific definition is R2 evidence, reached
631
+ // here through the language's declarations rather than an LSP — DEC-058:
632
+ // the level describes the evidence, not the machinery.
710
633
  if (!push({ from: member.id, to: found.id, type: resolvedType }, 2)) {
711
634
  ledger(member.id, "CALLS", ref.raw, file(unit, ref), REASONS.belowLevel);
712
635
  }
@@ -716,17 +639,16 @@ export function extract(input) {
716
639
  const receiver = ref.receiver;
717
640
  if (receiver === undefined || receiver === "this")
718
641
  return { declared: owner };
719
- // `this.types.findPetTypes()` — the receiver is a field, named explicitly.
720
- // Left unhandled, `this.types` was looked up as though it were a variable
721
- // called "this.types" and then as a type of that name, and neither exists:
722
- // three of the four recall misses on `spring-petclinic` were this one idiom,
723
- // which is how every constructor-injected Spring field is written.
642
+ // `this.types.findPetTypes()` — an explicitly-named field receiver. Left
643
+ // unhandled, `this.types` resolved as neither a variable nor a type and
644
+ // was 3 of 4 recall misses on `spring-petclinic` — how every constructor-
645
+ // injected Spring field is written.
724
646
  const viaThis = receiver.startsWith("this.");
725
647
  const bare = viaThis ? receiver.slice("this.".length) : receiver;
726
648
  if (bare.includes("."))
727
649
  return { reason: REASONS.untypedReceiver };
728
- // A local or parameter shadows a field, exactly as the language says — but
729
- // `this.x` names the field regardless of what locals are in scope.
650
+ // A local/parameter shadows a field, as the language says — but `this.x`
651
+ // names the field regardless of locals in scope.
730
652
  if (!viaThis) {
731
653
  const local = method.locals.get(bare);
732
654
  if (local === null)
@@ -744,28 +666,23 @@ export function extract(input) {
744
666
  const asType = resolveType(unit, receiver);
745
667
  if ("declared" in asType)
746
668
  return asType;
747
- // Anything else is a chained call, a field of an unread supertype, or an
748
- // expression. None of them wrote a type down at this reference.
669
+ // Anything else — a chained call, a field of an unread supertype, an
670
+ // expression — wrote no type down at this reference.
749
671
  return { reason: /^[A-Z]/.test(receiver) ? asType.reason : REASONS.untypedReceiver };
750
672
  }
751
673
  /**
752
- * The receiver's type **as written**, without resolving it.
753
- *
754
- * Deliberately not `receiverTarget`. That function answers "which analysed
755
- * declaration does this receiver name", and every HTTP client type —
756
- * `RestTemplate`, `WebClient` — is a Spring class **outside the analysed
757
- * set**, so it correctly returns `outside` for all of them. The client reader
758
- * does not need a declaration; it needs the name the source wrote, to compare
759
- * against a closed set.
674
+ * The receiver's type **as written**, without resolving it. Deliberately
675
+ * not `receiverTarget`: HTTP client types (`RestTemplate`, `WebClient`) are
676
+ * Spring classes outside the analysed set, so that function correctly
677
+ * returns `outside` for them — this one just needs the written name to
678
+ * compare against a closed set.
760
679
  *
761
- * The scope order is the same and is deliberately kept to the two cases that
762
- * carry a written type — a local or parameter, then a field. Anything else
763
- * (`Type.staticMethod()`, a chained builder, an expression) has no written
764
- * type at this reference and returns `undefined`, which the caller turns into
765
- * a disclosed refusal rather than a guess.
680
+ * Scope order kept to the two cases with a written type: local/parameter,
681
+ * then field. Anything else returns `undefined`, turned into a disclosed
682
+ * refusal rather than a guess.
766
683
  *
767
- * A `function` declaration rather than a `const` arrow so it hoists: the
768
- * method loop that calls it runs earlier in `extract`'s body than this point.
684
+ * `function` declaration, not `const` arrow, so it hoists — the method loop
685
+ * calling it runs earlier in `extract`'s body.
769
686
  */
770
687
  function writtenReceiverType(method, owner, receiver) {
771
688
  const viaThis = receiver.startsWith("this.");
@@ -786,25 +703,21 @@ export function extract(input) {
786
703
  return field.written;
787
704
  }
788
705
  ;
789
- // --- Spring routes -------------------------------------------------------
790
- //
791
- // R2 and no lower, for the reason the Go adapter records: telling a controller
792
- // from an ordinary class is provenance, and a route emitted from the R0 or R1
793
- // rung is a claim stronger than the run that produced it — which the
794
- // Normaliser rejects as RESOLUTION_EXCEEDS_BATCH.
706
+ // R2 and no lower — telling a controller from an ordinary class is
707
+ // provenance, and a route emitted below R2 claims more than the run
708
+ // produced (Normaliser rejects it as RESOLUTION_EXCEEDS_BATCH).
795
709
  if (input.reached >= 2) {
796
710
  const routeNodes = new Map();
797
711
  const endpointNodes = new Map();
798
712
  for (const unit of input.units) {
799
- // Spring and JAX-RS are read independently — each gated on its own
800
- // provenance in `spring.ts` — and merged here because both mint the
801
- // same `API_ROUTE`/`API_ENDPOINT` shape (`SpringRoute`). See
802
- // `language-problems.md` §3's JAX-RS row.
713
+ // Spring and JAX-RS read independently (each gated on its own
714
+ // provenance in `spring.ts`), merged here since both mint the same
715
+ // `API_ROUTE`/`API_ENDPOINT` shape.
803
716
  for (const route of [...springRoutes(unit), ...jaxrsRoutes(unit)]) {
804
717
  const template = normaliseEndpointPath(route.template);
805
718
  const name = `${route.method} ${route.template}`;
806
- // Repo-scoped and template-as-written: which service serves a path is
807
- // exactly what the join must not erase.
719
+ // Repo-scoped, template-as-written — which service serves a path is
720
+ // what the join must not erase.
808
721
  const routeId = nodeId(scope, "API_ROUTE", name, LANGUAGE);
809
722
  if (!routeNodes.has(routeId)) {
810
723
  routeNodes.set(routeId, {
@@ -826,24 +739,18 @@ export function extract(input) {
826
739
  });
827
740
  }
828
741
  else {
829
- // A second declaration claiming the same method+path — real (two
830
- // controllers mapped to overlapping prefixes, Spring and JAX-RS both
831
- // serving the same route) rather than a bug in this reader, but
832
- // silent until now: nothing distinguished it from "checked, only one
833
- // declaration exists". Same write pattern named across five language
834
- // adapters in `language-problems.md` §0.2 #10, fixed here the way
835
- // `adapter-python` already fixed it — a ledger row on the collision
836
- // branch, not a change to the underlying node-identity question
837
- // (`DEC-A-routes-ts-reconciliation`).
742
+ // A second declaration claiming the same method+path is real (two
743
+ // controllers on overlapping prefixes, Spring and JAX-RS both
744
+ // serving one route), not a bug — was silent until now. Fixed the
745
+ // way `adapter-python` did: a ledger row on the collision branch,
746
+ // not a change to node identity (`DEC-A-routes-ts-reconciliation`).
838
747
  ledger(
839
- // Every unit gets a MODULE node in the Nodes section above, before
840
- // this Edges section ever runs — always present.
748
+ // Every unit gets a MODULE node in the Nodes section above — always present.
841
749
  moduleIdOf.get(unit.file), "SERVES_API", name, { file: unit.file, line: route.line }, REASONS.routeIdCollision);
842
750
  }
843
- // Workspace-scoped, fileless, language-less, and minted with the SAME
844
- // endpointQsp every other producer uses — DEC-115's one hard constraint.
845
- // Two producers that mint different ids do not conflict, they silently
846
- // fail to join.
751
+ // Workspace-scoped, fileless, language-less, minted with the SAME
752
+ // endpointQsp every producer uses (DEC-115) — mismatched ids don't
753
+ // conflict, they silently fail to join.
847
754
  const endpointId = nodeId(scope, "API_ENDPOINT", endpointQsp(route.method, route.template), null);
848
755
  if (!endpointNodes.has(endpointId)) {
849
756
  endpointNodes.set(endpointId, {
@@ -861,13 +768,10 @@ export function extract(input) {
861
768
  push({ from: routeId, to: endpointId, type: "SERVES_API" }, 2);
862
769
  }
863
770
  }
864
- // --- The caller's half ---------------------------------------------------
865
- //
866
- // Mints into the SAME `endpointNodes` map, so a call to a route this run
867
- // also read produces one node with two edges rather than two nodes with one
868
- // each. A call to a route this run did *not* read still mints its endpoint —
869
- // that is the point of `API_ENDPOINT` being the join node: either side can
870
- // exist without the other (golden 08's note).
771
+ // The caller's half. Mints into the SAME `endpointNodes` map, so a call to
772
+ // a route this run also read produces one node with two edges, not two.
773
+ // A call to a route this run didn't read still mints its endpoint —
774
+ // `API_ENDPOINT` is the join node; either side can exist alone (golden 08).
871
775
  for (const call of clientCallSites) {
872
776
  const endpoint = endpointFor(scope, call, input.producedBy, normaliseEndpointPath);
873
777
  if (!endpointNodes.has(endpoint.id))
@@ -876,12 +780,9 @@ export function extract(input) {
876
780
  from: call.fromId,
877
781
  to: endpoint.id,
878
782
  type: "USES_API",
879
- // **The distinction the census forced.** 12 of Java's 16 relative-path
880
- // call sites are in test files and none are production, so a consumer
881
- // reading these as service-to-service coupling would be wrong about
882
- // nearly all of them. `test` means this edge feeds *Verification
883
- // status* — which routes have integration coverage — and not the
884
- // confidence label. DEC-189.
783
+ // 12 of Java's 16 relative-path call sites are test files, none
784
+ // production — `test` feeds Verification status (integration
785
+ // coverage), not the confidence label. DEC-189.
885
786
  attrs: { callerKind: call.callerKind, via: call.method },
886
787
  }, 2);
887
788
  }