@descryy/adapter-csharp 0.1.0 → 0.3.0

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,40 +1,25 @@
1
1
  /**
2
2
  * C# files to Canonical IR.
3
3
  *
4
- * ## Identity is the project plus the declared namespace
4
+ * Identity is project + declared namespace, not namespace alone: namespaces
5
+ * don't follow directories and can span projects, so two `.csproj` files may
6
+ * both declare `Shop.Orders.OrderService` (DEC-077's collision — the project
7
+ * is C#'s equivalent of Java's source set).
5
8
  *
6
- * Not the namespace alone. C# namespaces do not follow directories and the same
7
- * namespace legitimately spans projects, so two `.csproj` files in one
8
- * repository may both declare `Shop.Orders.OrderService`. That is DEC-077's
9
- * collision — the Java case where one fully-qualified name existed under two
10
- * source sets and stopped identifying anything — and the project is C#'s
11
- * equivalent separator.
9
+ * DEC-083: `partial class` is the first multi-file declaration here. Parts
10
+ * merge into one node (ids never carry a file path) — `file` is the
11
+ * lexicographically first declaring path, every part listed in `attrs.declaredIn`.
12
12
  *
13
- * ## Two decisions land here, both taken before this file was written
13
+ * DEC-084: `class A : B, IC` doesn't say which entry is the base class, but
14
+ * the language does — at most one, and it's first. So later entries are
15
+ * `IMPLEMENTS` unconditionally, and only the first entry per class ever needs
16
+ * resolving. The `IFoo` naming convention is never read (`System.Exception`
17
+ * disproves it; golden pattern 12 punishes inferring edges from naming habits).
14
18
  *
15
- * **DEC-083, a declaration that spans files.** `partial class` is the first
16
- * construct in this project whose declaration is legitimately multi-file. The
17
- * merge is free — a node id never contains a file path — so the parts collapse
18
- * to one node with no new machinery. `file` is the lexicographically first
19
- * declaring path and every part is listed in `attrs.declaredIn`, which is what
20
- * a precision adjudicator has to search instead of `file`.
21
- *
22
- * **DEC-084, the base list.** `class A : B, IC` does not say which entry is the
23
- * class, but the language does: at most one base class, and it must be first.
24
- * So entries after the first are `IMPLEMENTS` unconditionally, a struct's and an
25
- * interface's entries are all `IMPLEMENTS`, and exactly one entry per class ever
26
- * needs resolving. The `IFoo` prefix is never read — it is a convention,
27
- * `System.Exception` disproves it in the other direction, and inferring an edge
28
- * from a naming habit is what golden pattern 12 exists to punish.
29
- *
30
- * ## Ambiguity is disclosed, never broken
31
- *
32
- * A `using` imports a whole namespace, so a bare name can be found under two of
33
- * them at once. Real C# resolves that with the full type environment; this
34
- * adapter has names only, so two candidates produce a ledger entry and no edge.
35
- * Extension methods are the same answer for the same reason: `x.Foo()` where
36
- * `Foo` is a static method on an unrelated class, selected by the receiver's
37
- * type, is not decidable from names.
19
+ * Ambiguity is disclosed, not guessed: a `using` imports a whole namespace, so
20
+ * a bare name can match two of them — real C# resolves via the full type
21
+ * environment, this adapter has names only, so two candidates ledger and mint
22
+ * no edge. Extension methods are the same call for the same reason.
38
23
  */
39
24
  import { edgeId, endpointQsp, nodeId, normaliseEndpointPath, symbolQsp, testCaseQsp, } from "@descryy/ir";
40
25
  import { efModels, isGenerated } from "./efcore.js";
@@ -46,13 +31,11 @@ export const LANGUAGE = "csharp";
46
31
  const CONFIDENCE = { 0: 0.5, 1: 0.7, 2: 0.9, 3: 0.95 };
