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,477 @@
1
+ /**
2
+ * Type-compat v2 capture core: "tsc as the serializer"
3
+ * (docs/reference/type-compat-synthetic-monorepo.md, Capture phase).
4
+ *
5
+ * Produces a types-only stub package for one service:
6
+ *
7
+ * @carrick/<service>/
8
+ * |- package.json name, types entry, pinned deps (exact versions)
9
+ * |- tsconfig.snapshot.json
10
+ * |- carrick-manifest.json per-alias records + fidelity metric
11
+ * `- types/
12
+ * |- surface.d.ts entry: export type <alias> = ...
13
+ * `- nested .d.ts tree compiler-emitted declaration closure
14
+ *
15
+ * Two-phase flow:
16
+ * Phase A (analysis): a placeholder surface entry + the anchors' source
17
+ * files form a program; addressable anchors run their guards, anonymous
18
+ * anchors are located and printed via the SymbolTracker-backed node
19
+ * builder (anchored at their placeholder -- the destination file).
20
+ * Phase B (emit): the final entry runs `tsc --noCheck --declaration
21
+ * --emitDeclarationOnly` with the repo's own parsed options, plus every
22
+ * detected augmentation file as an extra root; the tree is relocated
23
+ * into the stub, specifiers are rewritten, deps pinned, and the
24
+ * per-alias self-check classifies the result.
25
+ *
26
+ * Seam note: this directory is the whole v2 capture bundle. It imports only
27
+ * node builtins and `typescript`; the rest of the sidecar reaches it only
28
+ * through ./api.js types and this file's `captureStub`.
29
+ */
30
+ import ts from 'typescript';
31
+ import * as fs from 'node:fs';
32
+ import * as path from 'node:path';
33
+ import * as os from 'node:os';
34
+ import { entryRelativeSpecifier, resolveAnchor } from './anchors.js';
35
+ import { findAugmentationFiles } from './augmentations.js';
36
+ import { installedVersions, lockfileVersions } from './lockfile.js';
37
+ import { rewriteEmittedSpecifiers } from './paths-rewrite.js';
38
+ import { selfCheckStub } from './self-check.js';
39
+ import { collectSpecifiers, isRelative, packageNameOf } from './specifiers.js';
40
+ // v2 check core ("tsc as the judge"). Same bundle, same seam: the sidecar
41
+ // reaches it only through this door (index.js).
42
+ export { runCheck } from './check.js';
43
+ const SURFACE_ENTRY_BASENAME = '__carrick_surface__';
44
+ /** Same normalization intent as bundle_file_stems on the Rust side. */
45
+ export function sanitizeServiceName(name) {
46
+ return name.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
47
+ }
48
+ function fail(stubDir, packageName, errors) {
49
+ return {
50
+ success: false,
51
+ stub_dir: stubDir,
52
+ package_name: packageName,
53
+ emitted_files: [],
54
+ pinned_dependencies: {},
55
+ unpinned_externals: [],
56
+ aliases: [],
57
+ fidelity: emptyFidelity(),
58
+ augmentation_files: [],
59
+ specifier_rewrites: 0,
60
+ bare_checkout: false,
61
+ ts_version: ts.version,
62
+ errors,
63
+ };
64
+ }
65
+ function emptyFidelity() {
66
+ return {
67
+ total_aliases: 0,
68
+ by_serialization: { emitted: 0, node_builder: 0, structural_fallback: 0 },
69
+ by_self_check: { ok: 0, allowlisted_external: 0, decayed_internal: 0 },
70
+ by_anchor_origin: { 'llm-symbol': 0, 'deterministic-infer': 0, 'anchor-backfill': 0 },
71
+ usable_rate: 0,
72
+ };
73
+ }
74
+ function computeFidelity(records) {
75
+ const fidelity = emptyFidelity();
76
+ fidelity.total_aliases = records.length;
77
+ for (const record of records) {
78
+ fidelity.by_serialization[record.serialization]++;
79
+ fidelity.by_self_check[record.self_check]++;
80
+ fidelity.by_anchor_origin[record.anchor_origin]++;
81
+ }
82
+ const usable = fidelity.by_self_check.ok + fidelity.by_self_check.allowlisted_external;
83
+ fidelity.usable_rate =
84
+ records.length === 0 ? 0 : Math.round((usable / records.length) * 1000) / 1000;
85
+ return fidelity;
86
+ }
87
+ export function captureStub(opts) {
88
+ const repoRoot = path.resolve(opts.repoRoot);
89
+ const packageName = `@carrick/${sanitizeServiceName(opts.serviceName)}`;
90
+ const stubDir = path.resolve(opts.outDir);
91
+ const errors = [];
92
+ const configPath = opts.tsconfigPath
93
+ ? path.resolve(opts.tsconfigPath)
94
+ : path.join(repoRoot, 'tsconfig.json');
95
+ let parsed;
96
+ if (!fs.existsSync(configPath)) {
97
+ if (opts.tsconfigPath) {
98
+ // An explicitly named tsconfig that does not exist is a caller bug.
99
+ return fail(stubDir, packageName, [`tsconfig not found at ${configPath}`]);
100
+ }
101
+ // No tsconfig in the repo: synthesize defaults (parity with the v1
102
+ // project loader's DEFAULT_COMPILER_OPTIONS) so tsconfig-less repos
103
+ // still capture instead of shipping no surface at all.
104
+ parsed = ts.parseJsonConfigFileContent({
105
+ compilerOptions: {
106
+ target: 'ESNext',
107
+ module: 'ESNext',
108
+ moduleResolution: 'Bundler',
109
+ strict: true,
110
+ esModuleInterop: true,
111
+ skipLibCheck: true,
112
+ allowJs: true,
113
+ checkJs: false,
114
+ resolveJsonModule: true,
115
+ },
116
+ }, ts.sys, repoRoot);
117
+ }
118
+ else {
119
+ const configHost = {
120
+ ...ts.sys,
121
+ onUnRecoverableConfigFileDiagnostic: (d) => {
122
+ throw new Error(ts.flattenDiagnosticMessageText(d.messageText, '\n'));
123
+ },
124
+ };
125
+ try {
126
+ parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, configHost) ?? undefined;
127
+ }
128
+ catch (err) {
129
+ return fail(stubDir, packageName, [err instanceof Error ? err.message : String(err)]);
130
+ }
131
+ }
132
+ if (!parsed) {
133
+ return fail(stubDir, packageName, [`failed to parse ${configPath}`]);
134
+ }
135
+ // The surface entry must live inside the effective rootDir (design doc
136
+ // Capture step 1: an entry at repo root with rootDir "src" fails TS6059).
137
+ const entryDir = parsed.options.rootDir
138
+ ? path.resolve(path.dirname(configPath), parsed.options.rootDir)
139
+ : repoRoot;
140
+ const entryPath = path.join(entryDir, `${SURFACE_ENTRY_BASENAME}.ts`);
141
+ // ---- Phase A: analysis program over placeholder entry + anchor sources ----
142
+ let resolved;
143
+ try {
144
+ resolved = resolveAnchors(opts, parsed, { repoRoot, entryDir, entryPath });
145
+ }
146
+ catch (err) {
147
+ return fail(stubDir, packageName, [err instanceof Error ? err.message : String(err)]);
148
+ }
149
+ // ---- Augmentation detection over the tsconfig's full file list ----
150
+ const augmentationSources = findAugmentationFiles(parsed.fileNames.filter((f) => !f.includes(`${path.sep}node_modules${path.sep}`)));
151
+ // ---- Phase B: declaration emit of the final entry ----
152
+ const entryLines = ['// Generated by Carrick capture v2. Deleted after emit.'];
153
+ for (const anchor of resolved) {
154
+ const comment = anchor.failureReason
155
+ ? ` // capture-degraded: ${anchor.failureReason.replace(/\n/g, ' ')}`
156
+ : '';
157
+ entryLines.push(`export type ${anchor.request.alias} = ${anchor.aliasText};${comment}`);
158
+ }
159
+ const staging = fs.mkdtempSync(path.join(os.tmpdir(), 'carrick-capture-v2-'));
160
+ const emitted = new Map();
161
+ // Input .d.ts files (ambient stubs, augmentation declarations, local
162
+ // hand-written declarations in the import closure) are never re-emitted by
163
+ // tsc; they must ship verbatim or the tree's references to them dangle.
164
+ const declarationSources = new Map();
165
+ let emitPartial = false;
166
+ try {
167
+ fs.writeFileSync(entryPath, entryLines.join('\n') + '\n');
168
+ const emitOptions = {
169
+ ...parsed.options,
170
+ // The load-bearing trio: emit declarations without checking, so
171
+ // type-error-laden and bare (no node_modules) checkouts still emit.
172
+ noCheck: true,
173
+ declaration: true,
174
+ emitDeclarationOnly: true,
175
+ noEmit: false,
176
+ declarationMap: false,
177
+ composite: false,
178
+ incremental: false,
179
+ outDir: staging,
180
+ rootDir: entryDir,
181
+ };
182
+ const program = ts.createProgram([entryPath, ...augmentationSources], emitOptions);
183
+ const emitResult = program.emit(undefined, (fileName, text) => emitted.set(fileName, text), undefined,
184
+ /* emitOnlyDtsFiles */ true);
185
+ // emitSkipped is PER-PROGRAM even when only one file's declaration emit
186
+ // failed (e.g. TS4023 from a hand-rolled ambient stub shadowing a real
187
+ // package): every other file's .d.ts was still written to the callback.
188
+ // Fail wholesale only when nothing at all emitted; otherwise keep the
189
+ // emitted subset and demote exactly the aliases it cannot support.
190
+ if (emitResult.emitSkipped && emitted.size === 0) {
191
+ return fail(stubDir, packageName, ['declaration emit was skipped']);
192
+ }
193
+ emitPartial = emitResult.emitSkipped;
194
+ for (const d of emitResult.diagnostics) {
195
+ errors.push(ts.flattenDiagnosticMessageText(d.messageText, '\n'));
196
+ }
197
+ for (const sourceFile of program.getSourceFiles()) {
198
+ if (!sourceFile.isDeclarationFile)
199
+ continue;
200
+ const abs = path.resolve(sourceFile.fileName);
201
+ const rel = path.relative(entryDir, abs).split(path.sep).join('/');
202
+ if (rel.startsWith('..') || rel.includes('node_modules/'))
203
+ continue;
204
+ declarationSources.set(rel, sourceFile.getFullText());
205
+ }
206
+ }
207
+ catch (err) {
208
+ return fail(stubDir, packageName, [err instanceof Error ? err.message : String(err)]);
209
+ }
210
+ finally {
211
+ if (fs.existsSync(entryPath))
212
+ fs.unlinkSync(entryPath);
213
+ fs.rmSync(staging, { recursive: true, force: true });
214
+ }
215
+ // ---- Partial-emit recovery ----
216
+ // The corpus-2 notifications-svc shape: one file's declaration emit was
217
+ // skipped but the rest of the tree emitted fine. Keep the tree; demote any
218
+ // alias whose surface reference would dangle (its module produced no .d.ts)
219
+ // and rewrite its surface line to `unknown` so the kept tree carries no
220
+ // dangling specifiers that would smear the healthy aliases at self-check or
221
+ // poison the whole service at check time. Fail-closed: a demoted alias is
222
+ // `unknown` at the surface, so the check phase's IsUnknown probe gate (or
223
+ // the poison rule, for unemitted modules still referenced by kept files)
224
+ // decays it to unverifiable — it can never read compatible.
225
+ if (emitPartial) {
226
+ errors.push(`declaration emit was partial: kept ${emitted.size} emitted file(s); ` +
227
+ 'aliases referencing unemitted modules are demoted to structural_fallback');
228
+ resolved = demoteDanglingAliases({ resolved, emitted, declarationSources, staging });
229
+ }
230
+ // ---- Relocate the emitted tree into the stub package ----
231
+ const typesDir = path.join(stubDir, 'types');
232
+ fs.rmSync(stubDir, { recursive: true, force: true });
233
+ fs.mkdirSync(typesDir, { recursive: true });
234
+ const emittedFiles = [];
235
+ let surfaceAbsPath = '';
236
+ for (const [fileName, text] of emitted) {
237
+ let rel = path.relative(staging, fileName).split(path.sep).join('/');
238
+ if (rel === `${SURFACE_ENTRY_BASENAME}.d.ts`)
239
+ rel = 'surface.d.ts';
240
+ const dest = path.join(typesDir, rel);
241
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
242
+ fs.writeFileSync(dest, text);
243
+ emittedFiles.push(rel);
244
+ if (rel === 'surface.d.ts')
245
+ surfaceAbsPath = dest;
246
+ }
247
+ if (!surfaceAbsPath) {
248
+ return fail(stubDir, packageName, ['no surface.d.ts produced by emit']);
249
+ }
250
+ // Verbatim copies of in-repo declaration sources (see declarationSources).
251
+ for (const [rel, text] of declarationSources) {
252
+ if (emittedFiles.includes(rel))
253
+ continue;
254
+ const dest = path.join(typesDir, rel);
255
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
256
+ fs.writeFileSync(dest, text);
257
+ emittedFiles.push(rel);
258
+ }
259
+ // Tree-relative names of augmentation files that made it into the tree
260
+ // (.ts augmentations arrive via declaration emit, .d.ts ones verbatim).
261
+ const augmentationFiles = augmentationSources
262
+ .map((abs) => {
263
+ const noDts = abs.replace(/\.d\.ts$/, '');
264
+ const noExt = noDts === abs ? abs.replace(/\.(ts|tsx|mts|cts)$/, '') : noDts;
265
+ const rel = path.relative(entryDir, noExt).split(path.sep).join('/');
266
+ return `${rel}.d.ts`;
267
+ })
268
+ .filter((rel) => emittedFiles.includes(rel))
269
+ .map((rel) => `types/${rel}`);
270
+ // ---- Post-emit specifier rewrite (paths mappings + absolute internals) ----
271
+ const specifierRewrites = rewriteEmittedSpecifiers({
272
+ typesDir,
273
+ files: emittedFiles,
274
+ options: parsed.options,
275
+ configPath,
276
+ entryDir,
277
+ });
278
+ // ---- Pin external deps: installed node_modules first, lockfile fallback ----
279
+ // Externals are collected AFTER the rewrite pass: a rewritten paths
280
+ // specifier is internal, not a dependency.
281
+ const externalSpecs = new Set();
282
+ for (const rel of emittedFiles) {
283
+ const text = fs.readFileSync(path.join(typesDir, rel), 'utf8');
284
+ for (const spec of collectSpecifiers(text)) {
285
+ if (!isRelative(spec) && !spec.startsWith('node:')) {
286
+ externalSpecs.add(packageNameOf(spec));
287
+ }
288
+ }
289
+ }
290
+ // Precedence: an installed checkout's node_modules is what the repo
291
+ // actually resolves against, so it wins over the lockfile — and it pins
292
+ // repos whose lockfiles we do not parse (yarn classic v1, binary
293
+ // bun.lockb). The parsed lockfile (npm, pnpm, yarn-berry, text bun.lock)
294
+ // remains the bare-checkout fallback. Both paths pin only
295
+ // the directly-referenced externals; transitives resolve at check-install
296
+ // (check-workspace NPMRC: "Direct deps are exact-pinned by the stubs;
297
+ // only transitives resolve").
298
+ const installed = installedVersions(repoRoot, externalSpecs);
299
+ const lockVersions = lockfileVersions(repoRoot);
300
+ const pinned = {};
301
+ const unpinned = [];
302
+ for (const name of [...externalSpecs].sort()) {
303
+ const version = installed.get(name) ?? lockVersions.get(name);
304
+ if (version)
305
+ pinned[name] = version;
306
+ else
307
+ unpinned.push(name);
308
+ }
309
+ const bareCheckout = !fs.existsSync(path.join(repoRoot, 'node_modules'));
310
+ fs.writeFileSync(path.join(stubDir, 'package.json'), JSON.stringify({
311
+ name: packageName,
312
+ version: '0.0.0-carrick',
313
+ private: true,
314
+ types: './types/surface.d.ts',
315
+ dependencies: pinned,
316
+ }, null, 2) + '\n');
317
+ fs.writeFileSync(path.join(stubDir, 'tsconfig.snapshot.json'), JSON.stringify({
318
+ ts_version: ts.version,
319
+ strict: parsed.options.strict ?? false,
320
+ strictNullChecks: parsed.options.strictNullChecks ?? parsed.options.strict ?? false,
321
+ exactOptionalPropertyTypes: parsed.options.exactOptionalPropertyTypes ?? false,
322
+ module: parsed.options.module !== undefined ? ts.ModuleKind[parsed.options.module] : undefined,
323
+ target: parsed.options.target !== undefined ? ts.ScriptTarget[parsed.options.target] : undefined,
324
+ }, null, 2) + '\n');
325
+ // ---- Capture-time self-check (per-alias closure attribution) ----
326
+ const aliases = selfCheckStub({
327
+ stubDir,
328
+ surfaceAbsPath,
329
+ resolved,
330
+ pinned,
331
+ bareCheckout,
332
+ repoRoot,
333
+ });
334
+ const fidelity = computeFidelity(aliases);
335
+ fs.writeFileSync(path.join(stubDir, 'carrick-manifest.json'), JSON.stringify({
336
+ package_name: packageName,
337
+ ts_version: ts.version,
338
+ bare_checkout: bareCheckout,
339
+ aliases,
340
+ fidelity,
341
+ }, null, 2) + '\n');
342
+ return {
343
+ success: true,
344
+ stub_dir: stubDir,
345
+ package_name: packageName,
346
+ emitted_files: emittedFiles.map((rel) => `types/${rel}`).sort(),
347
+ pinned_dependencies: pinned,
348
+ unpinned_externals: unpinned,
349
+ aliases,
350
+ fidelity,
351
+ augmentation_files: augmentationFiles.sort(),
352
+ specifier_rewrites: specifierRewrites,
353
+ bare_checkout: bareCheckout,
354
+ ts_version: ts.version,
355
+ errors,
356
+ };
357
+ }
358
+ /**
359
+ * Partial-emit demotion: with the set of modules that DID reach the tree
360
+ * (emitted .d.ts plus verbatim declaration sources), demote every anchor
361
+ * whose alias text references a relative module absent from that set, and
362
+ * rewrite the demoted aliases' lines in the emitted surface to `unknown`.
363
+ * Anchors already demoted stay as they are; anchors whose text is
364
+ * self-contained (node-builder structural prints, literal object text) are
365
+ * untouched even when their source file failed to emit — their surface line
366
+ * references nothing that can dangle.
367
+ */
368
+ function demoteDanglingAliases(args) {
369
+ // Extensionless, entryDir-relative POSIX module ids present in the tree.
370
+ const treeModules = new Set();
371
+ let surfaceKey;
372
+ for (const fileName of args.emitted.keys()) {
373
+ const rel = path.relative(args.staging, fileName).split(path.sep).join('/');
374
+ if (rel === `${SURFACE_ENTRY_BASENAME}.d.ts`)
375
+ surfaceKey = fileName;
376
+ if (rel.endsWith('.d.ts'))
377
+ treeModules.add(rel.slice(0, -'.d.ts'.length));
378
+ }
379
+ for (const rel of args.declarationSources.keys()) {
380
+ if (rel.endsWith('.d.ts'))
381
+ treeModules.add(rel.slice(0, -'.d.ts'.length));
382
+ }
383
+ // The surface sits at the tree root, so its relative specifiers resolve
384
+ // against the root; anything escaping the root cannot be in the tree.
385
+ const moduleInTree = (spec) => {
386
+ const id = path.posix.normalize(spec);
387
+ if (id.startsWith('..'))
388
+ return false;
389
+ return treeModules.has(id) || treeModules.has(`${id}/index`);
390
+ };
391
+ const demoted = new Set();
392
+ const next = args.resolved.map((anchor) => {
393
+ if (anchor.failureReason !== undefined)
394
+ return anchor;
395
+ const dangling = [...collectSpecifiers(anchor.aliasText)].find((spec) => isRelative(spec) && !moduleInTree(spec));
396
+ if (dangling === undefined)
397
+ return anchor;
398
+ demoted.add(anchor.request.alias);
399
+ return {
400
+ request: anchor.request,
401
+ aliasText: 'unknown',
402
+ serialization: 'structural_fallback',
403
+ failureReason: `declaration emit was skipped for module '${dangling}'; ` +
404
+ 'alias demoted to keep the partially emitted tree usable',
405
+ };
406
+ });
407
+ if (surfaceKey !== undefined && demoted.size > 0) {
408
+ args.emitted.set(surfaceKey, rewriteSurfaceAliasesToUnknown(args.emitted.get(surfaceKey), demoted));
409
+ }
410
+ return next;
411
+ }
412
+ /** Replace `export type <name> = ...;` with `= unknown` for each demoted
413
+ * alias in the emitted surface text (span-accurate, statement-level). */
414
+ function rewriteSurfaceAliasesToUnknown(text, demoted) {
415
+ const sf = ts.createSourceFile('surface.d.ts', text, ts.ScriptTarget.Latest, true);
416
+ const spans = [];
417
+ for (const stmt of sf.statements) {
418
+ if (ts.isTypeAliasDeclaration(stmt) && demoted.has(stmt.name.text)) {
419
+ spans.push({ start: stmt.getStart(sf), end: stmt.getEnd(), name: stmt.name.text });
420
+ }
421
+ }
422
+ let out = text;
423
+ for (const span of spans.reverse()) {
424
+ out =
425
+ out.slice(0, span.start) +
426
+ `export type ${span.name} = unknown;` +
427
+ out.slice(span.end);
428
+ }
429
+ return out;
430
+ }
431
+ /** Phase A: build the placeholder entry, then resolve every anchor. */
432
+ function resolveAnchors(opts, parsed, ctx) {
433
+ const placeholderLines = ['// Carrick capture v2 analysis placeholder.'];
434
+ for (const anchor of opts.anchors) {
435
+ placeholderLines.push(`export type ${anchor.alias} = unknown;`);
436
+ }
437
+ fs.writeFileSync(ctx.entryPath, placeholderLines.join('\n') + '\n');
438
+ try {
439
+ const anchorSources = [
440
+ ...new Set(opts.anchors
441
+ .filter((a) => a.kind !== 'literal')
442
+ .map((a) => path.join(ctx.repoRoot, a.source_file))),
443
+ ].filter((f) => fs.existsSync(f));
444
+ const program = ts.createProgram([ctx.entryPath, ...anchorSources], {
445
+ ...parsed.options,
446
+ noEmit: true,
447
+ });
448
+ const entrySource = program.getSourceFile(ctx.entryPath);
449
+ const placeholders = new Map();
450
+ if (entrySource) {
451
+ for (const stmt of entrySource.statements) {
452
+ if (ts.isTypeAliasDeclaration(stmt))
453
+ placeholders.set(stmt.name.text, stmt);
454
+ }
455
+ }
456
+ // A literal anchor whose text is a bare identifier resolves through a
457
+ // sibling symbol anchor's module when one names the same symbol.
458
+ const siblingSymbolSpecs = new Map();
459
+ for (const anchor of opts.anchors) {
460
+ if (anchor.kind !== 'symbol')
461
+ continue;
462
+ if (!siblingSymbolSpecs.has(anchor.symbol_name)) {
463
+ siblingSymbolSpecs.set(anchor.symbol_name, entryRelativeSpecifier(ctx.entryDir, ctx.repoRoot, anchor.source_file));
464
+ }
465
+ }
466
+ return opts.anchors.map((request) => resolveAnchor(program, request, {
467
+ repoRoot: ctx.repoRoot,
468
+ entryDir: ctx.entryDir,
469
+ placeholder: placeholders.get(request.alias),
470
+ siblingSymbolSpecs,
471
+ }));
472
+ }
473
+ finally {
474
+ if (fs.existsSync(ctx.entryPath))
475
+ fs.unlinkSync(ctx.entryPath);
476
+ }
477
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Exact-version dependency pins for the stub package, from two sources:
3
+ *
4
+ * 1. `installedVersions` — the repo's own node_modules, when installed.
5
+ * This is the version the repo actually resolves against, so it wins
6
+ * over the lockfile, and it covers repos whose lockfiles we do not
7
+ * parse at all (yarn classic v1, binary bun.lockb).
8
+ * 2. `lockfileVersions` — parsed lockfile fallback for bare checkouts
9
+ * (npm v1/v2/v3, pnpm v6/v9, yarn-berry v2+, and bun's text bun.lock
10
+ * in scope).
11
+ *
12
+ * Pin selection prefers the DIRECT (top-level) dependency version: a package
13
+ * present at multiple versions must pin to the version the emitted tree
14
+ * resolves against — the hoisted/direct install — never to whichever nested
15
+ * copy happens to appear first in the lockfile (npm sorts
16
+ * `node_modules/a/node_modules/b` before `node_modules/b`, so first-match
17
+ * picks a nested version the tree never sees). When a lockfile offers
18
+ * multiple versions and no direct install can be identified, the package
19
+ * stays UNPINNED (fail-closed abstain): a wrong pin fails or corrupts the
20
+ * synthetic-workspace typecheck, no pin merely abstains.
21
+ */
22
+ /**
23
+ * Resolve exact versions from the repo's installed node_modules by reading
24
+ * `node_modules/<pkg>/package.json` for each referenced external. On an
25
+ * installed checkout this is ground truth — the versions the repo actually
26
+ * runs against — and it needs no lockfile parser, which is what covers the
27
+ * formats without one (yarn classic v1, binary bun.lockb).
28
+ *
29
+ * Workspace-member guard (fail-closed): package managers link workspace
30
+ * siblings into node_modules as symlinks (`node_modules/@ws/member ->
31
+ * ../../packages/member`). Those carry unpublished versions; pinning one
32
+ * would make the whole synthetic-workspace install fail at check time. So a
33
+ * pin is taken only when the entry's realpath still lives under a
34
+ * node_modules directory inside the repo. pnpm's own
35
+ * `node_modules/<pkg> -> node_modules/.pnpm/<pkg>@<v>/node_modules/<pkg>`
36
+ * links pass that test and stay pinnable; realpaths inside the repo but
37
+ * outside every node_modules (workspace members) and realpaths outside the
38
+ * repo entirely (npm/yarn link to a dev checkout) are skipped — the
39
+ * external stays unpinned unless the lockfile supplies a version.
40
+ */
41
+ export declare function installedVersions(repoRoot: string, names: Iterable<string>): Map<string, string>;
42
+ /**
43
+ * Resolve exact versions for the repo. Walks up from repoRoot so monorepo
44
+ * members with a hoisted root lockfile still pin (nearest lockfile wins;
45
+ * package-lock.json > pnpm-lock.yaml > yarn.lock > bun.lock at the same
46
+ * level).
47
+ */
48
+ export declare function lockfileVersions(repoRoot: string): Map<string, string>;