carrick 0.3.53

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.
Files changed (114) hide show
  1. package/LICENSE.md +99 -0
  2. package/README.md +79 -0
  3. package/bin/carrick.mjs +168 -0
  4. package/dist/channel.d.ts +16 -0
  5. package/dist/channel.js +37 -0
  6. package/dist/channel.js.map +1 -0
  7. package/dist/cli.d.ts +29 -0
  8. package/dist/cli.js +60 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/contract.d.ts +154 -0
  11. package/dist/contract.js +141 -0
  12. package/dist/contract.js.map +1 -0
  13. package/dist/diagnostics.d.ts +54 -0
  14. package/dist/diagnostics.js +199 -0
  15. package/dist/diagnostics.js.map +1 -0
  16. package/dist/hook/post-edit.d.ts +13 -0
  17. package/dist/hook/post-edit.js +76 -0
  18. package/dist/hook/post-edit.js.map +1 -0
  19. package/dist/hook/session-start.d.ts +2 -0
  20. package/dist/hook/session-start.js +44 -0
  21. package/dist/hook/session-start.js.map +1 -0
  22. package/dist/init/identity.d.ts +20 -0
  23. package/dist/init/identity.js +60 -0
  24. package/dist/init/identity.js.map +1 -0
  25. package/dist/init/repos.d.ts +19 -0
  26. package/dist/init/repos.js +46 -0
  27. package/dist/init/repos.js.map +1 -0
  28. package/dist/init/run.d.ts +11 -0
  29. package/dist/init/run.js +228 -0
  30. package/dist/init/run.js.map +1 -0
  31. package/dist/init/settings.d.ts +50 -0
  32. package/dist/init/settings.js +138 -0
  33. package/dist/init/settings.js.map +1 -0
  34. package/dist/log.d.ts +2 -0
  35. package/dist/log.js +35 -0
  36. package/dist/log.js.map +1 -0
  37. package/dist/native.d.ts +48 -0
  38. package/dist/native.js +127 -0
  39. package/dist/native.js.map +1 -0
  40. package/dist/render.d.ts +65 -0
  41. package/dist/render.js +334 -0
  42. package/dist/render.js.map +1 -0
  43. package/dist/root.d.ts +30 -0
  44. package/dist/root.js +82 -0
  45. package/dist/root.js.map +1 -0
  46. package/dist/server.d.ts +2 -0
  47. package/dist/server.js +255 -0
  48. package/dist/server.js.map +1 -0
  49. package/dist/templates.d.ts +18 -0
  50. package/dist/templates.js +66 -0
  51. package/dist/templates.js.map +1 -0
  52. package/package.json +72 -0
  53. package/plugin/.claude-plugin/plugin.json +6 -0
  54. package/plugin/.lsp.json +14 -0
  55. package/plugin/hooks/hooks.json +27 -0
  56. package/sidecar/dist/src/bundler.d.ts +141 -0
  57. package/sidecar/dist/src/bundler.js +680 -0
  58. package/sidecar/dist/src/capture/anchors.d.ts +61 -0
  59. package/sidecar/dist/src/capture/anchors.js +1132 -0
  60. package/sidecar/dist/src/capture/api.d.ts +378 -0
  61. package/sidecar/dist/src/capture/api.js +10 -0
  62. package/sidecar/dist/src/capture/augmentations.d.ts +20 -0
  63. package/sidecar/dist/src/capture/augmentations.js +60 -0
  64. package/sidecar/dist/src/capture/check-classify.d.ts +58 -0
  65. package/sidecar/dist/src/capture/check-classify.js +189 -0
  66. package/sidecar/dist/src/capture/check-deep.d.ts +32 -0
  67. package/sidecar/dist/src/capture/check-deep.js +91 -0
  68. package/sidecar/dist/src/capture/check-poison.d.ts +40 -0
  69. package/sidecar/dist/src/capture/check-poison.js +155 -0
  70. package/sidecar/dist/src/capture/check-probe.d.ts +70 -0
  71. package/sidecar/dist/src/capture/check-probe.js +147 -0
  72. package/sidecar/dist/src/capture/check-scrub.d.ts +31 -0
  73. package/sidecar/dist/src/capture/check-scrub.js +71 -0
  74. package/sidecar/dist/src/capture/check-workspace.d.ts +52 -0
  75. package/sidecar/dist/src/capture/check-workspace.js +218 -0
  76. package/sidecar/dist/src/capture/check.d.ts +39 -0
  77. package/sidecar/dist/src/capture/check.js +450 -0
  78. package/sidecar/dist/src/capture/deep-walk.d.ts +62 -0
  79. package/sidecar/dist/src/capture/deep-walk.js +243 -0
  80. package/sidecar/dist/src/capture/index.d.ts +36 -0
  81. package/sidecar/dist/src/capture/index.js +477 -0
  82. package/sidecar/dist/src/capture/lockfile.d.ts +48 -0
  83. package/sidecar/dist/src/capture/lockfile.js +490 -0
  84. package/sidecar/dist/src/capture/machinery.d.ts +59 -0
  85. package/sidecar/dist/src/capture/machinery.js +160 -0
  86. package/sidecar/dist/src/capture/node-builder.d.ts +37 -0
  87. package/sidecar/dist/src/capture/node-builder.js +123 -0
  88. package/sidecar/dist/src/capture/paths-rewrite.d.ts +34 -0
  89. package/sidecar/dist/src/capture/paths-rewrite.js +100 -0
  90. package/sidecar/dist/src/capture/self-check.d.ts +38 -0
  91. package/sidecar/dist/src/capture/self-check.js +317 -0
  92. package/sidecar/dist/src/capture/specifiers.d.ts +39 -0
  93. package/sidecar/dist/src/capture/specifiers.js +56 -0
  94. package/sidecar/dist/src/definition-resolver.d.ts +57 -0
  95. package/sidecar/dist/src/definition-resolver.js +153 -0
  96. package/sidecar/dist/src/index.d.ts +14 -0
  97. package/sidecar/dist/src/index.js +564 -0
  98. package/sidecar/dist/src/monorepo-builder.d.ts +129 -0
  99. package/sidecar/dist/src/monorepo-builder.js +584 -0
  100. package/sidecar/dist/src/project-loader.d.ts +130 -0
  101. package/sidecar/dist/src/project-loader.js +399 -0
  102. package/sidecar/dist/src/type-inferrer.d.ts +940 -0
  103. package/sidecar/dist/src/type-inferrer.js +3540 -0
  104. package/sidecar/dist/src/type-structural-expander.d.ts +61 -0
  105. package/sidecar/dist/src/type-structural-expander.js +283 -0
  106. package/sidecar/dist/src/type-text-canonicalizer.d.ts +40 -0
  107. package/sidecar/dist/src/type-text-canonicalizer.js +297 -0
  108. package/sidecar/dist/src/types.d.ts +669 -0
  109. package/sidecar/dist/src/types.js +5 -0
  110. package/sidecar/dist/src/validators.d.ts +2214 -0
  111. package/sidecar/dist/src/validators.js +336 -0
  112. package/sidecar/package.json +6 -0
  113. package/templates/carrick.json +7 -0
  114. package/templates/carrick.yml +45 -0
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Diagnostic parsing + four-bucket classification for the v2 check phase.
3
+ *
4
+ * The judge is the vendored `tsc` CLI with `--pretty false`, run from the
5
+ * workspace root so file locations print workspace-relative (no temp path in
6
+ * the location prefix). This module turns that text into per-pair verdicts,
7
+ * classifying by diagnostic code + file + line (never by line position alone):
8
+ *
9
+ * poison (stub-file diagnostic) -> unverifiable [highest precedence]
10
+ * surface import error (probe lines 1-2)-> unverifiable
11
+ * IsAny gate fired (TS2344) -> gate_caught_baked_any
12
+ * IsUnknown/IsNever gate fired (TS2344) -> unverifiable
13
+ * assignment-class error -> incompatible
14
+ * no diagnostics -> compatible [lowest precedence]
15
+ *
16
+ * Gate precedence over the assignment line is load-bearing: an `unknown` side
17
+ * produces BOTH a gate TS2344 and an assignment TS2322, and reading the latter
18
+ * would mislabel an unverifiable pair as incompatible.
19
+ *
20
+ * Seam: node builtins + this bundle only.
21
+ */
22
+ import { scrubDiagnostic } from './check-scrub.js';
23
+ const PRIMARY_RE = /^(?<file>(?:[a-zA-Z]:)?[^(]*?)\((?<line>\d+),(?<col>\d+)\): error TS(?<code>\d+): (?<msg>.*)$/;
24
+ /** Parse `tsc --pretty false` output into structured diagnostics. */
25
+ export function parseTscOutput(stdout) {
26
+ const diags = [];
27
+ let current = null;
28
+ for (const rawLine of stdout.split('\n')) {
29
+ const line = rawLine.replace(/\r$/, '');
30
+ const m = line.match(PRIMARY_RE);
31
+ if (m && m.groups) {
32
+ current = {
33
+ file: m.groups.file.split('\\').join('/'),
34
+ line: Number(m.groups.line),
35
+ col: Number(m.groups.col),
36
+ code: Number(m.groups.code),
37
+ message: m.groups.msg,
38
+ };
39
+ diags.push(current);
40
+ continue;
41
+ }
42
+ // Indented continuation lines belong to the preceding primary diagnostic.
43
+ if (current && /^\s+\S/.test(line)) {
44
+ current.message += '\n' + line;
45
+ continue;
46
+ }
47
+ // Blank line or summary ("Found N errors.") ends the current run.
48
+ current = null;
49
+ }
50
+ return diags;
51
+ }
52
+ /** Assignment-class codes: a real structural mismatch on the value assignment. */
53
+ const ASSIGNMENT_CODES = new Set([2322, 2559, 2739, 2740, 2741, 2769, 2345]);
54
+ function sideForGate(name, plan) {
55
+ const [which, kind] = name.split(':');
56
+ const side = which === 'sent' ? plan.direction.sent : plan.direction.expected;
57
+ return { side, kind };
58
+ }
59
+ function endpointAliasFor(side, plan) {
60
+ return side === plan.direction.sent
61
+ ? plan.sentEndpoint.alias
62
+ : plan.expectedEndpoint.alias;
63
+ }
64
+ /** Classify one pair into exactly one bucket, honouring the precedence order. */
65
+ export function classifyPair(input) {
66
+ const { plan, probeDiags, poisonReason, scrubCtx } = input;
67
+ const codes = [...new Set(probeDiags.map((d) => d.code))].sort((a, b) => a - b);
68
+ const base = { pair_id: plan.pairId, pair_key: plan.spec.pair_key, codes };
69
+ // Every branch below this point except the last two returns a verdict about
70
+ // a type nobody could read; each states that in one place rather than
71
+ // repeating the reasoning.
72
+ const notAFact = (reason) => ({ resolved: false, unresolved_reason: reason });
73
+ // 1. Poison: a diagnostic in this pair's own alias closure (either side)
74
+ // makes the pair unverifiable, never "no probe error -> compatible". A
75
+ // sibling alias's poison in the same service no longer reaches here.
76
+ for (const side of ['producer', 'consumer']) {
77
+ const endpoint = plan.spec[side];
78
+ const reason = poisonReason(endpoint.service_name, endpoint.alias);
79
+ if (reason) {
80
+ return {
81
+ ...base,
82
+ bucket: 'unverifiable',
83
+ gate: `poison:${side}`,
84
+ diagnostic: `the type stub for service '${endpoint.service_name}' does not typecheck (its own declarations carry diagnostics); compatibility cannot be verified.`,
85
+ ...notAFact(`the ${side} stub does not typecheck`),
86
+ };
87
+ }
88
+ }
89
+ // 2. Surface import error (missing/renamed export): probe import lines.
90
+ const importDiag = probeDiags.find((d) => plan.importLines.includes(d.line));
91
+ if (importDiag) {
92
+ const side = plan.importLines[0] === importDiag.line ? plan.direction.sent : plan.direction.expected;
93
+ const alias = endpointAliasFor(side, plan);
94
+ return {
95
+ ...base,
96
+ bucket: 'unverifiable',
97
+ gate: `import:${side}`,
98
+ diagnostic: `surface export '${alias}' for the ${side} is missing or renamed; compatibility cannot be verified.`,
99
+ ...notAFact(`the ${side} surface export is missing or renamed`),
100
+ };
101
+ }
102
+ // 3/4. Probe gates (TS2344). IsAny outranks IsUnknown/IsNever.
103
+ const gateDiags = probeDiags.filter((d) => d.code === 2344 && plan.gateLines.has(d.line));
104
+ const anyGate = gateDiags
105
+ .map((d) => plan.gateLines.get(d.line))
106
+ .find((name) => name.endsWith(':any'));
107
+ if (anyGate) {
108
+ const { side } = sideForGate(anyGate, plan);
109
+ return {
110
+ ...base,
111
+ bucket: 'gate_caught_baked_any',
112
+ gate: `${side}:any`,
113
+ diagnostic: `the ${side} type resolved to 'any' at check time; compatibility cannot be verified (a type inferred through a missing library bakes to any).`,
114
+ ...notAFact(`the ${side} type is 'any'`),
115
+ };
116
+ }
117
+ const decayGate = gateDiags
118
+ .map((d) => plan.gateLines.get(d.line))
119
+ .find((name) => name.endsWith(':unknown') || name.endsWith(':never'));
120
+ if (decayGate) {
121
+ const { side, kind } = sideForGate(decayGate, plan);
122
+ return {
123
+ ...base,
124
+ bucket: 'unverifiable',
125
+ gate: `${side}:${kind}`,
126
+ diagnostic: `the ${side} type resolved to '${kind}' at check time; compatibility cannot be verified.`,
127
+ ...notAFact(`the ${side} type is '${kind}'`),
128
+ };
129
+ }
130
+ // 5. Assignment-class error on the value assignment line -> incompatible.
131
+ const assignDiag = probeDiags.find((d) => d.line === plan.assignmentLine && ASSIGNMENT_CODES.has(d.code));
132
+ if (assignDiag) {
133
+ return {
134
+ ...base,
135
+ bucket: 'incompatible',
136
+ diagnostic: scrubDiagnostic(assignDiag.message, scrubCtx, plan.sentEndpoint.alias, plan.expectedEndpoint.alias),
137
+ ...factness(input),
138
+ };
139
+ }
140
+ // Any other diagnostic on the assignment line that is not a known assignment
141
+ // code still means the pair could not be cleanly verified.
142
+ const otherAssign = probeDiags.find((d) => d.line === plan.assignmentLine);
143
+ if (otherAssign) {
144
+ return {
145
+ ...base,
146
+ bucket: 'unverifiable',
147
+ gate: 'assignment:other',
148
+ diagnostic: scrubDiagnostic(otherAssign.message, scrubCtx, plan.sentEndpoint.alias, plan.expectedEndpoint.alias),
149
+ ...notAFact('the probe raised a diagnostic that is not an assignment mismatch'),
150
+ };
151
+ }
152
+ // 6. No diagnostics -> compatible.
153
+ return { ...base, bucket: 'compatible', ...factness(input) };
154
+ }
155
+ /**
156
+ * Whether a compared pair is a FACT about two known types (carrick#707, R1d).
157
+ *
158
+ * The probe gates only rule out a WHOLLY top-typed side. A pair whose producer
159
+ * declares `{ id: string; meta: any }` clears every gate and then reads
160
+ * compatible against literally any counterparty `meta`, because `any` is
161
+ * bidirectionally assignable. That is not a compatibility result, and a reader
162
+ * who acts on it acts on nothing. So `resolved` additionally requires the deep
163
+ * walk to have RUN over both sides in the assembled workspace and come back
164
+ * empty. A walk that could not run leaves the verdict not-a-fact: absence of
165
+ * findings is not evidence.
166
+ */
167
+ function factness(input) {
168
+ const findings = input.deepFindings;
169
+ if (!findings) {
170
+ return {
171
+ resolved: false,
172
+ unresolved_reason: 'the type could not be walked for member-level any/unknown after install, so the comparison is not established as a fact',
173
+ };
174
+ }
175
+ for (const side of ['sent', 'expected']) {
176
+ const first = findings[side][0];
177
+ if (!first)
178
+ continue;
179
+ const label = side === 'sent' ? input.plan.direction.sent : input.plan.direction.expected;
180
+ const where = first.path === '' ? 'its root' : `'${first.path}'`;
181
+ return {
182
+ resolved: false,
183
+ unresolved_reason: first.kind === 'budget_exhausted'
184
+ ? `the ${label} type is too deep or wide to verify at ${where}`
185
+ : `the ${label} type carries '${first.kind}' at ${where}, which every counterparty shape satisfies`,
186
+ };
187
+ }
188
+ return { resolved: true };
189
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Check-time fact-ness walk (carrick#707, R1d).
3
+ *
4
+ * The probe gates prove the two sides are not WHOLLY `any`/`unknown`/`never`.
5
+ * They cannot see a member three levels down, and a member-level `any` accepts
6
+ * every counterparty shape — so "no diagnostic" there is not a compatibility
7
+ * result, it is the absence of one. The capture-time walk catches most of it,
8
+ * but only what the capture could resolve: a member that decayed through a
9
+ * pinned external is TypeScript's `error` placeholder at capture time and is
10
+ * deliberately excluded there, because it heals once the check installs the
11
+ * pin (carrick#450). This walk runs after that install, over the assembled
12
+ * probe workspace, so it sees what the capture could not.
13
+ *
14
+ * It is used for exactly one thing: setting `CheckVerdict.resolved`. No bucket
15
+ * changes because of it, so a scan's verdicts are identical with and without
16
+ * it — what changes is whether a reader is told the verdict is a fact.
17
+ */
18
+ import type { TypeProvenance } from './api.js';
19
+ import type { ProbePlan } from './check-probe.js';
20
+ /** Deep findings for one pair, per probe side. */
21
+ export interface PairDeepFindings {
22
+ sent: TypeProvenance[];
23
+ expected: TypeProvenance[];
24
+ }
25
+ /**
26
+ * Walk both sides of every probe in the assembled workspace.
27
+ *
28
+ * Returns an empty map when the program cannot be built — absence of findings
29
+ * must never be read as "clean", so the caller treats a missing entry as
30
+ * unresolved rather than resolved.
31
+ */
32
+ export declare function probeDeepFindings(probesDir: string, plans: ProbePlan[]): Map<string, PairDeepFindings>;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Check-time fact-ness walk (carrick#707, R1d).
3
+ *
4
+ * The probe gates prove the two sides are not WHOLLY `any`/`unknown`/`never`.
5
+ * They cannot see a member three levels down, and a member-level `any` accepts
6
+ * every counterparty shape — so "no diagnostic" there is not a compatibility
7
+ * result, it is the absence of one. The capture-time walk catches most of it,
8
+ * but only what the capture could resolve: a member that decayed through a
9
+ * pinned external is TypeScript's `error` placeholder at capture time and is
10
+ * deliberately excluded there, because it heals once the check installs the
11
+ * pin (carrick#450). This walk runs after that install, over the assembled
12
+ * probe workspace, so it sees what the capture could not.
13
+ *
14
+ * It is used for exactly one thing: setting `CheckVerdict.resolved`. No bucket
15
+ * changes because of it, so a scan's verdicts are identical with and without
16
+ * it — what changes is whether a reader is told the verdict is a fact.
17
+ */
18
+ import ts from 'typescript';
19
+ import * as fs from 'node:fs';
20
+ import * as path from 'node:path';
21
+ import { findDisqualifyingTopTypes, provenanceOf } from './deep-walk.js';
22
+ /**
23
+ * Walk both sides of every probe in the assembled workspace.
24
+ *
25
+ * Returns an empty map when the program cannot be built — absence of findings
26
+ * must never be read as "clean", so the caller treats a missing entry as
27
+ * unresolved rather than resolved.
28
+ */
29
+ export function probeDeepFindings(probesDir, plans) {
30
+ const results = new Map();
31
+ if (plans.length === 0)
32
+ return results;
33
+ const configPath = path.join(probesDir, 'tsconfig.json');
34
+ if (!fs.existsSync(configPath))
35
+ return results;
36
+ let program;
37
+ try {
38
+ const raw = ts.readConfigFile(configPath, (f) => fs.readFileSync(f, 'utf8'));
39
+ if (raw.error)
40
+ return results;
41
+ const parsed = ts.parseJsonConfigFileContent(raw.config, ts.sys, probesDir);
42
+ const fileNames = plans
43
+ .map((plan) => path.join(probesDir, 'probes', plan.fileName))
44
+ .filter((f) => fs.existsSync(f));
45
+ if (fileNames.length === 0)
46
+ return results;
47
+ program = ts.createProgram(fileNames, { ...parsed.options, noEmit: true });
48
+ }
49
+ catch {
50
+ return results;
51
+ }
52
+ const checker = program.getTypeChecker();
53
+ for (const plan of plans) {
54
+ const file = program.getSourceFile(path.join(probesDir, 'probes', plan.fileName));
55
+ if (!file)
56
+ continue;
57
+ const sent = walkImportedAlias(file, 'Sent', program, checker);
58
+ const expected = walkImportedAlias(file, 'Expected', program, checker);
59
+ if (sent === undefined || expected === undefined)
60
+ continue;
61
+ results.set(plan.pairId, { sent, expected });
62
+ }
63
+ return results;
64
+ }
65
+ /**
66
+ * Findings for one of the probe's two imported surface aliases, or `undefined`
67
+ * when the alias cannot be resolved at all (the import-error path, which the
68
+ * classifier already reports; there is nothing to add here).
69
+ */
70
+ function walkImportedAlias(file, localName, program, checker) {
71
+ for (const statement of file.statements) {
72
+ if (!ts.isImportDeclaration(statement))
73
+ continue;
74
+ const bindings = statement.importClause?.namedBindings;
75
+ if (!bindings || !ts.isNamedImports(bindings))
76
+ continue;
77
+ for (const element of bindings.elements) {
78
+ if (element.name.text !== localName)
79
+ continue;
80
+ const local = checker.getSymbolAtLocation(element.name);
81
+ if (!local)
82
+ return undefined;
83
+ const target = local.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(local) : local;
84
+ const type = checker.getDeclaredTypeOfSymbol(target);
85
+ if (!type)
86
+ return undefined;
87
+ return findDisqualifyingTopTypes(type, program, checker, element.name).map(provenanceOf);
88
+ }
89
+ }
90
+ return undefined;
91
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * #438 part 2: stub-tree poison containment.
3
+ *
4
+ * A diagnostic in a service's own stub tree (a broken surface line, a nested
5
+ * declaration file that fails to typecheck) used to poison the WHOLE service —
6
+ * every pair with that service as producer/consumer read `unverifiable`
7
+ * (gate=poison:*), masking every clean pair behind one bad alias. This module
8
+ * contains poison to the aliases actually reachable from the poisoned file:
9
+ *
10
+ * - a diagnostic on the surface entry is attributed to the alias whose
11
+ * `export type` statement SPAN covers the diagnostic's line (a multi-line
12
+ * alias reports on an interior line);
13
+ * - a diagnostic in a nested tree file is attributed to every alias whose
14
+ * import closure (surface import-type seeds, then BFS over relative
15
+ * imports — the same closure the capture self-check computes) includes that
16
+ * file.
17
+ *
18
+ * A stub-tree diagnostic reachable from NO alias (e.g. a cross-stub
19
+ * `declare global` TS2717 collision in an augmentation file, which
20
+ * `skipLibCheck:false` exists to surface — see check-workspace.ts) has no
21
+ * closure to contain it and genuinely can affect any alias's comparison, so it
22
+ * falls back to service-wide poison. Soundness (never falsely compatible) wins
23
+ * over precision in that one case; every attributable diagnostic is contained.
24
+ *
25
+ * Seam: node builtins + `typescript` + this bundle only.
26
+ */
27
+ import type { AssembledWorkspace } from './check-workspace.js';
28
+ export interface StubPoisonIndex {
29
+ serviceName: string;
30
+ /** Workspace-relative surface key: `packages/<dir>/types/surface.d.ts`. */
31
+ surfaceFile: string;
32
+ /** Every alias declared in the surface. */
33
+ allAliases: Set<string>;
34
+ /** Alias whose `export type` statement span covers the given surface line. */
35
+ aliasAtSurfaceLine(line: number): string | undefined;
36
+ /** Aliases whose import closure includes the given workspace-relative file. */
37
+ aliasesForFile(fileKey: string): string[];
38
+ }
39
+ /** One index per assembled stub, keyed by service name. */
40
+ export declare function buildPoisonIndexes(ws: AssembledWorkspace): Map<string, StubPoisonIndex>;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * #438 part 2: stub-tree poison containment.
3
+ *
4
+ * A diagnostic in a service's own stub tree (a broken surface line, a nested
5
+ * declaration file that fails to typecheck) used to poison the WHOLE service —
6
+ * every pair with that service as producer/consumer read `unverifiable`
7
+ * (gate=poison:*), masking every clean pair behind one bad alias. This module
8
+ * contains poison to the aliases actually reachable from the poisoned file:
9
+ *
10
+ * - a diagnostic on the surface entry is attributed to the alias whose
11
+ * `export type` statement SPAN covers the diagnostic's line (a multi-line
12
+ * alias reports on an interior line);
13
+ * - a diagnostic in a nested tree file is attributed to every alias whose
14
+ * import closure (surface import-type seeds, then BFS over relative
15
+ * imports — the same closure the capture self-check computes) includes that
16
+ * file.
17
+ *
18
+ * A stub-tree diagnostic reachable from NO alias (e.g. a cross-stub
19
+ * `declare global` TS2717 collision in an augmentation file, which
20
+ * `skipLibCheck:false` exists to surface — see check-workspace.ts) has no
21
+ * closure to contain it and genuinely can affect any alias's comparison, so it
22
+ * falls back to service-wide poison. Soundness (never falsely compatible) wins
23
+ * over precision in that one case; every attributable diagnostic is contained.
24
+ *
25
+ * Seam: node builtins + `typescript` + this bundle only.
26
+ */
27
+ import ts from 'typescript';
28
+ import * as fs from 'node:fs';
29
+ import * as path from 'node:path';
30
+ import { collectSpecifiers, isRelative } from './specifiers.js';
31
+ /** One index per assembled stub, keyed by service name. */
32
+ export function buildPoisonIndexes(ws) {
33
+ const indexes = new Map();
34
+ for (const stub of ws.stubs) {
35
+ const index = buildOne(ws, stub.serviceName, stub.packageDir);
36
+ if (index)
37
+ indexes.set(stub.serviceName, index);
38
+ }
39
+ return indexes;
40
+ }
41
+ function buildOne(ws, serviceName, packageDir) {
42
+ const typesDirAbs = path.join(ws.workspaceDir, 'packages', packageDir, 'types');
43
+ const surfaceAbs = path.join(typesDirAbs, 'surface.d.ts');
44
+ if (!fs.existsSync(surfaceAbs))
45
+ return undefined;
46
+ const keyPrefix = `packages/${packageDir}/types`;
47
+ const surfaceFile = `${keyPrefix}/surface.d.ts`;
48
+ // Collect the whole `.d.ts` tree, keyed as `packages/<dir>/types/<rel>`.
49
+ const treeAbs = [];
50
+ const walk = (dir) => {
51
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
52
+ const p = path.join(dir, entry.name);
53
+ if (entry.isDirectory())
54
+ walk(p);
55
+ else if (entry.name.endsWith('.d.ts'))
56
+ treeAbs.push(p);
57
+ }
58
+ };
59
+ walk(typesDirAbs);
60
+ const treeAbsSet = new Set(treeAbs.map((f) => path.resolve(f)));
61
+ const keyOf = (abs) => `${keyPrefix}/${path.relative(typesDirAbs, abs).split(path.sep).join('/')}`;
62
+ const resolveRel = (fromAbs, spec) => {
63
+ if (!isRelative(spec))
64
+ return undefined;
65
+ const base = path.resolve(path.dirname(fromAbs), spec);
66
+ const candidates = [
67
+ `${base}.d.ts`,
68
+ path.join(base, 'index.d.ts'),
69
+ base.endsWith('.js') ? `${base.slice(0, -3)}.d.ts` : undefined,
70
+ base, // already `.d.ts`
71
+ ].filter((c) => c !== undefined);
72
+ for (const c of candidates) {
73
+ if (treeAbsSet.has(path.resolve(c)))
74
+ return c;
75
+ }
76
+ return undefined;
77
+ };
78
+ // Per-file relative-import adjacency, over the fileKey namespace.
79
+ const adjacency = new Map();
80
+ for (const abs of treeAbs) {
81
+ const text = fs.readFileSync(abs, 'utf8');
82
+ const neighbors = [];
83
+ for (const spec of collectSpecifiers(text)) {
84
+ const target = resolveRel(abs, spec);
85
+ if (target)
86
+ neighbors.push(keyOf(target));
87
+ }
88
+ adjacency.set(keyOf(abs), neighbors);
89
+ }
90
+ // Surface alias statement spans + each alias's import-type seed files.
91
+ const surfaceText = fs.readFileSync(surfaceAbs, 'utf8');
92
+ const sf = ts.createSourceFile('surface.d.ts', surfaceText, ts.ScriptTarget.Latest, true);
93
+ const allAliases = new Set();
94
+ const spans = [];
95
+ const seedsByAlias = new Map();
96
+ for (const stmt of sf.statements) {
97
+ if (!ts.isTypeAliasDeclaration(stmt))
98
+ continue;
99
+ const alias = stmt.name.text;
100
+ allAliases.add(alias);
101
+ spans.push({
102
+ alias,
103
+ startLine: sf.getLineAndCharacterOfPosition(stmt.getStart(sf)).line + 1,
104
+ endLine: sf.getLineAndCharacterOfPosition(stmt.getEnd()).line + 1,
105
+ });
106
+ const seeds = [];
107
+ const visit = (n) => {
108
+ if (ts.isImportTypeNode(n) &&
109
+ ts.isLiteralTypeNode(n.argument) &&
110
+ ts.isStringLiteral(n.argument.literal) &&
111
+ isRelative(n.argument.literal.text)) {
112
+ const target = resolveRel(surfaceAbs, n.argument.literal.text);
113
+ if (target)
114
+ seeds.push(keyOf(target));
115
+ }
116
+ n.forEachChild(visit);
117
+ };
118
+ visit(stmt);
119
+ seedsByAlias.set(alias, seeds);
120
+ }
121
+ // Invert per-alias closures into fileKey -> aliases.
122
+ const fileToAliases = new Map();
123
+ for (const [alias, seeds] of seedsByAlias) {
124
+ const closure = new Set();
125
+ const queue = [...seeds];
126
+ while (queue.length > 0) {
127
+ const file = queue.pop();
128
+ if (closure.has(file))
129
+ continue;
130
+ closure.add(file);
131
+ for (const next of adjacency.get(file) ?? [])
132
+ queue.push(next);
133
+ }
134
+ for (const file of closure) {
135
+ if (!fileToAliases.has(file))
136
+ fileToAliases.set(file, new Set());
137
+ fileToAliases.get(file).add(alias);
138
+ }
139
+ }
140
+ return {
141
+ serviceName,
142
+ surfaceFile,
143
+ allAliases,
144
+ aliasAtSurfaceLine(line) {
145
+ for (const span of spans) {
146
+ if (line >= span.startLine && line <= span.endLine)
147
+ return span.alias;
148
+ }
149
+ return undefined;
150
+ },
151
+ aliasesForFile(fileKey) {
152
+ return [...(fileToAliases.get(fileKey) ?? [])];
153
+ },
154
+ };
155
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Probe generation for the v2 check phase ("tsc as the judge").
3
+ *
4
+ * One probe file per matched pair. The probe is a value-level assignment in
5
+ * the data-flow direction, guarded by six top/bottom-type gates on BOTH sides
6
+ * (IsAny/IsUnknown/IsNever). Value-level (not `[X] extends [Y]`) because the
7
+ * conditional-type relation diverges around `any`, and because the compiler's
8
+ * elaborated assignment error is the user-facing mismatch report.
9
+ *
10
+ * GraphQL pairs additionally unwrap the producer's resolver-return ENVELOPE
11
+ * before the assignment (see the `graphql` branch in `buildProbe`): a GraphQL
12
+ * producer's captured type is the resolver function's return type with
13
+ * transport layers (Promise / async-iterator) already peeled at inference
14
+ * time, which for wrapper-returning resolvers is a single-payload envelope
15
+ * (`{ data: Order; errors: string[] }`) — not the SDL field payload the
16
+ * consumer's selection set reads. The v1 checker unwrapped this structurally
17
+ * at compare time (ts_check type-checker `unwrapGraphqlPayload`, deleted in
18
+ * WP8); the type-level port here is its v2-native equivalent.
19
+ *
20
+ * Seam: node builtins + `typescript` + this bundle only. No imports needed here.
21
+ */
22
+ import type { CheckPairEndpoint, CheckPairSpec, ProbeProtocol, ProbeTypeKind } from './api.js';
23
+ /** Which pair endpoint supplies the `sent` value / the `expected` binding. */
24
+ export type Side = 'producer' | 'consumer';
25
+ export interface Direction {
26
+ sent: Side;
27
+ expected: Side;
28
+ }
29
+ /**
30
+ * The direction table, keyed on (protocol, type_kind) — one place. Structurally
31
+ * fixes the confirmed HTTP request-body inversion (data flows consumer ->
32
+ * producer for request bodies, so the check is consumer <= producer).
33
+ *
34
+ * | protocol | type_kind | sent | expected |
35
+ * | http, graphql | response | producer | consumer |
36
+ * | http | request | consumer | producer |
37
+ * | socket, pubsub | both | consumer | producer |
38
+ */
39
+ export declare function directionFor(protocol: ProbeProtocol, typeKind: ProbeTypeKind): Direction;
40
+ /** Deterministic FNV-1a (32-bit) over the pair's stable key. Never a path. */
41
+ export declare function fnv1a(input: string): string;
42
+ /** The pair ID is derived from the pair's semantic identity, not the workspace
43
+ * path, so it is byte-stable across runs. */
44
+ export declare function pairId(spec: CheckPairSpec): string;
45
+ /** Names each gate line so a TS2344 can be attributed to a specific side+kind. */
46
+ export type GateName = 'sent:any' | 'sent:unknown' | 'sent:never' | 'expected:any' | 'expected:unknown' | 'expected:never';
47
+ export interface ProbePlan {
48
+ pairId: string;
49
+ spec: CheckPairSpec;
50
+ /** Probe file basename, e.g. pair_1a2b3c4d.ts */
51
+ fileName: string;
52
+ /** Which endpoint is sent vs expected (resolved via the direction table). */
53
+ direction: Direction;
54
+ sentEndpoint: CheckPairEndpoint;
55
+ expectedEndpoint: CheckPairEndpoint;
56
+ /** Generated probe source. */
57
+ source: string;
58
+ /** Lines the surface imports sit on (1-based). Errors here => unverifiable. */
59
+ importLines: number[];
60
+ /** 1-based line -> gate name. TS2344 here => baked-any / unverifiable. */
61
+ gateLines: Map<number, GateName>;
62
+ /** 1-based line of the value-level assignment. Errors here => incompatible. */
63
+ assignmentLine: number;
64
+ }
65
+ /**
66
+ * Build one probe, recording the exact line of every gate and the assignment so
67
+ * the classifier never depends on hard-coded offsets. `packageOf` maps a
68
+ * service name to its workspace package specifier (e.g. `@carrick/orders`).
69
+ */
70
+ export declare function buildProbe(spec: CheckPairSpec, packageOf: (serviceName: string) => string): ProbePlan;