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.
- package/LICENSE.md +99 -0
- package/README.md +79 -0
- package/bin/carrick.mjs +168 -0
- package/dist/channel.d.ts +16 -0
- package/dist/channel.js +37 -0
- package/dist/channel.js.map +1 -0
- package/dist/cli.d.ts +29 -0
- package/dist/cli.js +60 -0
- package/dist/cli.js.map +1 -0
- package/dist/contract.d.ts +154 -0
- package/dist/contract.js +141 -0
- package/dist/contract.js.map +1 -0
- package/dist/diagnostics.d.ts +54 -0
- package/dist/diagnostics.js +199 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/hook/post-edit.d.ts +13 -0
- package/dist/hook/post-edit.js +76 -0
- package/dist/hook/post-edit.js.map +1 -0
- package/dist/hook/session-start.d.ts +2 -0
- package/dist/hook/session-start.js +44 -0
- package/dist/hook/session-start.js.map +1 -0
- package/dist/init/identity.d.ts +20 -0
- package/dist/init/identity.js +60 -0
- package/dist/init/identity.js.map +1 -0
- package/dist/init/repos.d.ts +19 -0
- package/dist/init/repos.js +46 -0
- package/dist/init/repos.js.map +1 -0
- package/dist/init/run.d.ts +11 -0
- package/dist/init/run.js +228 -0
- package/dist/init/run.js.map +1 -0
- package/dist/init/settings.d.ts +50 -0
- package/dist/init/settings.js +138 -0
- package/dist/init/settings.js.map +1 -0
- package/dist/log.d.ts +2 -0
- package/dist/log.js +35 -0
- package/dist/log.js.map +1 -0
- package/dist/native.d.ts +48 -0
- package/dist/native.js +127 -0
- package/dist/native.js.map +1 -0
- package/dist/render.d.ts +65 -0
- package/dist/render.js +334 -0
- package/dist/render.js.map +1 -0
- package/dist/root.d.ts +30 -0
- package/dist/root.js +82 -0
- package/dist/root.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +255 -0
- package/dist/server.js.map +1 -0
- package/dist/templates.d.ts +18 -0
- package/dist/templates.js +66 -0
- package/dist/templates.js.map +1 -0
- package/package.json +72 -0
- package/plugin/.claude-plugin/plugin.json +6 -0
- package/plugin/.lsp.json +14 -0
- package/plugin/hooks/hooks.json +27 -0
- package/sidecar/dist/src/bundler.d.ts +141 -0
- package/sidecar/dist/src/bundler.js +680 -0
- package/sidecar/dist/src/capture/anchors.d.ts +61 -0
- package/sidecar/dist/src/capture/anchors.js +1132 -0
- package/sidecar/dist/src/capture/api.d.ts +378 -0
- package/sidecar/dist/src/capture/api.js +10 -0
- package/sidecar/dist/src/capture/augmentations.d.ts +20 -0
- package/sidecar/dist/src/capture/augmentations.js +60 -0
- package/sidecar/dist/src/capture/check-classify.d.ts +58 -0
- package/sidecar/dist/src/capture/check-classify.js +189 -0
- package/sidecar/dist/src/capture/check-deep.d.ts +32 -0
- package/sidecar/dist/src/capture/check-deep.js +91 -0
- package/sidecar/dist/src/capture/check-poison.d.ts +40 -0
- package/sidecar/dist/src/capture/check-poison.js +155 -0
- package/sidecar/dist/src/capture/check-probe.d.ts +70 -0
- package/sidecar/dist/src/capture/check-probe.js +147 -0
- package/sidecar/dist/src/capture/check-scrub.d.ts +31 -0
- package/sidecar/dist/src/capture/check-scrub.js +71 -0
- package/sidecar/dist/src/capture/check-workspace.d.ts +52 -0
- package/sidecar/dist/src/capture/check-workspace.js +218 -0
- package/sidecar/dist/src/capture/check.d.ts +39 -0
- package/sidecar/dist/src/capture/check.js +450 -0
- package/sidecar/dist/src/capture/deep-walk.d.ts +62 -0
- package/sidecar/dist/src/capture/deep-walk.js +243 -0
- package/sidecar/dist/src/capture/index.d.ts +36 -0
- package/sidecar/dist/src/capture/index.js +477 -0
- package/sidecar/dist/src/capture/lockfile.d.ts +48 -0
- package/sidecar/dist/src/capture/lockfile.js +490 -0
- package/sidecar/dist/src/capture/machinery.d.ts +59 -0
- package/sidecar/dist/src/capture/machinery.js +160 -0
- package/sidecar/dist/src/capture/node-builder.d.ts +37 -0
- package/sidecar/dist/src/capture/node-builder.js +123 -0
- package/sidecar/dist/src/capture/paths-rewrite.d.ts +34 -0
- package/sidecar/dist/src/capture/paths-rewrite.js +100 -0
- package/sidecar/dist/src/capture/self-check.d.ts +38 -0
- package/sidecar/dist/src/capture/self-check.js +317 -0
- package/sidecar/dist/src/capture/specifiers.d.ts +39 -0
- package/sidecar/dist/src/capture/specifiers.js +56 -0
- package/sidecar/dist/src/definition-resolver.d.ts +57 -0
- package/sidecar/dist/src/definition-resolver.js +153 -0
- package/sidecar/dist/src/index.d.ts +14 -0
- package/sidecar/dist/src/index.js +564 -0
- package/sidecar/dist/src/monorepo-builder.d.ts +129 -0
- package/sidecar/dist/src/monorepo-builder.js +584 -0
- package/sidecar/dist/src/project-loader.d.ts +130 -0
- package/sidecar/dist/src/project-loader.js +399 -0
- package/sidecar/dist/src/type-inferrer.d.ts +940 -0
- package/sidecar/dist/src/type-inferrer.js +3540 -0
- package/sidecar/dist/src/type-structural-expander.d.ts +61 -0
- package/sidecar/dist/src/type-structural-expander.js +283 -0
- package/sidecar/dist/src/type-text-canonicalizer.d.ts +40 -0
- package/sidecar/dist/src/type-text-canonicalizer.js +297 -0
- package/sidecar/dist/src/types.d.ts +669 -0
- package/sidecar/dist/src/types.js +5 -0
- package/sidecar/dist/src/validators.d.ts +2214 -0
- package/sidecar/dist/src/validators.js +336 -0
- package/sidecar/package.json +6 -0
- package/templates/carrick.json +7 -0
- 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 {};
|