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,450 @@
1
+ /**
2
+ * v2 check core: "tsc as the judge"
3
+ * (docs/reference/type-compat-synthetic-monorepo.md, Check phase).
4
+ *
5
+ * Consumes two or more capture stub packages, assembles a scratch synthetic
6
+ * monorepo, installs pinned deps with the vendored pnpm (isolated linker),
7
+ * generates one probe per matched pair, runs the vendored `tsc` CLI, and
8
+ * classifies diagnostics into the four buckets. Deterministic given the stubs
9
+ * and pairs: verdicts are byte-stable across runs (the scrub pass removes the
10
+ * per-run temp path, and the pair IDs / ordering derive from the pair specs).
11
+ *
12
+ * Seam: this file imports only node builtins, `typescript` (for the version
13
+ * string), and the rest of this bundle. The sidecar reaches it only through
14
+ * ./index.js. The judge is the `tsc` CLI (no compiler API), which keeps the
15
+ * check phase TS7-ready by construction.
16
+ */
17
+ import { spawn } from 'node:child_process';
18
+ import * as fs from 'node:fs';
19
+ import * as path from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+ import ts from 'typescript';
22
+ import { buildProbe } from './check-probe.js';
23
+ import { classifyPair, parseTscOutput, } from './check-classify.js';
24
+ import { scrubPaths } from './check-scrub.js';
25
+ import { assembleWorkspace, writeProbes, } from './check-workspace.js';
26
+ import { buildPoisonIndexes } from './check-poison.js';
27
+ import { probeDeepFindings } from './check-deep.js';
28
+ function runProcess(command, args, cwd) {
29
+ return new Promise((resolve, reject) => {
30
+ const child = spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
31
+ let stdout = '';
32
+ let stderr = '';
33
+ child.stdout.on('data', (d) => (stdout += d.toString()));
34
+ child.stderr.on('data', (d) => (stderr += d.toString()));
35
+ child.on('error', reject);
36
+ child.on('close', (code) => resolve({ code, stdout, stderr }));
37
+ });
38
+ }
39
+ /** Resolve the sidecar root (where the vendored pnpm/tsc live) from this bundle. */
40
+ function sidecarRoot() {
41
+ // dist/src/capture/check.js -> up three -> sidecar root.
42
+ const here = path.dirname(fileURLToPath(import.meta.url));
43
+ return path.resolve(here, '..', '..', '..');
44
+ }
45
+ /**
46
+ * Locate a vendored executable the way node locates a package: walk up from
47
+ * the bundle and take the first `node_modules/.bin/<name>` that exists.
48
+ *
49
+ * Where the bins sit depends on how the sidecar arrived. A checkout and the
50
+ * release tarball both keep them in the sidecar's own `node_modules`, so the
51
+ * first candidate hits and nothing changes. An npm install never can: npm
52
+ * strips nested `node_modules` from a published tarball, so the sidecar ships
53
+ * as a plain directory inside the package and its dependencies are installed
54
+ * by the package that carries it — hoisted to the install root, or nested one
55
+ * level up when a version conflict forces it. Assuming one shape is what made
56
+ * an installed `carrick` degrade every pair to unverifiable (carrick#833).
57
+ *
58
+ * `from` is injectable so the layouts can be pinned by test; production always
59
+ * starts at the bundle. Returns the sidecar-local path when nothing exists,
60
+ * which is the caller's "not found" signal and the most useful thing to name.
61
+ */
62
+ export function resolveVendoredBin(name, from = sidecarRoot()) {
63
+ const local = path.join(from, 'node_modules', '.bin', name);
64
+ for (let dir = from;; dir = path.dirname(dir)) {
65
+ const candidate = path.join(dir, 'node_modules', '.bin', name);
66
+ if (fs.existsSync(candidate))
67
+ return candidate;
68
+ if (path.dirname(dir) === dir)
69
+ return local;
70
+ }
71
+ }
72
+ function binPath(name) {
73
+ return resolveVendoredBin(name);
74
+ }
75
+ function unverifiableAll(plans, gate, diagnostic) {
76
+ return plans.map((plan) => ({
77
+ pair_id: plan.pairId,
78
+ pair_key: plan.spec.pair_key,
79
+ bucket: 'unverifiable',
80
+ gate,
81
+ diagnostic,
82
+ codes: [],
83
+ resolved: false,
84
+ unresolved_reason: diagnostic,
85
+ }));
86
+ }
87
+ function sortVerdicts(verdicts) {
88
+ return [...verdicts].sort((a, b) => a.pair_id < b.pair_id ? -1 : a.pair_id > b.pair_id ? 1 : 0);
89
+ }
90
+ /** Run the deterministic check phase. Async: install + tsc run off the event
91
+ * loop so the sidecar stays responsive and can emit keepalive frames. */
92
+ export async function runCheck(opts, onProgress) {
93
+ const progress = onProgress ?? (() => { });
94
+ const cleanup = opts.cleanup !== false;
95
+ const pnpmPath = opts.pnpmPath ?? binPath('pnpm');
96
+ const tscPath = opts.tscPath ?? binPath('tsc');
97
+ const tsVersion = ts.version;
98
+ // Isolation is non-negotiable: without the vendored pnpm we would fall back
99
+ // to a flat npm install that manufactures nominal false-incompatibles. Fail
100
+ // explicitly instead (design Check step 2).
101
+ if (!fs.existsSync(pnpmPath)) {
102
+ const planned = planPairs(opts, (s) => `@carrick/${s}`);
103
+ return {
104
+ success: false,
105
+ workspace_dir: '',
106
+ isolation: 'unavailable',
107
+ install_ok: false,
108
+ install_error: 'vendored pnpm not found; type isolation is unavailable',
109
+ ts_version: tsVersion,
110
+ verdicts: sortVerdicts(unverifiableAll(planned, 'isolation:unavailable', 'type isolation unavailable (pnpm missing); compatibility cannot be verified.')),
111
+ degraded_services: opts.stubs.map((s) => ({
112
+ service_name: s.service_name,
113
+ reason: 'isolation unavailable',
114
+ })),
115
+ errors: ['vendored pnpm not found'],
116
+ };
117
+ }
118
+ progress('assembling', 'assembling synthetic workspace');
119
+ const ws = assembleWorkspace({
120
+ stubs: opts.stubs,
121
+ workspaceRoot: opts.workspaceRoot,
122
+ });
123
+ const scrubCtx = {
124
+ workspaceRoot: ws.workspaceDir,
125
+ packageLabelOf: ws.packageLabelOf,
126
+ };
127
+ // Generate probes. A pair naming a service with no stub is unverifiable.
128
+ const plans = [];
129
+ const unresolved = [];
130
+ for (const spec of opts.pairs) {
131
+ try {
132
+ plans.push(buildProbe(spec, ws.packageOf));
133
+ }
134
+ catch {
135
+ unresolved.push({
136
+ pair_id: '',
137
+ pair_key: spec.pair_key,
138
+ bucket: 'unverifiable',
139
+ gate: 'missing:stub',
140
+ diagnostic: 'a stub for one side of this pair was not provided; compatibility cannot be verified.',
141
+ codes: [],
142
+ resolved: false,
143
+ unresolved_reason: 'one side of this pair has no captured type surface',
144
+ });
145
+ }
146
+ }
147
+ // Backfill deterministic pair IDs for unresolved pairs from the spec.
148
+ for (const v of unresolved) {
149
+ const spec = opts.pairs.find((p) => p.pair_key === v.pair_key);
150
+ v.pair_id = fnvOfSpec(spec);
151
+ }
152
+ // Capture-time deep-decay pre-gate (adversarial-review finding 1): a
153
+ // member-level `any`/`unknown` recorded by the capture self-check with no
154
+ // failing-external explanation. The probe gates below are WHOLE-type only
155
+ // — `{ orderId: string; metadata: any }` sails through IsAny and the
156
+ // assignment compiles clean — so such pairs must never reach a probe.
157
+ // `any` routes to gate_caught_baked_any, `unknown` to unverifiable; both
158
+ // read as None downstream, never compatible.
159
+ const aliasRecords = readStubAliasRecords(opts.stubs);
160
+ const preGated = [];
161
+ const probing = [];
162
+ for (const plan of plans) {
163
+ const hit = deepDecayOf(plan, aliasRecords);
164
+ if (!hit) {
165
+ probing.push(plan);
166
+ continue;
167
+ }
168
+ preGated.push({
169
+ pair_id: plan.pairId,
170
+ pair_key: plan.spec.pair_key,
171
+ // `any` is a confirmed baked top type -> gate_caught_baked_any; `unknown`
172
+ // and `budget_exhausted` (a subtree the capture walk could not finish)
173
+ // are "cannot verify" -> unverifiable. All read as None downstream.
174
+ bucket: hit.kind === 'any' ? 'gate_caught_baked_any' : 'unverifiable',
175
+ gate: `capture:${hit.side}:${hit.kind}`,
176
+ diagnostic: hit.kind === 'budget_exhausted'
177
+ ? `the ${hit.side} type is too deep or wide to verify within the ` +
178
+ `capture budget (at '${hit.path}'); compatibility cannot be ` +
179
+ `verified — abstaining so a buried 'any' can never read compatible.`
180
+ : `the ${hit.side} type carries '${hit.kind}' at '${hit.path}' from ` +
181
+ `capture; compatibility cannot be verified (a partially-unresolved ` +
182
+ `type would let an arbitrary shape read compatible).`,
183
+ codes: [],
184
+ resolved: false,
185
+ unresolved_reason: hit.kind === 'budget_exhausted'
186
+ ? `the ${hit.side} type is too deep or wide to verify at '${hit.path}'`
187
+ : `the ${hit.side} type carries '${hit.kind}' at '${hit.path}'`,
188
+ });
189
+ }
190
+ writeProbes(ws, probing);
191
+ const errors = [];
192
+ const degraded = [];
193
+ // ---- Install (async, off the event loop) --------------------------------
194
+ progress('installing', 'installing pinned dependencies');
195
+ const install = await runProcess(pnpmPath, ['install'], ws.workspaceDir);
196
+ const installOk = install.code === 0;
197
+ let installError;
198
+ if (!installOk) {
199
+ installError = scrubPaths((install.stderr || install.stdout).trim().slice(0, 2000), scrubCtx);
200
+ for (const s of opts.stubs) {
201
+ degraded.push({ service_name: s.service_name, reason: 'workspace install failed' });
202
+ }
203
+ const verdicts = sortVerdicts([
204
+ ...unverifiableAll(probing, 'install:failed', 'workspace dependency install failed; compatibility cannot be verified.'),
205
+ // Capture-decay verdicts stand regardless of the install outcome.
206
+ ...preGated,
207
+ ...unresolved,
208
+ ]);
209
+ if (cleanup)
210
+ safeRm(ws.workspaceDir);
211
+ return {
212
+ success: false,
213
+ workspace_dir: cleanup ? '' : ws.workspaceDir,
214
+ isolation: 'pnpm',
215
+ install_ok: false,
216
+ install_error: installError,
217
+ ts_version: tsVersion,
218
+ verdicts,
219
+ degraded_services: degraded,
220
+ errors,
221
+ };
222
+ }
223
+ // ---- Judge (async, off the event loop) ----------------------------------
224
+ progress('checking', 'type-checking probes');
225
+ const tsc = await runProcess(tscPath, ['--noEmit', '--pretty', 'false', '-p', path.join(ws.probesRel, 'tsconfig.json')], ws.workspaceDir);
226
+ const diagnostics = parseTscOutput(tsc.stdout + '\n' + tsc.stderr);
227
+ // Abnormal tsc termination: OOM/SIGKILL (exit code null), an internal
228
+ // crash, or a fileless config error (e.g. TS18003, printed without a
229
+ // (line,col) prefix) all yield ZERO parseable diagnostics — and an empty
230
+ // diagnostic set must never fall through to "compatible". A non-zero exit
231
+ // WITH parsed diagnostics is the legitimate incompatible path (tsc exits 1
232
+ // on ordinary type errors) and stays untouched. Mirrors the install step's
233
+ // `code === 0` guard.
234
+ if (tsc.code !== 0 && diagnostics.length === 0) {
235
+ const excerpt = scrubPaths((tsc.stderr || tsc.stdout).trim().slice(0, 2000), scrubCtx);
236
+ errors.push(`tsc terminated abnormally (exit code ${tsc.code ?? 'null'}) ` +
237
+ `with no parseable diagnostics${excerpt ? `: ${excerpt}` : ''}`);
238
+ for (const s of opts.stubs) {
239
+ degraded.push({
240
+ service_name: s.service_name,
241
+ reason: 'type checker terminated abnormally',
242
+ });
243
+ }
244
+ const verdicts = sortVerdicts([
245
+ ...unverifiableAll(probing, 'tsc:abnormal-termination', 'the type checker terminated abnormally; compatibility cannot be verified.'),
246
+ ...preGated,
247
+ ...unresolved,
248
+ ]);
249
+ if (cleanup)
250
+ safeRm(ws.workspaceDir);
251
+ return {
252
+ success: false,
253
+ workspace_dir: cleanup ? '' : ws.workspaceDir,
254
+ isolation: 'pnpm',
255
+ install_ok: true,
256
+ ts_version: tsVersion,
257
+ verdicts,
258
+ degraded_services: dedupeDegraded(degraded),
259
+ errors,
260
+ };
261
+ }
262
+ const { probeDiagsByPair, poison, globalErrors } = attributeDiagnostics(diagnostics, ws, probing);
263
+ for (const e of globalErrors)
264
+ errors.push(scrubPaths(e, scrubCtx));
265
+ // Only SERVICE-WIDE poison (an unattributable stub diagnostic) degrades the
266
+ // whole service; contained alias-scoped poison rides its own verdicts.
267
+ for (const [service, scope] of poison) {
268
+ if (scope.all)
269
+ degraded.push({ service_name: service, reason: scope.reason });
270
+ }
271
+ const poisonReason = (service, alias) => {
272
+ const scope = poison.get(service);
273
+ if (!scope)
274
+ return undefined;
275
+ return scope.all || scope.aliases.has(alias) ? scope.reason : undefined;
276
+ };
277
+ // Fact-ness walk (carrick#707, R1d): the probe gates are whole-type only,
278
+ // so a compared pair is only a fact about two known types when neither side
279
+ // carries a member-level any/unknown HERE — after install, where the capture
280
+ // could not look (carrick#450). It sets `resolved` and nothing else; no
281
+ // bucket depends on it.
282
+ const deepByPair = probeDeepFindings(ws.probesDir, probing);
283
+ const verdicts = sortVerdicts([
284
+ ...probing.map((plan) => classifyPair({
285
+ plan,
286
+ probeDiags: probeDiagsByPair.get(plan.pairId) ?? [],
287
+ poisonReason,
288
+ scrubCtx,
289
+ deepFindings: deepByPair.get(plan.pairId),
290
+ })),
291
+ ...preGated,
292
+ ...unresolved,
293
+ ]);
294
+ const workspaceDirOut = cleanup ? '' : ws.workspaceDir;
295
+ if (cleanup)
296
+ safeRm(ws.workspaceDir);
297
+ return {
298
+ success: true,
299
+ workspace_dir: workspaceDirOut,
300
+ isolation: 'pnpm',
301
+ install_ok: true,
302
+ ts_version: tsVersion,
303
+ verdicts,
304
+ degraded_services: dedupeDegraded(degraded),
305
+ errors,
306
+ };
307
+ }
308
+ /**
309
+ * Per-service alias records from each stub's carrick-manifest.json. Absent
310
+ * or unparseable manifests (e.g. hand-authored stubs in tests) contribute
311
+ * nothing — only positively-recorded deep decay pre-gates a pair.
312
+ */
313
+ function readStubAliasRecords(stubs) {
314
+ const byService = new Map();
315
+ for (const stub of stubs) {
316
+ try {
317
+ const manifest = JSON.parse(fs.readFileSync(path.join(stub.stub_dir, 'carrick-manifest.json'), 'utf8'));
318
+ const byAlias = new Map();
319
+ for (const record of manifest.aliases ?? []) {
320
+ if (record?.alias)
321
+ byAlias.set(record.alias, record);
322
+ }
323
+ byService.set(stub.service_name, byAlias);
324
+ }
325
+ catch {
326
+ // No manifest: nothing to pre-gate for this stub.
327
+ }
328
+ }
329
+ return byService;
330
+ }
331
+ /** First side (producer, then consumer) whose capture recorded a deep decay. */
332
+ function deepDecayOf(plan, aliasRecords) {
333
+ for (const side of ['producer', 'consumer']) {
334
+ const endpoint = plan.spec[side];
335
+ const record = aliasRecords.get(endpoint.service_name)?.get(endpoint.alias);
336
+ // The capture emits every deep finding it made; the FIRST is the one the
337
+ // pre-gate reads, exactly as when the walk stopped at the first hit.
338
+ const finding = record?.any_provenance?.[0];
339
+ if (finding) {
340
+ return { side, kind: finding.kind, path: finding.path };
341
+ }
342
+ }
343
+ return undefined;
344
+ }
345
+ const POISON_REASON = 'stub type tree carries its own diagnostics';
346
+ /**
347
+ * Group diagnostics: per-probe (for classification), plus stub-tree poison
348
+ * CONTAINED to the aliases reachable from the poisoned file (#438 part 2). An
349
+ * unattributable stub diagnostic (an augmentation-file collision in no alias's
350
+ * closure) falls back to service-wide poison; everything else is scoped.
351
+ */
352
+ function attributeDiagnostics(diagnostics, ws, plans) {
353
+ const probeFileToPair = new Map();
354
+ for (const plan of plans) {
355
+ probeFileToPair.set(`${ws.probesRel}/probes/${plan.fileName}`, plan.pairId);
356
+ }
357
+ const stubDirs = new Set(ws.stubs.map((s) => s.packageDir));
358
+ // Lazy: the clean path (no stub-tree diagnostic) must not walk and parse
359
+ // every stub `.d.ts` tree. Built on first stub-tree diagnostic, at most once.
360
+ let indexesMemo;
361
+ const getIndexes = () => (indexesMemo ??= buildPoisonIndexes(ws));
362
+ const probeDiagsByPair = new Map();
363
+ const poison = new Map();
364
+ const globalErrors = [];
365
+ const scopeFor = (service) => {
366
+ let scope = poison.get(service);
367
+ if (!scope) {
368
+ scope = { all: false, aliases: new Set(), reason: POISON_REASON };
369
+ poison.set(service, scope);
370
+ }
371
+ return scope;
372
+ };
373
+ for (const d of diagnostics) {
374
+ if (!d.file) {
375
+ globalErrors.push(`TS${d.code}: ${d.message}`);
376
+ continue;
377
+ }
378
+ const pairId = probeFileToPair.get(d.file);
379
+ if (pairId) {
380
+ if (!probeDiagsByPair.has(pairId))
381
+ probeDiagsByPair.set(pairId, []);
382
+ probeDiagsByPair.get(pairId).push(d);
383
+ continue;
384
+ }
385
+ // Stub-tree diagnostics poison that stub (its OWN sources, not deps).
386
+ const pkgMatch = d.file.match(/^packages\/([^/]+)\//);
387
+ if (pkgMatch && !d.file.includes('/node_modules/') && stubDirs.has(pkgMatch[1])) {
388
+ const service = ws.serviceOfPackageDir(pkgMatch[1]);
389
+ if (!service)
390
+ continue;
391
+ const scope = scopeFor(service);
392
+ const index = getIndexes().get(service);
393
+ if (index && d.file === index.surfaceFile) {
394
+ // Surface diagnostic: attribute by the alias statement span it sits in.
395
+ const alias = index.aliasAtSurfaceLine(d.line);
396
+ if (alias)
397
+ scope.aliases.add(alias);
398
+ else
399
+ scope.all = true;
400
+ }
401
+ else if (index) {
402
+ // Nested-file diagnostic: attribute to every alias whose closure reads
403
+ // it; a file in no closure (augmentation collision) falls back wide.
404
+ const affected = index.aliasesForFile(d.file);
405
+ if (affected.length > 0)
406
+ for (const a of affected)
407
+ scope.aliases.add(a);
408
+ else
409
+ scope.all = true;
410
+ }
411
+ else {
412
+ scope.all = true;
413
+ }
414
+ }
415
+ // Anything else (node_modules, TS lib) is environmental: ignored for
416
+ // verdicts so it cannot inject nondeterministic noise.
417
+ }
418
+ return { probeDiagsByPair, poison, globalErrors };
419
+ }
420
+ function dedupeDegraded(list) {
421
+ const seen = new Map();
422
+ for (const d of list)
423
+ if (!seen.has(d.service_name))
424
+ seen.set(d.service_name, d);
425
+ return [...seen.values()].sort((a, b) => a.service_name < b.service_name ? -1 : a.service_name > b.service_name ? 1 : 0);
426
+ }
427
+ /** Plan pairs without a workspace (for the isolation-unavailable early exit). */
428
+ function planPairs(opts, packageOf) {
429
+ const plans = [];
430
+ for (const spec of opts.pairs) {
431
+ try {
432
+ plans.push(buildProbe(spec, packageOf));
433
+ }
434
+ catch {
435
+ /* skipped in the early-exit path */
436
+ }
437
+ }
438
+ return plans;
439
+ }
440
+ function fnvOfSpec(spec) {
441
+ return buildProbe(spec, (s) => `@carrick/${s}`).pairId;
442
+ }
443
+ function safeRm(dir) {
444
+ try {
445
+ fs.rmSync(dir, { recursive: true, force: true });
446
+ }
447
+ catch {
448
+ /* best effort */
449
+ }
450
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The structural any/unknown walk, shared by the two type surfaces that need it
3
+ * (carrick#448, carrick#707).
4
+ *
5
+ * Capture-time it runs over the emitted stub tree and decides whether an alias
6
+ * carries a member-level decay the check phase must pre-gate. Check-time it
7
+ * runs over the assembled probe workspace, where the pinned externals ARE
8
+ * installed, and decides whether a verdict is a fact about two known types or
9
+ * an artefact of a type nobody could see (`CheckVerdict.resolved`). The
10
+ * whole-type probe gates cannot answer that: `any` three members down passes
11
+ * every one of them.
12
+ *
13
+ * One walk, one budget, one path notation, so a finding means the same thing
14
+ * wherever it is reported.
15
+ */
16
+ import ts from 'typescript';
17
+ import type { TypeProvenance } from './api.js';
18
+ /** A disqualifying finding below the root of a captured type: an author-baked
19
+ * `any`/`unknown`, or `budget_exhausted` — a subtree the walk could not finish
20
+ * within its depth/node budget, treated as unverifiable (fail closed) rather
21
+ * than silently clean. */
22
+ export type DeepTopType = Pick<TypeProvenance, 'kind' | 'path'>;
23
+ /**
24
+ * Depth-bounded structural walk for a disqualifying top type at ANY depth:
25
+ * a member, array element, index signature, type argument, or callable RETURN
26
+ * that resolved to `any` (bidirectionally assignable — an arbitrary
27
+ * counterparty shape reads compatible) or `unknown` (a failed-inference bake;
28
+ * carries no shape information). The check phase's probe gates are WHOLE-type
29
+ * only, so this walk owns the depths they cannot see. It is a genuine SUPERSET
30
+ * of v1's text-scan disqualifier (`contains_disqualifying_top_type`) at ALL
31
+ * depths/widths — so removing the `type_state == Unknown` pre-verdict (carrick
32
+ * #448) never turns a shape v1 would have abstained on into a false-compatible.
33
+ * The superset holds unconditionally because budget EXHAUSTION FAILS CLOSED
34
+ * (returns the `budget_exhausted` sentinel, not "clean"): a subtree too deep or
35
+ * wide to finish within budget is unverifiable, never silently compatible. A
36
+ * bigger bound would only relocate the fail-open cliff; failing closed removes
37
+ * it. v1's text scan is itself unbounded, so any disqualifier it would flag
38
+ * that lies past this walk's finite budget is still caught — as
39
+ * `budget_exhausted` rather than its exact kind.
40
+ *
41
+ * Cycle-safe. Two deliberate exceptions to "flag any/unknown anywhere":
42
+ * - callable PARAMETER types are NOT descended (only return types): a
43
+ * parameter `any` is contravariant and genuinely permissive (`(x: any) =>
44
+ * void` safely accepts a stricter counterparty), so it is not a masked
45
+ * mismatch and demoting it would over-demote a sound shape;
46
+ * - TypeScript's unresolved-reference `error` placeholder (`intrinsicName ===
47
+ * 'error'`) is excluded (see `flagOf`): it heals when the check installs the
48
+ * pinned external, so it is a healable decay, not an author-baked `any`.
49
+ * A type the walk cannot cheaply finish is NOT flagged — over-demoting a
50
+ * legitimately fully-resolved type is the failure mode this guard must not have.
51
+ */
52
+ export declare function findDisqualifyingTopTypes(root: ts.Type, program: ts.Program, checker: ts.TypeChecker, location: ts.Node): DeepTopType[];
53
+ /**
54
+ * Turn a deep finding into the published provenance entry (carrick#376).
55
+ *
56
+ * The self-check reads EMITTED declaration text, so a top type it finds is
57
+ * text: whatever produced it — an author annotation, or an emitter that
58
+ * printed a value it could not resolve — no install re-resolves it. That is
59
+ * `declared`, and saying so is more useful than the bare `any` a reader gets
60
+ * today. The one other cause it can distinguish is its own budget.
61
+ */
62
+ export declare function provenanceOf(finding: DeepTopType): TypeProvenance;