@descryy/adapter-csharp 0.4.1 → 0.5.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/dto.d.ts ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `DTO` for C# — the transport shapes a ROUTE declares, never a type that
3
+ * looks like one.
4
+ *
5
+ * ## The rule this keeps
6
+ *
7
+ * DEC-043 refused `DTO` because the corpus supplies structurally identical
8
+ * types separable only by name, and `*Request`/`*Response` -> `DTO` is a
9
+ * heuristic. **Nothing in this file reads the candidate type to decide whether
10
+ * it is one.** A type becomes a `DTO` because an ASP.NET Core controller names
11
+ * it, under two rules the framework states itself:
12
+ *
13
+ * - a controller action's **return type is the response body** — a
14
+ * `[ApiController]` (or `ControllerBase`-derived) action is serialized
15
+ * straight to the response, with no view resolution in the path;
16
+ * - **`[FromBody]` on a parameter** says the parameter's type is bound from
17
+ * the request body.
18
+ *
19
+ * Both markers are on the action. The entity beside them keeps its fields and
20
+ * its name and stays a `CLASS`, which is the discrimination golden pattern 07
21
+ * exists to test.
22
+ *
23
+ * ## Refusals
24
+ *
25
+ * - **`[FromRoute]`/`[FromQuery]`/`[FromHeader]`/`[FromServices]` are
26
+ * parameter attributes and are not bodies**, and an unattributed parameter
27
+ * declares nothing either. `[ApiController]`'s binding *inference* would
28
+ * treat a complex-typed parameter as a body with no attribute at all; that
29
+ * is real ASP.NET and deliberately NOT read here — it is an inference rule
30
+ * layered on top of the written one, and admitting it would mint a `DTO` for
31
+ * every service and options object an action happens to take.
32
+ * - **A framework or built-in return type names no shape**: `IActionResult`,
33
+ * `ActionResult<T>`, `Task<T>`, `string`, `void`, a collection. `T` is a
34
+ * generic argument and resolving it is a read this file does not do, so it
35
+ * refuses rather than minting the wrapper.
36
+ * - **A type resolving to no declaration here, or to two, is refused** — a
37
+ * `DTO` with no shape joins nothing, and the wrong one carries the wrong
38
+ * shape into a comparison.
39
+ * - **An EF Core entity stays a `MODEL`** (applied by the caller, which holds
40
+ * both maps): a wrong `MODEL` is a claim about a database, a wrong `DTO` a
41
+ * claim about a wire format.
42
+ *
43
+ * ## Field NAMES, and deliberately not field detail
44
+ *
45
+ * `attrs.fields` carries declared member names — a name-level fact, which rule
46
+ * 3 permits at R2. No `fieldDetail`, no `shapeResolution`, no resolution
47
+ * raise, and `shape-reach.ts` untouched: whether a plain class body's member
48
+ * list is R3-grade evidence is a filed, unanswered question with fleet-wide
49
+ * blast radius. Golden 05's note asks for exactly this shape.
50
+ */
51
+ import type { CsFile } from "./parse.ts";
52
+ /** The fully-qualified name, spelled exactly as `extract.ts`'s `qualify` does —
53
+ * the key `modelsByName` and the type table both use, so one lookup answers both.
54
+ * Exported for `shape-reach.ts`/`json.ts`, which need the identical key at
55
+ * `prepare()` time, before `extract.ts`'s own copy has run. */
56
+ export declare function qualify(namespace: string, path: readonly string[]): string;
57
+ /** The declaration marker every `DTO` this file mints names in `attrs.schema`. */
58
+ export declare const SCHEMA = "aspnetcore";
59
+ export interface TransportShape {
60
+ /** Declared member names, sorted. Names only — see the header. */
61
+ readonly fields: readonly string[];
62
+ readonly roles: readonly ("request" | "response")[];
63
+ }
64
+ /**
65
+ * Every type an ASP.NET action declares as a request or response body, across
66
+ * the whole batch. Keyed by the same `path.join(".")` identity the caller's
67
+ * own type map uses.
68
+ *
69
+ * Resolution is by simple name against the declared types — the reach this
70
+ * adapter has. A name declared twice is refused rather than ranked.
71
+ */
72
+ export declare function readTransportShapes(files: readonly CsFile[]): ReadonlyMap<string, TransportShape>;
73
+ //# sourceMappingURL=dto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dto.d.ts","sourceRoot":"","sources":["../src/dto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAU,MAAM,YAAY,CAAC;AAEjD;;;gEAGgE;AAChE,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAG1E;AAED,kFAAkF;AAClF,eAAO,MAAM,MAAM,eAAe,CAAC;AAoEnC,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,SAAS,GAAG,UAAU,CAAC,EAAE,CAAC;CACrD;AAwBD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,CAgDrC"}
package/dist/dto.js ADDED
@@ -0,0 +1,205 @@
1
+ /**
2
+ * `DTO` for C# — the transport shapes a ROUTE declares, never a type that
3
+ * looks like one.
4
+ *
5
+ * ## The rule this keeps
6
+ *
7
+ * DEC-043 refused `DTO` because the corpus supplies structurally identical
8
+ * types separable only by name, and `*Request`/`*Response` -> `DTO` is a
9
+ * heuristic. **Nothing in this file reads the candidate type to decide whether
10
+ * it is one.** A type becomes a `DTO` because an ASP.NET Core controller names
11
+ * it, under two rules the framework states itself:
12
+ *
13
+ * - a controller action's **return type is the response body** — a
14
+ * `[ApiController]` (or `ControllerBase`-derived) action is serialized
15
+ * straight to the response, with no view resolution in the path;
16
+ * - **`[FromBody]` on a parameter** says the parameter's type is bound from
17
+ * the request body.
18
+ *
19
+ * Both markers are on the action. The entity beside them keeps its fields and
20
+ * its name and stays a `CLASS`, which is the discrimination golden pattern 07
21
+ * exists to test.
22
+ *
23
+ * ## Refusals
24
+ *
25
+ * - **`[FromRoute]`/`[FromQuery]`/`[FromHeader]`/`[FromServices]` are
26
+ * parameter attributes and are not bodies**, and an unattributed parameter
27
+ * declares nothing either. `[ApiController]`'s binding *inference* would
28
+ * treat a complex-typed parameter as a body with no attribute at all; that
29
+ * is real ASP.NET and deliberately NOT read here — it is an inference rule
30
+ * layered on top of the written one, and admitting it would mint a `DTO` for
31
+ * every service and options object an action happens to take.
32
+ * - **A framework or built-in return type names no shape**: `IActionResult`,
33
+ * `ActionResult<T>`, `Task<T>`, `string`, `void`, a collection. `T` is a
34
+ * generic argument and resolving it is a read this file does not do, so it
35
+ * refuses rather than minting the wrapper.
36
+ * - **A type resolving to no declaration here, or to two, is refused** — a
37
+ * `DTO` with no shape joins nothing, and the wrong one carries the wrong
38
+ * shape into a comparison.
39
+ * - **An EF Core entity stays a `MODEL`** (applied by the caller, which holds
40
+ * both maps): a wrong `MODEL` is a claim about a database, a wrong `DTO` a
41
+ * claim about a wire format.
42
+ *
43
+ * ## Field NAMES, and deliberately not field detail
44
+ *
45
+ * `attrs.fields` carries declared member names — a name-level fact, which rule
46
+ * 3 permits at R2. No `fieldDetail`, no `shapeResolution`, no resolution
47
+ * raise, and `shape-reach.ts` untouched: whether a plain class body's member
48
+ * list is R3-grade evidence is a filed, unanswered question with fleet-wide
49
+ * blast radius. Golden 05's note asks for exactly this shape.
50
+ */
51
+ /** The fully-qualified name, spelled exactly as `extract.ts`'s `qualify` does —
52
+ * the key `modelsByName` and the type table both use, so one lookup answers both.
53
+ * Exported for `shape-reach.ts`/`json.ts`, which need the identical key at
54
+ * `prepare()` time, before `extract.ts`'s own copy has run. */
55
+ export function qualify(namespace, path) {
56
+ const joined = path.join(".");
57
+ return namespace === "" ? joined : `${namespace}.${joined}`;
58
+ }
59
+ /** The declaration marker every `DTO` this file mints names in `attrs.schema`. */
60
+ export const SCHEMA = "aspnetcore";
61
+ /** ASP.NET's own statement that a parameter is bound from the request body. */
62
+ const BODY_PARAMETER = "FromBody";
63
+ /** Action attributes that make a method a route handler at all. */
64
+ const VERB_ATTRIBUTES = new Set([
65
+ "HttpGet",
66
+ "HttpPost",
67
+ "HttpPut",
68
+ "HttpPatch",
69
+ "HttpDelete",
70
+ "HttpHead",
71
+ "HttpOptions",
72
+ "Route",
73
+ ]);
74
+ /**
75
+ * Framework plumbing and built-ins. A return type here is refused: the real
76
+ * payload of `ActionResult<T>`/`Task<T>` is its type argument, and this reader
77
+ * does not resolve generics.
78
+ */
79
+ const NOT_A_SHAPE = new Set([
80
+ "IActionResult",
81
+ "ActionResult",
82
+ "Task",
83
+ "ValueTask",
84
+ "IResult",
85
+ "JsonResult",
86
+ "ObjectResult",
87
+ "ContentResult",
88
+ "FileResult",
89
+ "IEnumerable",
90
+ "ICollection",
91
+ "IList",
92
+ "List",
93
+ "IQueryable",
94
+ "Dictionary",
95
+ "IDictionary",
96
+ "string",
97
+ "object",
98
+ "void",
99
+ "String",
100
+ "Object",
101
+ "bool",
102
+ "byte",
103
+ "sbyte",
104
+ "char",
105
+ "decimal",
106
+ "double",
107
+ "float",
108
+ "int",
109
+ "uint",
110
+ "long",
111
+ "ulong",
112
+ "short",
113
+ "ushort",
114
+ "Boolean",
115
+ "Int32",
116
+ "Int64",
117
+ "Decimal",
118
+ "Double",
119
+ "Guid",
120
+ "DateTime",
121
+ "DateTimeOffset",
122
+ "Stream",
123
+ ]);
124
+ /**
125
+ * Is this type an ASP.NET controller whose actions return bodies? Read from
126
+ * its own declaration only — `[ApiController]`, or a `Controller`/
127
+ * `ControllerBase` base. `aspnetcore.ts` additionally walks the base chain to
128
+ * admit a controller whose evidence is on an in-repo base class; this reader
129
+ * deliberately does not, because an unresolved base there produces a disclosed
130
+ * refusal while here it would produce a node type, and rule 2 prefers the
131
+ * narrower claim. Measured consequence: a jellyfin-style
132
+ * `XController : BaseJellyfinApiController` yields no DTO, a disclosed gap.
133
+ */
134
+ function declaresBodies(type) {
135
+ if (type.attributes.includes("ApiController"))
136
+ return true;
137
+ return type.baseList.some((base) => base === "Controller" || base === "ControllerBase");
138
+ }
139
+ /** A simple type name this reader is willing to carry, or `undefined`. */
140
+ function shapeName(name) {
141
+ if (name === undefined || name === null || name === "")
142
+ return undefined;
143
+ const bare = name.replace(/\?$/, "");
144
+ return NOT_A_SHAPE.has(bare) ? undefined : bare;
145
+ }
146
+ /**
147
+ * Every type an ASP.NET action declares as a request or response body, across
148
+ * the whole batch. Keyed by the same `path.join(".")` identity the caller's
149
+ * own type map uses.
150
+ *
151
+ * Resolution is by simple name against the declared types — the reach this
152
+ * adapter has. A name declared twice is refused rather than ranked.
153
+ */
154
+ export function readTransportShapes(files) {
155
+ /** Simple name -> key, or `null` once ambiguous. */
156
+ const declared = new Map();
157
+ const fieldsOf = new Map();
158
+ for (const file of files) {
159
+ for (const type of file.types) {
160
+ const key = qualify(file.namespace, type.path);
161
+ fieldsOf.set(key, [...type.members.keys()].sort());
162
+ declared.set(type.name, declared.has(type.name) ? null : key);
163
+ }
164
+ }
165
+ const roles = new Map();
166
+ const record = (simple, role) => {
167
+ if (simple === undefined)
168
+ return;
169
+ const key = declared.get(simple);
170
+ if (key === undefined || key === null)
171
+ return;
172
+ const existing = roles.get(key);
173
+ if (existing === undefined)
174
+ roles.set(key, new Set([role]));
175
+ else
176
+ existing.add(role);
177
+ };
178
+ for (const file of files) {
179
+ for (const type of file.types) {
180
+ if (!declaresBodies(type))
181
+ continue;
182
+ for (const action of file.funcs) {
183
+ // Actions of THIS type only: `ownerPath` is the declaring type's
184
+ // nesting path, so comparing it to the type's own path keeps a second
185
+ // controller in the same file from lending its actions to this one.
186
+ if (action.ownerPath.join(".") !== type.path.join("."))
187
+ continue;
188
+ if (!action.attributes.some((each) => VERB_ATTRIBUTES.has(each)))
189
+ continue;
190
+ record(shapeName(action.returns), "response");
191
+ for (const [name, attributes] of action.scope.parameterAttributes) {
192
+ if (!attributes.includes(BODY_PARAMETER))
193
+ continue;
194
+ record(shapeName(action.scope.locals.get(name)), "request");
195
+ }
196
+ }
197
+ }
198
+ }
199
+ const shapes = new Map();
200
+ for (const [key, directions] of roles) {
201
+ shapes.set(key, { fields: fieldsOf.get(key) ?? [], roles: [...directions].sort() });
202
+ }
203
+ return shapes;
204
+ }
205
+ //# sourceMappingURL=dto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dto.js","sourceRoot":"","sources":["../src/dto.ts"],"names":[],"mappings":"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"}
package/dist/extract.d.ts CHANGED
@@ -64,5 +64,32 @@ export interface Extraction {
64
64
  readonly edges: readonly IREdge[];
65
65
  readonly unresolved: readonly UnresolvedRef[];
66
66
  }
