carrick 0.3.85 → 0.3.87

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.
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Repair an emitted declaration whose own import does not resolve
3
+ * (carrick#1397).
4
+ *
5
+ * A `symbol` anchor prints nothing: its surface line is
6
+ * `import('./m').RenderRequest` and the shape lives in the `.d.ts` the
7
+ * compiler emitted for `m`. When that file imports a module the scanned
8
+ * checkout does not have — a dependency that was not installed, a generated
9
+ * module that was never generated — the stub reports the module missing, the
10
+ * self-check records a dangling internal specifier, and the scanner's publish
11
+ * gate refuses the alias whole. Twenty resolved members are thrown away with
12
+ * the one that did not resolve, which is exactly the loss carrick#1377 named
13
+ * for the two PRINT paths.
14
+ *
15
+ * So the declaration is repaired: the import that did not resolve is dropped
16
+ * and every type it bound is written `unknown` at its own position. This is
17
+ * file-granular — the same emitted file backs every alias whose closure
18
+ * reaches it — and it says nothing the source program did not already say:
19
+ * those names were TypeScript's unresolved-reference placeholder there too,
20
+ * which is why the anchor recorded their member paths as unresolved.
21
+ *
22
+ * Fail-closed in two places. A name used where `unknown` is not a type — a
23
+ * heritage clause, a type-parameter position — leaves the file untouched, so
24
+ * the alias keeps its honest refusal rather than gaining a broken declaration.
25
+ * And the caller re-checks the repaired tree: a name the rewrite did not reach
26
+ * turns into a `Cannot find name` diagnostic, which the self-check reads back
27
+ * as the same dangling specifier.
28
+ *
29
+ * Seam: node builtins + `typescript`, like the rest of this directory.
30
+ */
31
+ import ts from 'typescript';
32
+ import * as fs from 'node:fs';
33
+ /**
34
+ * Drop every import of a failing specifier from each file and write `unknown`
35
+ * where its names were used.
36
+ *
37
+ * `failing` maps an absolute file path to the specifiers the stub could not
38
+ * resolve from it. Returns the files actually rewritten; a file whose names
39
+ * cannot all be replaced by `unknown` is left exactly as it was.
40
+ */
41
+ export function repairDanglingImports(failing) {
42
+ const repaired = new Map();
43
+ for (const [file, specifiers] of failing) {
44
+ const result = repairFile(file, specifiers);
45
+ if (result)
46
+ repaired.set(file, result);
47
+ }
48
+ return repaired;
49
+ }
50
+ function repairFile(file, specifiers) {
51
+ let text;
52
+ try {
53
+ text = fs.readFileSync(file, 'utf8');
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
59
+ const names = new Set();
60
+ const edits = [];
61
+ const removedSpecifiers = new Set();
62
+ // A re-export of the failing module binds nothing in THIS file and is what
63
+ // another file reads through: dropping it would turn "cannot find module"
64
+ // into "has no exported member" over there, which is neither diagnostic the
65
+ // re-check reads, and the importer's member would publish as clean. There is
66
+ // no `unknown` to write for an export, so the file keeps its refusal.
67
+ let unreplaceable = false;
68
+ for (const statement of source.statements) {
69
+ const specifier = importSpecifierOf(statement);
70
+ if (specifier === undefined || !specifiers.has(specifier))
71
+ continue;
72
+ if (ts.isExportDeclaration(statement)) {
73
+ unreplaceable = true;
74
+ break;
75
+ }
76
+ for (const name of boundNames(statement))
77
+ names.add(name);
78
+ removedSpecifiers.add(specifier);
79
+ edits.push({ start: statement.getFullStart(), end: statement.getEnd(), text: '' });
80
+ }
81
+ // An `import(...)` type written inline carries its own specifier and has no
82
+ // statement to drop; the whole node becomes `unknown`.
83
+ const visit = (node) => {
84
+ if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
85
+ const literal = node.argument.literal;
86
+ if (ts.isStringLiteral(literal) && specifiers.has(literal.text)) {
87
+ removedSpecifiers.add(literal.text);
88
+ edits.push({ start: node.getStart(source), end: node.getEnd(), text: 'unknown' });
89
+ return;
90
+ }
91
+ }
92
+ if (ts.isTypeReferenceNode(node) && names.has(leftmostName(node.typeName))) {
93
+ edits.push({ start: node.getStart(source), end: node.getEnd(), text: 'unknown' });
94
+ return;
95
+ }
96
+ if (ts.isTypeQueryNode(node) && names.has(leftmostName(node.exprName))) {
97
+ edits.push({ start: node.getStart(source), end: node.getEnd(), text: 'unknown' });
98
+ return;
99
+ }
100
+ // Positions where `unknown` is not a type: what a declaration EXTENDS or
101
+ // IMPLEMENTS, and a type parameter's own name. Leave the file alone rather
102
+ // than emit a declaration that does not parse as it used to read.
103
+ if (ts.isExpressionWithTypeArguments(node) &&
104
+ ts.isIdentifier(node.expression) &&
105
+ names.has(node.expression.text)) {
106
+ unreplaceable = true;
107
+ return;
108
+ }
109
+ node.forEachChild(visit);
110
+ };
111
+ visit(source);
112
+ if (unreplaceable || removedSpecifiers.size === 0)
113
+ return undefined;
114
+ edits.sort((a, b) => b.start - a.start);
115
+ let out = text;
116
+ for (const edit of edits) {
117
+ out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
118
+ }
119
+ fs.writeFileSync(file, out);
120
+ return { specifiers: [...removedSpecifiers].sort(), names: [...names].sort() };
121
+ }
122
+ /** The module specifier an import STATEMENT names, if it names one. */
123
+ function importSpecifierOf(statement) {
124
+ let expression;
125
+ if (ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) {
126
+ expression = statement.moduleSpecifier;
127
+ }
128
+ else if (ts.isImportEqualsDeclaration(statement) &&
129
+ ts.isExternalModuleReference(statement.moduleReference)) {
130
+ expression = statement.moduleReference.expression;
131
+ }
132
+ return expression && ts.isStringLiteral(expression) ? expression.text : undefined;
133
+ }
134
+ /** Every local name an import statement binds. */
135
+ function boundNames(statement) {
136
+ const out = [];
137
+ if (ts.isImportDeclaration(statement)) {
138
+ const clause = statement.importClause;
139
+ if (!clause)
140
+ return out;
141
+ if (clause.name)
142
+ out.push(clause.name.text);
143
+ const bindings = clause.namedBindings;
144
+ if (bindings && ts.isNamespaceImport(bindings))
145
+ out.push(bindings.name.text);
146
+ if (bindings && ts.isNamedImports(bindings)) {
147
+ for (const element of bindings.elements)
148
+ out.push(element.name.text);
149
+ }
150
+ }
151
+ else if (ts.isImportEqualsDeclaration(statement)) {
152
+ out.push(statement.name.text);
153
+ }
154
+ return out;
155
+ }
156
+ /** The leftmost identifier of `A`, `A.B` or `A.B.C`. */
157
+ function leftmostName(name) {
158
+ let current = name;
159
+ while (ts.isQualifiedName(current))
160
+ current = current.left;
161
+ return current.text;
162
+ }
@@ -33,6 +33,7 @@ import ts from 'typescript';
33
33
  import * as fs from 'node:fs';
34
34
  import * as path from 'node:path';
35
35
  import { collectSpecifiers, isRelative, packageNameOf } from './specifiers.js';
36
+ import { repairDanglingImports } from './repair-dangling.js';
36
37
  import { findDisqualifyingTopTypes, provenanceOf, } from './deep-walk.js';
37
38
  export function selfCheckStub(args) {
38
39
  const typesDir = path.join(args.stubDir, 'types');
@@ -56,7 +57,18 @@ export function selfCheckStub(args) {
56
57
  linked = true;
57
58
  }
58
59
  try {
59
- return runSelfCheck(args, treeFiles);
60
+ const first = runSelfCheck(args, treeFiles);
61
+ // carrick#1397: an emitted declaration that imports a module the checkout
62
+ // does not have makes every alias reaching that file unpublishable, however
63
+ // much of it resolved. Repair the file — drop the import, write `unknown`
64
+ // where its names were used — and check the tree again. The second pass is
65
+ // the verdict: it reads the repaired text, and a name the rewrite did not
66
+ // reach comes back as a `Cannot find name` diagnostic that
67
+ // `repairedNameFailures` folds back into the same dangling specifier.
68
+ const repaired = repairDanglingImports(first.internalFailuresByFile);
69
+ if (repaired.size === 0)
70
+ return first.records;
71
+ return runSelfCheck(args, treeFiles, repaired).records;
60
72
  }
61
73
  finally {
62
74
  // unlinkSync, not rmSync: the link target is a directory and rmSync
@@ -65,7 +77,7 @@ export function selfCheckStub(args) {
65
77
  fs.unlinkSync(linkPath);
66
78
  }
67
79
  }
68
- function runSelfCheck(args, treeFiles) {
80
+ function runSelfCheck(args, treeFiles, repaired) {
69
81
  const options = {
70
82
  noEmit: true,
71
83
  strict: true,
@@ -102,14 +114,25 @@ function runSelfCheck(args, treeFiles) {
102
114
  return entry;
103
115
  };
104
116
  for (const d of diagnostics) {
105
- if ((d.code !== 2307 && d.code !== 2792) || !d.file)
117
+ if (!d.file)
106
118
  continue;
119
+ const abs = path.resolve(d.file.fileName);
107
120
  const msg = ts.flattenDiagnosticMessageText(d.messageText, ' ');
121
+ // A name the repair did not reach: the import that bound it is gone, so
122
+ // the module is no longer reported missing and only this diagnostic is
123
+ // left to say the tree is incomplete. Blamed on the specifier that bound
124
+ // the name, which is the sentence a reader can act on (carrick#1397).
125
+ const orphaned = repairedNameFailure(repaired, abs, d, msg);
126
+ if (orphaned) {
127
+ bucketIn(failuresByFile, abs).internal.add(orphaned);
128
+ continue;
129
+ }
130
+ if (d.code !== 2307 && d.code !== 2792)
131
+ continue;
108
132
  const m = /Cannot find module '([^']+)'/.exec(msg);
109
133
  if (!m)
110
134
  continue;
111
135
  const spec = m[1];
112
- const abs = path.resolve(d.file.fileName);
113
136
  // A surface diagnostic outside every alias statement (a file-level import,
114
137
  // a reference directive) is attributable to no alias and keeps the
115
138
  // service-wide file bucket: soundness over precision, the same fallback
@@ -158,7 +181,40 @@ function runSelfCheck(args, treeFiles) {
158
181
  surfaceFailuresByAlias,
159
182
  }));
160
183
  }
161
- return records;
184
+ const internalFailuresByFile = new Map();
185
+ for (const [file, failures] of failuresByFile) {
186
+ // The surface's own failures are a literal anchor's pasted text
187
+ // (carrick#1361), not a declaration with an import to drop.
188
+ if (file === surfaceAbs || failures.internal.size === 0)
189
+ continue;
190
+ // A file already repaired has had its turn: repairing it again would chase
191
+ // its own leftover diagnostics.
192
+ if (repaired?.has(file))
193
+ continue;
194
+ internalFailuresByFile.set(file, failures.internal);
195
+ }
196
+ return { records, internalFailuresByFile };
197
+ }
198
+ /**
199
+ * The specifier to blame for a `Cannot find name` diagnostic in a file this
200
+ * capture repaired, when the name is one the dropped import bound.
201
+ *
202
+ * The rewrite writes `unknown` at every type position it can reach; a use it
203
+ * cannot — and there should be none, since a position where `unknown` is not a
204
+ * type leaves the whole file untouched — would otherwise read as a clean tree,
205
+ * because the module that is missing is no longer imported to be reported.
206
+ * Fail closed: the alias keeps the refusal it had before the repair.
207
+ */
208
+ function repairedNameFailure(repaired, file, diagnostic, message) {
209
+ if (!repaired || diagnostic.code !== 2304)
210
+ return undefined;
211
+ const entry = repaired.get(file);
212
+ if (!entry)
213
+ return undefined;
214
+ const named = /Cannot find name '([^']+)'/.exec(message);
215
+ if (!named || !entry.names.includes(named[1]))
216
+ return undefined;
217
+ return entry.specifiers[0];
162
218
  }
