@descryy/adapter-kotlin 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.d.ts CHANGED
@@ -3,35 +3,24 @@
3
3
  *
4
4
  * ## The identity rule, and why it refuses
5
5
  *
6
- * Kotlin shares `@descryy/adapter-jvm`'s anchor with Java: a symbol's identity is
7
- * `module/sourceSet/package`, derived by subtracting the package path from the
8
- * file's directory. Measured before this was written — okhttp 570 of 576 files
9
- * derive, ktor 2,146 of 2,324.
6
+ * Kotlin shares `@descryy/adapter-jvm`'s anchor with Java: identity is `module/sourceSet/package`,
7
+ * derived by subtracting the package path from the file's directory (measured: okhttp 570/576
8
+ * files derive, ktor 2,146/2,324).
10
9
  *
11
- * **When the subtraction fails, the node is refused.** Java falls back to the
12
- * bare package name; Kotlin must not, because Kotlin Multiplatform declares one
13
- * qualified name in several source sets **by design** — `expect`/`actual`. A
14
- * bare-package fallback collapses those onto one id. Measured on ktor: 4 FQNs,
15
- * `io.ktor.utils.io.charsets.Charsets` among them, declared across seven
16
- * platform files.
10
+ * **When the subtraction fails, the node is refused** rather than falling back to the bare
11
+ * package name as Java does — Kotlin Multiplatform declares one qualified name across several
12
+ * source sets *by design* (`expect`/`actual`), and a bare-package fallback would collapse those
13
+ * onto one id (measured on ktor: 4 FQNs, including `io.ktor.utils.io.charsets.Charsets`, across
14
+ * seven platform files). `attrs` was rejected as the disclosure channel instead: nothing above
15
+ * the IR reads it, so a refusal beats a merged node with an unseen note (DEC-173).
17
16
  *
18
- * Disclosing the collapse on `attrs` instead was considered and rejected:
19
- * nothing above the IR reads `attrs`, so scoring, governance and blast radius
20
- * would act on a merged node while the note sat somewhere they cannot see.
21
- * A refusal is visible to the layers that act on it; that is the whole
22
- * difference (DEC-173).
17
+ * ## No cross-language edges — a measurement, not an omission
23
18
  *
24
- * ## No cross-language edges, and that is a measurement
25
- *
26
- * The build plan has this adapter mint Java-shaped target ids for references
27
- * into `.java` files. **It is not implemented, because the corpus chosen to
28
- * measure it cannot witness it.** okhttp's 68 `.java` files are 56 samples with
29
- * `kt/` twins, 11 `module-info.java` declaring no types, and 3 `*JavaTest.java`
30
- * exercising the Kotlin API. The traffic runs Java→Kotlin. ktor is 2,324 `.kt`
31
- * against 1 `.java`.
32
- *
33
- * A rule with zero witnesses is an untaken branch, not a weak rule — so it is
34
- * declared absent rather than written blind.
19
+ * The build plan calls for Java-shaped target ids for references into `.java` files. Not
20
+ * implemented: the corpus can't witness it. okhttp's 68 `.java` files are 56 samples with `kt/`
21
+ * twins, 11 `module-info.java` (no types), 3 `*JavaTest.java` exercising the Kotlin API — traffic
22
+ * runs Java→Kotlin; ktor is 2,324 `.kt` against 1 `.java`. A rule with zero witnesses is declared
23
+ * absent rather than written blind.
35
24
  */
36
25
  import { type IRBatch, type ResolutionLevel } from "@descryy/ir";
37
26
  import type { KotlinFile } from "./parse.ts";
@@ -43,12 +32,9 @@ export interface ExtractInput {
43
32
  readonly workspace?: string | undefined;
44
33
  };
45
34
  /**
46
- * The resolution level `prepare()` actually reached, gating `push()` below.
47
- * Without it every edge always carries its full resolution regardless of
48
- * `maxResolution`, and a batch claiming R0 would still emit R1 edges — which
49
- * the conformance harness's boundary check reports as
50
- * `RESOLUTION_EXCEEDS_BATCH`: an edge whose own resolution exceeds what the
51
- * batch that carries it claims.
35
+ * The resolution level `prepare()` actually reached, gating `push()` below —
36
+ * without it a batch claiming R0 could still emit R1 edges, which the
37
+ * conformance harness flags as `RESOLUTION_EXCEEDS_BATCH`.
52
38
  */