67
+ /**
68
+ * The fallback graph when `extract()` throws mid-run, per
69
+ * `DEC-NEXT-degraded-graph-shape-for-adapters-without-one.md`.
70
+ *
71
+ * `FILE`, not `MODULE`: a C# module's identity here is
72
+ * `assembly::namespace::stem`, and both the assembly (`.csproj` resolution,
73
+ * `projects.ts`) and the namespace (`namespace Foo.Bar;`, a declaration
74
+ * *inside* the file) are exactly the machinery a mid-extraction throw means
75
+ * just failed or cannot be trusted to have finished. Unlike Go's import path
76
+ * or Rust's crate/module path — both resolved from the file's own location
77
+ * plus a manifest, never from the file's parsed content — nothing about a C#
78
+ * file's identity is recoverable without a successful parse. `FILE` (via the
79
+ * shared `@descryy/adapter-common#fileNode` helper) is the fact that
80
+ * survives: the file exists, on disk, at this path, and that is all this
81
+ * function claims.
82
+ *
83
+ * One disclosure row, not one per file — the fact is about the run. Anchored
84
+ * to the first file's `FILE` node because the ledger requires a `fromNodeId`
85
+ * and `FILE` is the only node type this path emits.
86
+ */
87
+ export declare function degradedGraph(input: {
88
+ readonly repo: string;
89
+ readonly workspace: string | undefined;
90
+ readonly producedBy: string;
91
+ readonly files: readonly string[];
92
+ readonly cause: string;
93
+ }): Extraction;
67
94
  export declare function extract(input: ExtractInput): Extraction;
