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,490 @@
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
+ import * as fs from 'node:fs';
23
+ import * as path from 'node:path';
24
+ /** package-lock.json (v2/v3 `packages` map, v1 `dependencies` fallback). */
25
+ function npmLockfileVersions(lockPath) {
26
+ const versions = new Map();
27
+ const nested = new Map();
28
+ try {
29
+ const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
30
+ if (lock.packages && typeof lock.packages === 'object') {
31
+ for (const [key, entry] of Object.entries(lock.packages)) {
32
+ const idx = key.lastIndexOf('node_modules/');
33
+ if (idx === -1 || !entry?.version)
34
+ continue;
35
+ const name = key.slice(idx + 'node_modules/'.length);
36
+ // Direct dependency: the top-level install path, exactly
37
+ // `node_modules/<name>` (idx === 0 means no nesting prefix).
38
+ if (idx === 0) {
39
+ if (!versions.has(name))
40
+ versions.set(name, entry.version);
41
+ }
42
+ else if (!nested.has(name)) {
43
+ nested.set(name, entry.version);
44
+ }
45
+ }
46
+ // Nested copies only pin packages with no top-level install at all.
47
+ for (const [name, version] of nested) {
48
+ if (!versions.has(name))
49
+ versions.set(name, version);
50
+ }
51
+ }
52
+ else if (lock.dependencies && typeof lock.dependencies === 'object') {
53
+ for (const [name, entry] of Object.entries(lock.dependencies)) {
54
+ if (entry?.version)
55
+ versions.set(name, entry.version);
56
+ }
57
+ }
58
+ }
59
+ catch {
60
+ // Unparseable lockfile: every external ends up in unpinned_externals.
61
+ }
62
+ return versions;
63
+ }
64
+ /** Strip a pnpm peer suffix / quoting from a resolved version value. */
65
+ function cleanPnpmVersion(raw) {
66
+ return raw.trim().replace(/^['"]|['"]$/g, '').split('(')[0].trim();
67
+ }
68
+ /**
69
+ * pnpm-lock.yaml. Two tiers, both via a targeted line parse (no YAML dep):
70
+ *
71
+ * 1. `importers:` — every importer's dependencies/devDependencies/
72
+ * optionalDependencies with their RESOLVED `version:` values. These are
73
+ * the direct (top-level) installs and win.
74
+ * 2. `packages:` — the flat closure. Keys look like `/zod@3.23.0:` (v6),
75
+ * `zod@3.23.0:` (v9), `/@scope/pkg@1.2.3(peer@x)…:`. Fallback for
76
+ * packages that are not a direct dependency of any importer; the
77
+ * packages map is name-sorted, so first-match here would otherwise pin
78
+ * the LOWEST version of a multi-version package.
79
+ */
80
+ function pnpmLockfileVersions(lockPath) {
81
+ const direct = new Map();
82
+ const fallback = new Map();
83
+ try {
84
+ const text = fs.readFileSync(lockPath, 'utf8');
85
+ const lines = text.split('\n');
86
+ let inPackages = false;
87
+ let inImporters = false;
88
+ let inDepsSection = false;
89
+ let currentDep;
90
+ for (const line of lines) {
91
+ if (/^\S/.test(line)) {
92
+ // New top-level key.
93
+ inPackages = /^packages:\s*$/.test(line);
94
+ inImporters = /^importers:\s*$/.test(line);
95
+ inDepsSection = false;
96
+ currentDep = undefined;
97
+ continue;
98
+ }
99
+ if (inImporters) {
100
+ // ` <importer>:` (2-space) resets; ` dependencies:` (4-space)
101
+ // opens a deps section; ` <name>:` (6-space) names a dep;
102
+ // ` version: <v>` (8-space) resolves it.
103
+ if (/^ {2}\S/.test(line)) {
104
+ inDepsSection = false;
105
+ currentDep = undefined;
106
+ }
107
+ else if (/^ {4}\S/.test(line)) {
108
+ inDepsSection = /^ {4}(dependencies|devDependencies|optionalDependencies):\s*$/.test(line);
109
+ currentDep = undefined;
110
+ }
111
+ else if (inDepsSection && /^ {6}\S/.test(line)) {
112
+ const m = /^ {6}['"]?([^'":]+)['"]?:\s*(\S.*)?$/.exec(line);
113
+ currentDep = m?.[1];
114
+ // Old inline form: ` zod: 3.23.8`.
115
+ if (m?.[1] && m[2] && !direct.has(m[1])) {
116
+ const v = cleanPnpmVersion(m[2]);
117
+ if (/^\d/.test(v))
118
+ direct.set(m[1], v);
119
+ }
120
+ }
121
+ else if (inDepsSection && currentDep && /^ {8}version:/.test(line)) {
122
+ const v = cleanPnpmVersion(line.slice(line.indexOf(':') + 1));
123
+ if (v && !direct.has(currentDep) && /^\d/.test(v))
124
+ direct.set(currentDep, v);
125
+ currentDep = undefined;
126
+ }
127
+ continue;
128
+ }
129
+ if (inPackages) {
130
+ // Two-space-indented keys only (package entries), e.g.
131
+ // /zod@3.23.0: | 'zod@3.23.0': | /@scope/pkg@1.2.3(peer@1.0.0):
132
+ const m = /^ {2}['"]?\/?((?:@[^/@'"]+\/)?[^/@'"]+)@([^('":]+)/.exec(line);
133
+ if (!m)
134
+ continue;
135
+ const [, name, version] = m;
136
+ if (!fallback.has(name))
137
+ fallback.set(name, version.trim());
138
+ }
139
+ }
140
+ }
141
+ catch {
142
+ // Unparseable lockfile: every external ends up in unpinned_externals.
143
+ }
144
+ const versions = new Map(direct);
145
+ for (const [name, version] of fallback) {
146
+ if (!versions.has(name))
147
+ versions.set(name, version);
148
+ }
149
+ return versions;
150
+ }
151
+ /**
152
+ * Published-semver gate for node_modules pins. Rejects anything a registry
153
+ * install could not satisfy: workspace/link protocol leakage
154
+ * (`workspace:*`, `link:...`) and yarn-berry's `0.0.0-use.local` local
155
+ * sentinel. A rejected version means "unpinned" (fail-closed abstain
156
+ * downstream), never a pin that would fail the synthetic-workspace install.
157
+ */
158
+ function isPublishedSemver(version) {
159
+ return (/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version) &&
160
+ !version.startsWith('0.0.0-use.'));
161
+ }
162
+ /**
163
+ * Direct-dependency ranges declared by the manifest sitting next to a
164
+ * lockfile: the workspace root's dependencies/devDependencies/
165
+ * optionalDependencies. Used to disambiguate multi-version packages in
166
+ * lockfiles that key entries by requested range (yarn-berry).
167
+ */
168
+ function rootPackageJsonRanges(pkgJsonPath) {
169
+ const ranges = new Map();
170
+ try {
171
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
172
+ for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
173
+ const deps = pkg?.[section];
174
+ if (!deps || typeof deps !== 'object')
175
+ continue;
176
+ for (const [name, range] of Object.entries(deps)) {
177
+ if (typeof range === 'string' && !ranges.has(name))
178
+ ranges.set(name, range);
179
+ }
180
+ }
181
+ }
182
+ catch {
183
+ // No/unreadable root manifest: multi-version packages abstain.
184
+ }
185
+ return ranges;
186
+ }
187
+ /**
188
+ * yarn-berry (v2+) yarn.lock. Detected by its `__metadata:` block; a yarn
189
+ * CLASSIC (v1) lockfile has no such block and a different entry syntax, so
190
+ * it bails to an empty map here (classic is not parsed — `installedVersions`
191
+ * covers installed classic checkouts).
192
+ *
193
+ * Entries key a comma-separated descriptor set on the resolved package:
194
+ *
195
+ * "ms@npm:2.0.0": → version: 2.0.0
196
+ * "ms@npm:^2.1.3": → version: 2.1.3
197
+ * "app@workspace:.": → version: 0.0.0-use.local
198
+ *
199
+ * Only `npm:` protocol descriptors are considered (`workspace:`, `patch:`,
200
+ * `portal:`, `link:`, git — none registry-installable as the bare name).
201
+ * npm-alias descriptors (`foo@npm:bar@^1.0.0`) resolve a DIFFERENT package
202
+ * than the install name, so they never pin either.
203
+ *
204
+ * Multi-version selection: the lockfile keys by requested range, not by
205
+ * install position, so the direct install is identified through the
206
+ * workspace-root package.json — the entry whose descriptor set contains
207
+ * `<name>@npm:<root-declared-range>` wins. Multiple versions and no direct
208
+ * match → unpinned (abstain).
209
+ */
210
+ function yarnBerryLockfileVersions(lockPath) {
211
+ const versions = new Map();
212
+ try {
213
+ const text = fs.readFileSync(lockPath, 'utf8');
214
+ if (!/^__metadata:/m.test(text))
215
+ return versions;
216
+ // name → (requested range → resolved version), npm: descriptors only.
217
+ const byName = new Map();
218
+ let current = [];
219
+ for (const line of text.split('\n')) {
220
+ if (line.startsWith('#'))
221
+ continue;
222
+ const keyMatch = /^(\S.*):\s*$/.exec(line);
223
+ if (keyMatch) {
224
+ // New top-level entry: a descriptor set (or `__metadata`).
225
+ current = [];
226
+ if (keyMatch[1] === '__metadata')
227
+ continue;
228
+ for (const rawDesc of keyMatch[1].split(',')) {
229
+ const desc = rawDesc.trim().replace(/^['"]|['"]$/g, '');
230
+ const at = desc.indexOf('@', 1); // 1: skip a scope's leading @
231
+ if (at <= 0)
232
+ continue;
233
+ const name = desc.slice(0, at);
234
+ const protocolAndRange = desc.slice(at + 1);
235
+ if (!protocolAndRange.startsWith('npm:'))
236
+ continue;
237
+ const range = protocolAndRange.slice('npm:'.length);
238
+ if (range.includes('@'))
239
+ continue; // npm alias: foo@npm:bar@^1.0.0
240
+ current.push({ name, range });
241
+ }
242
+ continue;
243
+ }
244
+ const versionMatch = /^ {2}version:\s*(.+?)\s*$/.exec(line);
245
+ if (versionMatch && current.length > 0) {
246
+ const version = versionMatch[1].replace(/^['"]|['"]$/g, '');
247
+ for (const { name, range } of current) {
248
+ let ranges = byName.get(name);
249
+ if (!ranges)
250
+ byName.set(name, (ranges = new Map()));
251
+ ranges.set(range, version);
252
+ }
253
+ current = [];
254
+ }
255
+ }
256
+ const rootRanges = rootPackageJsonRanges(path.join(path.dirname(lockPath), 'package.json'));
257
+ for (const [name, ranges] of byName) {
258
+ const distinct = new Set(ranges.values());
259
+ let resolved;
260
+ if (distinct.size === 1) {
261
+ resolved = distinct.values().next().value;
262
+ }
263
+ else {
264
+ const declared = rootRanges.get(name);
265
+ if (declared !== undefined) {
266
+ const expected = declared.startsWith('npm:')
267
+ ? declared.slice('npm:'.length)
268
+ : declared;
269
+ resolved = ranges.get(expected);
270
+ }
271
+ // No direct match among multiple versions: abstain.
272
+ }
273
+ if (resolved !== undefined && isPublishedSemver(resolved)) {
274
+ versions.set(name, resolved);
275
+ }
276
+ }
277
+ }
278
+ catch {
279
+ // Unparseable lockfile: every external ends up in unpinned_externals.
280
+ }
281
+ return versions;
282
+ }
283
+ /**
284
+ * Strip JSONC affordances (line/block comments, trailing commas) so bun's
285
+ * text lockfile can be JSON.parsed. String-aware: `//` inside a string
286
+ * (registry URLs) is content, not a comment.
287
+ */
288
+ function stripJsonc(text) {
289
+ let noComments = '';
290
+ let inString = false;
291
+ for (let i = 0; i < text.length; i++) {
292
+ const c = text[i];
293
+ if (inString) {
294
+ noComments += c;
295
+ if (c === '\\') {
296
+ noComments += text[i + 1] ?? '';
297
+ i++;
298
+ }
299
+ else if (c === '"') {
300
+ inString = false;
301
+ }
302
+ continue;
303
+ }
304
+ if (c === '"') {
305
+ inString = true;
306
+ noComments += c;
307
+ }
308
+ else if (c === '/' && text[i + 1] === '/') {
309
+ while (i < text.length && text[i] !== '\n')
310
+ i++;
311
+ i--;
312
+ }
313
+ else if (c === '/' && text[i + 1] === '*') {
314
+ i += 2;
315
+ while (i < text.length && !(text[i] === '*' && text[i + 1] === '/'))
316
+ i++;
317
+ i++;
318
+ }
319
+ else {
320
+ noComments += c;
321
+ }
322
+ }
323
+ let out = '';
324
+ inString = false;
325
+ for (let i = 0; i < noComments.length; i++) {
326
+ const c = noComments[i];
327
+ if (inString) {
328
+ out += c;
329
+ if (c === '\\') {
330
+ out += noComments[i + 1] ?? '';
331
+ i++;
332
+ }
333
+ else if (c === '"') {
334
+ inString = false;
335
+ }
336
+ continue;
337
+ }
338
+ if (c === '"') {
339
+ inString = true;
340
+ }
341
+ else if (c === ',') {
342
+ let j = i + 1;
343
+ while (j < noComments.length && /\s/.test(noComments[j]))
344
+ j++;
345
+ if (noComments[j] === '}' || noComments[j] === ']')
346
+ continue; // trailing
347
+ }
348
+ out += c;
349
+ }
350
+ return out;
351
+ }
352
+ /**
353
+ * bun's TEXT bun.lock (JSONC). The binary bun.lockb is out of scope. The
354
+ * `packages` map is keyed by hoisted install path — `"ms"` is the top-level
355
+ * install, `"debug/ms"` a copy nested under debug — with the resolution as
356
+ * the first tuple element (`"ms@2.1.3"`, `"@probe/member@workspace:…"`).
357
+ *
358
+ * Mirrors the npm parser's direct-first rule: the top-level key (key ===
359
+ * package name) is what the emitted tree resolves against and wins. A
360
+ * top-level entry that fails the published-semver gate (workspace member,
361
+ * git, tarball) blocks nested fallback entirely — the tree resolves to the
362
+ * unpinnable thing, so pinning a nested registry copy would be wrong.
363
+ * Nested-only packages pin solely when every nested copy agrees on one
364
+ * version; several distinct nested versions → unpinned (abstain). Alias
365
+ * installs (key name ≠ resolved package name) never pin.
366
+ */
367
+ function bunLockfileVersions(lockPath) {
368
+ const versions = new Map();
369
+ try {
370
+ const lock = JSON.parse(stripJsonc(fs.readFileSync(lockPath, 'utf8')));
371
+ const packages = lock?.packages;
372
+ if (!packages || typeof packages !== 'object')
373
+ return versions;
374
+ const directNames = new Set();
375
+ const nested = new Map();
376
+ for (const [key, entry] of Object.entries(packages)) {
377
+ if (!Array.isArray(entry) || typeof entry[0] !== 'string')
378
+ continue;
379
+ const spec = entry[0]; // "<name>@<resolution>"
380
+ const at = spec.indexOf('@', 1); // 1: skip a scope's leading @
381
+ if (at <= 0)
382
+ continue;
383
+ const name = spec.slice(0, at);
384
+ const resolved = spec.slice(at + 1);
385
+ if (key === name) {
386
+ directNames.add(name);
387
+ if (isPublishedSemver(resolved))
388
+ versions.set(name, resolved);
389
+ }
390
+ else if (key.endsWith(`/${name}`)) {
391
+ let copies = nested.get(name);
392
+ if (!copies)
393
+ nested.set(name, (copies = new Set()));
394
+ copies.add(resolved);
395
+ }
396
+ }
397
+ for (const [name, copies] of nested) {
398
+ if (directNames.has(name) || copies.size !== 1)
399
+ continue;
400
+ const version = copies.values().next().value;
401
+ if (version !== undefined && isPublishedSemver(version))
402
+ versions.set(name, version);
403
+ }
404
+ }
405
+ catch {
406
+ // Unparseable lockfile: every external ends up in unpinned_externals.
407
+ }
408
+ return versions;
409
+ }
410
+ /**
411
+ * Resolve exact versions from the repo's installed node_modules by reading
412
+ * `node_modules/<pkg>/package.json` for each referenced external. On an
413
+ * installed checkout this is ground truth — the versions the repo actually
414
+ * runs against — and it needs no lockfile parser, which is what covers the
415
+ * formats without one (yarn classic v1, binary bun.lockb).
416
+ *
417
+ * Workspace-member guard (fail-closed): package managers link workspace
418
+ * siblings into node_modules as symlinks (`node_modules/@ws/member ->
419
+ * ../../packages/member`). Those carry unpublished versions; pinning one
420
+ * would make the whole synthetic-workspace install fail at check time. So a
421
+ * pin is taken only when the entry's realpath still lives under a
422
+ * node_modules directory inside the repo. pnpm's own
423
+ * `node_modules/<pkg> -> node_modules/.pnpm/<pkg>@<v>/node_modules/<pkg>`
424
+ * links pass that test and stay pinnable; realpaths inside the repo but
425
+ * outside every node_modules (workspace members) and realpaths outside the
426
+ * repo entirely (npm/yarn link to a dev checkout) are skipped — the
427
+ * external stays unpinned unless the lockfile supplies a version.
428
+ */
429
+ export function installedVersions(repoRoot, names) {
430
+ const versions = new Map();
431
+ const nodeModules = path.join(path.resolve(repoRoot), 'node_modules');
432
+ let realRepoRoot;
433
+ try {
434
+ realRepoRoot = fs.realpathSync(path.resolve(repoRoot));
435
+ }
436
+ catch {
437
+ return versions;
438
+ }
439
+ for (const name of names) {
440
+ // Package names come from bare import specifiers; anything with empty
441
+ // or dot segments cannot be a node_modules entry (and must not escape).
442
+ const segments = name.split('/');
443
+ if (segments.some((s) => s === '' || s === '.' || s === '..'))
444
+ continue;
445
+ const pkgDir = path.join(nodeModules, ...segments);
446
+ try {
447
+ const real = fs.realpathSync(pkgDir);
448
+ const rel = path.relative(realRepoRoot, real);
449
+ const inRepo = rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
450
+ if (!inRepo || !rel.split(path.sep).includes('node_modules'))
451
+ continue;
452
+ const manifest = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8'));
453
+ if (typeof manifest?.version === 'string' && isPublishedSemver(manifest.version)) {
454
+ versions.set(name, manifest.version);
455
+ }
456
+ }
457
+ catch {
458
+ // Missing dir, dangling symlink, unreadable or corrupt package.json:
459
+ // the external stays unpinned here (lockfile fallback or abstain).
460
+ }
461
+ }
462
+ return versions;
463
+ }
464
+ /**
465
+ * Resolve exact versions for the repo. Walks up from repoRoot so monorepo
466
+ * members with a hoisted root lockfile still pin (nearest lockfile wins;
467
+ * package-lock.json > pnpm-lock.yaml > yarn.lock > bun.lock at the same
468
+ * level).
469
+ */
470
+ export function lockfileVersions(repoRoot) {
471
+ let dir = path.resolve(repoRoot);
472
+ for (;;) {
473
+ const npmLock = path.join(dir, 'package-lock.json');
474
+ if (fs.existsSync(npmLock))
475
+ return npmLockfileVersions(npmLock);
476
+ const pnpmLock = path.join(dir, 'pnpm-lock.yaml');
477
+ if (fs.existsSync(pnpmLock))
478
+ return pnpmLockfileVersions(pnpmLock);
479
+ const yarnLock = path.join(dir, 'yarn.lock');
480
+ if (fs.existsSync(yarnLock))
481
+ return yarnBerryLockfileVersions(yarnLock);
482
+ const bunLock = path.join(dir, 'bun.lock');
483
+ if (fs.existsSync(bunLock))
484
+ return bunLockfileVersions(bunLock);
485
+ const parent = path.dirname(dir);
486
+ if (parent === dir)
487
+ return new Map();
488
+ dir = parent;
489
+ }
490
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Framework-machinery detection for the v2 capture path (carrick#371).
3
+ *
4
+ * A producer response anchor whose resolved type IS or CONTAINS HTTP transport
5
+ * machinery — a fetch/DOM `Response`/`Request`, a Node `http.ServerResponse`, a
6
+ * wrapper envelope `{ response: Response; error?: undefined } | { ...; error }`,
7
+ * or a wrapper function `(req: Request) => Promise<Response>` — must never be
8
+ * emitted as a comparable contract. Comparing it against the consumer's real
9
+ * payload manufactures a false compat mismatch (the bug this module fixes).
10
+ *
11
+ * This is the raw-`ts` seam mirror of `type-inferrer.ts`'s
12
+ * `typeIsOrContainsResponseMachinery` (ts-morph): the capture/ seam forbids
13
+ * importing a module from outside it, so the canonical indicator set and the
14
+ * detection shape are duplicated here in lockstep — the same pattern by which
15
+ * `BUILTIN_ANCHOR_SYMBOLS` mirrors `socket_io.rs`. Detection is STRUCTURAL and
16
+ * framework-agnostic: no framework NAME appears, only the shared HTTP-message
17
+ * member surface, gated by a lib / `node_modules` declaration origin so a user
18
+ * payload that merely shares a member name can never trip it.
19
+ */
20
+ import ts from 'typescript';
21
+ /**
22
+ * Strongly-discriminating member names of HTTP transport machinery. Kept
23
+ * identical to `MACHINERY_MEMBER_INDICATORS` in `type-inferrer.ts`. These are
24
+ * the names no JSON payload carries (`ok`, `redirected`, `bodyUsed`,
25
+ * `writeHead`, ...), so the origin gate + threshold never fire on real data.
26
+ * Exported so a drift-guard test (`machinery-indicator-mirror.test.ts`) asserts
27
+ * it stays equal to the `type-inferrer.ts` copy — nothing else enforces the
28
+ * lockstep, and if the two drift one path silently stops abstaining.
29
+ */
30
+ export declare const MACHINERY_MEMBER_INDICATORS: Set<string>;
31
+ /**
32
+ * True when `type`, resolved against `node`, IS or CONTAINS framework
33
+ * machinery.
34
+ *
35
+ * DETECTS, exactly:
36
+ * 1. the type itself is machinery (`isFrameworkMachinery`);
37
+ * 2. a union/intersection member is machinery (the envelope union);
38
+ * 3. a DIRECT property whose type is machinery (`{ response: Response }`),
39
+ * ONE level of descent only;
40
+ * 4. a CALL SIGNATURE whose AWAITED return type is machinery — the wrapper
41
+ * function `(req) => Promise<Response>` the Infer fallback resolves to.
42
+ * (Only the call return is awaited; property types below are not.)
43
+ *
44
+ * DELIBERATELY NOT DETECTED — stated so this comment never overstates the
45
+ * guarantee (mirrors the same list in `type-inferrer.ts`). Each is a
46
+ * non-regression (pre-existing verdict unchanged, never a new wrong one), a
47
+ * tracked follow-up:
48
+ * - machinery nested deeper than one property level;
49
+ * - a PROPERTY typed `Promise<Response>` (property types are not awaited /
50
+ * Promise-unwrapped before the check — only call-signature returns are);
51
+ * - an array element type: `Response[]` is not descended to its element;
52
+ * - `interface X extends Response` declared in USER source — the origin gate
53
+ * is lib/`node_modules` only, so a user-declared subtype reads as a real
54
+ * contract.
55
+ *
56
+ * The depth cap + origin gate keep a legitimate payload that merely references a
57
+ * machinery type far inside from over-abstaining.
58
+ */
59
+ export declare function typeIsOrContainsMachinery(checker: ts.TypeChecker, type: ts.Type, node: ts.Node): boolean;