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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carrick",
3
- "version": "0.3.77",
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.77",
61
- "@carrick-tools/cli-darwin-x64": "0.3.77",
62
- "@carrick-tools/cli-linux-arm64": "0.3.77",
63
- "@carrick-tools/cli-linux-x64": "0.3.77",
64
- "@carrick-tools/cli-win32-x64": "0.3.77"
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(checker, type, located)) {
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, `${SURFACE_ENTRY_BASENAME}.ts`)
158
- : path.join(entryDir, `${SURFACE_ENTRY_BASENAME}.ts`);
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) === `${SURFACE_ENTRY_BASENAME}.d.ts`) {
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) === `${SURFACE_ENTRY_BASENAME}.d.ts`)
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 / `node_modules` declaration origin so a user
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/`node_modules` only, so a user-declared subtype reads as a real
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(checker: ts.TypeChecker, type: ts.Type, node: ts.Node): boolean;
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 path is runtime/library origin: a TS lib
62
- * (`lib.*.d.ts`), an installed package, or the runtime declarations Carrick
63
- * materialises for a non-Node runtime under `.carrick/deno/` (carrick#1017 —
64
- * there the platform `Response` lives in Carrick's own generated file, so
65
- * without this the gate stayed shut and the wrapper was published as a
66
- * contract). Lockstep mirror of `isExternalOrigin` in `type-inferrer.ts`;
67
- * `machinery-indicator-mirror.test.ts` guards the pair.
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(filePath: string): boolean;
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 / `node_modules` declaration origin so a user
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/`node_modules` only, so a user-declared subtype reads as a real
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(checker, type, node) {
81
- return isOrContains(checker, type, node, 0);
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(checker, type, node, depth) {
84
- if (isFrameworkMachinery(checker, type)) {
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(checker, part, node, depth));
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(checker, awaited, node, depth + 1)) {
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(checker, propType, node, depth + 1)) {
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 (`lib.*.d.ts`) or `node_modules` origin.
117
+ * is declared in a lib or installed-dependency origin.
114
118
  */
115
- function isFrameworkMachinery(checker, type) {
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(checker, type.getSymbol() ?? type.aliasSymbol);
131
+ return symbolIsLibOrExternalOrigin(scope, type.getSymbol() ?? type.aliasSymbol);
128
132
  }
129
133
  /**
130
- * True when a declaration file path is runtime/library origin: a TS lib
131
- * (`lib.*.d.ts`), an installed package, or the runtime declarations Carrick
132
- * materialises for a non-Node runtime under `.carrick/deno/` (carrick#1017 —
133
- * there the platform `Response` lives in Carrick's own generated file, so
134
- * without this the gate stayed shut and the wrapper was published as a
135
- * contract). Lockstep mirror of `isExternalOrigin` in `type-inferrer.ts`;
136
- * `machinery-indicator-mirror.test.ts` guards the pair.
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(filePath) {
139
- const normalized = filePath.replace(/\\/g, '/');
140
- return (normalized.includes('/node_modules/') ||
141
- /\/lib\.[^/]*\.d\.ts$/.test(normalized) ||
142
- normalized.includes('/.carrick/deno/'));
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(checker, symbol) {
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().fileName)) {
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(checker, aliased);
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 `node_modules` origin that carries
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 path is runtime/library origin rather than user
43
- * source: a TypeScript lib (`lib.dom.d.ts`), an installed package, or the
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 third case is carrick#1017: on a Deno service the platform `Response`
49
- * is declared in Carrick's own generated runtime file, which is neither a
50
- * `lib.*.d.ts` nor under `node_modules`, so the machinery origin gate stayed
51
- * shut and every route published the fetch `Response` wrapper as its response
52
- * contract. The path is Carrick's own artefact, not a framework name.
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
- * Kept in lockstep with `capture/machinery.ts`'s copy (the capture seam forbids
55
- * sharing a module across it); `machinery-indicator-mirror.test.ts` guards the
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(filePath: string): boolean;
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/`node_modules` only, so a user-declared subtype reads as a real
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 `node_modules` origin. Both gates are required —
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 TypeScript lib file (`lib.*.d.ts`),
331
- * under `node_modules`, or in the runtime declarations Carrick materialises
332
- * for a non-Node runtime under `.carrick/deno/` — i.e. framework/runtime
333
- * machinery, not user source. Works on a bare checkout: the DOM
334
- * `Response`/`Request` resolve from the bundled `lib.dom.d.ts` even with no
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 `node_modules` origin that carries
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 path is runtime/library origin rather than user
155
- * source: a TypeScript lib (`lib.dom.d.ts`), an installed package, or the
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 third case is carrick#1017: on a Deno service the platform `Response`
161
- * is declared in Carrick's own generated runtime file, which is neither a
162
- * `lib.*.d.ts` nor under `node_modules`, so the machinery origin gate stayed
163
- * shut and every route published the fetch `Response` wrapper as its response
164
- * contract. The path is Carrick's own artefact, not a framework name.
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
- * Kept in lockstep with `capture/machinery.ts`'s copy (the capture seam forbids
167
- * sharing a module across it); `machinery-indicator-mirror.test.ts` guards the
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(filePath) {
171
- const normalized = filePath.replace(/\\/g, '/');
172
- return (normalized.includes('/node_modules/') ||
173
- /\/lib\.[^/]*\.d\.ts$/.test(normalized) ||
174
- normalized.includes('/.carrick/deno/'));
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/`node_modules` only, so a user-declared subtype reads as a real
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 `node_modules` origin. Both gates are required —
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 TypeScript lib file (`lib.*.d.ts`),
1893
- * under `node_modules`, or in the runtime declarations Carrick materialises
1894
- * for a non-Node runtime under `.carrick/deno/` — i.e. framework/runtime
1895
- * machinery, not user source. Works on a bare checkout: the DOM
1896
- * `Response`/`Request` resolve from the bundled `lib.dom.d.ts` even with no
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().getFilePath())) {
1956
+ if (isExternalOrigin(program, decl.getSourceFile().compilerNode, this.repoRoot)) {
1905
1957
  return true;
1906
1958
  }
1907
1959
  }