68
95
  //# sourceMappingURL=extract.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAOL,KAAK,aAAa,EAClB,KAAK,MAAM,EACX,KAAK,MAAM,EACX,KAAK,eAAe,EACpB,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,MAAM,EAAwD,MAAM,YAAY,CAAC;AAC/F,OAAO,EAAyB,KAAK,QAAQ,EAAgB,MAAM,aAAa,CAAC;AAGjF,OAAO,EAAuC,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAI7F,eAAO,MAAM,QAAQ,WAAW,CAAC;AA+CjC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/C;;;;;;;;OAQG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,eAAe,CAAC;IAC/D;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,UAAU,EAAE,SAAS,aAAa,EAAE,CAAC;CAC/C;AAgCD,wBAAgB,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,UAAU,CA6jCvD"}
1
+ {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAOL,KAAK,aAAa,EAClB,KAAK,MAAM,EACX,KAAK,MAAM,EACX,KAAK,eAAe,EACpB,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAIrB,OAAO,KAAK,EAAE,MAAM,EAAwD,MAAM,YAAY,CAAC;AAC/F,OAAO,EAAyB,KAAK,QAAQ,EAAgB,MAAM,aAAa,CAAC;AAKjF,OAAO,EAAuC,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAI7F,eAAO,MAAM,QAAQ,WAAW,CAAC;AAmDjC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC;;;;;;;OAOG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/C;;;;;;;;OAQG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,eAAe,CAAC;IAC/D;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,UAAU,EAAE,SAAS,aAAa,EAAE,CAAC;CAC/C;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB,GAAG,UAAU,CA0Bb;AAiCD,wBAAgB,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,UAAU,CAwtCvD"}
package/dist/extract.js CHANGED
@@ -22,7 +22,10 @@
22
22
  * no edge. Extension methods are the same call for the same reason.
23
23
  */
24
24
  import { edgeId, endpointQsp, nodeId, normaliseEndpointPath, symbolQsp, testCaseQsp, } from "@descryy/ir";
25
+ import { fileNode } from "@descryy/adapter-common";
25
26
  import { efModels, isGenerated } from "./efcore.js";
27
+ import { readTransportShapes, SCHEMA as DTO_SCHEMA } from "./dto.js";
28
+ import { hasJsonShapeEvidence } from "./json.js";
26
29
  import { decidedShape, SHAPE_LEVEL } from "./shape-reach.js";
27
30
  import { aspNetRoutes } from "./aspnetcore.js";
28
31
  import { resolveNullable, nullableDirectives } from "./nullability.js";
@@ -52,9 +55,12 @@ const REASONS = {
52
55
  noProject: "no .csproj covers this file, so it belongs to no assembly and nothing else can name its " +
53
56
  "types. Disclosed rather than given an invented identity.",
54
57
  noBaseContext: "`base` in a type whose base class is not in the analysed set.",
55
- handlerUnresolved: "this route's handler could not be resolved to a FUNCTION node: a minimal-API registration " +
56
- "reads only its path argument textually and does not capture the handler expression, or an " +
57
- "attribute-routed handler's declaring type left the analysed set.",
58
+ belowLevel: "resolved to a declaration, but this run was capped below the level that evidence earns. " +
59
+ "Not an analysis gap and not a scope boundary — a limit the caller asked for.",
60
+ handlerUnresolved: "this route's handler could not be resolved to a FUNCTION node: a minimal-API registration's " +
61
+ "second argument is an inline lambda (no name to resolve), names a delegate this run could " +
62
+ "not join to a declared method, or an attribute-routed handler's declaring type left the " +
63
+ "analysed set.",
58
64
  };
59
65
  const INHERITANCE_HOPS = 12;
60
66
  const PENDING_PASSES = 3;
@@ -64,6 +70,49 @@ const PENDING_PASSES = 3;
64
70
  * as an annotation — C# has no naming convention for tests the way XCTest does.
65
71
  */
66
72
  const TEST_ATTRIBUTES = new Set(["Fact", "Theory", "Test", "TestCase", "TestMethod", "DataTestMethod"]);
73
+ /**
74
+ * The fallback graph when `extract()` throws mid-run, per
75
+ * `DEC-NEXT-degraded-graph-shape-for-adapters-without-one.md`.
76
+ *
77
+ * `FILE`, not `MODULE`: a C# module's identity here is
78
+ * `assembly::namespace::stem`, and both the assembly (`.csproj` resolution,
79
+ * `projects.ts`) and the namespace (`namespace Foo.Bar;`, a declaration
80
+ * *inside* the file) are exactly the machinery a mid-extraction throw means
81
+ * just failed or cannot be trusted to have finished. Unlike Go's import path
82
+ * or Rust's crate/module path — both resolved from the file's own location
83
+ * plus a manifest, never from the file's parsed content — nothing about a C#
84
+ * file's identity is recoverable without a successful parse. `FILE` (via the
85
+ * shared `@descryy/adapter-common#fileNode` helper) is the fact that
86
+ * survives: the file exists, on disk, at this path, and that is all this
87
+ * function claims.
88
+ *
89
+ * One disclosure row, not one per file — the fact is about the run. Anchored
90
+ * to the first file's `FILE` node because the ledger requires a `fromNodeId`
91
+ * and `FILE` is the only node type this path emits.
92
+ */
93
+ export function degradedGraph(input) {
94
+ const scope = { repo: input.repo, workspace: input.workspace };
95
+ const nodes = input.files.map((file) => fileNode(scope, file, input.producedBy));
96
+ const anchor = nodes[0];
97
+ const unresolved = anchor === undefined
98
+ ? []
99
+ : [
100
+ {
101
+ fromNodeId: anchor.id,
102
+ edgeType: "IMPORTS",
103
+ rawTarget: "(entire repository)",
104
+ file: anchor.file,
105
+ line: 1,
106
+ producedBy: input.producedBy,
107
+ reason: `NO GRAPH WAS PRODUCED for ${String(input.files.length)} C# file(s). ${input.cause}. ` +
108
+ "Files are listed because their existence and path are facts about the filesystem; their " +
109
+ "contents are absent. Every finding, coverage figure and 'not affected' statement over " +
110
+ "this repository is UNSUPPORTED — this is a failed analysis, not an empty repository.",
111
+ attrs: { refusalClass: "capability-gap" },
112
+ },
113
+ ];
114
+ return { nodes, edges: [], unresolved };
115
+ }
67
116
  function qualify(namespace, path) {
68
117
  const joined = path.join(".");
69
118
  return namespace === "" ? joined : `${namespace}.${joined}`;
@@ -117,6 +166,9 @@ export function extract(input) {
117
166
  * `ViewComponent` carried the entity's columns.
118
167
  */
119
168
  const modelsByName = new Map(orm.models.map((model) => [model.fqn, model]));
169
+ // Transport shapes an ASP.NET ROUTE declares — never a type this reader
170
+ // guessed at from its name (DEC-043). Own file, `dto.ts`.
171
+ const shapesByType = readTransportShapes(input.files);
120
172
  const push = (edge, level) => {
121
173
  if (level > input.reached)
122
174
  return false;
@@ -162,6 +214,8 @@ export function extract(input) {
162
214
  // Sorted: DEC-083's "lexicographically first" is only reproducible if the
163
215
  // input is ordered, not left to filesystem enumeration order.
164
216
  const units = [...input.files].sort((a, b) => a.file.localeCompare(b.file));
217
+ /** Path -> the `CsFile` declared there, for `responseShapeEvidence`'s per-part provenance lookup. */
218
+ const fileByPath = new Map(units.map((unit) => [unit.file, unit]));
165
219
  const keyOf = (unit, fqn) => `${unit.assembly}::${fqn}`;
166
220
  for (const unit of units) {
167
221
  const stem = unit.file.slice(unit.file.lastIndexOf("/") + 1).replace(/\.cs$/i, "");
@@ -204,7 +258,16 @@ export function extract(input) {
204
258
  }
205
259
  // The kind is decided here and the id is minted from it. DEC-004 hashes
206
260
  // the kind, so a MODEL cannot be patched onto a CLASS afterwards.
207
- const kind = modelsByName.has(fqn) ? "MODEL" : "CLASS";
261
+ // An EF Core entity outranks a declared transport shape where both
262
+ // somehow apply: a wrong `MODEL` is a claim about a database and a wrong
263
+ // `DTO` a claim about a wire format, so the narrower evidence wins — the
264
+ // same tie-break, for the same reason, as adapter-python's and
265
+ // adapter-java's.
266
+ const kind = modelsByName.has(fqn)
267
+ ? "MODEL"
268
+ : shapesByType.has(fqn)
269
+ ? "DTO"
270
+ : "CLASS";
208
271
  const declared = {
209
272
  kind,
210
273
  id: nodeId(scope, kind, symbolQsp(holder, [fqn]), LANGUAGE),
@@ -232,13 +295,26 @@ export function extract(input) {
232
295
  declared.members.set(member, { type: written });
233
296
  }
234
297
  const model = declared.kind === "MODEL" ? modelsByName.get(declared.fqn) : undefined;
298
+ const shape = declared.kind === "DTO" ? shapesByType.get(declared.fqn) : undefined;
235
299
  /**
236
300
  * The level this node's strongest fact carries. A `MODEL` bundles two
237
301
  * different claims — *this type is persisted* (a `DbSet<T>` or an
238
302
  * `IEntityTypeConfiguration<T>`, read at R0/R1) and *this is its mapped
239
303
  * shape* — and only the second is R3-grade, so the shape's own level is
240
- * also stated in `attrs.shapeResolution` (DEC-058's carrier, which
241
- * `descry-core`'s contract engine already reads).
304
+ * also stated in `attrs.shapeResolution` (DEC-058's carrier).
305
+ *
306
+ * **Node-scoped, and deliberately unrelated to the `USES_API`-edge
307
+ * `shapeResolution` this file also now writes** (see
308
+ * `responseShapeEvidence` above,
309
+ * `DEC-NEXT-dto-shape-evidence-for-api-contracts`). This one is the EF
310
+ * Core-mapped, PERSISTED database shape — `descry-core`'s contract engine
311
+ * (`engine.ts:286`) reads only the edge-level attribute for API
312
+ * wire-shape comparison and never reads a node-level `shapeResolution` at
313
+ * all, on a `MODEL` or otherwise. A type can be both — an entity that is
314
+ * also serialized straight to the wire — but the two facts rest on
315
+ * different attributes (EF Core's vs. JSON serialization's) and are
316
+ * asserted independently; this one is not the mechanism the API-shape
317
+ * ruling built.
242
318
  *
243
319
  * `decidedShape` and not `columns.length > 0`: a model whose every column
244
320
  * came back `unresolved` has had no shape read (DEC-180 §2 — unknown is
@@ -264,6 +340,18 @@ export function extract(input) {
264
340
  // Written whenever the type has more than one part, so an adjudicator
265
341
  // that searches only `file` has something to search instead.
266
342
  ...(files.length > 1 || partial ? { partial: true, declaredIn: files } : {}),
343
+ ...(shape === undefined
344
+ ? {}
345
+ : {
346
+ // The declaration this node rests on, named the way a `MODEL`
347
+ // names its ORM. NAMES only — no `fieldDetail`, no
348
+ // `shapeResolution`, no level raise: whether a plain class
349
+ // body's member list is R3-grade evidence is a filed,
350
+ // unanswered question and `shape-reach.ts` is untouched.
351
+ schema: DTO_SCHEMA,
352
+ declaredDirections: shape.roles,
353
+ ...(shape.fields.length > 0 ? { fields: shape.fields } : {}),
354
+ }),
267
355
  ...(model === undefined
268
356
  ? {}
269
357
  : {
@@ -438,6 +526,48 @@ export function extract(input) {
438
526
  return { declared: claimants[0] };
439
527
  return { reason: REASONS.ambiguousName };
440
528
  }
529
+ /**
530
+ * `DEC-NEXT-dto-shape-evidence-for-api-contracts`'s ruling, applied at the
531
+ * one place a C# `USES_API` edge states a response type: a call's sole
532
+ * generic type argument (`GetFromJsonAsync<OrderResponse>(url)`, read by
533
+ * `genericArgumentOf` in `parse.ts`). Mirrors `adapter-java`'s
534
+ * `responseShapeEvidence` and `adapter-python`'s Pydantic gate — resolve
535
+ * the written name through the SAME scope rules every other name in this
536
+ * adapter goes through, then require real `System.Text.Json`/
537
+ * `Newtonsoft.Json` evidence before claiming R3, never the type argument
538
+ * alone.
539
+ *
540
+ * Gated on `input.reached >= 3` INSIDE the returned attrs, not by an early
541
+ * return one level up — a guard placed above this function would be green
542
+ * at R3 and silently wrong at R2, the monotonic sweep's own failure mode.
543
+ *
544
+ * A `MODEL` is excluded even when JSON-annotated: that is an EF Core
545
+ * persistence claim (see the comment beside `attrs.shapeResolution` at the
546
+ * `MODEL` node above), and DEC-NEXT scopes this ruling to *wire* shape
547
+ * evidence, a different fact resting on different attributes.
548
+ */
549
+ function responseShapeEvidence(unit, written) {
550
+ if (input.reached < 3 || written === undefined)
551
+ return {};
552
+ const resolved = resolveType(written, unit);
553
+ if (!("declared" in resolved))
554
+ return {};
555
+ const { declared } = resolved;
556
+ if (declared.kind !== "DTO")
557
+ return {}; // a MODEL: persisted shape, not wire shape, or a plain CLASS
558
+ // `parts[i]` was declared in `files[i]` — each file contributes at most
559
+ // one part for a given type (one `unit.types` entry per declaration), so
560
+ // the two arrays grow in lockstep. A part's OWN file gates its OWN
561
+ // provenance: a `using` in one file of a `partial` type does not make an
562
+ // attribute in a different file of it verified.
563
+ const evidenced = declared.parts.some((part, index) => {
564
+ const partFile = fileByPath.get(declared.files[index] ?? declared.unit.file) ?? declared.unit;
565
+ return hasJsonShapeEvidence(partFile, part);
566
+ });
567
+ if (!evidenced)
568
+ return {};
569
+ return { responseType: declared.id, shapeResolution: 3 };
570
+ }
441
571
  /** Every type in a declaration's supertype chain, across all its parts. */
442
572
  function* ancestry(start) {
443
573
  const queue = [start];
@@ -766,7 +896,9 @@ export function extract(input) {
766
896
  return { state: "none" };
767
897
  };
768
898
  const read = clientCalls(fn.scope.refs, declared.id, (ref) => writtenReceiverType(ref, fn.scope, host), declared.isTest, fn.scope.parameterNames, clientBaseOf);
769
- clientCallSites.push(...read.calls);
899
+ for (const call of read.calls) {
900
+ clientCallSites.push({ ...call, ...responseShapeEvidence(unit, call.responseTypeWritten) });
901
+ }
770
902
  for (const call of read.calls) {
771
903
  if (!functionClientBase.has(call.fromId))
772
904
  functionClientBase.set(call.fromId, call.clientBase);
@@ -791,14 +923,32 @@ export function extract(input) {
791
923
  });
792
924
  }
793
925
  }
926
+ /**
927
+ * Mapped-column reads, grouped by the entity they land on. Edge
928
+ * identity is `(from, to, type)`, so a method reading three columns of
929
+ * one entity is **one** edge naming three columns, not three edges — a
930
+ * singular `field` attribute would make the second unrepresentable
931
+ * (DEC-046). Sorted on the way out so the attribute is a property of
932
+ * the source and not of statement order.
933
+ */
934
+ const columnReads = new Map();
794
935
  for (const ref of fn.scope.refs) {
795
936
  switch (ref.kind) {
796
937
  case "dynamic":
797
938
  ledger(declared.id, "CALLS", ref.raw, unit, ref.line, REASONS.valueTarget);
798
939
  break;
799
- case "type":
940
+ case "type": {
941
+ const read = mappedColumnRead(ref, fn.scope, unit, host);
942
+ if (read !== undefined) {
943
+ const columns = columnReads.get(read.to);
944
+ if (columns === undefined)
945
+ columnReads.set(read.to, new Set([read.column]));
946
+ else
947
+ columns.add(read.column);
948
+ }
800
949
  emitType(declared.id, ref, unit, host, fn.scope);
801
950
  break;
951
+ }
802
952
  case "new":
803
953
  emitType(declared.id, ref, unit, host, fn.scope);
804
954
  emitConstructor(declared.id, ref, unit, host, declared.isTest);
@@ -810,8 +960,54 @@ export function extract(input) {
810
960
  break;
811
961
  }
812
962
  }
963
+ for (const [to, columns] of columnReads) {
964
+ if (push({ from: declared.id, to, type: "READS", attrs: { fields: [...columns].sort() } }, 2))
965
+ continue;
966
+ ledger(declared.id, "READS", [...columns].sort().join(", "), unit, fn.startLine, REASONS.belowLevel);
967
+ }
813
968
  }
814
969
  }
970
+ /**
971
+ * `FUNCTION --READS--> MODEL` for `order.TotalAmount` — golden 07.
972
+ *
973
+ * C# needs no accessor convention the way Java does: an EF Core entity's
974
+ * columns **are** its properties, so the read is written directly and the
975
+ * only question is whether the receiver and the member are both known.
976
+ * Three written things have to agree, and no name decides any of them:
977
+ *
978
+ * 1. the access is a **read** — `parse.ts` withholds `ref.member` for an
979
+ * invocation's callee and for an assignment's left-hand side, which
980
+ * are the same tree shape and are, respectively, a method and a write;
981
+ * 2. the receiver's type is written down and resolves, through the same
982
+ * `ownerOfReceiver` every `CALLS` edge here goes through; and
983
+ * 3. the resolved type was admitted as a `MODEL` by `efcore.ts`, and the
984
+ * member is one of the **columns that mapping declares** — not merely
985
+ * a property, so `[NotMapped]` and a computed property are refused.
986
+ *
987
+ * Level 2: the edge names a member. It compares no shape and proves none,
988
+ * and the cap makes a name-level fact R2 — the column's shape is already on
989
+ * the `MODEL` node at R3, carrying its own `shapeResolution`.
990
+ */
991
+ function mappedColumnRead(ref, scope_, unit, host) {
992
+ if (ref.speculative !== true || ref.member === undefined)
993
+ return undefined;
994
+ // `this.X` / `base.X` reach the enclosing type, not a receiver whose type
995
+ // the site wrote down; an entity reading its own column is not the fact
996
+ // golden 07 is about and `push` would drop the self-edge anyway.
997
+ if (ref.name === "this" || ref.name === "base")
998
+ return undefined;
999
+ const owner = ownerOfReceiver(ref.name, scope_, unit, host);
1000
+ if (!("declared" in owner))
1001
+ return undefined;
1002
+ if (owner.declared.kind !== "MODEL")
1003
+ return undefined;
1004
+ const model = modelsByName.get(owner.declared.fqn);
1005
+ if (model === undefined)
1006
+ return undefined;
1007
+ if (!model.columns.some((column) => column.name === ref.member))
1008
+ return undefined;
1009
+ return { to: owner.declared.id, column: ref.member };
1010
+ }
815
1011
  /**
816
1012
  * DEC-084 in one function: base-list position is a language guarantee, not
817
1013
  * a convention, so only the first entry of a class base list is ever
@@ -1047,7 +1243,14 @@ export function extract(input) {
1047
1243
  // (traced base, DEC-097) or `none` (no traced mechanism, literal
1048
1244
  // path stands alone). See `client-base.ts` for why `unresolved` is
1049
1245
  // never produced by this build.
1050
- attrs: { callerKind: call.callerKind, via: call.method, clientBase: call.clientBase },
1246
+ attrs: {
1247
+ callerKind: call.callerKind,
1248
+ via: call.method,
1249
+ clientBase: call.clientBase,
1250
+ ...(call.responseType === undefined
1251
+ ? {}
1252
+ : { responseType: call.responseType, shapeResolution: call.shapeResolution }),
1253
+ },
1051
1254
  }, 2);
1052
1255
  }
1053
1256
  nodes.push(...routeNodes.values(), ...endpointNodes.values());