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,317 @@
1
+ /**
2
+ * Capture-time self-check (design doc, Capture step 8, amendment 1), with
3
+ * per-alias closure attribution.
4
+ *
5
+ * The stub tree is typechecked standalone with `skipLibCheck: false` --
6
+ * spike-verified as load-bearing: the tree is entirely .d.ts and skipLibCheck
7
+ * skips declaration files wholesale, making the gate vacuous. When the
8
+ * source repo has node_modules, resolution is pointed at it via a temporary
9
+ * node_modules symlink inside the stub, so externals resolve exactly as they
10
+ * will at check time against installed pins.
11
+ *
12
+ * Classification per alias (three-way, keyed on diagnostics, never on
13
+ * printed type text):
14
+ * - ok: resolves to a concrete type.
15
+ * - allowlisted_external: resolution failed only through external
16
+ * specifiers pinned in the stub's dependencies, on a bare checkout. The
17
+ * alias KEEPS its serialization tier; the check-phase probe gates
18
+ * (any/unknown/never, both sides) are the backstop.
19
+ * - decayed_internal: a dangling internal specifier, an unpinned external,
20
+ * or a top-type resolution with no allowlisted explanation.
21
+ *
22
+ * Attribution is per-alias closure: failed specifiers are blamed on an alias
23
+ * only if they occur in a file reachable from that alias's surface statement
24
+ * (import-type seeds, then BFS over relative imports). The spike's
25
+ * file-granularity shortcut is gone.
26
+ */
27
+ import ts from 'typescript';
28
+ import * as fs from 'node:fs';
29
+ import * as path from 'node:path';
30
+ import { collectSpecifiers, isRelative, packageNameOf } from './specifiers.js';
31
+ import { findDisqualifyingTopTypes, provenanceOf, } from './deep-walk.js';
32
+ export function selfCheckStub(args) {
33
+ const typesDir = path.join(args.stubDir, 'types');
34
+ const treeFiles = [];
35
+ const walk = (dir) => {
36
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
37
+ const p = path.join(dir, entry.name);
38
+ if (entry.isDirectory())
39
+ walk(p);
40
+ else if (entry.name.endsWith('.d.ts'))
41
+ treeFiles.push(p);
42
+ }
43
+ };
44
+ walk(typesDir);
45
+ // Point resolution at the producer repo's installed deps when present.
46
+ const repoNodeModules = path.join(args.repoRoot, 'node_modules');
47
+ const linkPath = path.join(args.stubDir, 'node_modules');
48
+ let linked = false;
49
+ if (!args.bareCheckout && fs.existsSync(repoNodeModules) && !fs.existsSync(linkPath)) {
50
+ fs.symlinkSync(repoNodeModules, linkPath, 'dir');
51
+ linked = true;
52
+ }
53
+ try {
54
+ return runSelfCheck(args, treeFiles);
55
+ }
56
+ finally {
57
+ // unlinkSync, not rmSync: the link target is a directory and rmSync
58
+ // refuses symlinks-to-directories with EISDIR.
59
+ if (linked)
60
+ fs.unlinkSync(linkPath);
61
+ }
62
+ }
63
+ function runSelfCheck(args, treeFiles) {
64
+ const program = ts.createProgram(treeFiles, {
65
+ noEmit: true,
66
+ strict: true,
67
+ // MUST be false: the whole stub tree is .d.ts (see module header).
68
+ skipLibCheck: false,
69
+ module: ts.ModuleKind.ESNext,
70
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
71
+ types: [],
72
+ });
73
+ const checker = program.getTypeChecker();
74
+ const diagnostics = ts.getPreEmitDiagnostics(program);
75
+ // Failed module specifiers, split external-pinned vs internal, per FILE.
76
+ const failuresByFile = new Map();
77
+ const failuresFor = (fileName) => {
78
+ let entry = failuresByFile.get(fileName);
79
+ if (!entry) {
80
+ entry = { externalPinned: new Set(), internal: new Set() };
81
+ failuresByFile.set(fileName, entry);
82
+ }
83
+ return entry;
84
+ };
85
+ for (const d of diagnostics) {
86
+ if ((d.code !== 2307 && d.code !== 2792) || !d.file)
87
+ continue;
88
+ const msg = ts.flattenDiagnosticMessageText(d.messageText, ' ');
89
+ const m = /Cannot find module '([^']+)'/.exec(msg);
90
+ if (!m)
91
+ continue;
92
+ const spec = m[1];
93
+ const bucket = failuresFor(path.resolve(d.file.fileName));
94
+ if (!isRelative(spec) && args.pinned[packageNameOf(spec)]) {
95
+ bucket.externalPinned.add(spec);
96
+ }
97
+ else {
98
+ bucket.internal.add(spec);
99
+ }
100
+ }
101
+ // Per-file relative-import adjacency for closure walks.
102
+ const adjacency = new Map();
103
+ const treeSet = new Set(treeFiles.map((f) => path.resolve(f)));
104
+ for (const file of treeFiles) {
105
+ const abs = path.resolve(file);
106
+ const text = fs.readFileSync(abs, 'utf8');
107
+ const neighbors = [];
108
+ for (const spec of collectSpecifiers(text)) {
109
+ const resolved = resolveTreeSpecifier(abs, spec, treeSet);
110
+ if (resolved)
111
+ neighbors.push(resolved);
112
+ }
113
+ adjacency.set(abs, neighbors);
114
+ }
115
+ const surfaceAbs = path.resolve(args.surfaceAbsPath);
116
+ const surfaceSource = program.getSourceFile(surfaceAbs);
117
+ const records = [];
118
+ for (const anchor of args.resolved) {
119
+ // Demotions (failureReason present) never reached the surface with a
120
+ // real type; everything else — including successful literal anchors,
121
+ // which sit at the structural_fallback TIER but did produce text — is
122
+ // classified by the real self-check.
123
+ records.push(anchor.failureReason !== undefined
124
+ ? demotedRecord(anchor)
125
+ : checkedRecord(anchor, {
126
+ args,
127
+ program,
128
+ checker,
129
+ surfaceAbs,
130
+ surfaceSource,
131
+ adjacency,
132
+ failuresByFile,
133
+ }));
134
+ }
135
+ return records;
136
+ }
137
+ /** An alias that never reached a capture-native tier: the failure reason was
138
+ * recorded at demotion time; the surface line is `unknown` by construction. */
139
+ function demotedRecord(anchor) {
140
+ return {
141
+ alias: anchor.request.alias,
142
+ anchor_kind: anchor.request.kind,
143
+ symbol_name: 'symbol_name' in anchor.request ? anchor.request.symbol_name : undefined,
144
+ source_file: 'source_file' in anchor.request ? anchor.request.source_file : '<inline>',
145
+ anchor_origin: anchor.request.anchor_origin,
146
+ serialization: 'structural_fallback',
147
+ self_check: 'decayed_internal',
148
+ self_check_detail: anchor.failureReason,
149
+ capture_failure_reason: anchor.failureReason,
150
+ top_type_at_self_check: true,
151
+ };
152
+ }
153
+ function checkedRecord(anchor, ctx) {
154
+ const alias = anchor.request.alias;
155
+ let topType = true;
156
+ let deepFindings = [];
157
+ const seeds = [];
158
+ if (ctx.surfaceSource) {
159
+ for (const stmt of ctx.surfaceSource.statements) {
160
+ if (!ts.isTypeAliasDeclaration(stmt) || stmt.name.text !== alias)
161
+ continue;
162
+ const type = ctx.checker.getTypeAtLocation(stmt.name);
163
+ topType =
164
+ (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) !== 0;
165
+ if (!topType) {
166
+ deepFindings = findDisqualifyingTopTypes(type, ctx.program, ctx.checker, stmt.name);
167
+ }
168
+ // Seed the closure with the alias's own import-type targets.
169
+ const visit = (node) => {
170
+ if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
171
+ const lit = node.argument.literal;
172
+ if (ts.isStringLiteral(lit) && isRelative(lit.text)) {
173
+ const resolved = resolveTreeSpecifier(ctx.surfaceAbs, lit.text, new Set(ctx.adjacency.keys()));
174
+ if (resolved)
175
+ seeds.push(resolved);
176
+ }
177
+ }
178
+ node.forEachChild(visit);
179
+ };
180
+ visit(stmt);
181
+ }
182
+ }
183
+ // Closure = surface itself + BFS from the alias's import-type seeds.
184
+ const closure = new Set([ctx.surfaceAbs]);
185
+ const queue = [...seeds];
186
+ while (queue.length > 0) {
187
+ const file = queue.pop();
188
+ if (closure.has(file))
189
+ continue;
190
+ closure.add(file);
191
+ for (const next of ctx.adjacency.get(file) ?? [])
192
+ queue.push(next);
193
+ }
194
+ let blamedExternal;
195
+ let internalFailure;
196
+ for (const file of closure) {
197
+ const failures = ctx.failuresByFile.get(file);
198
+ if (!failures)
199
+ continue;
200
+ if (!blamedExternal)
201
+ blamedExternal = [...failures.externalPinned][0];
202
+ if (!internalFailure)
203
+ internalFailure = [...failures.internal][0];
204
+ }
205
+ // Classification consults the closure failures REGARDLESS of the root
206
+ // type: a dangling internal specifier means part of this alias's closure
207
+ // is missing and the emitted tree is silently wrong even when the root
208
+ // resolves to a concrete shape (adversarial-review finding 1).
209
+ let outcome = 'ok';
210
+ let detail;
211
+ const allowlisted = () => {
212
+ const pkg = packageNameOf(blamedExternal);
213
+ outcome = 'allowlisted_external';
214
+ detail =
215
+ `unresolved external '${blamedExternal}' is pinned ` +
216
+ `(${pkg}@${ctx.args.pinned[pkg]}); kept at tier ${anchor.serialization} ` +
217
+ 'on bare checkout; probe gates are the backstop';
218
+ };
219
+ // `unexplainedDeep`: a member-level author-baked any/unknown. The deep walk
220
+ // excludes TypeScript's unresolved-reference `error` placeholder (see
221
+ // `flagOf`), so `deep` is ALWAYS an author-baked disqualifier that survives
222
+ // into the emitted `.d.ts` text — never a healable external decay. The check
223
+ // phase must pre-gate exactly these: its probe gates are whole-type only and
224
+ // cannot see a member-level any.
225
+ let unexplainedDeep = [];
226
+ const deep = deepFindings[0];
227
+ if (internalFailure) {
228
+ outcome = 'decayed_internal';
229
+ detail = `dangling internal specifier '${internalFailure}'`;
230
+ }
231
+ else if (topType || deep) {
232
+ // `deepFindings` is only computed when `!topType`, so the two are mutually
233
+ // exclusive here.
234
+ if (deep) {
235
+ // A member-level disqualifier. Either an author-baked any/unknown (baked
236
+ // into the emitted text; does NOT heal when the check installs pins) or a
237
+ // `budget_exhausted` sentinel (a subtree too deep/wide to finish). Record
238
+ // it even when the closure ALSO carries a failing pinned external (which
239
+ // would otherwise allowlist the alias) — fail closed.
240
+ unexplainedDeep = deepFindings;
241
+ outcome = 'decayed_internal';
242
+ detail =
243
+ deep.kind === 'budget_exhausted'
244
+ ? `type too deep or wide to verify within the capture budget at ` +
245
+ `'${deep.path}'; abstaining (fail closed) rather than risk a buried ` +
246
+ 'any reading compatible'
247
+ : `type carries '${deep.kind}' at '${deep.path}'; an arbitrary ` +
248
+ 'counterparty shape would read compatible';
249
+ }
250
+ else if (blamedExternal) {
251
+ // Root top type from a pinned external decay: heals when the check
252
+ // installs the pin.
253
+ if (ctx.args.bareCheckout) {
254
+ allowlisted();
255
+ }
256
+ else {
257
+ outcome = 'decayed_internal';
258
+ detail = `unresolved pinned external '${blamedExternal}' on an installed checkout`;
259
+ }
260
+ }
261
+ else {
262
+ // Root top type with no external explanation.
263
+ outcome = 'decayed_internal';
264
+ detail = 'alias resolved to a top type with no allowlisted external failure';
265
+ }
266
+ }
267
+ else if (blamedExternal) {
268
+ // The alias's own type is fully concrete, but its closure has failing
269
+ // pinned-external specifiers — previously invisible behind the topType
270
+ // gate. On a bare checkout that is the expected amendment-1 shape; on an
271
+ // installed checkout it means the tree references something the repo's
272
+ // own node_modules could not resolve.
273
+ if (ctx.args.bareCheckout) {
274
+ allowlisted();
275
+ }
276
+ else {
277
+ outcome = 'decayed_internal';
278
+ detail = `unresolved pinned external '${blamedExternal}' on an installed checkout`;
279
+ }
280
+ }
281
+ return {
282
+ alias,
283
+ anchor_kind: anchor.request.kind,
284
+ symbol_name: 'symbol_name' in anchor.request ? anchor.request.symbol_name : undefined,
285
+ source_file: 'source_file' in anchor.request ? anchor.request.source_file : '<inline>',
286
+ anchor_origin: anchor.request.anchor_origin,
287
+ serialization: anchor.serialization,
288
+ self_check: outcome,
289
+ // A #438/#439 re-aim note is provenance only; a real decay detail always
290
+ // wins, so it surfaces exactly when the re-aimed alias self-checks clean.
291
+ // An abstain (carrick#766) is the most specific statement available: the
292
+ // surface line is `unknown` because this layer wrote it that way, so its
293
+ // reason outranks whatever the closure walk blames.
294
+ self_check_detail: anchor.abstainReason ?? detail ?? anchor.reaimNote,
295
+ top_type_at_self_check: topType,
296
+ ...(unexplainedDeep.length > 0
297
+ ? { any_provenance: unexplainedDeep.map(provenanceOf) }
298
+ : {}),
299
+ };
300
+ }
301
+ /** Resolve a relative specifier from `fromAbs` to a tree file, if present. */
302
+ function resolveTreeSpecifier(fromAbs, spec, tree) {
303
+ if (!isRelative(spec))
304
+ return undefined;
305
+ const base = path.resolve(path.dirname(fromAbs), spec);
306
+ const candidates = [
307
+ `${base}.d.ts`,
308
+ path.join(base, 'index.d.ts'),
309
+ base.endsWith('.js') ? `${base.slice(0, -3)}.d.ts` : undefined,
310
+ base, // already .d.ts
311
+ ].filter((c) => c !== undefined);
312
+ for (const candidate of candidates) {
313
+ if (tree.has(candidate))
314
+ return candidate;
315
+ }
316
+ return undefined;
317
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Module-specifier helpers shared across the capture bundle: extraction from
3
+ * declaration text, external/internal classification, and tsconfig-`paths`
4
+ * pattern matching for the post-emit rewrite pass.
5
+ */
6
+ export declare function isRelative(spec: string): boolean;
7
+ /** zod -> zod, @scope/pkg/sub -> @scope/pkg, pkg/sub -> pkg */
8
+ export declare function packageNameOf(spec: string): string;
9
+ /** Extract every module specifier mentioned in a .d.ts text: `from "x"`,
10
+ * `import "x"`, and `import("x")` type references. */
11
+ export declare function collectSpecifiers(text: string): Set<string>;
12
+ /**
13
+ * One parsed tsconfig `paths` mapping: pattern "prefix*suffix" (or exact) to
14
+ * substitution targets resolved against the config's path base.
15
+ */
16
+ export interface PathsPattern {
17
+ /** Original pattern, e.g. "@app/*" */
18
+ pattern: string;
19
+ prefix: string;
20
+ /** undefined for exact (starless) patterns */
21
+ suffix?: string;
22
+ /** Absolute target templates ("*" preserved), in declaration order. */
23
+ targets: string[];
24
+ }
25
+ /**
26
+ * Match a specifier against a pattern. Returns the "*" capture ('' for exact
27
+ * matches) or undefined when there is no match.
28
+ */
29
+ export declare function matchPathsPattern(spec: string, p: PathsPattern): string | undefined;
30
+ /**
31
+ * Rewrite every specifier occurrence in a .d.ts text through `map`. The map
32
+ * receives each specifier and returns a replacement or undefined (keep).
33
+ * Operates on the same syntactic positions collectSpecifiers finds, so the
34
+ * two stay consistent by construction.
35
+ */
36
+ export declare function rewriteSpecifiers(text: string, map: (spec: string) => string | undefined): {
37
+ text: string;
38
+ rewrites: number;
39
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Module-specifier helpers shared across the capture bundle: extraction from
3
+ * declaration text, external/internal classification, and tsconfig-`paths`
4
+ * pattern matching for the post-emit rewrite pass.
5
+ */
6
+ export function isRelative(spec) {
7
+ return spec.startsWith('./') || spec.startsWith('../') || spec.startsWith('/');
8
+ }
9
+ /** zod -> zod, @scope/pkg/sub -> @scope/pkg, pkg/sub -> pkg */
10
+ export function packageNameOf(spec) {
11
+ const parts = spec.split('/');
12
+ return spec.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
13
+ }
14
+ /** Extract every module specifier mentioned in a .d.ts text: `from "x"`,
15
+ * `import "x"`, and `import("x")` type references. */
16
+ export function collectSpecifiers(text) {
17
+ const specs = new Set();
18
+ const re = /(?:from\s+|import\s+|import\s*\(\s*)["']([^"']+)["']/g;
19
+ let m;
20
+ while ((m = re.exec(text)) !== null) {
21
+ specs.add(m[1]);
22
+ }
23
+ return specs;
24
+ }
25
+ /**
26
+ * Match a specifier against a pattern. Returns the "*" capture ('' for exact
27
+ * matches) or undefined when there is no match.
28
+ */
29
+ export function matchPathsPattern(spec, p) {
30
+ if (p.suffix === undefined) {
31
+ return spec === p.pattern ? '' : undefined;
32
+ }
33
+ if (spec.length >= p.prefix.length + p.suffix.length &&
34
+ spec.startsWith(p.prefix) &&
35
+ spec.endsWith(p.suffix)) {
36
+ return spec.slice(p.prefix.length, spec.length - p.suffix.length);
37
+ }
38
+ return undefined;
39
+ }
40
+ /**
41
+ * Rewrite every specifier occurrence in a .d.ts text through `map`. The map
42
+ * receives each specifier and returns a replacement or undefined (keep).
43
+ * Operates on the same syntactic positions collectSpecifiers finds, so the
44
+ * two stay consistent by construction.
45
+ */
46
+ export function rewriteSpecifiers(text, map) {
47
+ let rewrites = 0;
48
+ const out = text.replace(/((?:from\s+|import\s+|import\s*\(\s*)["'])([^"']+)(["'])/g, (whole, open, spec, close) => {
49
+ const replacement = map(spec);
50
+ if (replacement === undefined || replacement === spec)
51
+ return whole;
52
+ rewrites++;
53
+ return `${open}${replacement}${close}`;
54
+ });
55
+ return { text: out, rewrites };
56
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * DefinitionResolver - Resolves surface type aliases from a v2 capture stub
3
+ * package's declaration tree using the TypeScript compiler.
4
+ *
5
+ * Two forms are produced per alias:
6
+ * - `definition`: the declaration *as written* (named refs preserved). For a
7
+ * surface alias `export type A = import('./m').Order;` this follows the
8
+ * alias to its target declaration in the stub tree (`interface Order {...}`)
9
+ * so the definition keeps its real name and members; when the target is
10
+ * anonymous (inline object types, node-builder prints) the surface alias
11
+ * line itself is the as-written form.
12
+ * - `expanded`: the fully *structural* form, with every named member type
13
+ * inlined to its member structure, recursively.
14
+ *
15
+ * `type.getText(node, NoTruncation)` does NOT inline named members — the
16
+ * compiler prints a referenced type by its symbol name when that symbol is in
17
+ * scope (`total: Money`, not `total: { amountCents: number; currency: string }`).
18
+ * The structural form is produced by `expandTypeStructural` (shared with the
19
+ * inference path in `type-inferrer.ts`), which walks the resolved `Type` and
20
+ * rebuilds the inlined text.
21
+ *
22
+ * Each resolve call builds its own throwaway in-memory project over the stub
23
+ * tree, so the warm sidecar's long-lived project never sees stub files and
24
+ * cannot accumulate stale trees across requests.
25
+ */
26
+ import { Project } from 'ts-morph';
27
+ export interface ResolvedDefinition {
28
+ type_alias: string;
29
+ /** Original declaration text as written (preserves named types) */
30
+ definition: string;
31
+ /** Fully structural form: named member types inlined to their structure */
32
+ expanded: string;
33
+ }
34
+ export declare class DefinitionResolver {
35
+ private readonly project;
36
+ constructor(options: {
37
+ project: Project;
38
+ });
39
+ /**
40
+ * Resolve surface aliases from a capture stub package directory
41
+ * (`<stub_dir>/types/surface.d.ts` + its declaration tree).
42
+ *
43
+ * Uses a DEDICATED project with `moduleResolution: Bundler`, not the
44
+ * repo's own project: the stub tree's relative import-types are
45
+ * extensionless, which a NodeNext-configured repo project silently fails
46
+ * to resolve (the alias then reads as `any`). Bundler resolution accepts
47
+ * both extensionless and `.js`-suffixed specifiers — the same policy the
48
+ * check-phase workspace uses.
49
+ */
50
+ resolveFromStub(stubDir: string, aliases: string[]): ResolvedDefinition[];
51
+ /**
52
+ * Resolve a single alias: the original text and the structural form.
53
+ */
54
+ private resolveAlias;
55
+ private log;
56
+ private logError;
57
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * DefinitionResolver - Resolves surface type aliases from a v2 capture stub
3
+ * package's declaration tree using the TypeScript compiler.
4
+ *
5
+ * Two forms are produced per alias:
6
+ * - `definition`: the declaration *as written* (named refs preserved). For a
7
+ * surface alias `export type A = import('./m').Order;` this follows the
8
+ * alias to its target declaration in the stub tree (`interface Order {...}`)
9
+ * so the definition keeps its real name and members; when the target is
10
+ * anonymous (inline object types, node-builder prints) the surface alias
11
+ * line itself is the as-written form.
12
+ * - `expanded`: the fully *structural* form, with every named member type
13
+ * inlined to its member structure, recursively.
14
+ *
15
+ * `type.getText(node, NoTruncation)` does NOT inline named members — the
16
+ * compiler prints a referenced type by its symbol name when that symbol is in
17
+ * scope (`total: Money`, not `total: { amountCents: number; currency: string }`).
18
+ * The structural form is produced by `expandTypeStructural` (shared with the
19
+ * inference path in `type-inferrer.ts`), which walks the resolved `Type` and
20
+ * rebuilds the inlined text.
21
+ *
22
+ * Each resolve call builds its own throwaway in-memory project over the stub
23
+ * tree, so the warm sidecar's long-lived project never sees stub files and
24
+ * cannot accumulate stale trees across requests.
25
+ */
26
+ import * as path from 'node:path';
27
+ import * as fs from 'node:fs';
28
+ import { Project, Node } from 'ts-morph';
29
+ import { expandTypeStructural } from './type-structural-expander.js';
30
+ export class DefinitionResolver {
31
+ project;
32
+ constructor(options) {
33
+ this.project = options.project;
34
+ }
35
+ /**
36
+ * Resolve surface aliases from a capture stub package directory
37
+ * (`<stub_dir>/types/surface.d.ts` + its declaration tree).
38
+ *
39
+ * Uses a DEDICATED project with `moduleResolution: Bundler`, not the
40
+ * repo's own project: the stub tree's relative import-types are
41
+ * extensionless, which a NodeNext-configured repo project silently fails
42
+ * to resolve (the alias then reads as `any`). Bundler resolution accepts
43
+ * both extensionless and `.js`-suffixed specifiers — the same policy the
44
+ * check-phase workspace uses.
45
+ */
46
+ resolveFromStub(stubDir, aliases) {
47
+ const typesDir = path.join(stubDir, 'types');
48
+ const surfacePath = path.join(typesDir, 'surface.d.ts');
49
+ if (!fs.existsSync(surfacePath)) {
50
+ this.log(`No surface.d.ts under ${stubDir}; nothing to resolve`);
51
+ return [];
52
+ }
53
+ try {
54
+ const stubProject = new Project({
55
+ compilerOptions: {
56
+ target: 99, // ESNext
57
+ module: 99, // ESNext
58
+ moduleResolution: 100, // Bundler
59
+ strict: true,
60
+ skipLibCheck: true,
61
+ },
62
+ skipAddingFilesFromTsConfig: true,
63
+ });
64
+ for (const filePath of walkDtsFiles(typesDir)) {
65
+ stubProject.addSourceFileAtPath(filePath);
66
+ }
67
+ const surface = stubProject.getSourceFile(surfacePath);
68
+ if (!surface) {
69
+ this.logError(`Failed to load ${surfacePath}`);
70
+ return [];
71
+ }
72
+ const results = [];
73
+ for (const alias of aliases) {
74
+ const result = this.resolveAlias(surface, alias);
75
+ if (result) {
76
+ results.push(result);
77
+ }
78
+ else {
79
+ this.log(`Could not resolve alias: ${alias}`);
80
+ }
81
+ }
82
+ return results;
83
+ }
84
+ catch (err) {
85
+ this.logError(`Resolution failed: ${err instanceof Error ? err.message : String(err)}`);
86
+ return [];
87
+ }
88
+ }
89
+ /**
90
+ * Resolve a single alias: the original text and the structural form.
91
+ */
92
+ resolveAlias(sourceFile, alias) {
93
+ const decl = sourceFile.getTypeAlias(alias) ??
94
+ sourceFile.getInterface(alias) ??
95
+ sourceFile.getClass(alias) ??
96
+ sourceFile.getEnum(alias);
97
+ if (!decl)
98
+ return null;
99
+ try {
100
+ const type = decl.getType();
101
+ // As-written form: prefer the alias target's own declaration (the real
102
+ // `interface Order {...}` in the tree) over the surface's import-type
103
+ // line, so named shapes read naturally. Fall back to the alias line for
104
+ // anonymous targets, self-referential alias symbols, or lib/external
105
+ // declarations outside the stub tree.
106
+ let definition = decl.getText();
107
+ for (const symbol of [type.getAliasSymbol(), type.getSymbol()]) {
108
+ const targetDecl = symbol?.getDeclarations()?.[0];
109
+ if (targetDecl &&
110
+ targetDecl !== decl &&
111
+ (Node.isInterfaceDeclaration(targetDecl) ||
112
+ Node.isTypeAliasDeclaration(targetDecl) ||
113
+ Node.isClassDeclaration(targetDecl) ||
114
+ Node.isEnumDeclaration(targetDecl)) &&
115
+ !targetDecl.getSourceFile().getFilePath().includes('node_modules')) {
116
+ definition = targetDecl.getText();
117
+ break;
118
+ }
119
+ }
120
+ // Structural form — every named member inlined to its shape.
121
+ const expanded = expandTypeStructural(type);
122
+ return { type_alias: alias, definition, expanded };
123
+ }
124
+ catch (err) {
125
+ this.logError(`Failed to resolve ${alias}: ${err instanceof Error ? err.message : String(err)}`);
126
+ return null;
127
+ }
128
+ }
129
+ log(message) {
130
+ console.error(`[sidecar:definition-resolver] ${message}`);
131
+ }
132
+ logError(message) {
133
+ console.error(`[sidecar:definition-resolver:error] ${message}`);
134
+ }
135
+ }
136
+ /** All .d.ts files under a directory, depth-first, deterministic order. */
137
+ function walkDtsFiles(dir) {
138
+ const out = [];
139
+ const walk = (current) => {
140
+ const entries = fs
141
+ .readdirSync(current, { withFileTypes: true })
142
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
143
+ for (const entry of entries) {
144
+ const p = path.join(current, entry.name);
145
+ if (entry.isDirectory())
146
+ walk(p);
147
+ else if (entry.name.endsWith('.d.ts'))
148
+ out.push(p);
149
+ }
150
+ };
151
+ walk(dir);
152
+ return out;
153
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Main entry point for the type-sidecar
3
+ *
4
+ * This module implements a message loop that:
5
+ * 1. Listens on stdin for JSON requests
6
+ * 2. Processes each request (init, bundle, emit_surface, infer, build_workspace, check_compatibility, health, shutdown)
7
+ * 3. Writes JSON responses to stdout
8
+ *
9
+ * IMPORTANT:
10
+ * - stdout is ONLY for JSON responses
11
+ * - stderr is for logging
12
+ * - Process stays alive between requests (warm standby)
13
+ */
14
+ export {};