47
32
  const REASONS = {
48
33
  outside:
49
- // Same correction as the Java adapter's, for the same reason, on weaker
50
- // evidence. There the final sentence "A scope boundary, not an analysis
51
- // gap." was measured false for 238 of 458 entries on `okhttp`, which is
52
- // Kotlin-and-Java. No C# corpus here holds another CLR language — `.fs` and
53
- // `.vb` counts are 0 across all six — so the claim is not *known* wrong on
54
- // C#. It is unverifiable either way from inside one adapter, and a
55
- // disclosure must not assert what its author cannot check.
34
+ // Same correction as Java's, weaker evidence: Java's "scope boundary, not
35
+ // an analysis gap" was measured false for 238/458 entries on `okhttp`
36
+ // (Kotlin+Java). No C# corpus here holds another CLR language (.fs/.vb
37
+ // counts are 0 across all six), so unverifiable either way — don't assert
38
+ // what can't be checked.
56
39
  "names a type or member this run did not analyse. Either a scope boundary — a NuGet " +
57
40
  "package, the .NET base class library, a project outside the analysed set — or a " +
58
41
  "declaration this run could not read, in another language in this same repository. " +
@@ -72,12 +55,9 @@ const REASONS = {
72
55
  const INHERITANCE_HOPS = 12;
73
56
  const PENDING_PASSES = 3;
74
57
  /**
75
- * Test attributes, by framework. All three are declared in source.
76
- *
77
- * xUnit `[Fact]`/`[Theory]`, NUnit `[Test]`/`[TestCase]`, MSTest `[TestMethod]`.
78
- * This is the same shape Java's `@Test` already handles and it is reading an
79
- * annotation, not guessing from a method name — C# has no naming convention for
80
- * tests the way XCTest does.
58
+ * Test attributes, by framework: xUnit `[Fact]`/`[Theory]`, NUnit
59
+ * `[Test]`/`[TestCase]`, MSTest `[TestMethod]`. All declared in source, read
60
+ * as an annotation — C# has no naming convention for tests the way XCTest does.
81
61
  */
82
62
  const TEST_ATTRIBUTES = new Set(["Fact", "Theory", "Test", "TestCase", "TestMethod", "DataTestMethod"]);
83
63
  function qualify(namespace, path) {
@@ -90,41 +70,30 @@ export function extract(input) {
90
70
  const unresolved = [];
91
71
  const seenEdge = new Set();
92
72
  const scope = input.scope;
93
- // The caller's half of the HTTP boundary — collected per-method below,
94
- // where the enclosing FUNCTION node and receiver scopes are both in hand,
95
- // and emitted with the routes below so both halves mint one `API_ENDPOINT`.
73
+ // Caller's half of the HTTP boundary — collected per-method, emitted with
74
+ // the routes below so both halves mint one `API_ENDPOINT`.
96
75
  const clientCallSites = [];
97
76
  /**
98
- * `FUNCTION`/`TEST_CASE` node id -> its index in `nodes`, so D-FIX-3's
99
- * client-base extractor (DEC-164) can patch `attrs.clientBase` onto the
100
- * caller node once `clientCallSites` is fully known — golden patterns
101
- * 08a/08b/08c bind their `caller_*` role to the `FUNCTION`, not the edge,
102
- * per `capabilities.ts`'s own "on every caller" phrasing.
77
+ * `FUNCTION`/`TEST_CASE` node id -> its index in `nodes`, so the client-base
78
+ * extractor (DEC-164) can patch `attrs.clientBase` onto the caller node once
79
+ * `clientCallSites` is known — golden patterns 08a/08b/08c bind `caller_*`
80
+ * to the `FUNCTION`, not the edge.
103
81
  */
104
82
  const functionNodeIndex = new Map();
105
83
  /** `FUNCTION`/`TEST_CASE` node id -> the `clientBase` this row attaches to it. First writer wins. */
106
84
  const functionClientBase = new Map();
107
- // -------------------------------------------------------------------------
108
- // Framework extractors, resolved for the whole batch before any id is minted
109
- // -------------------------------------------------------------------------
110
- //
111
- // A MODEL is a MODEL rather than a CLASS, and the node id hashes the kind
112
- // (DEC-004), so the kind has to be settled BEFORE the id exists. Both
113
- // extractors also read across files — a `DbSet<Order>` names a type declared
114
- // elsewhere, and the evidence making a class a controller is usually on a base
115
- // in another file — so neither can run per-unit.
85
+ // Framework extractors, resolved for the whole batch before any id is minted:
86
+ // the node id hashes MODEL vs CLASS (DEC-004), so kind must be settled first,
87
+ // and both extractors read cross-file (a `DbSet<Order>` names a type declared
88
+ // elsewhere), so neither can run per-unit.
116
89
  const sources = input.sources ?? new Map();
117
90
  const frameworkFiles = input.files
118
91
  .map((parsed) => ({ parsed, source: sources.get(parsed.file) ?? "" }))
119
92
  .filter((each) => each.source !== "" && !isGenerated(each.parsed.file, each.source));
120
93
  /**
121
- * The NRT context governing a file, resolved from its directives alone here.
122
- *
123
- * The project and the imported props are the adapter's to supply and are not
124
- * reachable from a `CsFile`, so a file with no directive resolves to `absent`
125
- * and its reference-typed columns come out `unresolved` and counted. That is
126
- * the honest degradation: unknown is not false, and DEC-180 §2 is the whole
127
- * reason this is not allowed to guess.
94
+ * NRT context from directives alone (project/props aren't reachable from a
95
+ * `CsFile`), so an undirected file resolves `absent` and its reference-typed
96
+ * columns come out `unresolved`. Honest degradation — DEC-180 §2 forbids guessing.
128
97
  */
129
98
  const contextOf = (file) => {
130
99
  if (input.nullableContextOf !== undefined)
@@ -135,14 +104,13 @@ export function extract(input) {
135
104
  return resolveNullable({ directives: nullableDirectives(source), project: "absent", directoryProps: "absent" }, source.split("\n").length).context;
136
105
  };
137
106
  const orm = efModels({ files: frameworkFiles, contextOf });
138
- /** D-FIX-3's client-base extractor (DEC-164), the `AddHttpClient<T>(...)` half — `client-base.ts`'s own header has the account. */
107
+ /** Client-base extractor (DEC-164), the `AddHttpClient<T>(...)` half — see `client-base.ts`. */
139
108
  const typedClientBaseMap = typedClientBases(frameworkFiles);
140
109
  /**
141
- * **Qualified** type name -> its mapping, for the CLASS/MODEL decision below.
142
- *
143
- * Keyed on the simple name until a resolved collision showed what that costs:
144
- * two types named `Basket` in one repository both became `MODEL`, and the
145
- * Razor `ViewComponent` carried the entity's columns.
110
+ * Qualified type name -> its mapping, for the CLASS/MODEL decision below.
111
+ * Keyed on the qualified name after a simple-name collision cost real data:
112
+ * two `Basket` types in one repo both became `MODEL`, and a Razor
113
+ * `ViewComponent` carried the entity's columns.
146
114
  */
147
115
  const modelsByName = new Map(orm.models.map((model) => [model.fqn, model]));
148
116
  const push = (edge, level) => {
@@ -169,32 +137,26 @@ export function extract(input) {
169
137
  ...(attrs === undefined ? {} : { attrs }),
170
138
  });
171
139
  };
172
- // -------------------------------------------------------------------------
173
- // Nodes — types first, so partial parts merge before any member is placed
174
- // -------------------------------------------------------------------------
140
+ // Nodes — types first, so partial parts merge before any member is placed.
175
141
  /** Keyed by `assembly::FullyQualifiedName`, which is what makes it identity. */
176
142
  const types = new Map();
177
143
  /**
178
- * The same names, keyed by fully-qualified name alone.
179
- *
180
- * Identity and resolution want different indexes and conflating them is a
181
- * defect in both directions. The assembly separates two declarations of one
182
- * name, so it belongs in the id. But a `using` reaches *across* projects — a
183
- * test project referencing the app project is the ordinary case — so keying
184
- * resolution by assembly too would silently drop every cross-project edge.
144
+ * The same names, keyed by fully-qualified name alone. Identity and
145
+ * resolution want different indexes: assembly separates two declarations of
146
+ * one name (belongs in the id), but a `using` reaches across projects (a
147
+ * test project referencing the app is ordinary), so keying resolution by
148
+ * assembly too would drop cross-project edges.
185
149
  *
186
- * `<ProjectReference>` is not read, so which projects may see each other is
187
- * unknown here. The rule that follows is the precision rule: prefer a match in
188
- * the referencing file's own assembly, and otherwise accept a match only when
189
- * exactly one assembly declares the name. Two claimants is an ambiguity to
190
- * disclose, not a tie to break.
150
+ * `<ProjectReference>` isn't read, so cross-project visibility is unknown.
151
+ * Precision rule: prefer a match in the referencing file's own assembly,
152
+ * else accept only if exactly one assembly declares the name — two
153
+ * claimants is an ambiguity to disclose, not a tie to break.
191
154
  */
192
155
  const byFqn = new Map();
193
156
  const moduleIdOf = new Map();
194
157
  const moduleKeys = new Map();
195
- // Sorted, so the primary declaration of a partial type is a property of the
196
- // repository and not of filesystem enumeration order. DEC-083 requires this:
197
- // "lexicographically first" is only reproducible if the input is ordered.
158
+ // Sorted: DEC-083's "lexicographically first" is only reproducible if the
159
+ // input is ordered, not left to filesystem enumeration order.
198
160
  const units = [...input.files].sort((a, b) => a.file.localeCompare(b.file));
199
161
  const keyOf = (unit, fqn) => `${unit.assembly}::${fqn}`;
200
162
  for (const unit of units) {
@@ -287,10 +249,9 @@ export function extract(input) {
287
249
  : {
288
250
  orm: "efcore",
289
251
  ...(model.table === null ? {} : { table: model.table }),
290
- // **Not gated on `input.reached`.** A mapped column is a
291
- // declaration the framework itself enforces, not an inference, so
292
- // its resolution is the evidence's rather than the run's (DEC-058)
293
- // — the same exception `adapter-java` records for JPA.
252
+ // Not gated on `input.reached`: a mapped column is a framework
253
+ // declaration, not an inference — resolution is the evidence's,
254
+ // not the run's (DEC-058), same exception as adapter-java's JPA.
294
255
  ...(model.columns.length > 0
295
256
  ? {
296
257
  fields: model.columns.map((column) => ({
@@ -333,10 +294,9 @@ export function extract(input) {
333
294
  const id = isCase
334
295
  ? nodeId(scope, "TEST_CASE", testCaseQsp(holder, [unit.namespace, ...fn.ownerPath].filter((s) => s !== ""), fn.name), LANGUAGE)
335
296
  : nodeId(scope, "FUNCTION", symbolQsp(holder, [ownerFqn, fn.name]), LANGUAGE);
336
- // An overload set is one node. C# picks between overloads by argument
337
- // types, which is R3 evidence this adapter does not have — so an edge to
338
- // "the method called Format" is the honest claim, and pretending to have
339
- // chosen an overload would be a claim it cannot support.
297
+ // An overload set is one node: choosing between overloads needs argument
298
+ // types (R3), which this adapter doesn't have — "the method called
299
+ // Format" is the honest claim.
340
300
  if (owner.methods.has(fn.name))
341
301
  continue;
342
302
  owner.methods.set(fn.name, { id, fn, unit, ownerFqn, isTest: isCase });
@@ -358,24 +318,18 @@ export function extract(input) {
358
318
  });
359
319
  }
360
320
  }
361
- // -------------------------------------------------------------------------
362
- // Resolution
363
- // -------------------------------------------------------------------------
321
+ // Resolution.
364
322
  /**
365
- * Every fully-qualified name a written name could denote, in this file.
366
- *
367
- * C# searches the enclosing namespaces outward and then the imports, and a
368
- * name found under two imports is genuinely ambiguous without the full type
369
- * environment. Candidates are collected rather than short-circuited so that
370
- * ambiguity can be *seen* instead of silently resolved to whichever rule ran
371
- * first.
323
+ * Every fully-qualified name a written name could denote, in this file. C#
324
+ * searches enclosing namespaces outward then imports; a name found under
325
+ * two imports is genuinely ambiguous without the full type environment, so
326
+ * candidates are collected rather than short-circuited — ambiguity is seen,
327
+ * not silently resolved to whichever rule ran first.
372
328
  */
373
329
  /**
374
- * `global using` declarations, by assembly.
375
- *
376
- * They are in force for every file in their project, so they are gathered once
377
- * and consulted for every unit — the alternative is that a name introduced by
378
- * one resolves nowhere else, which is the whole point of the construct.
330
+ * `global using` declarations, by assembly — in force for every file in
331
+ * their project, gathered once. Otherwise a name introduced by one file
332
+ * would resolve nowhere else, defeating the construct's point.
379
333
  */
380
334
  const globalUsings = new Map();
381
335
  for (const unit of units) {
@@ -395,13 +349,10 @@ export function extract(input) {
395
349
  return [];
396
350
  const found = new Set();
397
351
  const visible = usingsFor(unit);
398
- // **Enclosing TYPES are searched before enclosing namespaces.** A nested
399
- // type written by its simple name from inside the type that declares it —
400
- // `DbLock.EnterWrite(...)` inside `PessimisticLockBehavior`, which declares
401
- // `private sealed class DbLock` — resolved nowhere, because only the
402
- // namespace chain was walked. Innermost wins outright, as in C#: returning
403
- // here rather than adding to `found` keeps a nested `Foo` from colliding
404
- // with a namespace-level `Foo` and being reported ambiguous.
352
+ // Enclosing TYPES are searched before namespaces: a nested type used by
353
+ // its simple name from inside its declaring type resolved nowhere when
354
+ // only the namespace chain was walked. Innermost wins outright — return
355
+ // here (not `found`) so a nested `Foo` never collides with a namespace `Foo`.
405
356
  if (host !== undefined) {
406
357
  let scope_ = host.fqn;
407
358
  while (scope_ !== "") {
@@ -504,22 +455,16 @@ export function extract(input) {
504
455
  }
505
456
  return undefined;
506
457
  }
507
- // ---------------------------------------------------------------------------
508
- // D-FIX-3's client-base extractor (DEC-164) — see `client-base.ts`'s own
509
- // header for the shape and why it is scoped exactly here.
510
- // ---------------------------------------------------------------------------
458
+ // Client-base extractor (DEC-164) — see `client-base.ts` for shape and scope.
511
459
  /**
512
- * Does `rawName`'s own declaration — resolved in `unit`/`host`'s context,
513
- * the same resolution every other type reference in this file goes
514
- * through — carry `WebApplicationFactory<` in its written base list,
515
- * directly or through one further in-repo indirection?
460
+ * Does `rawName`'s declaration (resolved in `unit`/`host`'s context, same
461
+ * as any other type reference) carry `WebApplicationFactory<` in its
462
+ * written base list, directly or via one further in-repo indirection?
516
463
  *
517
- * `WebApplicationFactory<TEntryPoint>` is itself external (`Microsoft.
518
- * AspNetCore.Mvc.Testing`), so `resolveType` never resolves it as a
519
- * `DeclaredType` and `ancestry` never yields it — this reads the *written*
520
- * text of each yielded type's own base list instead of trying to resolve
521
- * that final hop, the same reason `baseListWritten` exists rather than
522
- * `baseList` alone (see `CsType.baseListWritten`'s own doc).
464
+ * `WebApplicationFactory<TEntryPoint>` is external (`Microsoft.AspNetCore.Mvc.Testing`),
465
+ * so `resolveType`/`ancestry` never yield it — this reads the written text
466
+ * of each yielded type's base list instead, same reason `baseListWritten`
467
+ * exists rather than `baseList` alone.
523
468
  */
524
469
  function derivesFromWebApplicationFactory(rawName, unit, host) {
525
470
  const resolved = resolveType(rawName, unit, host);
@@ -534,10 +479,10 @@ export function extract(input) {
534
479
  return false;
535
480
  }
536
481
  /**
537
- * The one traceable base-setting mechanism this row builds. `receiverType`
538
- * is the *written* type name of whatever received a `.CreateClient()` call
539
- * — see `client-base.ts`'s own header for why `IHttpClientFactory`'s named-
540
- * client form and every other DI shape are deliberately left untouched.
482
+ * The one traceable base-setting mechanism built here. `receiverType` is
483
+ * the written type name of whatever received `.CreateClient()` — see
484
+ * `client-base.ts` for why `IHttpClientFactory`'s named-client form and
485
+ * other DI shapes are deliberately left untouched.
541
486
  */
542
487
  function creatorBaseOf(receiverType, unit, host) {
543
488
  if (receiverType === undefined)
@@ -545,21 +490,17 @@ export function extract(input) {
545
490
  if (!derivesFromWebApplicationFactory(receiverType, unit, host))
546
491
  return undefined;
547
492
  // `WebApplicationFactory<T>.CreateClient()`'s documented default
548
- // `BaseAddress` is `http://localhost/` — origin only, no path. No
549
- // corpus witness in this row overrides it (`WebApplicationFactoryClientOptions`
550
- // is never constructed with its own `BaseAddress` in any of the six), so
551
- // that override is not read here — disclosed, not silently assumed.
493
+ // `BaseAddress` is `http://localhost/`, origin only. No corpus witness
494
+ // overrides it, so the override path isn't read — disclosed, not assumed.
552
495
  return { state: "resolved", path: "" };
553
496
  }
554
497
  /**
555
- * `var client = _factory.CreateClient();` — resolved from `scope_.pending`
556
- * *before* `settle()` runs, because `settle()`'s own fallback (`locals.set(name,
557
- * null)`) is exactly the silence this row exists to fix: `CreateClient` is
558
- * never declared in-repo (it is inherited from an external base), so
559
- * `findMethod` never finds it and every such local stayed permanently
560
- * unresolved before this function existed — invisible to `clientCalls`,
561
- * not even ledgered. Mutates `scope_` in place (same contract `settle`
562
- * already has) and returns the per-local bases this pass resolved.
498
+ * `var client = _factory.CreateClient();` — read from `scope_.pending`
499
+ * before `settle()` runs, because `settle()`'s fallback nulls exactly this:
500
+ * `CreateClient` is never declared in-repo (inherited from an external
501
+ * base), so every such local was permanently unresolved, invisible to
502
+ * `clientCalls`, before this existed. Mutates `scope_` in place and returns
503
+ * the per-local bases resolved.
563
504
  */
564
505
  function resolveClientBaseLocals(scope_, unit, host) {
565
506
  const bases = new Map();
@@ -588,11 +529,9 @@ export function extract(input) {
588
529
  }
589
530
  /**
590
531
  * A property or field's own base, traced once per `DeclaredType` and
591
- * cached — a field's inline initialiser (`memberProvenance`, read at the
592
- * declaration) or a `.ctor`'s own assignment (`propertyAssignments`, read
593
- * from the constructor's body) are the two shapes measured; the first
594
- * writer for a given name wins, matching `findMember`'s own "nearest
595
- * declaration" precedence.
532
+ * cached. Two shapes: a field's inline initialiser (`memberProvenance`) or
533
+ * a `.ctor`'s assignment (`propertyAssignments`) — first writer wins,
534
+ * matching `findMember`'s "nearest declaration" precedence.
596
535
  */
597
536
  const memberClientBaseCache = new Map();
598
537
  function memberClientBase(declared) {
@@ -657,18 +596,15 @@ export function extract(input) {
657
596
  return resolveType(written, unit, host);
658
597
  }
659
598
  /**
660
- * The receiver's type **as written**, without resolving it to a declaration —
661
- * `HttpClient` has none in this repository, and `ownerOfReceiver` above would
662
- * refuse it for exactly that reason. D-FIX-3 needs the name, not the node, so
663
- * this stops one step earlier: same lookup order (`locals`, then the host
664
- * type's own members), same `null`-is-poisoned handling, no `resolveType` call.
599
+ * The receiver's type as written, without resolving to a declaration —
600
+ * `HttpClient` has none in this repo, so `ownerOfReceiver` would refuse it.
601
+ * This needs the name, not the node: same lookup order (`locals`, then host
602
+ * members), same `null`-is-poisoned handling, no `resolveType` call.
665
603
  *
666
- * `ref.receiverProperty` is resolved only through `this` — `this._client.Get()`
667
- * — not through an arbitrary chain (`_service.Client.Get()`), which is a
668
- * disclosed limit rather than an attempt to walk a chain this reader does not
669
- * track elsewhere. `ref.receiverCall` (`Factory.CreateClient().Get()`) is left
670
- * unresolved for the same reason Java's chained builder is: no receiver here
671
- * has a written type, so nothing is silently guessed.
604
+ * Disclosed limit: `ref.receiverProperty` resolves only through `this`
605
+ * (`this._client.Get()`), not an arbitrary chain (`_service.Client.Get()`).
606
+ * `ref.receiverCall` (`Factory.CreateClient().Get()`) stays unresolved for
607
+ * the same reason as Java's chained builder — nothing is silently guessed.
672
608
  */
673
609
  function writtenReceiverType(ref, scope_, host) {
674
610
  if (ref.receiverType !== undefined)
@@ -720,16 +656,14 @@ export function extract(input) {
720
656
  scope_.locals.set(name, null);
721
657
  scope_.pending.clear();
722
658
  }
723
- // -------------------------------------------------------------------------
724
- // Edges
725
- // -------------------------------------------------------------------------
659
+ // Edges.
726
660
  for (const unit of units) {
727
661
  const moduleId = moduleIdOf.get(unit.file);
728
662
  if (moduleId === undefined)
729
663
  continue;
730
- // IMPORTS. A C# `using` names a *namespace*, so like Go's package import it
731
- // fans out — to every file declaring a type in that namespace. That is not
732
- // an approximation: importing a namespace makes every type in it visible.
664
+ // IMPORTS. A `using` names a namespace, so like Go's package import it
665
+ // fans out to every file declaring a type in it — not an approximation,
666
+ // importing a namespace makes every type in it visible.
733
667
  for (const using of unit.usings) {
734
668
  const targets = new Set();
735
669
  for (const declared of types.values()) {
@@ -769,30 +703,23 @@ export function extract(input) {
769
703
  const declared = host?.methods.get(fn.name);
770
704
  if (declared === undefined || declared.unit.file !== unit.file || declared.fn !== fn)
771
705
  continue;
772
- // D-FIX-3's client-base extractor (DEC-164): `var client =
773
- // _factory.CreateClient();` must be read out of `fn.scope.pending`
774
- // *before* `settle()` runs — `settle()`'s own fallback nulls out
775
- // anything it cannot resolve, and `CreateClient` is never declared
776
- // in-repo (inherited from an external base), so every such local
777
- // stayed permanently, silently unresolved before this pass existed.
706
+ // Client-base extractor (DEC-164): must run before `settle()`, whose
707
+ // fallback nulls out anything unresolved (see `resolveClientBaseLocals`).
778
708
  const localClientBase = resolveClientBaseLocals(fn.scope, unit, host);
779
709
  settle(fn.scope, unit, host);
780
- // The caller's half of the HTTP boundary (DEC-241 §4). Read alongside
781
- // the general ref loop below rather than inside it — `clientCalls` owns
782
- // its own candidate predicate (`candidateClientRefs`), the same
783
- // separation Java's `client.ts` keeps from `extract.ts`'s general
784
- // `CALLS` handling.
710
+ // Caller's half of the HTTP boundary (DEC-241 §4). Read alongside the
711
+ // general ref loop, not inside it — `clientCalls` owns its own
712
+ // candidate predicate, same separation Java's `client.ts` keeps.
785
713
  {
786
714
  const clientBaseOf = (ref) => {
787
715
  // A local traced through `resolveClientBaseLocals` above.
788
716
  const local = localClientBase.get(ref.receiver);
789
717
  if (local !== undefined)
790
718
  return local;
791
- // `this.Client` / a bare `Client` reaching a member on the host type.
792
- // A bare receiver only ever names a member here when it is not
793
- // *also* a local or parameter — same precedence `writtenReceiverType`
794
- // already applies, so a local that happens to shadow a same-named
795
- // member is never misattributed to the member's own base.
719
+ // `this.Client` / a bare `Client` reaching a host member. Only
720
+ // counts when the receiver isn't also a local or parameter — same
721
+ // precedence `writtenReceiverType` applies, so a shadowing local is
722
+ // never misattributed to the member's own base.
796
723
  const propertyName = ref.receiverProperty !== undefined
797
724
  ? ref.receiverProperty.of === "this"
798
725
  ? ref.receiverProperty.name
@@ -805,12 +732,10 @@ export function extract(input) {
805
732
  if (member !== undefined)
806
733
  return member;
807
734
  }
808
- // `AddHttpClient<TClient>(...)` — DI hands the configured client
809
- // straight to `TClient`'s own constructor, so a call inside
810
- // `TClient` whose receiver reached here untraced is exactly that
811
- // typed client. Matched against the host's own simple name — see
812
- // `typedClientBases`'s own doc for why this is textual rather than
813
- // a full type resolution.
735
+ // `AddHttpClient<TClient>(...)`: DI hands the configured client
736
+ // straight to `TClient`'s constructor, so an untraced receiver
737
+ // inside `TClient` is exactly that typed client — matched against
738
+ // the host's simple name (see `typedClientBases` for why textual).
814
739
  if (host !== undefined) {
815
740
  const simple = host.fqn.slice(host.fqn.lastIndexOf(".") + 1);
816
741
  const typed = typedClientBaseMap.get(simple);
@@ -826,11 +751,9 @@ export function extract(input) {
826
751
  functionClientBase.set(call.fromId, call.clientBase);
827
752
  }
828
753
  for (const refusal of read.refusals) {
829
- // A refusal still carries `clientBase` when DEC-164's own
830
- // `unresolved` state is *why* it refused — golden pattern 08b binds
831
- // its `caller_unresolved_base` role to the `FUNCTION` even though
832
- // no `USES_API` edge exists to look at, so the node is the only
833
- // place that fact can live.
754
+ // A refusal still carries `clientBase` when DEC-164's `unresolved`
755
+ // state is why it refused — golden 08b binds `caller_unresolved_base`
756
+ // to the `FUNCTION` node, the only place the fact can live.
834
757
  if (refusal.clientBase !== undefined && !functionClientBase.has(refusal.fromId)) {
835
758
  functionClientBase.set(refusal.fromId, refusal.clientBase);
836
759
  }
@@ -869,11 +792,9 @@ export function extract(input) {
869
792
  }
870
793
  }
871
794
  /**
872
- * DEC-084 in one function.
873
- *
874
- * Position is a specification guarantee, not a convention: a class has at most
875
- * one base class and it must come first. So only the first entry of a class
876
- * base list is ever ambiguous, and everything else is decided by the language.
795
+ * DEC-084 in one function: base-list position is a language guarantee, not
796
+ * a convention, so only the first entry of a class base list is ever
797
+ * ambiguous — everything else is decided by the language.
877
798
  */
878
799
  function emitBaseList(declared, part, unit) {
879
800
  part.baseList.forEach((raw, index) => {
@@ -994,31 +915,24 @@ export function extract(input) {
994
915
  }
995
916
  return ownerOfReceiver(ref.receiver, scope_, unit, host);
996
917
  }
997
- // -------------------------------------------------------------------------
998
- // Routes
999
- // -------------------------------------------------------------------------
1000
- //
1001
- // R2 and no lower, for the reason `adapter-go` records and `adapter-java`
1002
- // repeats: telling a controller from an ordinary class is provenance, and a
1003
- // route emitted from the R0 or R1 rung is a claim stronger than the run that
1004
- // produced it — which the Normaliser rejects as RESOLUTION_EXCEEDS_BATCH.
918
+ // Routes. R2 and no lower (adapter-go/adapter-java's reason too): telling a
919
+ // controller from an ordinary class is provenance, and an R0/R1 route is a
920
+ // stronger claim than the run supports — Normaliser rejects it as
921
+ // RESOLUTION_EXCEEDS_BATCH.
1005
922
  //
1006
- // Gated on `reached >= 2` alone, not on `frameworkFiles.length > 0` — a
1007
- // repository can make outbound `HttpClient` calls without being an ASP.NET
1008
- // server itself (a pure API-consuming client, an integration test project),
1009
- // so the caller's half below must not depend on route files existing. Only
1010
- // the route loop itself stays gated on ASP.NET files being present.
923
+ // Gated on `reached >= 2` alone, not `frameworkFiles.length > 0`: a repo can
924
+ // make outbound `HttpClient` calls without being an ASP.NET server itself,
925
+ // so the caller's half must not depend on route files existing. Only the
926
+ // route loop itself is gated on ASP.NET files being present.
1011
927
  if (input.reached >= 2) {
1012
928
  const routeNodes = new Map();
1013
929
  const endpointNodes = new Map();
1014
930
  if (frameworkFiles.length > 0) {
1015
931
  const census = aspNetRoutes({ files: frameworkFiles });
1016
- // D-FIX-3's own finding: `census.unresolvedBaseSites` was computed
1017
- // correctly and never reached this ledger before this row — a real,
1018
- // disclosed-by-the-adapter-internally refusal that was nonetheless a
1019
- // true silence from `emit()`'s own caller's point of view. `SERVES_API`
1020
- // is the edge type this class's controller status would have produced
1021
- // evidence for, had the base resolved.
932
+ // `census.unresolvedBaseSites` was computed correctly but never reached
933
+ // this ledger before — a real refusal that was still a silence from
934
+ // `emit()`'s caller's point of view. `SERVES_API` is the edge type this
935
+ // class's controller status would have produced, had the base resolved.
1022
936
  const unitByFile = new Map(units.map((each) => [each.file, each]));
1023
937
  for (const site of census.unresolvedBaseSites) {
1024
938
  const unit = unitByFile.get(site.file);
@@ -1055,10 +969,9 @@ export function extract(input) {
1055
969
  },
1056
970
  });
1057
971
  }
1058
- // Workspace-scoped, fileless, language-less, and minted with the SAME
1059
- // `endpointQsp` every other producer uses — DEC-115's one hard
1060
- // constraint. Two producers that mint different ids do not conflict,
1061
- // they silently fail to join.
972
+ // Workspace-scoped, fileless, language-less, minted with the SAME
973
+ // `endpointQsp` every producer uses (DEC-115): mismatched ids don't
974
+ // conflict, they silently fail to join.
1062
975
  const endpointId = nodeId(scope, "API_ENDPOINT", endpointQsp(route.method, route.written), null);
1063
976
  if (!endpointNodes.has(endpointId)) {
1064
977
  endpointNodes.set(endpointId, {
@@ -1076,12 +989,10 @@ export function extract(input) {
1076
989
  push({ from: routeId, to: endpointId, type: "SERVES_API" }, 2);
1077
990
  }
1078
991
  }
1079
- // --- The caller's half ---------------------------------------------------
1080
- //
1081
- // Mints into the SAME `endpointNodes` map, so a call to a route this run
1082
- // also read produces one node with two edges rather than two nodes with
1083
- // one each. A call to a route this run did *not* read still mints its
1084
- // endpoint — either side can exist without the other (golden 08's note).
992
+ // Caller's half. Mints into the SAME `endpointNodes` map, so a call to a
993
+ // route this run also read produces one node with two edges, not two. A
994
+ // call to an unread route still mints its endpoint — either side can
995
+ // exist without the other (golden 08).
1085
996
  for (const call of clientCallSites) {
1086
997
  const endpoint = endpointFor(scope, call, input.producedBy, normaliseEndpointPath);
1087
998
  if (!endpointNodes.has(endpoint.id))
@@ -1090,24 +1001,19 @@ export function extract(input) {
1090
1001
  from: call.fromId,
1091
1002
  to: endpoint.id,
1092
1003
  type: "USES_API",
1093
- // DEC-164's contract: every `USES_API` edge this adapter mints now
1094
- // carries `clientBase` — `resolved` (a traced base composed per
1095
- // DEC-097) or `none` (no traced mechanism; the call's own literal
1096
- // path stood alone, unchanged from before this row). See
1097
- // `client-base.ts`'s own header for why `unresolved` is never
1098
- // produced by this build.
1004
+ // DEC-164: every `USES_API` edge carries `clientBase` — `resolved`
1005
+ // (traced base, DEC-097) or `none` (no traced mechanism, literal
1006
+ // path stands alone). See `client-base.ts` for why `unresolved` is
1007
+ // never produced by this build.
1099
1008
  attrs: { callerKind: call.callerKind, via: call.method, clientBase: call.clientBase },
1100
1009
  }, 2);
1101
1010
  }
1102
1011
  nodes.push(...routeNodes.values(), ...endpointNodes.values());
1103
1012
  }
1104
- // D-FIX-3's client-base extractor (DEC-164): patch `attrs.clientBase` onto
1105
- // the caller `FUNCTION`/`TEST_CASE` node itself, not just the `USES_API`
1106
- // edge — golden patterns 08a/08b/08c bind their `caller_*` role to the
1107
- // node, and 08b's refused call has no edge for the fact to live on at all.
1108
- // Done here, after every unit's calls and refusals are known, because
1109
- // `functionNodeIndex` and `functionClientBase` are both built incrementally
1110
- // across the whole loop above.
1013
+ // Client-base extractor (DEC-164): patch `attrs.clientBase` onto the caller
1014
+ // node itself, not just the edge — golden 08a/08b/08c bind `caller_*` to
1015
+ // the node, and 08b's refused call has no edge to carry the fact. Done here
1016
+ // since `functionNodeIndex`/`functionClientBase` build incrementally above.
1111
1017
  for (const [fnId, base] of functionClientBase) {
1112
1018
  const index = functionNodeIndex.get(fnId);
1113
1019
  const node = index === undefined ? undefined : nodes[index];