53
39
  readonly reached: ResolutionLevel;
54
40
  }
@@ -1 +1 @@
1
- {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAKL,KAAK,OAAO,EAGZ,KAAK,eAAe,EAErB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,UAAU,EAAmB,MAAM,YAAY,CAAC;AAqB9D,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;IACtC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IACnF;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;CACnC;AASD,wBAAgB,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,GAAG,YAAY,CAAC,CAqsB5F"}
1
+ {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAKL,KAAK,OAAO,EAGZ,KAAK,eAAe,EAErB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,UAAU,EAAmB,MAAM,YAAY,CAAC;AAqB9D,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;IACtC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IACnF;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;CACnC;AASD,wBAAgB,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,GAAG,YAAY,CAAC,CAipB5F"}
package/dist/extract.js CHANGED
@@ -3,35 +3,24 @@
3
3
  *
4
4
  * ## The identity rule, and why it refuses
5
5
  *
6
- * Kotlin shares `@descryy/adapter-jvm`'s anchor with Java: a symbol's identity is
7
- * `module/sourceSet/package`, derived by subtracting the package path from the
8
- * file's directory. Measured before this was written — okhttp 570 of 576 files
9
- * derive, ktor 2,146 of 2,324.
6
+ * Kotlin shares `@descryy/adapter-jvm`'s anchor with Java: identity is `module/sourceSet/package`,
7
+ * derived by subtracting the package path from the file's directory (measured: okhttp 570/576
8
+ * files derive, ktor 2,146/2,324).
10
9
  *
11
- * **When the subtraction fails, the node is refused.** Java falls back to the
12
- * bare package name; Kotlin must not, because Kotlin Multiplatform declares one
13
- * qualified name in several source sets **by design** — `expect`/`actual`. A
14
- * bare-package fallback collapses those onto one id. Measured on ktor: 4 FQNs,
15
- * `io.ktor.utils.io.charsets.Charsets` among them, declared across seven
16
- * platform files.
10
+ * **When the subtraction fails, the node is refused** rather than falling back to the bare
11
+ * package name as Java does — Kotlin Multiplatform declares one qualified name across several
12
+ * source sets *by design* (`expect`/`actual`), and a bare-package fallback would collapse those
13
+ * onto one id (measured on ktor: 4 FQNs, including `io.ktor.utils.io.charsets.Charsets`, across
14
+ * seven platform files). `attrs` was rejected as the disclosure channel instead: nothing above
15
+ * the IR reads it, so a refusal beats a merged node with an unseen note (DEC-173).
17
16
  *
18
- * Disclosing the collapse on `attrs` instead was considered and rejected:
19
- * nothing above the IR reads `attrs`, so scoring, governance and blast radius
20
- * would act on a merged node while the note sat somewhere they cannot see.
21
- * A refusal is visible to the layers that act on it; that is the whole
22
- * difference (DEC-173).
17
+ * ## No cross-language edges — a measurement, not an omission
23
18
  *
24
- * ## No cross-language edges, and that is a measurement
25
- *
26
- * The build plan has this adapter mint Java-shaped target ids for references
27
- * into `.java` files. **It is not implemented, because the corpus chosen to
28
- * measure it cannot witness it.** okhttp's 68 `.java` files are 56 samples with
29
- * `kt/` twins, 11 `module-info.java` declaring no types, and 3 `*JavaTest.java`
30
- * exercising the Kotlin API. The traffic runs Java→Kotlin. ktor is 2,324 `.kt`
31
- * against 1 `.java`.
32
- *
33
- * A rule with zero witnesses is an untaken branch, not a weak rule — so it is
34
- * declared absent rather than written blind.
19
+ * The build plan calls for Java-shaped target ids for references into `.java` files. Not
20
+ * implemented: the corpus can't witness it. okhttp's 68 `.java` files are 56 samples with `kt/`
21
+ * twins, 11 `module-info.java` (no types), 3 `*JavaTest.java` exercising the Kotlin API — traffic
22
+ * runs Java→Kotlin; ktor is 2,324 `.kt` against 1 `.java`. A rule with zero witnesses is declared
23
+ * absent rather than written blind.
35
24
  */
36
25
  import { endpointQsp, nodeId, normaliseEndpointPath, symbolQsp, } from "@descryy/ir";
37
26
  const LANGUAGE = "kotlin";
@@ -48,9 +37,8 @@ export function extract(input) {
48
37
  const funcsByKey = new Map();
49
38
  const moduleIdOf = new Map();
50
39
  const seenEdges = new Set();
51
- // `IREdge` carries no `language` — an edge's language is the language of the
52
- // nodes it joins, and duplicating it here would be a second place for it to
53
- // be wrong.
40
+ // `IREdge` carries no `language`: an edge's language is its nodes' language,
41
+ // and duplicating it here is a second place for it to be wrong.
54
42
  const push = (edge, confidence, resolution) => {
55
43
  if (resolution > input.reached)
56
44
  return false;
@@ -73,27 +61,15 @@ export function extract(input) {
73
61
  ...(attrs === undefined ? {} : { attrs }),
74
62
  });
75
63
  };
76
- // -------------------------------------------------------------------------
77
- // Pass 0 — count declarations sharing an identity key: same identityPackage,
78
- // same owner chain, same name. Two shapes produce this, and both share one
79
- // cause: this adapter's identity has no parameter-type or file component to
80
- // break the tie, by design (DEC-004 — a file component churns on rename).
81
- //
82
- // - Two files in one package each declaring a top-level `fun main()`. The
83
- // JVM synthesises a distinct `FileNameKt` class per file; nothing here
84
- // does. okhttp's `samples/` alone has 20+ such files.
85
- // - Overloads: two methods on the same class sharing a name over different
86
- // parameters, which this adapter cannot distinguish without a type
87
- // checker. Measured on ktor: `AuthenticationConfig.provider(name)` and
88
- // `.provider(name, configure, body)` collide; the second's own body is
89
- // the one that calls `requireProviderNotRegistered`, and it was silently
90
- // unreachable through the merged node.
91
- //
92
- // Pass 1 used to keep the first declaration under a key and silently drop
93
- // the rest with no disclosure — a rule-7 violation — which cross-attributes
94
- // every edge that belonged to a dropped declaration onto whichever one
95
- // happened to parse first, rather than losing them cleanly.
96
- // -------------------------------------------------------------------------
64
+ // Pass 0 — count declarations sharing an identity key (same identityPackage, owner chain, name):
65
+ // this adapter's identity has no parameter-type or file component to break a tie, by design
66
+ // (DEC-004 — a file component churns on rename). Two shapes collide: (1) two files in one package
67
+ // each declaring top-level `fun main()` — the JVM synthesises a distinct `FileNameKt` class per
68
+ // file, nothing here does (okhttp's `samples/` has 20+); (2) overloads, indistinguishable without
69
+ // a type checker (ktor's `AuthenticationConfig.provider(name)` vs `.provider(name, configure,
70
+ // body)` — the second's body calls `requireProviderNotRegistered`, silently unreachable when
71
+ // merged). Previously the first declaration under a key won silently, cross-attributing dropped
72
+ // declarations' edges onto it (rule-7 violation) — this pass refuses the collision instead.
97
73
  const declNameCount = new Map();
98
74
  for (const unit of input.files) {
99
75
  if (unit.identityPackage === undefined)
@@ -107,10 +83,8 @@ export function extract(input) {
107
83
  declNameCount.set(k, (declNameCount.get(k) ?? 0) + 1);
108
84
  }
109
85
  }
110
- // -------------------------------------------------------------------------
111
- // Pass 1 — declarations. A file with no derivable identity package mints
112
- // nothing at all, and says so once for the file rather than once per symbol.
113
- // -------------------------------------------------------------------------
86
+ // Pass 1 — declarations. A file with no derivable identity mints nothing,
87
+ // disclosed once per file rather than once per symbol.
114
88
  for (const unit of input.files) {
115
89
  if (unit.identityPackage === undefined) {
116
90
  refuse("", "IMPORTS", unit.file, unit.file, 1, "This file does not sit where its package declaration says it should, so its module and " +
@@ -136,9 +110,8 @@ export function extract(input) {
136
110
  ...(unit.hasError ? { parsedWithErrors: true } : {}),
137
111
  },
138
112
  });
139
- // Counted in parse.ts, disclosed here. Silence is the worst direction under
140
- // rule 7, and a declaration this adapter deliberately declined to place is
141
- // exactly the thing a reader would otherwise assume absent from the source.
113
+ // Counted in parse.ts, disclosed here — a declined declaration is exactly
114
+ // what a reader would otherwise assume simply absent (rule 7).
142
115
  if (unit.unparseableDeclarations > 0) {
143
116
  refuse(moduleId, "IMPORTS", fileStem(unit.file), unit.file, 1, `${unit.unparseableDeclarations} declaration(s) in this file could not be placed. The pinned ` +
144
117
  "grammar does not parse Kotlin's `fun interface`, so the body arrives as an orphaned lambda " +
@@ -231,34 +204,31 @@ export function extract(input) {
231
204
  ...node,
232
205
  attrs: {
233
206
  ...node.attrs,
234
- // `type` is the element type and `list` a boolean, per the contract
235
- // Lane E stated and every other adapter follows. Kotlin infers, so an
236
- // absent type is "not written", never "no type".
207
+ // `type` is the element type, `list` a boolean — the contract Lane E
208
+ // stated and every adapter follows. Kotlin infers, so an absent
209
+ // type means "not written," never "no type."
237
210
  fields: fields.map((f) => ({ name: f.name, type: f.type ?? null, list: false })),
238
211
  },
239
212
  };
240
213
  }
241
214
  }
242
- // -------------------------------------------------------------------------
243
215
  // Pass 2 — edges.
244
- // -------------------------------------------------------------------------
245
216
  /** Types a file can name: its own package, plus everything it imports. */
246
217
  function resolveType(unit, written) {
247
218
  if (unit.identityPackage === undefined)
248
219
  return undefined;
249
- // A written type may be a dotted path to a NESTED type —
250
- // `DiskLruCache.Snapshot`. The whole path is tried first, because resolving
251
- // it to its outermost segment is how `snapshot.getSource()` came to be
252
- // refused against `DiskLruCache`, where the member genuinely is not.
220
+ // A written type may be a dotted nested path (`DiskLruCache.Snapshot`);
221
+ // the whole path is tried first — resolving only the outermost segment is
222
+ // how `snapshot.getSource()` was wrongly refused against `DiskLruCache`.
253
223
  const segments = written.split(".");
254
224
  const name = segments[segments.length - 1];
255
225
  if (segments.length > 1) {
256
226
  const nested = typesByKey.get(typeKey(unit.identityPackage, segments));
257
227
  if (nested !== undefined)
258
228
  return nested;
259
- // The enclosing type may be imported from another package while the path
260
- // is written relative to it, so the tail is matched against declared
261
- // paths rather than assumed to be package-local.
229
+ // The enclosing type may be imported from elsewhere while the path is
230
+ // written relative to it, so the tail is matched against declared
231
+ // paths, not assumed package-local.
262
232
  const byPath = [...typesByKey.values()].filter((candidate) => candidate.path.join(".").endsWith(segments.join(".")));
263
233
  if (byPath.length === 1)
264
234
  return byPath[0];
@@ -275,10 +245,9 @@ export function extract(input) {
275
245
  if (candidate.path[candidate.path.length - 1] === name)
276
246
  return candidate;
277
247
  }
278
- // An explicit import names the symbol; a wildcard names its package. Both
279
- // are matched against the DECLARATION table rather than assumed to exist —
280
- // an import of something this adapter never read resolves to nothing and is
281
- // refused, not invented.
248
+ // Explicit import names the symbol; wildcard names its package. Both
249
+ // matched against the DECLARATION table — an import of something never
250
+ // read resolves to nothing and is refused, not invented.
282
251
  const matches = [];
283
252
  for (const imported of unit.imports) {
284
253
  const wanted = imported.isWildcard ? name : imported.path.split(".").pop();
@@ -295,15 +264,12 @@ export function extract(input) {
295
264
  return matches[0];
296
265
  if (matches.length > 1)
297
266
  return undefined;
298
- // Same declared PACKAGE name, but a different source set — `src/test/kotlin`
299
- // naming a class in `src/main/kotlin` with no import, which Kotlin compiles
300
- // without one because a test source set's classpath includes main. Tried
301
- // last, and only when there is exactly one candidate: two declarations
302
- // sharing a package name across different SOURCE SETS OR MODULES is the
303
- // exact collision `adapter-jvm`'s identity design exists to catch (airbyte's
304
- // `RedisDataFactory`, declared under both `src/test/java` and
305
- // `src/test-integration/java`) — so ambiguity here still refuses rather than
306
- // guesses, the same rule Java's own `pick` applies.
267
+ // Same declared package, different source set (`src/test/kotlin` naming a class in
268
+ // `src/main/kotlin` with no import — legal, since a test source set's classpath includes main).
269
+ // Tried last, only with exactly one candidate: cross-source-set/module name collisions are
270
+ // exactly what `adapter-jvm`'s identity design exists to catch (airbyte's `RedisDataFactory`
271
+ // under both `src/test/java` and `src/test-integration/java`), so ambiguity still refuses,
272
+ // same as Java's own `pick`.
307
273
  if (unit.packageName === "")
308
274
  return undefined;
309
275
  const samePackageName = [...typesByKey.values()].filter((candidate) => packageOf(candidate.identityPackage) === unit.packageName &&
@@ -314,15 +280,12 @@ export function extract(input) {
314
280
  return funcsByKey.get(typeKey(owner.identityPackage, [...owner.path, name]));
315
281
  }
316
282
  /**
317
- * A top-level function a bare unqualified call may name: one in the file's
318
- * own package, or one reached through an explicit or wildcard import.
319
- *
320
- * The same gap `resolveType` closes for types, but for functions: an
321
- * unqualified call to an imported top-level function (`validateOrder`,
322
- * imported from a different package) was refused as "no declaration" because
323
- * this lookup only ever checked the caller's OWN package. Kotlin needs no
324
- * import for a same-package call, which is what made the gap invisible until
325
- * a construct genuinely needing a cross-package import existed to hit it.
283
+ * A top-level function a bare unqualified call may name — own package, or
284
+ * reached through an import. Mirrors `resolveType`'s gap for functions: a
285
+ * call to an imported top-level function was wrongly refused because this
286
+ * lookup only checked the caller's own package (invisible until a
287
+ * cross-package call actually needed it, since same-package calls need no
288
+ * import).
326
289
  */
327
290
  function resolveFreeFunction(unit, name) {
328
291
  if (unit.identityPackage === undefined)
@@ -354,9 +317,8 @@ export function extract(input) {
354
317
  return matches[0];
355
318
  if (matches.length > 1)
356
319
  return undefined;
357
- // Same declared package, different source set — see `resolveType`'s
358
- // identical fallback for the reasoning; a bare call to a same-package
359
- // top-level function needs it exactly as a bare type reference does.
320
+ // Same declared package, different source set — same reasoning as
321
+ // `resolveType`'s identical fallback.
360
322
  if (unit.packageName === "")
361
323
  return undefined;
362
324
  const samePackageName = [...funcsByKey.values()].filter((fn) => fn.owners.length === 0 && fn.name === name && packageOf(fn.identityPackage) === unit.packageName);
@@ -369,17 +331,12 @@ export function extract(input) {
369
331
  // IMPORTS — file-granular, to the module of the imported symbol.
370
332
  if (moduleId !== undefined) {
371
333
  for (const imported of unit.imports) {
372
- // A STAR IMPORT IS REFUSED. `import io.ktor.util.*` names a package,
373
- // and the IR has no PACKAGE node — so the only available edge is
374
- // MODULE->MODULE, once per file in that package. Measured on ktor: 144
375
- // of 183 sampled such edges point at a file whose declarations the
376
- // importing file never names anywhere in its text, and ktor writes
377
- // 9,222 star imports against 4,877 explicit ones.
378
- //
379
- // That made IMPORTS 89,080 of 98,551 edges at roughly 79% wrong — 90%
380
- // of the graph, and the single reason both precision gates failed.
381
- // Rule 2 decides it: a wrong edge corrupts every layer above, a missing
382
- // one is a disclosed gap.
334
+ // Star imports are refused: `import io.ktor.util.*` names a package, but the IR has no
335
+ // PACKAGE node, so the only edge available is MODULE->MODULE once per file in that package.
336
+ // Measured on ktor: 144 of 183 sampled such edges point at a file the importer never
337
+ // actually names (9,222 star imports vs 4,877 explicit) — this made IMPORTS 89,080/98,551
338
+ // edges (~79% wrong, ~90% of the graph) and was the reason both precision gates failed.
339
+ // Rule 2: a wrong edge corrupts every layer above; a missing one is a disclosed gap.
383
340
  if (imported.isWildcard) {
384
341
  refuse(moduleId, "IMPORTS", `${imported.path}.*`, unit.file, imported.line, "A star import names a package, and the IR has no PACKAGE node. The only edge " +
385
342
  "available is one per file in that package, and most of those files declare nothing " +
@@ -387,11 +344,9 @@ export function extract(input) {
387
344
  "rather than emitted; the dependency is real but not expressible at this resolution.");
388
345
  continue;
389
346
  }
390
- // An explicit import is matched on its FULL PACKAGE PATH, not on the
391
- // last segment. Matching by simple name gave `import okhttp3.Request` an
392
- // edge to a nested `data class Request` inside another Gradle module's
393
- // ShadowDnsResolver, and made mockwebserver and mockwebserver-deprecated
394
- // resolve into each other in both directions.
347
+ // Matched on the FULL PACKAGE PATH, not the last segment — matching by simple name gave
348
+ // `import okhttp3.Request` an edge to an unrelated nested `Request` class, and made
349
+ // mockwebserver/mockwebserver-deprecated resolve into each other both ways.
395
350
  const wanted = imported.path;
396
351
  const targets = new Set();
397
352
  for (const candidate of typesByKey.values()) {
@@ -402,12 +357,10 @@ export function extract(input) {
402
357
  if (target !== undefined && target !== moduleId)
403
358
  targets.add(target);
404
359
  }
405
- // A Kotlin import may name a TOP-LEVEL FUNCTION or property, not only a
406
- // type — `import io.ktor.server.request.receive` is an extension
407
- // function. Matching only against types made 21 of 22 adjudicated wrong
408
- // refusals on ktor, and the shape looked like a filename problem from
409
- // the outside because the declaring file is named for its subject
410
- // (`ApplicationReceiveFunctions.kt`) rather than for the symbol.
360
+ // An import may name a top-level function, not only a type (`io.ktor.server.request.receive`
361
+ // is an extension function). Matching only types caused 21 of 22 adjudicated wrong refusals
362
+ // on ktor — it looked like a filename problem, since the declaring file is named for its
363
+ // subject (`ApplicationReceiveFunctions.kt`), not the symbol.
411
364
  if (targets.size === 0) {
412
365
  for (const fn of funcsByKey.values()) {
413
366
  if (fn.owners.length > 0)
@@ -466,10 +419,10 @@ export function extract(input) {
466
419
  const free = resolveFreeFunction(unit, ref.name);
467
420
  const target = member ?? free;
468
421
  if (target === undefined) {
469
- // A constructor invocation is syntactically identical to a call —
470
- // Kotlin has no `new` keyword — so a name that resolves to a TYPE
471
- // rather than a function is a class or enum named as a VALUE
472
- // (DEC-068), not a missing declaration.
422
+ // Kotlin has no `new` keyword, so a constructor call is
423
+ // syntactically identical to an ordinary one — a name resolving
424
+ // to a TYPE rather than a function is a class/enum used as a
425
+ // VALUE (DEC-068), not a missing declaration.
473
426
  const asType = resolveType(unit, ref.name);
474
427
  if (asType !== undefined) {
475
428
  push({ from: from.id, to: asType.id, type: "USES_TYPE" }, 0.85, 1);
@@ -482,12 +435,10 @@ export function extract(input) {
482
435
  continue;
483
436
  }
484
437
  if (ref.receiverType === undefined) {
485
- // The receiver has no written LOCAL type — but the receiver name
486
- // itself may resolve to a declared type, which is `object`,
487
- // `companion object` and static-style access: `Routes.getOrder(id)`
488
- // has no local variable named `Routes` to look up, because `Routes`
489
- // IS the type. Tried before refusing, not instead of the ordinary
490
- // path — a local variable always wins when one exists.
438
+ // No written local type — but the receiver name may itself resolve to a declared type
439
+ // (`object`/companion, static-style access): `Routes.getOrder(id)` has no local var
440
+ // `Routes`, because `Routes` IS the type. Tried before refusing; a local variable still
441
+ // wins when one exists.
491
442
  const asStatic = ref.receiver === undefined ? undefined : resolveType(unit, ref.receiver);
492
443
  const staticMember = asStatic === undefined ? undefined : memberOf(asStatic, ref.name);
493
444
  if (staticMember !== undefined) {
@@ -510,13 +461,10 @@ export function extract(input) {
510
461
  "be inherited, an extension function, or defined in a dependency.");
511
462
  continue;
512
463
  }
513
- // R1, NOT R2. The ladder defines R2 as "+ LSP references/definitions"
514
- // and there is no LSP here — this edge rests on a WRITTEN type
515
- // annotation resolved against the declaration table, which is
516
- // module/import resolution plus a piece of source text. Claiming R2 for
517
- // it would be claiming a capability the adapter does not have. Raised by
518
- // the lane grading this adapter, who could not read this file and asked
519
- // what the 2 was backed by; the honest answer was nothing.
464
+ // R1, not R2: the ladder defines R2 as "+ LSP references/definitions," and there's no LSP
465
+ // here — this rests on a written type annotation resolved against the declaration table
466
+ // (module/import resolution + source text). Claiming R2 would claim a capability this
467
+ // adapter doesn't have.
520
468
  push({ from: from.id, to: target.id, type: "CALLS" }, 0.9, 1);
521
469
  }
522
470
  }
@@ -530,10 +478,8 @@ export function extract(input) {
530
478
  continue;
531
479
  push({ from: edge.from, to: edge.to, type: "TESTS" }, edge.confidence, edge.resolution);
532
480
  }
533
- // ---------------------------------------------------------------------------
534
481
  // Routes — Ktor's routing DSL. Last, because every ledger row is attributed
535
482
  // to a MODULE the pass above has already minted.
536
- // ---------------------------------------------------------------------------
537
483
  const routeNodes = new Map();
538
484
  const endpointNodes = new Map();
539
485
  for (const unit of input.files) {
@@ -591,16 +537,13 @@ export function extract(input) {
591
537
  push({ from: routeId, to: endpointId, type: "SERVES_API" }, 0.9, 2);
592
538
  }
593
539
  }
594
- // ---------------------------------------------------------------------------
595
- // Client calls — the caller half. Minted through the same `endpointQsp`, so a
596
- // call and the route that serves it land on ONE endpoint node rather than two
597
- // that never meet.
598
- // ---------------------------------------------------------------------------
540
+ // Client calls — the caller half, minted through the same `endpointQsp` so a
541
+ // call and the route serving it land on one endpoint node, not two that
542
+ // never meet.
599
543
  /**
600
- * DEC-242's `attrs.blockedBy`/`attrs.refusalClass`, plus the diagnostic-only
601
- * `argumentKind` — `undefined` when `refusalClass` was never set, so
602
- * `refuse` omits `attrs` entirely rather than sending it with an
603
- * `undefined` field (unset stays legal and means unclassified).
544
+ * DEC-242's `attrs.blockedBy`/`attrs.refusalClass`, plus diagnostic-only
545
+ * `argumentKind`. `undefined` when `refusalClass` was never set, so
546
+ * `refuse` omits `attrs` entirely rather than an undefined field.
604
547
  */
605
548
  const clientAttrsOf = (refusal) => refusal.refusalClass === undefined
606
549
  ? undefined
@@ -619,17 +562,16 @@ export function extract(input) {
619
562
  if (input.reached < 2)
620
563
  continue;
621
564
  for (const call of unit.clientCalls) {
622
- // The caller is the innermost function whose span contains the call, so
623
- // the edge starts at the thing a reader would blame. A call outside any
624
- // function — a property initialiser at file scope — is attributed to the
625
- // MODULE, which is the file-scoped carrier by DEC-124.
565
+ // Caller = innermost function containing the call (the thing a reader
566
+ // would blame). Outside any function (a file-scope property
567
+ // initialiser) attributes to the MODULE, the file-scoped carrier
568
+ // (DEC-124).
626
569
  const enclosing = unit.funcs
627
570
  .filter((fn) => fn.startLine <= call.line && call.line <= fn.endLine)
628
571
  .sort((a, b) => b.startLine - a.startLine)[0];
629
- // A file that does not sit where its package declaration says has no
630
- // identity package, so its functions have no ids to point at (DEC-173).
631
- // The MODULE still does, and it is the file-scoped carrier by DEC-124 —
632
- // so the edge degrades to the file rather than being dropped.
572
+ // A file with no derivable identity package has no function ids to
573
+ // point at (DEC-173); the MODULE still does (DEC-124), so the edge
574
+ // degrades to the file rather than dropping.
633
575
  const from = enclosing === undefined || unit.identityPackage === undefined
634
576
  ? moduleId
635
577
  : funcsByKey.get(typeKey(unit.identityPackage, [...enclosing.owners, enclosing.name]))?.id ?? moduleId;
@@ -651,12 +593,9 @@ export function extract(input) {
651
593
  push({ from, to: endpointId, type: "USES_API", attrs: { client: call.client, rawPath: call.rawPath } }, 0.9, 2);
652
594
  }
653
595
  }
654
- // ---------------------------------------------------------------------------
655
- // Retrofit — a declarative client interface. Unlike every call above, there is
656
- // no call SITE to attribute the edge to: the annotated function's own id IS
657
- // the caller, and ordinary CALLS resolution (unchanged) is what connects a
658
- // real caller to it, one hop further out. See `retrofit.ts`'s own header.
659
- // ---------------------------------------------------------------------------
596
+ // Retrofit — a declarative client interface. No call site to attribute to:
597
+ // the annotated function's own id IS the caller; ordinary CALLS resolution
598
+ // connects a real caller one hop further out. See `retrofit.ts`.
660
599
  for (const unit of input.files) {
661
600
  const moduleId = moduleIdOf.get(unit.file);
662
601
  if (moduleId === undefined)
@@ -671,11 +610,9 @@ export function extract(input) {
671
610
  ? undefined
672
611
  : funcsByKey.get(typeKey(unit.identityPackage, [...endpoint.owners, endpoint.funcName]))?.id;
673
612
  if (from === undefined) {
674
- // Either the file has no derivable identity, or the declaring
675
- // function itself collided with another of the same name and was
676
- // refused in Pass 1 — both already disclosed there. Attributing this
677
- // edge to the MODULE instead would be a second, misleading claim
678
- // about a function this adapter declined to name.
613
+ // No derivable identity, or the declaring function collided and was
614
+ // refused in Pass 1 — already disclosed there. Falling back to the
615
+ // MODULE would be a second, misleading claim about a declined function.
679
616
  continue;
680
617
  }
681
618
  const template = normaliseEndpointPath(endpoint.path);
@@ -700,22 +637,19 @@ export function extract(input) {
700
637
  return { nodes, edges, unresolved };
701
638
  }
702
639
  /**
703
- * `INHERITS` or `IMPLEMENTS`, decided against the declaration table.
704
- *
705
- * The parenthesised form is Kotlin's own discriminator and it is checked first
706
- * — but only as a **confirmation** of what the table says. A `class` target that
707
- * was invoked is unambiguously the superclass; an `interface` target is
708
- * unambiguously implemented whatever the syntax. Anything else falls to
709
- * `IMPLEMENTS`, which is the weaker claim of the two.
640
+ * `INHERITS` or `IMPLEMENTS`, decided against the declaration table — the
641
+ * parenthesised invocation form is checked only as confirmation. An invoked
642
+ * `class` target is unambiguously the superclass; an `interface` target is
643
+ * unambiguously implemented regardless of syntax. Anything else falls to the
644
+ * weaker `IMPLEMENTS`.
710
645
  */
711
646
  function classifySupertype(target, written) {
712
647
  if (target.kind === "interface")
713
648
  return "IMPLEMENTS";
714
649
  if (written.invoked)
715
650
  return "INHERITS";
716
- // A bare class supertype: `interface A : B` where B resolved to a class is
717
- // malformed Kotlin, so the likelier reading is that the table is incomplete.
718
- // IMPLEMENTS understates rather than inventing an inheritance chain.
651
+ // `interface A : B` where B resolves to a class is malformed Kotlin —
652
+ // likelier the table is incomplete. IMPLEMENTS understates rather than invents.
719
653
  return target.kind === "class" ? "INHERITS" : "IMPLEMENTS";
720
654
  }
721
655
  const fileStem = (file) => file.slice(file.lastIndexOf("/") + 1).replace(/\.kt$/, "");