163
219
  /**
164
220
  * Position -> the alias whose `export type` statement span covers it, for
@@ -12,6 +12,13 @@
12
12
  * - `expanded`: the fully *structural* form, with every named member type
13
13
  * inlined to its member structure, recursively.
14
14
  *
15
+ * An alias whose own type did not resolve produces neither: the tree is read
16
+ * with nothing installed, so a reference that leaves it lands on TypeScript's
17
+ * unresolved-reference placeholder, which the compiler prints as the reference
18
+ * text rather than as `any` (see `isUnresolvedReference`). Both forms are then
19
+ * the top type, which is what every downstream rule about an empty answer is
20
+ * written to refuse.
21
+ *
15
22
  * `type.getText(node, NoTruncation)` does NOT inline named members — the
16
23
  * compiler prints a referenced type by its symbol name when that symbol is in
17
24
  * scope (`total: Money`, not `total: { amountCents: number; currency: string }`).
@@ -12,6 +12,13 @@
12
12
  * - `expanded`: the fully *structural* form, with every named member type
13
13
  * inlined to its member structure, recursively.
14
14
  *
15
+ * An alias whose own type did not resolve produces neither: the tree is read
16
+ * with nothing installed, so a reference that leaves it lands on TypeScript's
17
+ * unresolved-reference placeholder, which the compiler prints as the reference
18
+ * text rather than as `any` (see `isUnresolvedReference`). Both forms are then
19
+ * the top type, which is what every downstream rule about an empty answer is
20
+ * written to refuse.
21
+ *
15
22
  * `type.getText(node, NoTruncation)` does NOT inline named members — the
16
23
  * compiler prints a referenced type by its symbol name when that symbol is in
17
24
  * scope (`total: Money`, not `total: { amountCents: number; currency: string }`).
@@ -26,7 +33,7 @@
26
33
  import * as path from 'node:path';
27
34
  import * as fs from 'node:fs';
28
35
  import { Project, Node } from 'ts-morph';
29
- import { expandTypeStructural } from './type-structural-expander.js';
36
+ import { expandTypeStructural, } from './type-structural-expander.js';
30
37
  export class DefinitionResolver {
31
38
  project;
32
39
  constructor(options) {
@@ -71,7 +78,10 @@ export class DefinitionResolver {
71
78
  }
72
79
  const results = [];
73
80
  for (const alias of aliases) {
74
- const result = this.resolveAlias(surface, alias);
81
+ const result = this.resolveAlias(surface, alias, {
82
+ program: stubProject.getProgram().compilerObject,
83
+ repoRoot: stubDir,
84
+ });
75
85
  if (result) {
76
86
  results.push(result);
77
87
  }
@@ -89,7 +99,7 @@ export class DefinitionResolver {
89
99
  /**
90
100
  * Resolve a single alias: the original text and the structural form.
91
101
  */
