carrick 0.3.77 → 0.3.79
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/package.json +6 -6
- package/sidecar/dist/src/capture/anchors.js +4 -4
- package/sidecar/dist/src/capture/index.d.ts +19 -0
- package/sidecar/dist/src/capture/index.js +32 -5
- package/sidecar/dist/src/capture/machinery.d.ts +31 -12
- package/sidecar/dist/src/capture/machinery.js +88 -30
- package/sidecar/dist/src/index.js +1 -0
- package/sidecar/dist/src/type-inferrer.d.ts +43 -24
- package/sidecar/dist/src/type-inferrer.js +80 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carrick",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.79",
|
|
4
4
|
"description": "The API contract index for a TypeScript workspace: what the other services do with the routes and calls in the file you are editing, in your editor and in your agent's context",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
"zod": "^3.23.0"
|
|
58
58
|
},
|
|
59
59
|
"optionalDependencies": {
|
|
60
|
-
"@carrick-tools/cli-darwin-arm64": "0.3.
|
|
61
|
-
"@carrick-tools/cli-darwin-x64": "0.3.
|
|
62
|
-
"@carrick-tools/cli-linux-arm64": "0.3.
|
|
63
|
-
"@carrick-tools/cli-linux-x64": "0.3.
|
|
64
|
-
"@carrick-tools/cli-win32-x64": "0.3.
|
|
60
|
+
"@carrick-tools/cli-darwin-arm64": "0.3.79",
|
|
61
|
+
"@carrick-tools/cli-darwin-x64": "0.3.79",
|
|
62
|
+
"@carrick-tools/cli-linux-arm64": "0.3.79",
|
|
63
|
+
"@carrick-tools/cli-linux-x64": "0.3.79",
|
|
64
|
+
"@carrick-tools/cli-win32-x64": "0.3.79"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@types/node": "^24.13.3",
|
|
@@ -162,7 +162,7 @@ export function resolveAnchor(program, request, args) {
|
|
|
162
162
|
return demote(`no handler parameter '${request.param_name}' resolved in ` +
|
|
163
163
|
`${request.source_file} (${paramLocatorHints(request)})`);
|
|
164
164
|
}
|
|
165
|
-
return finishInferAnchor(program, sourceFile, request, param, args.placeholder, undefined);
|
|
165
|
+
return finishInferAnchor(program, sourceFile, request, param, args.placeholder, undefined, args.repoRoot);
|
|
166
166
|
}
|
|
167
167
|
let located = locateNode(sourceFile, request);
|
|
168
168
|
if (!located) {
|
|
@@ -187,7 +187,7 @@ export function resolveAnchor(program, request, args) {
|
|
|
187
187
|
located = builderReaim.node;
|
|
188
188
|
reaimNote = builderReaim.note;
|
|
189
189
|
}
|
|
190
|
-
return finishInferAnchor(program, sourceFile, request, located, args.placeholder, reaimNote);
|
|
190
|
+
return finishInferAnchor(program, sourceFile, request, located, args.placeholder, reaimNote, args.repoRoot);
|
|
191
191
|
}
|
|
192
192
|
/**
|
|
193
193
|
* Shared tail of the infer paths (locator-resolved node and #498
|
|
@@ -195,7 +195,7 @@ export function resolveAnchor(program, request, args) {
|
|
|
195
195
|
* transport layer, run the #433 recovery and the carrick#371 machinery guard,
|
|
196
196
|
* and print through the node builder.
|
|
197
197
|
*/
|
|
198
|
-
function finishInferAnchor(program, sourceFile, request, located, placeholder, reaimNote) {
|
|
198
|
+
function finishInferAnchor(program, sourceFile, request, located, placeholder, reaimNote, repoRoot) {
|
|
199
199
|
const checker = program.getTypeChecker();
|
|
200
200
|
const demote = (reason) => ({
|
|
201
201
|
request,
|
|
@@ -265,7 +265,7 @@ function finishInferAnchor(program, sourceFile, request, located, placeholder, r
|
|
|
265
265
|
// type. No source locator points at a distinct clean payload here (the payload
|
|
266
266
|
// lives inside the wrapper's response builder), so degrade to `unknown` —
|
|
267
267
|
// abstain, never a concrete verdict off the machinery.
|
|
268
|
-
if (typeIsOrContainsMachinery(
|
|
268
|
+
if (typeIsOrContainsMachinery(program, type, located, repoRoot)) {
|
|
269
269
|
return demote('resolved type is or contains framework machinery (Response/Request-shaped); ' +
|
|
270
270
|
'degraded to unknown rather than emit a wrapper envelope as a response contract');
|
|
271
271
|
}
|
|
@@ -32,6 +32,25 @@ export type { CaptureStubOptions, CaptureStubResult } from './api.js';
|
|
|
32
32
|
export { DenoProject, findDenoConfig } from './deno-project.js';
|
|
33
33
|
export { runCheck } from './check.js';
|
|
34
34
|
export type { CheckProgress } from './check.js';
|
|
35
|
+
/**
|
|
36
|
+
* The name of THIS capture's surface entry, without an extension
|
|
37
|
+
* (carrick#1046).
|
|
38
|
+
*
|
|
39
|
+
* The entry has to live inside the effective `rootDir` — an entry beside a
|
|
40
|
+
* `rootDir` of `src` fails TS6059 — so it is written into the scanned tree and
|
|
41
|
+
* unlinked afterwards. One name per repo root made two captures of the same
|
|
42
|
+
* tree share a single file: whichever finished first unlinked it while the
|
|
43
|
+
* other's program was still reading it, and every alias whose print anchors in
|
|
44
|
+
* that destination then demoted to `structural_fallback` with an accessibility
|
|
45
|
+
* reason that described the harness rather than the code. Concurrent captures
|
|
46
|
+
* of one tree are ordinary — the test suite does it on every run, and two
|
|
47
|
+
* services of a monorepo can share a root — so the name, not the locking, is
|
|
48
|
+
* what has to give.
|
|
49
|
+
*
|
|
50
|
+
* A leftover from an interrupted capture is also identifiable as one process's
|
|
51
|
+
* (carrick#1069), rather than a fixed name the next scan reads as source.
|
|
52
|
+
*/
|
|
53
|
+
export declare function surfaceEntryFileName(): string;
|
|
35
54
|
/** Same normalization intent as bundle_file_stems on the Rust side. */
|
|
36
55
|
export declare function sanitizeServiceName(name: string): string;
|
|
37
56
|
export declare function captureStub(opts: CaptureStubOptions): CaptureStubResult;
|
|
@@ -44,6 +44,31 @@ export { DenoProject, findDenoConfig } from './deno-project.js';
|
|
|
44
44
|
// reaches it only through this door (index.js).
|
|
45
45
|
export { runCheck } from './check.js';
|
|
46
46
|
const SURFACE_ENTRY_BASENAME = '__carrick_surface__';
|
|
47
|
+
/** Captures made by this process, so each one's entry file has a name of its
|
|
48
|
+
* own. Paired with the pid it is unique across processes too. */
|
|
49
|
+
let surfaceEntrySequence = 0;
|
|
50
|
+
/**
|
|
51
|
+
* The name of THIS capture's surface entry, without an extension
|
|
52
|
+
* (carrick#1046).
|
|
53
|
+
*
|
|
54
|
+
* The entry has to live inside the effective `rootDir` — an entry beside a
|
|
55
|
+
* `rootDir` of `src` fails TS6059 — so it is written into the scanned tree and
|
|
56
|
+
* unlinked afterwards. One name per repo root made two captures of the same
|
|
57
|
+
* tree share a single file: whichever finished first unlinked it while the
|
|
58
|
+
* other's program was still reading it, and every alias whose print anchors in
|
|
59
|
+
* that destination then demoted to `structural_fallback` with an accessibility
|
|
60
|
+
* reason that described the harness rather than the code. Concurrent captures
|
|
61
|
+
* of one tree are ordinary — the test suite does it on every run, and two
|
|
62
|
+
* services of a monorepo can share a root — so the name, not the locking, is
|
|
63
|
+
* what has to give.
|
|
64
|
+
*
|
|
65
|
+
* A leftover from an interrupted capture is also identifiable as one process's
|
|
66
|
+
* (carrick#1069), rather than a fixed name the next scan reads as source.
|
|
67
|
+
*/
|
|
68
|
+
export function surfaceEntryFileName() {
|
|
69
|
+
surfaceEntrySequence += 1;
|
|
70
|
+
return `${SURFACE_ENTRY_BASENAME}.${process.pid}.${surfaceEntrySequence}`;
|
|
71
|
+
}
|
|
47
72
|
/** Same normalization intent as bundle_file_stems on the Rust side. */
|
|
48
73
|
export function sanitizeServiceName(name) {
|
|
49
74
|
return name.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
@@ -153,9 +178,11 @@ export function captureStub(opts) {
|
|
|
153
178
|
const entryDir = parsed.options.rootDir
|
|
154
179
|
? path.resolve(path.dirname(configPath), parsed.options.rootDir)
|
|
155
180
|
: repoRoot;
|
|
181
|
+
const surfaceEntry = surfaceEntryFileName();
|
|
182
|
+
const surfaceDeclaration = `${surfaceEntry}.d.ts`;
|
|
156
183
|
const entryPath = deno
|
|
157
|
-
? path.join(deno.cacheDir, `${
|
|
158
|
-
: path.join(entryDir, `${
|
|
184
|
+
? path.join(deno.cacheDir, `${surfaceEntry}.ts`)
|
|
185
|
+
: path.join(entryDir, `${surfaceEntry}.ts`);
|
|
159
186
|
fs.mkdirSync(path.dirname(entryPath), { recursive: true });
|
|
160
187
|
// ---- Phase A: analysis program over placeholder entry + anchor sources ----
|
|
161
188
|
let resolved;
|
|
@@ -250,7 +277,7 @@ export function captureStub(opts) {
|
|
|
250
277
|
if (emitPartial) {
|
|
251
278
|
errors.push(`declaration emit was partial: kept ${emitted.size} emitted file(s); ` +
|
|
252
279
|
'aliases referencing unemitted modules are demoted to structural_fallback');
|
|
253
|
-
resolved = demoteDanglingAliases({ resolved, emitted, declarationSources, staging });
|
|
280
|
+
resolved = demoteDanglingAliases({ resolved, emitted, declarationSources, staging, surfaceDeclaration });
|
|
254
281
|
}
|
|
255
282
|
// ---- Relocate the emitted tree into the stub package ----
|
|
256
283
|
const typesDir = path.join(stubDir, 'types');
|
|
@@ -260,7 +287,7 @@ export function captureStub(opts) {
|
|
|
260
287
|
let surfaceAbsPath = '';
|
|
261
288
|
for (const [fileName, text] of emitted) {
|
|
262
289
|
let rel = path.relative(staging, fileName).split(path.sep).join('/');
|
|
263
|
-
if (path.basename(rel) ===
|
|
290
|
+
if (path.basename(rel) === surfaceDeclaration) {
|
|
264
291
|
const source = sourceByEmitted.get(rel);
|
|
265
292
|
sourceByEmitted.delete(rel);
|
|
266
293
|
rel = 'surface.d.ts';
|
|
@@ -422,7 +449,7 @@ function demoteDanglingAliases(args) {
|
|
|
422
449
|
let surfaceKey;
|
|
423
450
|
for (const fileName of args.emitted.keys()) {
|
|
424
451
|
const rel = path.relative(args.staging, fileName).split(path.sep).join('/');
|
|
425
|
-
if (path.basename(rel) ===
|
|
452
|
+
if (path.basename(rel) === args.surfaceDeclaration)
|
|
426
453
|
surfaceKey = fileName;
|
|
427
454
|
if (rel.endsWith('.d.ts'))
|
|
428
455
|
treeModules.add(rel.slice(0, -'.d.ts'.length));
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* detection shape are duplicated here in lockstep — the same pattern by which
|
|
15
15
|
* `BUILTIN_ANCHOR_SYMBOLS` mirrors `socket_io.rs`. Detection is STRUCTURAL and
|
|
16
16
|
* framework-agnostic: no framework NAME appears, only the shared HTTP-message
|
|
17
|
-
* member surface, gated by a lib /
|
|
18
|
-
* payload that merely shares a member name can never trip it.
|
|
17
|
+
* member surface, gated by a lib / installed-dependency declaration origin so a
|
|
18
|
+
* user payload that merely shares a member name can never trip it.
|
|
19
19
|
*/
|
|
20
20
|
import ts from 'typescript';
|
|
21
21
|
/**
|
|
@@ -50,20 +50,39 @@ export declare const MACHINERY_MEMBER_INDICATORS: Set<string>;
|
|
|
50
50
|
* Promise-unwrapped before the check — only call-signature returns are);
|
|
51
51
|
* - an array element type: `Response[]` is not descended to its element;
|
|
52
52
|
* - `interface X extends Response` declared in USER source — the origin gate
|
|
53
|
-
* is lib
|
|
53
|
+
* is lib/installed-dependency only, so a user-declared subtype reads as a real
|
|
54
54
|
* contract.
|
|
55
55
|
*
|
|
56
56
|
* The depth cap + origin gate keep a legitimate payload that merely references a
|
|
57
57
|
* machinery type far inside from over-abstaining.
|
|
58
58
|
*/
|
|
59
|
-
export declare function typeIsOrContainsMachinery(
|
|
59
|
+
export declare function typeIsOrContainsMachinery(program: ts.Program, type: ts.Type, node: ts.Node, repoRoot: string): boolean;
|
|
60
60
|
/**
|
|
61
|
-
* True when a declaration file
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
61
|
+
* True when a declaration's source file is runtime/library origin rather than
|
|
62
|
+
* user source. Four answers, in order:
|
|
63
|
+
*
|
|
64
|
+
* 1. The runtime declarations Carrick materialises for a non-Node runtime
|
|
65
|
+
* under `.carrick/deno/` (carrick#1017), and the remote (JSR, `https:`)
|
|
66
|
+
* modules it copies beside them. Carrick's own artefact layout, not a
|
|
67
|
+
* guess about anyone else's.
|
|
68
|
+
* 2. A TypeScript default library (`lib.dom.d.ts`, ...), as the program
|
|
69
|
+
* classifies it; on a bare checkout the DOM `Response` resolves from here.
|
|
70
|
+
* 3. An install under a `node_modules` segment, however it entered the
|
|
71
|
+
* program (an import, or a root the loader registered).
|
|
72
|
+
* 4. A file the PROGRAM'S RESOLVER marked as an external library import.
|
|
73
|
+
* This is the graph-backed answer for a project whose resolution does not
|
|
74
|
+
* go through `node_modules` (carrick#1264): Deno serves an npm dependency's
|
|
75
|
+
* types from its own cache, a path with no `node_modules` segment, and
|
|
76
|
+
* `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
|
|
77
|
+
* the graph, which the compiler records on the file. Nothing crosses the
|
|
78
|
+
* capture seam; both programs are built with that host. One exclusion: a
|
|
79
|
+
* workspace package reached through a `node_modules` symlink is also
|
|
80
|
+
* marked external by the compiler but is the user's own source, so a file
|
|
81
|
+
* inside the checkout (the nearest `.git` above the service root) that
|
|
82
|
+
* carries no `node_modules` segment stays user source.
|
|
83
|
+
*
|
|
84
|
+
* Lockstep mirror of `isExternalOrigin` in `type-inferrer.ts` (the capture seam
|
|
85
|
+
* forbids sharing a module); `machinery-indicator-mirror.test.ts` guards the
|
|
86
|
+
* pair on a real program.
|
|
68
87
|
*/
|
|
69
|
-
export declare function isExternalOrigin(
|
|
88
|
+
export declare function isExternalOrigin(program: ts.Program, sourceFile: ts.SourceFile, repoRoot: string): boolean;
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
* detection shape are duplicated here in lockstep — the same pattern by which
|
|
15
15
|
* `BUILTIN_ANCHOR_SYMBOLS` mirrors `socket_io.rs`. Detection is STRUCTURAL and
|
|
16
16
|
* framework-agnostic: no framework NAME appears, only the shared HTTP-message
|
|
17
|
-
* member surface, gated by a lib /
|
|
18
|
-
* payload that merely shares a member name can never trip it.
|
|
17
|
+
* member surface, gated by a lib / installed-dependency declaration origin so a
|
|
18
|
+
* user payload that merely shares a member name can never trip it.
|
|
19
19
|
*/
|
|
20
20
|
import ts from 'typescript';
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as path from 'node:path';
|
|
21
23
|
/**
|
|
22
24
|
* Strongly-discriminating member names of HTTP transport machinery. Kept
|
|
23
25
|
* identical to `MACHINERY_MEMBER_INDICATORS` in `type-inferrer.ts`. These are
|
|
@@ -71,21 +73,23 @@ const MACHINERY_INDICATOR_THRESHOLD = 3;
|
|
|
71
73
|
* Promise-unwrapped before the check — only call-signature returns are);
|
|
72
74
|
* - an array element type: `Response[]` is not descended to its element;
|
|
73
75
|
* - `interface X extends Response` declared in USER source — the origin gate
|
|
74
|
-
* is lib
|
|
76
|
+
* is lib/installed-dependency only, so a user-declared subtype reads as a real
|
|
75
77
|
* contract.
|
|
76
78
|
*
|
|
77
79
|
* The depth cap + origin gate keep a legitimate payload that merely references a
|
|
78
80
|
* machinery type far inside from over-abstaining.
|
|
79
81
|
*/
|
|
80
|
-
export function typeIsOrContainsMachinery(
|
|
81
|
-
|
|
82
|
+
export function typeIsOrContainsMachinery(program, type, node, repoRoot) {
|
|
83
|
+
const scope = { program, checker: program.getTypeChecker(), repoRoot };
|
|
84
|
+
return isOrContains(scope, type, node, 0);
|
|
82
85
|
}
|
|
83
|
-
function isOrContains(
|
|
84
|
-
|
|
86
|
+
function isOrContains(scope, type, node, depth) {
|
|
87
|
+
const { checker } = scope;
|
|
88
|
+
if (isFrameworkMachinery(scope, type)) {
|
|
85
89
|
return true;
|
|
86
90
|
}
|
|
87
91
|
if (type.isUnion() || type.isIntersection()) {
|
|
88
|
-
return type.types.some((part) => isOrContains(
|
|
92
|
+
return type.types.some((part) => isOrContains(scope, part, node, depth));
|
|
89
93
|
}
|
|
90
94
|
if (depth >= 1) {
|
|
91
95
|
return false;
|
|
@@ -94,14 +98,14 @@ function isOrContains(checker, type, node, depth) {
|
|
|
94
98
|
for (const sig of checker.getSignaturesOfType(type, ts.SignatureKind.Call)) {
|
|
95
99
|
const returnType = sig.getReturnType();
|
|
96
100
|
const awaited = checker.getAwaitedType?.(returnType) ?? returnType;
|
|
97
|
-
if (isOrContains(
|
|
101
|
+
if (isOrContains(scope, awaited, node, depth + 1)) {
|
|
98
102
|
return true;
|
|
99
103
|
}
|
|
100
104
|
}
|
|
101
105
|
// Direct properties: the `{ response: Response; error }` envelope shape.
|
|
102
106
|
for (const prop of checker.getPropertiesOfType(type)) {
|
|
103
107
|
const propType = checker.getTypeOfSymbolAtLocation(prop, node);
|
|
104
|
-
if (isOrContains(
|
|
108
|
+
if (isOrContains(scope, propType, node, depth + 1)) {
|
|
105
109
|
return true;
|
|
106
110
|
}
|
|
107
111
|
}
|
|
@@ -110,11 +114,11 @@ function isOrContains(checker, type, node, depth) {
|
|
|
110
114
|
/**
|
|
111
115
|
* True when `type` itself is an HTTP-machinery type: it structurally carries at
|
|
112
116
|
* least `MACHINERY_INDICATOR_THRESHOLD` of the indicator members AND its symbol
|
|
113
|
-
* is declared in a lib
|
|
117
|
+
* is declared in a lib or installed-dependency origin.
|
|
114
118
|
*/
|
|
115
|
-
function isFrameworkMachinery(
|
|
119
|
+
function isFrameworkMachinery(scope, type) {
|
|
116
120
|
let hits = 0;
|
|
117
|
-
for (const prop of checker.getPropertiesOfType(type)) {
|
|
121
|
+
for (const prop of scope.checker.getPropertiesOfType(type)) {
|
|
118
122
|
if (MACHINERY_MEMBER_INDICATORS.has(prop.getName())) {
|
|
119
123
|
hits += 1;
|
|
120
124
|
if (hits >= MACHINERY_INDICATOR_THRESHOLD)
|
|
@@ -124,42 +128,96 @@ function isFrameworkMachinery(checker, type) {
|
|
|
124
128
|
if (hits < MACHINERY_INDICATOR_THRESHOLD) {
|
|
125
129
|
return false;
|
|
126
130
|
}
|
|
127
|
-
return symbolIsLibOrExternalOrigin(
|
|
131
|
+
return symbolIsLibOrExternalOrigin(scope, type.getSymbol() ?? type.aliasSymbol);
|
|
128
132
|
}
|
|
129
133
|
/**
|
|
130
|
-
* True when a declaration file
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
134
|
+
* True when a declaration's source file is runtime/library origin rather than
|
|
135
|
+
* user source. Four answers, in order:
|
|
136
|
+
*
|
|
137
|
+
* 1. The runtime declarations Carrick materialises for a non-Node runtime
|
|
138
|
+
* under `.carrick/deno/` (carrick#1017), and the remote (JSR, `https:`)
|
|
139
|
+
* modules it copies beside them. Carrick's own artefact layout, not a
|
|
140
|
+
* guess about anyone else's.
|
|
141
|
+
* 2. A TypeScript default library (`lib.dom.d.ts`, ...), as the program
|
|
142
|
+
* classifies it; on a bare checkout the DOM `Response` resolves from here.
|
|
143
|
+
* 3. An install under a `node_modules` segment, however it entered the
|
|
144
|
+
* program (an import, or a root the loader registered).
|
|
145
|
+
* 4. A file the PROGRAM'S RESOLVER marked as an external library import.
|
|
146
|
+
* This is the graph-backed answer for a project whose resolution does not
|
|
147
|
+
* go through `node_modules` (carrick#1264): Deno serves an npm dependency's
|
|
148
|
+
* types from its own cache, a path with no `node_modules` segment, and
|
|
149
|
+
* `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
|
|
150
|
+
* the graph, which the compiler records on the file. Nothing crosses the
|
|
151
|
+
* capture seam; both programs are built with that host. One exclusion: a
|
|
152
|
+
* workspace package reached through a `node_modules` symlink is also
|
|
153
|
+
* marked external by the compiler but is the user's own source, so a file
|
|
154
|
+
* inside the checkout (the nearest `.git` above the service root) that
|
|
155
|
+
* carries no `node_modules` segment stays user source.
|
|
156
|
+
*
|
|
157
|
+
* Lockstep mirror of `isExternalOrigin` in `type-inferrer.ts` (the capture seam
|
|
158
|
+
* forbids sharing a module); `machinery-indicator-mirror.test.ts` guards the
|
|
159
|
+
* pair on a real program.
|
|
137
160
|
*/
|
|
138
|
-
export function isExternalOrigin(
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
161
|
+
export function isExternalOrigin(program, sourceFile, repoRoot) {
|
|
162
|
+
const file = sourceFile.fileName.replace(/\\/g, '/');
|
|
163
|
+
if (file.includes('/.carrick/deno/')) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
if (program.isSourceFileDefaultLibrary(sourceFile)) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
if (file.includes('/node_modules/')) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return program.isSourceFileFromExternalLibrary(sourceFile) && !isInsideCheckout(file, repoRoot);
|
|
173
|
+
}
|
|
174
|
+
const checkoutRoots = new Map();
|
|
175
|
+
/** The checkout the service root sits in: the nearest ancestor holding a
|
|
176
|
+
* `.git` entry (a directory, or the file a worktree carries), else the service
|
|
177
|
+
* root itself. */
|
|
178
|
+
function checkoutRootOf(repoRoot) {
|
|
179
|
+
const key = path.resolve(repoRoot);
|
|
180
|
+
const cached = checkoutRoots.get(key);
|
|
181
|
+
if (cached)
|
|
182
|
+
return cached;
|
|
183
|
+
let dir = key;
|
|
184
|
+
let root = key;
|
|
185
|
+
for (;;) {
|
|
186
|
+
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
187
|
+
root = dir;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
const parent = path.dirname(dir);
|
|
191
|
+
if (parent === dir)
|
|
192
|
+
break;
|
|
193
|
+
dir = parent;
|
|
194
|
+
}
|
|
195
|
+
checkoutRoots.set(key, root);
|
|
196
|
+
return root;
|
|
197
|
+
}
|
|
198
|
+
function isInsideCheckout(file, repoRoot) {
|
|
199
|
+
const root = checkoutRootOf(repoRoot).replace(/\\/g, '/');
|
|
200
|
+
return file === root || file.startsWith(root.endsWith('/') ? root : root + '/');
|
|
143
201
|
}
|
|
144
202
|
/**
|
|
145
203
|
* True when the symbol is declared in a runtime/library origin. Works on a bare
|
|
146
204
|
* checkout: the DOM `Response`/`Request` resolve from the bundled
|
|
147
205
|
* `lib.dom.d.ts` even with no installed dependencies.
|
|
148
206
|
*/
|
|
149
|
-
function symbolIsLibOrExternalOrigin(
|
|
207
|
+
function symbolIsLibOrExternalOrigin(scope, symbol) {
|
|
150
208
|
if (!symbol) {
|
|
151
209
|
return false;
|
|
152
210
|
}
|
|
153
211
|
for (const decl of symbol.getDeclarations() ?? []) {
|
|
154
|
-
if (isExternalOrigin(decl.getSourceFile().
|
|
212
|
+
if (isExternalOrigin(scope.program, decl.getSourceFile(), scope.repoRoot)) {
|
|
155
213
|
return true;
|
|
156
214
|
}
|
|
157
215
|
}
|
|
158
216
|
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
159
217
|
try {
|
|
160
|
-
const aliased = checker.getAliasedSymbol(symbol);
|
|
218
|
+
const aliased = scope.checker.getAliasedSymbol(symbol);
|
|
161
219
|
if (aliased && aliased !== symbol) {
|
|
162
|
-
return symbolIsLibOrExternalOrigin(
|
|
220
|
+
return symbolIsLibOrExternalOrigin(scope, aliased);
|
|
163
221
|
}
|
|
164
222
|
}
|
|
165
223
|
catch {
|
|
@@ -51,6 +51,7 @@ function projectComponents() {
|
|
|
51
51
|
// resolves nothing under `node_modules` (carrick#1260).
|
|
52
52
|
typeInferrer: new TypeInferrer({
|
|
53
53
|
project,
|
|
54
|
+
repoRoot,
|
|
54
55
|
packageOf: (filePath) => loader.packageOf(filePath),
|
|
55
56
|
}),
|
|
56
57
|
definitionResolver: new DefinitionResolver({ project }),
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
* (redirects, 204s), the LLM emits null and we fall back to the containing
|
|
18
18
|
* function's return type.
|
|
19
19
|
*/
|
|
20
|
-
import { Project } from 'ts-morph';
|
|
20
|
+
import { Project, ts } from 'ts-morph';
|
|
21
21
|
import type { InferRequestItem, InferResult, ExtractionConfig } from './types.js';
|
|
22
22
|
/**
|
|
23
23
|
* Strongly-discriminating member names of HTTP transport machinery — the
|
|
24
24
|
* fetch/DOM `Response` & `Request`, a Node `http.ServerResponse`, a framework
|
|
25
|
-
* reply object. A type declared in a lib or
|
|
25
|
+
* reply object. A type declared in a lib or installed-dependency origin that carries
|
|
26
26
|
* a subset of these is framework machinery, never a user contract: the
|
|
27
27
|
* PRODUCER-side structural mirror of the consumer `machineryIndicators`
|
|
28
28
|
* (ExtractionConfig), used to reject a wrapper envelope whose response field is
|
|
@@ -39,23 +39,34 @@ import type { InferRequestItem, InferResult, ExtractionConfig } from './types.js
|
|
|
39
39
|
*/
|
|
40
40
|
export declare const MACHINERY_MEMBER_INDICATORS: Set<string>;
|
|
41
41
|
/**
|
|
42
|
-
* True when a declaration file
|
|
43
|
-
* source
|
|
44
|
-
* runtime declarations Carrick itself materialises for a non-Node runtime
|
|
45
|
-
* (`.carrick/deno/<hash>/runtime.d.ts`, and the remote modules cached beside
|
|
46
|
-
* it).
|
|
42
|
+
* True when a declaration's source file is runtime/library origin rather than
|
|
43
|
+
* user source. Four answers, in order:
|
|
47
44
|
*
|
|
48
|
-
* The
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
45
|
+
* 1. The runtime declarations Carrick materialises for a non-Node runtime
|
|
46
|
+
* under `.carrick/deno/` (carrick#1017), and the remote (JSR, `https:`)
|
|
47
|
+
* modules it copies beside them. Carrick's own artefact layout, not a
|
|
48
|
+
* guess about anyone else's.
|
|
49
|
+
* 2. A TypeScript default library (`lib.dom.d.ts`, ...), as the program
|
|
50
|
+
* classifies it; on a bare checkout the DOM `Response` resolves from here.
|
|
51
|
+
* 3. An install under a `node_modules` segment, however it entered the
|
|
52
|
+
* program (an import, or a root the loader registered).
|
|
53
|
+
* 4. A file the PROGRAM'S RESOLVER marked as an external library import.
|
|
54
|
+
* This is the graph-backed answer for a project whose resolution does not
|
|
55
|
+
* go through `node_modules` (carrick#1264): Deno serves an npm dependency's
|
|
56
|
+
* types from its own cache, a path with no `node_modules` segment, and
|
|
57
|
+
* `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
|
|
58
|
+
* the graph, which the compiler records on the file. Nothing crosses the
|
|
59
|
+
* capture seam; both programs are built with that host. One exclusion: a
|
|
60
|
+
* workspace package reached through a `node_modules` symlink is also
|
|
61
|
+
* marked external by the compiler but is the user's own source, so a file
|
|
62
|
+
* inside the checkout (the nearest `.git` above the service root) that
|
|
63
|
+
* carries no `node_modules` segment stays user source.
|
|
53
64
|
*
|
|
54
|
-
*
|
|
55
|
-
* sharing a module
|
|
56
|
-
* pair.
|
|
65
|
+
* Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
|
|
66
|
+
* seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
|
|
67
|
+
* the pair on a real program.
|
|
57
68
|
*/
|
|
58
|
-
export declare function isExternalOrigin(
|
|
69
|
+
export declare function isExternalOrigin(program: ts.Program, sourceFile: ts.SourceFile, repoRoot: string): boolean;
|
|
59
70
|
/**
|
|
60
71
|
* Options for TypeInferrer construction
|
|
61
72
|
*/
|
|
@@ -69,6 +80,12 @@ export interface TypeInferrerOptions {
|
|
|
69
80
|
* absent otherwise, and the path itself is then the only thing to read.
|
|
70
81
|
*/
|
|
71
82
|
packageOf?: (filePath: string) => string | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* The service root the project was built for. The machinery origin gate
|
|
85
|
+
* reads it to tell a workspace package the compiler reached through a
|
|
86
|
+
* `node_modules` symlink from an installed dependency (carrick#1264).
|
|
87
|
+
*/
|
|
88
|
+
repoRoot: string;
|
|
72
89
|
}
|
|
73
90
|
/**
|
|
74
91
|
* TypeInferrer - Extracts types from source code, both explicit and inferred
|
|
@@ -80,6 +97,7 @@ export interface TypeInferrerOptions {
|
|
|
80
97
|
export declare class TypeInferrer {
|
|
81
98
|
private readonly project;
|
|
82
99
|
private readonly packageOf;
|
|
100
|
+
private readonly repoRoot;
|
|
83
101
|
constructor(options: TypeInferrerOptions);
|
|
84
102
|
/**
|
|
85
103
|
* Infer types for the given requests
|
|
@@ -296,7 +314,7 @@ export declare class TypeInferrer {
|
|
|
296
314
|
* Promise-unwrapped before the machinery check);
|
|
297
315
|
* - an array element type: `Response[]` is not descended to its element;
|
|
298
316
|
* - `interface X extends Response` declared in USER source — the origin gate
|
|
299
|
-
* is lib
|
|
317
|
+
* is lib/installed-dependency only, so a user-declared subtype reads as a real
|
|
300
318
|
* contract, not machinery;
|
|
301
319
|
* - a function / call-signature return type: the response paths that call
|
|
302
320
|
* this resolve a handler's RETURN (an envelope/object), never a function
|
|
@@ -313,7 +331,7 @@ export declare class TypeInferrer {
|
|
|
313
331
|
* True when `type` itself is an HTTP-machinery type: it structurally carries
|
|
314
332
|
* at least `MACHINERY_INDICATOR_THRESHOLD` of the strongly-discriminating
|
|
315
333
|
* `MACHINERY_MEMBER_INDICATORS`, AND its symbol is declared in a lib
|
|
316
|
-
* (`lib.dom.d.ts`, ...) or
|
|
334
|
+
* (`lib.dom.d.ts`, ...) or installed-dependency origin. Both gates are required —
|
|
317
335
|
* the indicator subset alone essentially never matches a JSON payload, and
|
|
318
336
|
* the origin gate makes certain a user's own local type sharing those member
|
|
319
337
|
* names is never mistaken for framework machinery (the advisor's guard).
|
|
@@ -327,12 +345,13 @@ export declare class TypeInferrer {
|
|
|
327
345
|
*/
|
|
328
346
|
private hasMachineryIndicatorThreshold;
|
|
329
347
|
/**
|
|
330
|
-
* True when the symbol is declared in a
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
* machinery, not user source.
|
|
334
|
-
* `Response`/`Request` resolve from the
|
|
335
|
-
* installed dependencies.
|
|
348
|
+
* True when the symbol is declared in a runtime/library origin
|
|
349
|
+
* (`isExternalOrigin`): a TypeScript lib, an installed dependency however
|
|
350
|
+
* the program resolved it, or the runtime declarations Carrick materialises
|
|
351
|
+
* under `.carrick/deno/` — framework/runtime machinery, not user source.
|
|
352
|
+
* Works on a bare checkout: the DOM `Response`/`Request` resolve from the
|
|
353
|
+
* bundled `lib.dom.d.ts` even with no installed dependencies. The program is
|
|
354
|
+
* read per call: the project adds source files lazily and rebuilds it.
|
|
336
355
|
*/
|
|
337
356
|
private symbolIsLibOrExternalOrigin;
|
|
338
357
|
/**
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* function's return type.
|
|
19
19
|
*/
|
|
20
20
|
import { Node, SyntaxKind, ts, } from 'ts-morph';
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as path from 'node:path';
|
|
21
23
|
import { validateInferRequestItem } from './validators.js';
|
|
22
24
|
import { expandTypeStructural, } from './type-structural-expander.js';
|
|
23
25
|
/**
|
|
@@ -75,7 +77,7 @@ const BUILTIN_ANCHOR_SYMBOLS = new Set([
|
|
|
75
77
|
/**
|
|
76
78
|
* Strongly-discriminating member names of HTTP transport machinery — the
|
|
77
79
|
* fetch/DOM `Response` & `Request`, a Node `http.ServerResponse`, a framework
|
|
78
|
-
* reply object. A type declared in a lib or
|
|
80
|
+
* reply object. A type declared in a lib or installed-dependency origin that carries
|
|
79
81
|
* a subset of these is framework machinery, never a user contract: the
|
|
80
82
|
* PRODUCER-side structural mirror of the consumer `machineryIndicators`
|
|
81
83
|
* (ExtractionConfig), used to reject a wrapper envelope whose response field is
|
|
@@ -151,27 +153,73 @@ const RESPONSE_INIT_MEMBER_NAMES = new Set([
|
|
|
151
153
|
'headers',
|
|
152
154
|
]);
|
|
153
155
|
/**
|
|
154
|
-
* True when a declaration file
|
|
155
|
-
* source
|
|
156
|
-
* runtime declarations Carrick itself materialises for a non-Node runtime
|
|
157
|
-
* (`.carrick/deno/<hash>/runtime.d.ts`, and the remote modules cached beside
|
|
158
|
-
* it).
|
|
156
|
+
* True when a declaration's source file is runtime/library origin rather than
|
|
157
|
+
* user source. Four answers, in order:
|
|
159
158
|
*
|
|
160
|
-
* The
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
159
|
+
* 1. The runtime declarations Carrick materialises for a non-Node runtime
|
|
160
|
+
* under `.carrick/deno/` (carrick#1017), and the remote (JSR, `https:`)
|
|
161
|
+
* modules it copies beside them. Carrick's own artefact layout, not a
|
|
162
|
+
* guess about anyone else's.
|
|
163
|
+
* 2. A TypeScript default library (`lib.dom.d.ts`, ...), as the program
|
|
164
|
+
* classifies it; on a bare checkout the DOM `Response` resolves from here.
|
|
165
|
+
* 3. An install under a `node_modules` segment, however it entered the
|
|
166
|
+
* program (an import, or a root the loader registered).
|
|
167
|
+
* 4. A file the PROGRAM'S RESOLVER marked as an external library import.
|
|
168
|
+
* This is the graph-backed answer for a project whose resolution does not
|
|
169
|
+
* go through `node_modules` (carrick#1264): Deno serves an npm dependency's
|
|
170
|
+
* types from its own cache, a path with no `node_modules` segment, and
|
|
171
|
+
* `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
|
|
172
|
+
* the graph, which the compiler records on the file. Nothing crosses the
|
|
173
|
+
* capture seam; both programs are built with that host. One exclusion: a
|
|
174
|
+
* workspace package reached through a `node_modules` symlink is also
|
|
175
|
+
* marked external by the compiler but is the user's own source, so a file
|
|
176
|
+
* inside the checkout (the nearest `.git` above the service root) that
|
|
177
|
+
* carries no `node_modules` segment stays user source.
|
|
165
178
|
*
|
|
166
|
-
*
|
|
167
|
-
* sharing a module
|
|
168
|
-
* pair.
|
|
179
|
+
* Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
|
|
180
|
+
* seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
|
|
181
|
+
* the pair on a real program.
|
|
169
182
|
*/
|
|
170
|
-
export function isExternalOrigin(
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
183
|
+
export function isExternalOrigin(program, sourceFile, repoRoot) {
|
|
184
|
+
const file = sourceFile.fileName.replace(/\\/g, '/');
|
|
185
|
+
if (file.includes('/.carrick/deno/')) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (program.isSourceFileDefaultLibrary(sourceFile)) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
if (file.includes('/node_modules/')) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
return program.isSourceFileFromExternalLibrary(sourceFile) && !isInsideCheckout(file, repoRoot);
|
|
195
|
+
}
|
|
196
|
+
const checkoutRoots = new Map();
|
|
197
|
+
/** The checkout the service root sits in: the nearest ancestor holding a
|
|
198
|
+
* `.git` entry (a directory, or the file a worktree carries), else the service
|
|
199
|
+
* root itself. */
|
|
200
|
+
function checkoutRootOf(repoRoot) {
|
|
201
|
+
const key = path.resolve(repoRoot);
|
|
202
|
+
const cached = checkoutRoots.get(key);
|
|
203
|
+
if (cached)
|
|
204
|
+
return cached;
|
|
205
|
+
let dir = key;
|
|
206
|
+
let root = key;
|
|
207
|
+
for (;;) {
|
|
208
|
+
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
209
|
+
root = dir;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
const parent = path.dirname(dir);
|
|
213
|
+
if (parent === dir)
|
|
214
|
+
break;
|
|
215
|
+
dir = parent;
|
|
216
|
+
}
|
|
217
|
+
checkoutRoots.set(key, root);
|
|
218
|
+
return root;
|
|
219
|
+
}
|
|
220
|
+
function isInsideCheckout(file, repoRoot) {
|
|
221
|
+
const root = checkoutRootOf(repoRoot).replace(/\\/g, '/');
|
|
222
|
+
return file === root || file.startsWith(root.endsWith('/') ? root : root + '/');
|
|
175
223
|
}
|
|
176
224
|
/**
|
|
177
225
|
* How far the response-helper recovery (carrick#631) descends through nested
|
|
@@ -250,9 +298,11 @@ function typeText(type, enclosingNode) {
|
|
|
250
298
|
export class TypeInferrer {
|
|
251
299
|
project;
|
|
252
300
|
packageOf;
|
|
301
|
+
repoRoot;
|
|
253
302
|
constructor(options) {
|
|
254
303
|
this.project = options.project;
|
|
255
304
|
this.packageOf = options.packageOf;
|
|
305
|
+
this.repoRoot = options.repoRoot;
|
|
256
306
|
}
|
|
257
307
|
/**
|
|
258
308
|
* Infer types for the given requests
|
|
@@ -1805,7 +1855,7 @@ export class TypeInferrer {
|
|
|
1805
1855
|
* Promise-unwrapped before the machinery check);
|
|
1806
1856
|
* - an array element type: `Response[]` is not descended to its element;
|
|
1807
1857
|
* - `interface X extends Response` declared in USER source — the origin gate
|
|
1808
|
-
* is lib
|
|
1858
|
+
* is lib/installed-dependency only, so a user-declared subtype reads as a real
|
|
1809
1859
|
* contract, not machinery;
|
|
1810
1860
|
* - a function / call-signature return type: the response paths that call
|
|
1811
1861
|
* this resolve a handler's RETURN (an envelope/object), never a function
|
|
@@ -1852,7 +1902,7 @@ export class TypeInferrer {
|
|
|
1852
1902
|
* True when `type` itself is an HTTP-machinery type: it structurally carries
|
|
1853
1903
|
* at least `MACHINERY_INDICATOR_THRESHOLD` of the strongly-discriminating
|
|
1854
1904
|
* `MACHINERY_MEMBER_INDICATORS`, AND its symbol is declared in a lib
|
|
1855
|
-
* (`lib.dom.d.ts`, ...) or
|
|
1905
|
+
* (`lib.dom.d.ts`, ...) or installed-dependency origin. Both gates are required —
|
|
1856
1906
|
* the indicator subset alone essentially never matches a JSON payload, and
|
|
1857
1907
|
* the origin gate makes certain a user's own local type sharing those member
|
|
1858
1908
|
* names is never mistaken for framework machinery (the advisor's guard).
|
|
@@ -1889,19 +1939,21 @@ export class TypeInferrer {
|
|
|
1889
1939
|
return false;
|
|
1890
1940
|
}
|
|
1891
1941
|
/**
|
|
1892
|
-
* True when the symbol is declared in a
|
|
1893
|
-
*
|
|
1894
|
-
*
|
|
1895
|
-
* machinery, not user source.
|
|
1896
|
-
* `Response`/`Request` resolve from the
|
|
1897
|
-
* installed dependencies.
|
|
1942
|
+
* True when the symbol is declared in a runtime/library origin
|
|
1943
|
+
* (`isExternalOrigin`): a TypeScript lib, an installed dependency however
|
|
1944
|
+
* the program resolved it, or the runtime declarations Carrick materialises
|
|
1945
|
+
* under `.carrick/deno/` — framework/runtime machinery, not user source.
|
|
1946
|
+
* Works on a bare checkout: the DOM `Response`/`Request` resolve from the
|
|
1947
|
+
* bundled `lib.dom.d.ts` even with no installed dependencies. The program is
|
|
1948
|
+
* read per call: the project adds source files lazily and rebuilds it.
|
|
1898
1949
|
*/
|
|
1899
1950
|
symbolIsLibOrExternalOrigin(symbol) {
|
|
1900
1951
|
if (!symbol) {
|
|
1901
1952
|
return false;
|
|
1902
1953
|
}
|
|
1954
|
+
const program = this.project.getProgram().compilerObject;
|
|
1903
1955
|
for (const decl of symbol.getDeclarations()) {
|
|
1904
|
-
if (isExternalOrigin(decl.getSourceFile().
|
|
1956
|
+
if (isExternalOrigin(program, decl.getSourceFile().compilerNode, this.repoRoot)) {
|
|
1905
1957
|
return true;
|
|
1906
1958
|
}
|
|
1907
1959
|
}
|