92
- resolveAlias(sourceFile, alias) {
102
+ resolveAlias(sourceFile, alias, origin) {
93
103
  const decl = sourceFile.getTypeAlias(alias) ??
94
104
  sourceFile.getInterface(alias) ??
95
105
  sourceFile.getClass(alias) ??
@@ -98,6 +108,12 @@ export class DefinitionResolver {
98
108
  return null;
99
109
  try {
100
110
  const type = decl.getType();
111
+ // An alias whose own type did not resolve answers the top type it IS,
112
+ // not the reference the compiler echoes for it (carrick#1444).
113
+ if (isUnresolvedReference(type)) {
114
+ this.log(`${alias} names a type this tree cannot resolve; answering any`);
115
+ return { type_alias: alias, definition: 'any', expanded: 'any' };
116
+ }
101
117
  // As-written form: prefer the alias target's own declaration (the real
102
118
  // `interface Order {...}` in the tree) over the surface's import-type
103
119
  // line, so named shapes read naturally. Fall back to the alias line for
@@ -118,7 +134,7 @@ export class DefinitionResolver {
118
134
  }
119
135
  }
120
136
  // Structural form — every named member inlined to its shape.
121
- const expanded = expandTypeStructural(type);
137
+ const expanded = expandTypeStructural(type, origin);
122
138
  return { type_alias: alias, definition, expanded };
123
139
  }
124
140
  catch (err) {
@@ -133,6 +149,29 @@ export class DefinitionResolver {
133
149
  console.error(`[sidecar:definition-resolver:error] ${message}`);
134
150
  }
135
151
  }
152
+ /**
153
+ * True when a type is TypeScript's unresolved-reference placeholder:
154
+ * `TypeFlags.Any` carrying the internal `intrinsicName === 'error'` (stable
155
+ * since TS 1.x). `capture/deep-walk.ts` tests the same two facts for the same
156
+ * reason; the seam forbids importing it from here, so the pin is that both read
157
+ * the flag and the intrinsic name and nothing else.
158
+ *
159
+ * The compiler prints this placeholder as the reference text it FAILED to
160
+ * resolve, never as `any`. That print is what makes an unresolvable alias read
161
+ * as a confident name: an instantiation over a dependency's internal generics
162
+ * has no members in it and resolves nowhere, and every scanner-side rule that
163
+ * refuses an empty answer asks the TEXT (`text_is_bare_top_type`), so nothing
164
+ * downstream can tell the difference (carrick#1444).
165
+ *
166
+ * Asked only of the alias's own type. At a member position the same echo is
167
+ * the producer's own vocabulary (`status: OrderStatus`) inside a shape that
168
+ * otherwise resolved, and replacing it with `any` would remove a name a reader
169
+ * can look up in the producing repo.
170
+ */
171
+ function isUnresolvedReference(type) {
172
+ return (type.isAny() &&
173
+ type.compilerType.intrinsicName === 'error');
174
+ }
136
175
  /** All .d.ts files under a directory, depth-first, deterministic order. */
137
176
  function walkDtsFiles(dir) {
138
177
  const out = [];
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Where a declaration comes from: the user's own source, or the runtime and
3
+ * the packages it installed.
4
+ *
5
+ * One module because more than one layer asks the question — the inference
6
+ * path (`type-inferrer.ts`, deciding whether a type is framework machinery)
7
+ * and the structural printer (`type-structural-expander.ts`, deciding whether
8
+ * to inline a type's members or keep it by name). They must answer it the same
9
+ * way or one layer inlines what the other suppresses.
10
+ *
11
+ * The capture side keeps its own copy in `capture/machinery.ts`: that seam
12
+ * forbids importing anything but node builtins, `typescript` and its own
13
+ * bundle, so the two are kept in lockstep by `machinery-indicator-mirror.test.ts`
14
+ * rather than by sharing this module.
15
+ */
16
+ import type { ts } from 'ts-morph';
17
+ /**
18
+ * True when a declaration's source file is runtime/library origin rather than
19
+ * user source. Four answers, in order:
20
+ *
21
+ * 1. The runtime declarations Carrick materialises for a non-Node runtime
22
+ * under `.carrick/deno/` (carrick#1017), and the remote (JSR, `https:`)
23
+ * modules it copies beside them. Carrick's own artefact layout, not a
24
+ * guess about anyone else's.
25
+ * 2. A TypeScript default library (`lib.dom.d.ts`, ...), as the program
26
+ * classifies it; on a bare checkout the DOM `Response` resolves from here.
27
+ * 3. An install under a `node_modules` segment, however it entered the
28
+ * program (an import, or a root the loader registered).
29
+ * 4. A file the PROGRAM'S RESOLVER marked as an external library import.
30
+ * This is the graph-backed answer for a project whose resolution does not
31
+ * go through `node_modules` (carrick#1264): Deno serves an npm dependency's
32
+ * types from its own cache, a path with no `node_modules` segment, and
33
+ * `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
34
+ * the graph, which the compiler records on the file. Nothing crosses the
35
+ * capture seam; both programs are built with that host. One exclusion: a
36
+ * workspace package reached through a `node_modules` symlink is also
37
+ * marked external by the compiler but is the user's own source, so a file
38
+ * inside the checkout (the nearest `.git` above the service root) that
39
+ * carries no `node_modules` segment stays user source.
40
+ *
41
+ * Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
42
+ * seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
43
+ * the pair on a real program.
44
+ */
45
+ export declare function isExternalOrigin(program: ts.Program, sourceFile: ts.SourceFile, repoRoot: string): boolean;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Where a declaration comes from: the user's own source, or the runtime and
3
+ * the packages it installed.
4
+ *
5
+ * One module because more than one layer asks the question — the inference
6
+ * path (`type-inferrer.ts`, deciding whether a type is framework machinery)
7
+ * and the structural printer (`type-structural-expander.ts`, deciding whether
8
+ * to inline a type's members or keep it by name). They must answer it the same
9
+ * way or one layer inlines what the other suppresses.
10
+ *
11
+ * The capture side keeps its own copy in `capture/machinery.ts`: that seam
12
+ * forbids importing anything but node builtins, `typescript` and its own
13
+ * bundle, so the two are kept in lockstep by `machinery-indicator-mirror.test.ts`
14
+ * rather than by sharing this module.
15
+ */
16
+ import * as fs from 'node:fs';
17
+ import * as path from 'node:path';
18
+ /**
19
+ * True when a declaration's source file is runtime/library origin rather than
20
+ * user source. Four answers, in order:
21
+ *
22
+ * 1. The runtime declarations Carrick materialises for a non-Node runtime
23
+ * under `.carrick/deno/` (carrick#1017), and the remote (JSR, `https:`)
24
+ * modules it copies beside them. Carrick's own artefact layout, not a
25
+ * guess about anyone else's.
26
+ * 2. A TypeScript default library (`lib.dom.d.ts`, ...), as the program
27
+ * classifies it; on a bare checkout the DOM `Response` resolves from here.
28
+ * 3. An install under a `node_modules` segment, however it entered the
29
+ * program (an import, or a root the loader registered).
30
+ * 4. A file the PROGRAM'S RESOLVER marked as an external library import.
31
+ * This is the graph-backed answer for a project whose resolution does not
32
+ * go through `node_modules` (carrick#1264): Deno serves an npm dependency's
33
+ * types from its own cache, a path with no `node_modules` segment, and
34
+ * `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
35
+ * the graph, which the compiler records on the file. Nothing crosses the
36
+ * capture seam; both programs are built with that host. One exclusion: a
37
+ * workspace package reached through a `node_modules` symlink is also
38
+ * marked external by the compiler but is the user's own source, so a file
39
+ * inside the checkout (the nearest `.git` above the service root) that
40
+ * carries no `node_modules` segment stays user source.
41
+ *
42
+ * Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
43
+ * seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
44
+ * the pair on a real program.
45
+ */
46
+ export function isExternalOrigin(program, sourceFile, repoRoot) {
47
+ const file = sourceFile.fileName.replace(/\\/g, '/');
48
+ if (file.includes('/.carrick/deno/')) {
49
+ return true;
50
+ }
51
+ if (program.isSourceFileDefaultLibrary(sourceFile)) {
52
+ return true;
53
+ }
54
+ if (file.includes('/node_modules/')) {
55
+ return true;
56
+ }
57
+ return program.isSourceFileFromExternalLibrary(sourceFile) && !isInsideCheckout(file, repoRoot);
58
+ }
59
+ const checkoutRoots = new Map();
60
+ /** The checkout the service root sits in: the nearest ancestor holding a
61
+ * `.git` entry (a directory, or the file a worktree carries), else the service
62
+ * root itself. */
63
+ function checkoutRootOf(repoRoot) {
64
+ const key = path.resolve(repoRoot);
65
+ const cached = checkoutRoots.get(key);
66
+ if (cached)
67
+ return cached;
68
+ let dir = key;
69
+ let root = key;
70
+ for (;;) {
71
+ if (fs.existsSync(path.join(dir, '.git'))) {
72
+ root = dir;
73
+ break;
74
+ }
75
+ const parent = path.dirname(dir);
76
+ if (parent === dir)
77
+ break;
78
+ dir = parent;
79
+ }
80
+ checkoutRoots.set(key, root);
81
+ return root;
82
+ }
83
+ function isInsideCheckout(file, repoRoot) {
84
+ const root = checkoutRootOf(repoRoot).replace(/\\/g, '/');
85
+ return file === root || file.startsWith(root.endsWith('/') ? root : root + '/');
86
+ }
@@ -17,7 +17,7 @@
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, ts } from 'ts-morph';
20
+ import { Project } 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
@@ -38,35 +38,6 @@ import type { InferRequestItem, InferResult, ExtractionConfig } from './types.js
38
38
  * test (`machinery-indicator-mirror.test.ts`) asserts the two sets stay equal.
39
39
  */
40
40
  export declare const MACHINERY_MEMBER_INDICATORS: Set<string>;
41
- /**
42
- * True when a declaration's source file is runtime/library origin rather than
43
- * user source. Four answers, in order:
44
- *
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.
64
- *
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.
68
- */
69
- export declare function isExternalOrigin(program: ts.Program, sourceFile: ts.SourceFile, repoRoot: string): boolean;
70
41
  /**
71
42
  * Options for TypeInferrer construction
72
43
  */
@@ -99,6 +70,13 @@ export declare class TypeInferrer {
99
70
  private readonly packageOf;
100
71
  private readonly repoRoot;
101
72
  constructor(options: TypeInferrerOptions);
73
+ /**
74
+ * What the structural printer needs to tell the user's own declarations from
75
+ * the runtime's and its packages' — the same program and service root this
76
+ * class asks `isExternalOrigin` about machinery, so the two layers cannot
77
+ * classify one declaration two ways.
78
+ */
79
+ private expandOrigin;
102
80
  /**
103
81
  * Infer types for the given requests
104
82
  *
@@ -708,7 +686,16 @@ export declare class TypeInferrer {
708
686
  * True when an object literal argument is response INIT rather than a body:
709
687
  * every property it declares is one the standard `ResponseInit` declares
710
688
  * (`status`, `statusText`, `headers`), and it states at least one of them as
711
- * init really does — a numeric status in the HTTP range, or headers.
689
+ * init really does — a status in the HTTP range, or headers.
690
+ *
691
+ * The status does NOT have to be a literal code. A route that carries its
692
+ * outcome in a value writes `new Response(body, { status: result.status })`,
693
+ * where the source fixes no code and `statedStatusCodes` answers
694
+ * `'variable'` — status-shaped, just not pinned. Requiring a literal there
695
+ * left the whole init object reading as a body, so an endpoint whose payload
696
+ * this layer does not publish (a string body) published `{ status: number }`
697
+ * as its response contract instead: a wrong contract, served, where an
698
+ * abstention was the honest answer.
712
699
  *
713
700
  * A payload that merely has a `status` member of its own (`{ status: "ok",
714
701
  * service: "ledger" }`) declares members init does not, or states `status` as