carrick 0.3.104 → 0.3.105
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/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/capture/anchors.d.ts +6 -0
- package/sidecar/dist/src/capture/anchors.js +78 -13
- package/sidecar/dist/src/capture/api.d.ts +22 -3
- package/sidecar/dist/src/capture/check-classify.d.ts +1 -0
- package/sidecar/dist/src/capture/check-classify.js +8 -12
- package/sidecar/dist/src/capture/check-fields.js +4 -6
- package/sidecar/dist/src/capture/check-probe.js +16 -6
- package/sidecar/dist/src/capture/check-scrub.d.ts +3 -0
- package/sidecar/dist/src/capture/check-scrub.js +8 -4
- package/sidecar/dist/src/capture/check.js +64 -50
- package/sidecar/dist/src/capture/deep-walk.d.ts +20 -0
- package/sidecar/dist/src/capture/deep-walk.js +51 -11
- package/sidecar/dist/src/capture/guarded-fs.d.ts +5 -1
- package/sidecar/dist/src/capture/guarded-fs.js +23 -1
- package/sidecar/dist/src/capture/index.js +6 -2
- package/sidecar/dist/src/capture/member-name.d.ts +20 -0
- package/sidecar/dist/src/capture/member-name.js +24 -0
- package/sidecar/dist/src/capture/self-check.js +41 -9
- package/sidecar/dist/src/failure-path.d.ts +36 -0
- package/sidecar/dist/src/failure-path.js +212 -0
- package/sidecar/dist/src/retype.js +15 -2
- package/sidecar/dist/src/type-inferrer.d.ts +2 -0
- package/sidecar/dist/src/type-inferrer.js +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carrick",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.105",
|
|
4
4
|
"description": "Maps your entire TypeScript codebase across services and repositories, giving AI agents full context on existing types, routes, and function behaviours over MCP before they write duplicate or breaking code.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -58,11 +58,11 @@
|
|
|
58
58
|
"zod": "^3.23.0"
|
|
59
59
|
},
|
|
60
60
|
"optionalDependencies": {
|
|
61
|
-
"@carrick-tools/cli-darwin-arm64": "0.3.
|
|
62
|
-
"@carrick-tools/cli-darwin-x64": "0.3.
|
|
63
|
-
"@carrick-tools/cli-linux-arm64": "0.3.
|
|
64
|
-
"@carrick-tools/cli-linux-x64": "0.3.
|
|
65
|
-
"@carrick-tools/cli-win32-x64": "0.3.
|
|
61
|
+
"@carrick-tools/cli-darwin-arm64": "0.3.105",
|
|
62
|
+
"@carrick-tools/cli-darwin-x64": "0.3.105",
|
|
63
|
+
"@carrick-tools/cli-linux-arm64": "0.3.105",
|
|
64
|
+
"@carrick-tools/cli-linux-x64": "0.3.105",
|
|
65
|
+
"@carrick-tools/cli-win32-x64": "0.3.105"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/node": "^24.13.3",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carrick",
|
|
3
3
|
"description": "Claude Code plugin for Carrick, which indexes TypeScript codebases across service and repository boundaries. After each edit it adds the file's routes, calls, and cross-service type mismatches to the session, and it registers Carrick's language server. It pairs with the Carrick MCP server, which lets agents search functions by intent rather than name.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.105",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Carrick",
|
|
7
7
|
"email": "hello@carrick.tools"
|
|
@@ -49,6 +49,12 @@ export interface ResolvedAnchor {
|
|
|
49
49
|
* anchor's type holds no unresolved placeholder.
|
|
50
50
|
*/
|
|
51
51
|
unresolved?: UnresolvedAtAnchor;
|
|
52
|
+
/**
|
|
53
|
+
* carrick#1446: set on a demotion whose alias text names a module the
|
|
54
|
+
* declaration emit skipped. The text dangles in the emitted tree, which the
|
|
55
|
+
* record states for a literal anchor, whose text is what the index serves.
|
|
56
|
+
*/
|
|
57
|
+
namesUnemittedModule?: true;
|
|
52
58
|
}
|
|
53
59
|
/** Repo-root-relative source file -> extensionless specifier from entryDir. */
|
|
54
60
|
export declare function entryRelativeSpecifier(entryDir: string, repoRoot: string, sourceFile: string): string;
|
|
@@ -185,10 +185,33 @@ export function resolveAnchor(program, request, args) {
|
|
|
185
185
|
}
|
|
186
186
|
return finishInferAnchor(program, sourceFile, request, param, args.placeholder, undefined, args.repoRoot);
|
|
187
187
|
}
|
|
188
|
-
|
|
189
|
-
if (!
|
|
188
|
+
const found = locate(sourceFile, request);
|
|
189
|
+
if (!found) {
|
|
190
190
|
return demote(locatorFailureReason(request));
|
|
191
191
|
}
|
|
192
|
+
// carrick#1785: the line fallback takes the first expression on the line,
|
|
193
|
+
// and a declaration's NAME is an expression to `ts.isExpression`. On a line
|
|
194
|
+
// that declares something, that name is all the fallback finds, and its type
|
|
195
|
+
// is the declared entity's own (a whole alias, a function, a class), never a
|
|
196
|
+
// payload. The line names the declaration, so the anchor abstains: an
|
|
197
|
+
// abstain, not a demotion, because the backfill has nothing better to
|
|
198
|
+
// re-anchor it with (carrick#766). It does not walk on to the next node
|
|
199
|
+
// either, which on a function's line is its first parameter.
|
|
200
|
+
if (found.by === 'line') {
|
|
201
|
+
const declaration = declarationNamedBy(found.node);
|
|
202
|
+
if (declaration) {
|
|
203
|
+
return {
|
|
204
|
+
request,
|
|
205
|
+
aliasText: 'unknown',
|
|
206
|
+
serialization: 'structural_fallback',
|
|
207
|
+
abstainReason: `the line fallback resolved the name of ` +
|
|
208
|
+
`${describeNode(sourceFile, request.source_file, declaration)}; a ` +
|
|
209
|
+
`declaration's name is not a payload, so the anchor abstains rather ` +
|
|
210
|
+
`than publish the declared entity's own type`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
let located = found.node;
|
|
192
215
|
// carrick#1162: a serialised body is the JSON of its argument. The call's own
|
|
193
216
|
// `string` result is never the payload's contract, and publishing it reads
|
|
194
217
|
// incompatible against every object-typed counterparty.
|
|
@@ -252,17 +275,18 @@ function finishInferAnchor(program, sourceFile, request, located, placeholder, r
|
|
|
252
275
|
// here.
|
|
253
276
|
//
|
|
254
277
|
// A line alone names nothing. `firstExpressionOnLine` picks whatever comes
|
|
255
|
-
// first on the line
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
278
|
+
// first on the line. When THAT resolves to a top type the capture holds no
|
|
279
|
+
// payload and no type, and `export type <alias> = any;` states "a type was
|
|
280
|
+
// inferred and it collapsed" — a claim the scan cannot back. The honest word
|
|
281
|
+
// is `unknown` ("no contract stated here"), with the node the line resolved
|
|
282
|
+
// as the reason.
|
|
260
283
|
//
|
|
261
284
|
// Live shape: a route whose handlers are built by a framework factory and
|
|
262
285
|
// re-exported at the bottom of the file. Both of the file's operations
|
|
263
|
-
// anchor at the export statement
|
|
264
|
-
//
|
|
265
|
-
//
|
|
286
|
+
// anchor at the export statement and the v1 walk abstains there
|
|
287
|
+
// (carrick#771). The export line itself now abstains earlier, as a
|
|
288
|
+
// declaration's name (carrick#1785); this guard still holds for a line whose
|
|
289
|
+
// first node is a value that decayed, such as the factory call's binding.
|
|
266
290
|
if (isTopType(type) && isLineOnly(request)) {
|
|
267
291
|
return {
|
|
268
292
|
request,
|
|
@@ -914,21 +938,62 @@ function locatorFailureReason(request) {
|
|
|
914
938
|
* else the first expression starting on line_number.
|
|
915
939
|
*/
|
|
916
940
|
export function locateNode(sourceFile, request) {
|
|
941
|
+
return locate(sourceFile, request)?.node;
|
|
942
|
+
}
|
|
943
|
+
/** The node `locateNode` resolves, and which of its three locators found it. */
|
|
944
|
+
function locate(sourceFile, request) {
|
|
917
945
|
if (request.span_start !== undefined && request.span_end !== undefined) {
|
|
918
946
|
const bySpan = tightestCoveringNode(sourceFile, request.span_start, request.span_end);
|
|
919
947
|
if (bySpan)
|
|
920
|
-
return bySpan;
|
|
948
|
+
return { node: bySpan, by: 'span' };
|
|
921
949
|
}
|
|
922
950
|
if (request.expression_text) {
|
|
923
951
|
const byText = nodeByExpressionText(sourceFile, request.expression_text, request.line_number);
|
|
924
952
|
if (byText)
|
|
925
|
-
return byText;
|
|
953
|
+
return { node: byText, by: 'text' };
|
|
926
954
|
}
|
|
927
955
|
if (request.line_number !== undefined) {
|
|
928
|
-
|
|
956
|
+
const byLine = firstExpressionOnLine(sourceFile, request.line_number);
|
|
957
|
+
if (byLine)
|
|
958
|
+
return { node: byLine, by: 'line' };
|
|
929
959
|
}
|
|
930
960
|
return undefined;
|
|
931
961
|
}
|
|
962
|
+
/**
|
|
963
|
+
* The declaration `node` is the name of, when it names one (carrick#1785): a
|
|
964
|
+
* type alias, interface, class, function, method, property, accessor, enum or
|
|
965
|
+
* enum member, namespace, or an import or export binding (whose `propertyName`
|
|
966
|
+
* names the binding too: `export { a as b }`).
|
|
967
|
+
*
|
|
968
|
+
* Not a binding whose name is also the value read at that position: a
|
|
969
|
+
* destructured element and a shorthand property keep resolving. A variable,
|
|
970
|
+
* a parameter and a property assignment never reach here, because each is a
|
|
971
|
+
* preferred target the line walk takes before its name.
|
|
972
|
+
*/
|
|
973
|
+
function declarationNamedBy(node) {
|
|
974
|
+
const parent = node.parent;
|
|
975
|
+
if (!parent)
|
|
976
|
+
return undefined;
|
|
977
|
+
// A specifier's only children are its names.
|
|
978
|
+
if (ts.isImportSpecifier(parent) || ts.isExportSpecifier(parent))
|
|
979
|
+
return parent;
|
|
980
|
+
const named = ts.isTypeAliasDeclaration(parent) ||
|
|
981
|
+
ts.isInterfaceDeclaration(parent) ||
|
|
982
|
+
ts.isClassDeclaration(parent) ||
|
|
983
|
+
ts.isFunctionDeclaration(parent) ||
|
|
984
|
+
ts.isMethodDeclaration(parent) ||
|
|
985
|
+
ts.isMethodSignature(parent) ||
|
|
986
|
+
ts.isPropertyDeclaration(parent) ||
|
|
987
|
+
ts.isPropertySignature(parent) ||
|
|
988
|
+
ts.isGetAccessorDeclaration(parent) ||
|
|
989
|
+
ts.isSetAccessorDeclaration(parent) ||
|
|
990
|
+
ts.isEnumDeclaration(parent) ||
|
|
991
|
+
ts.isEnumMember(parent) ||
|
|
992
|
+
ts.isModuleDeclaration(parent) ||
|
|
993
|
+
ts.isImportClause(parent) ||
|
|
994
|
+
ts.isNamespaceImport(parent);
|
|
995
|
+
return named && parent.name === node ? parent : undefined;
|
|
996
|
+
}
|
|
932
997
|
function isPreferredTarget(node) {
|
|
933
998
|
return (ts.isExpression(node) ||
|
|
934
999
|
ts.isVariableDeclaration(node) ||
|
|
@@ -251,6 +251,26 @@ export interface CaptureAliasRecord {
|
|
|
251
251
|
* absent when there are none.
|
|
252
252
|
*/
|
|
253
253
|
undeclared_names?: string[];
|
|
254
|
+
/**
|
|
255
|
+
* Every position at which this alias's type, as the emitted tree states it,
|
|
256
|
+
* holds TypeScript's unresolved-reference placeholder (carrick#1446): `''`
|
|
257
|
+
* when the alias's own type does not resolve, member paths otherwise, in the
|
|
258
|
+
* deep walk's notation, sorted by path. The compiler prints the placeholder
|
|
259
|
+
* as the name it could not follow, so the text reads like a type while every
|
|
260
|
+
* reader of the tree reads `any` there.
|
|
261
|
+
*
|
|
262
|
+
* Read on the self-check program, where the producer's installed packages
|
|
263
|
+
* resolve, so a name only a missing install leaves unresolved is listed only
|
|
264
|
+
* on a bare checkout. A literal anchor demoted because its text names a
|
|
265
|
+
* module the emit skipped is listed at its root: that text is what the
|
|
266
|
+
* index serves for it, and it names a module the tree does not hold.
|
|
267
|
+
*
|
|
268
|
+
* Kept apart from `any_provenance` on purpose: the check phase pre-gates
|
|
269
|
+
* on `any_provenance[0]`, and this list is what the index tells a reader,
|
|
270
|
+
* not a verdict. The scanner joins it onto the manifest entry beside the
|
|
271
|
+
* entry's other provenance. Absent when every position resolves.
|
|
272
|
+
*/
|
|
273
|
+
unresolved_in_tree?: TypeProvenance[];
|
|
254
274
|
}
|
|
255
275
|
/** Aggregate fidelity metric, emitted per capture (one service). */
|
|
256
276
|
export interface CaptureFidelity {
|
|
@@ -295,7 +315,8 @@ export interface CaptureStubOptions {
|
|
|
295
315
|
/**
|
|
296
316
|
* The scanned repo's root, the upper bound of the search for a tsconfig
|
|
297
317
|
* above `repoRoot` when none is named (carrick#1776). Without it only
|
|
298
|
-
* `repoRoot` is searched.
|
|
318
|
+
* `repoRoot` is searched. It is a protected tree as well: `outDir` may
|
|
319
|
+
* lie inside it only beneath a `.carrick` directory (carrick#1768).
|
|
299
320
|
*/
|
|
300
321
|
scanRoot?: string;
|
|
301
322
|
}
|
|
@@ -431,8 +452,6 @@ export interface CheckResult {
|
|
|
431
452
|
* missing (soundness over availability — pinned design, Check step 2). */
|
|
432
453
|
isolation: 'pnpm' | 'unavailable';
|
|
433
454
|
install_ok: boolean;
|
|
434
|
-
/** Scrubbed install-failure summary when install_ok is false. */
|
|
435
|
-
install_error?: string;
|
|
436
455
|
ts_version: string;
|
|
437
456
|
/** Verdicts, sorted by pair_id for byte-stable output. */
|
|
438
457
|
verdicts: CheckVerdict[];
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* IsUnknown/IsNever gate fired (TS2344) -> unverifiable
|
|
13
13
|
* IsVoid gate fired (TS2344) -> unverifiable (no body read)
|
|
14
14
|
* IsFormBody gate fired (TS2344) -> unverifiable (form-encoded body)
|
|
15
|
+
* IsByteBody gate fired (TS2344) -> unverifiable (bytes, even agreeing)
|
|
15
16
|
* assignment-class error -> incompatible
|
|
16
17
|
* no diagnostics -> compatible [lowest precedence]
|
|
17
18
|
*
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* IsUnknown/IsNever gate fired (TS2344) -> unverifiable
|
|
13
13
|
* IsVoid gate fired (TS2344) -> unverifiable (no body read)
|
|
14
14
|
* IsFormBody gate fired (TS2344) -> unverifiable (form-encoded body)
|
|
15
|
+
* IsByteBody gate fired (TS2344) -> unverifiable (bytes, even agreeing)
|
|
15
16
|
* assignment-class error -> incompatible
|
|
16
17
|
* no diagnostics -> compatible [lowest precedence]
|
|
17
18
|
*
|
|
@@ -172,19 +173,18 @@ export function classifyPair(input) {
|
|
|
172
173
|
};
|
|
173
174
|
}
|
|
174
175
|
// 4c. A body of bytes (carrick#1793): a blob, a buffer or a stream has no JSON
|
|
175
|
-
// shape,
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
// where a stream is read
|
|
179
|
-
//
|
|
180
|
-
// diagnostics came in.
|
|
176
|
+
// shape, and the check cannot read a file's content. So a pair with
|
|
177
|
+
// bytes on either side states no contract, whether the two sides
|
|
178
|
+
// mismatch (a `Uint8Array` sent, read with `.blob()`) or agree (a stream
|
|
179
|
+
// sent where a stream is read, carrick#1812). When both sides are bytes
|
|
180
|
+
// the sent side is named, whatever order the diagnostics came in.
|
|
181
181
|
const firedGates = new Set(gateDiags.map((d) => plan.gateLines.get(d.line)));
|
|
182
182
|
const bytesGate = firedGates.has('sent:bytes')
|
|
183
183
|
? 'sent:bytes'
|
|
184
184
|
: firedGates.has('expected:bytes')
|
|
185
185
|
? 'expected:bytes'
|
|
186
186
|
: undefined;
|
|
187
|
-
|
|
187
|
+
if (bytesGate) {
|
|
188
188
|
const { side } = sideForGate(bytesGate, plan);
|
|
189
189
|
return {
|
|
190
190
|
...base,
|
|
@@ -193,7 +193,7 @@ export function classifyPair(input) {
|
|
|
193
193
|
diagnostic: `the ${side} body is bytes (a blob, a buffer or a stream), which has no JSON shape to compare with the other side.`,
|
|
194
194
|
...notAFact(`the ${side} body is bytes`, side),
|
|
195
195
|
};
|
|
196
|
-
}
|
|
196
|
+
}
|
|
197
197
|
// 5. Assignment-class error on the DECISIVE assignment line -> incompatible.
|
|
198
198
|
//
|
|
199
199
|
// On an `http` pair that line is the JSON wire assignment, not the declared
|
|
@@ -206,8 +206,6 @@ export function classifyPair(input) {
|
|
|
206
206
|
const decisiveLine = decisiveAssignmentLine(plan);
|
|
207
207
|
const assignDiag = probeDiags.find((d) => d.line === decisiveLine && ASSIGNMENT_CODES.has(d.code));
|
|
208
208
|
if (assignDiag) {
|
|
209
|
-
if (bytesGate)
|
|
210
|
-
return bytesVerdict();
|
|
211
209
|
const text = scrubDiagnostic(assignDiag.message, scrubCtx, plan.sentEndpoint.alias, plan.expectedEndpoint.alias);
|
|
212
210
|
return {
|
|
213
211
|
...base,
|
|
@@ -243,8 +241,6 @@ export function classifyPair(input) {
|
|
|
243
241
|
if (wireOther) {
|
|
244
242
|
const declaredMismatch = probeDiags.find((d) => d.line === plan.assignmentLine && ASSIGNMENT_CODES.has(d.code));
|
|
245
243
|
if (declaredMismatch) {
|
|
246
|
-
if (bytesGate)
|
|
247
|
-
return bytesVerdict();
|
|
248
244
|
return {
|
|
249
245
|
...base,
|
|
250
246
|
bucket: 'incompatible',
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* Seam: node builtins + `typescript` + this bundle only.
|
|
30
30
|
*/
|
|
31
31
|
import ts from 'typescript';
|
|
32
|
+
import { memberPath } from './member-name.js';
|
|
32
33
|
/**
|
|
33
34
|
* Cap on named fields. A mismatch with more differing members than this is
|
|
34
35
|
* better described as two unrelated shapes than as a list, and the text says
|
|
@@ -172,7 +173,7 @@ function walk(sent, expected, path, depth, ctx) {
|
|
|
172
173
|
* makes a sender-only member worth naming beside it. */
|
|
173
174
|
let absent = 0;
|
|
174
175
|
for (const [name, expectedProp] of expectedProps) {
|
|
175
|
-
const at =
|
|
176
|
+
const at = memberPath(path, expectedProp, checker);
|
|
176
177
|
const sentProp = sentProps.get(name);
|
|
177
178
|
const supplied = ctx.serverSupplied.has(name);
|
|
178
179
|
if (!sentProp) {
|
|
@@ -226,15 +227,12 @@ function walk(sent, expected, path, depth, ctx) {
|
|
|
226
227
|
// producer returns — nothing is absent and nothing is named.
|
|
227
228
|
if (absent === 0)
|
|
228
229
|
return;
|
|
229
|
-
for (const [name] of sentProps) {
|
|
230
|
+
for (const [name, sentProp] of sentProps) {
|
|
230
231
|
if (expectedProps.has(name))
|
|
231
232
|
continue;
|
|
232
|
-
ctx.found.push({ path:
|
|
233
|
+
ctx.found.push({ path: memberPath(path, sentProp, checker), nature: 'extra_in_sent' });
|
|
233
234
|
}
|
|
234
235
|
}
|
|
235
|
-
function join(path, name) {
|
|
236
|
-
return path === '' ? name : `${path}.${name}`;
|
|
237
|
-
}
|
|
238
236
|
function isOptional(symbol) {
|
|
239
237
|
return (symbol.flags & ts.SymbolFlags.Optional) !== 0;
|
|
240
238
|
}
|
|
@@ -155,8 +155,8 @@ export function buildProbe(spec, packageOf) {
|
|
|
155
155
|
// them in, so a route sending a `Uint8Array` read with `.blob()` is not a
|
|
156
156
|
// mismatch, and neither is the same route read as text. Either side, either
|
|
157
157
|
// half: a file upload states no fields a declared body could be compared
|
|
158
|
-
// with.
|
|
159
|
-
//
|
|
158
|
+
// with. Bytes that assign to bytes are not a contract either: the check
|
|
159
|
+
// cannot read a file's content (carrick#1812). `null`/`undefined` are
|
|
160
160
|
// dropped first, so `Blob | null` still gates, while a bare `null`,
|
|
161
161
|
// `undefined` or `never` leaves nothing to test.
|
|
162
162
|
if (spec.protocol === 'http') {
|
|
@@ -182,6 +182,12 @@ export function buildProbe(spec, packageOf) {
|
|
|
182
182
|
// or several (ambiguous envelope, or a bare payload whose own fields
|
|
183
183
|
// include more than one object-shaped property) keep the whole type —
|
|
184
184
|
// the unwrap never fires on anything but an unambiguous envelope;
|
|
185
|
+
// - a consumer that names a field only the outer object has (not the
|
|
186
|
+
// payload under its one object-shaped property) reads that object, so
|
|
187
|
+
// it keeps the whole type (carrick#1764): a bare payload with one
|
|
188
|
+
// object-shaped property is not an envelope around it. A field both
|
|
189
|
+
// declare says nothing either way, and neither does `__typename`, which
|
|
190
|
+
// the server puts on every object (see below);
|
|
185
191
|
// - `GqlObjLike` can never select `any`/`unknown`/`never` (top/bottom
|
|
186
192
|
// types fail both its array and its object arm), and the comparand is
|
|
187
193
|
// re-gated below anyway — the v2 port of v1's comparand re-guard, so
|
|
@@ -191,6 +197,10 @@ export function buildProbe(spec, packageOf) {
|
|
|
191
197
|
push(`type GqlPayloadKeys<T> = { [K in keyof T]-?: [GqlObjLike<T[K]>] extends [true] ? K : never }[keyof T];`);
|
|
192
198
|
push(`type GqlSingleKey<K> = [K] extends [never] ? never : ([GqlIsUnion<K>] extends [false] ? K : never);`);
|
|
193
199
|
push(`type GqlPayloadOf<S> = [S] extends [readonly unknown[]] ? never : [GqlIsUnion<S>] extends [true] ? never : [S] extends [object] ? ([GqlSingleKey<GqlPayloadKeys<S>>] extends [never] ? never : S[GqlSingleKey<GqlPayloadKeys<S>> & keyof S]) : never;`);
|
|
200
|
+
// The field names a reading declares: the keys of every object member, so
|
|
201
|
+
// a nullable or union consumer names what each member names.
|
|
202
|
+
push(`type GqlNames<T> = T extends object ? keyof T : never;`);
|
|
203
|
+
push(`type GqlReadsOuter<S, E> = [Extract<Exclude<GqlNames<S>, GqlNames<GqlPayloadOf<S>>>, Exclude<GqlNames<E>, '__typename'>>] extends [never] ? false : true;`);
|
|
194
204
|
// carrick#1759: the server supplies `__typename` on every object a
|
|
195
205
|
// selection asks for, so the consumer's `__typename` is read as OPTIONAL,
|
|
196
206
|
// at any depth. It keeps its declared type: a producer that states a
|
|
@@ -208,14 +218,14 @@ export function buildProbe(spec, packageOf) {
|
|
|
208
218
|
// nothing observable, so a consumer with no `__typename` to relax is
|
|
209
219
|
// judged, and named in the headline, exactly as before.
|
|
210
220
|
//
|
|
211
|
-
// The envelope short-circuit below tests the RELAXED consumer: a
|
|
212
|
-
//
|
|
213
|
-
// `__typename` alone and unwrap onto that
|
|
221
|
+
// The envelope short-circuit below tests the RELAXED consumer: a consumer
|
|
222
|
+
// that names only fields the payload shares would otherwise fail it on
|
|
223
|
+
// `__typename` alone and unwrap onto that payload.
|
|
214
224
|
push(`type GqlDepth = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];`);
|
|
215
225
|
push(`type GqlTypenameParts<T, D extends number> = { [K in keyof T as K extends '__typename' ? never : K]: GqlTypenameOptional<T[K], GqlDepth[D]> } & { [K in keyof T as K extends '__typename' ? K : never]?: T[K] };`);
|
|
216
226
|
push(`type GqlTypenameOptional<T, D extends number = 16> = [D] extends [never] ? T : T extends (...args: any[]) => any ? T : T extends object ? ('__typename' extends keyof T ? { [K in keyof GqlTypenameParts<T, D>]: GqlTypenameParts<T, D>[K] } : { [K in keyof T]: GqlTypenameOptional<T[K], GqlDepth[D]> }) : T;`);
|
|
217
227
|
push(`type GqlExpected = [GqlTypenameOptional<Expected>] extends [Expected] ? Expected : GqlTypenameOptional<Expected>;`);
|
|
218
|
-
push(`type GqlComparand = [Sent] extends [GqlExpected] ? Sent : ([GqlPayloadOf<Sent>] extends [never] ? Sent : GqlPayloadOf<Sent>);`);
|
|
228
|
+
push(`type GqlComparand = [Sent] extends [GqlExpected] ? Sent : ([GqlPayloadOf<Sent>] extends [never] ? Sent : [GqlReadsOuter<Sent, Expected>] extends [true] ? Sent : GqlPayloadOf<Sent>);`);
|
|
219
229
|
gateLines.set(push(`type _G_comparand_any = Assert<Not<IsAny<GqlComparand>>>;`), 'sent:any');
|
|
220
230
|
gateLines.set(push(`type _G_comparand_unknown = Assert<Not<IsUnknown<GqlComparand>>>;`), 'sent:unknown');
|
|
221
231
|
gateLines.set(push(`type _G_comparand_never = Assert<Not<IsNever<GqlComparand>>>;`), 'sent:never');
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
export interface ScrubContext {
|
|
17
17
|
/** Absolute path of the scratch workspace root (fully removed from output). */
|
|
18
18
|
workspaceRoot: string;
|
|
19
|
+
/** The same root with symlinks resolved, as a tool that prints real paths
|
|
20
|
+
* names it (pnpm, under a macOS temp dir). Absent when there is no tree. */
|
|
21
|
+
workspaceRealRoot?: string;
|
|
19
22
|
/** Workspace package dir (== sanitized service) -> the @carrick/<dir> label. */
|
|
20
23
|
packageLabelOf: (packageDir: string) => string | undefined;
|
|
21
24
|
}
|
|
@@ -46,10 +46,14 @@ export function scrubPaths(text, ctx) {
|
|
|
46
46
|
let out = text.replace(IMPORT_PATH_RE, (_m, p1) => {
|
|
47
47
|
return `import("${labelForImportPath(p1, ctx)}")`;
|
|
48
48
|
});
|
|
49
|
-
// Defensive: no raw temp path may survive anywhere in the message.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
// Defensive: no raw temp path may survive anywhere in the message. The real
|
|
50
|
+
// root goes first: the given root can be its suffix (`/private/var/...`
|
|
51
|
+
// against `/var/...`), and stripping that first would strand the prefix.
|
|
52
|
+
for (const root of [ctx.workspaceRealRoot, ctx.workspaceRoot]) {
|
|
53
|
+
if (!root)
|
|
54
|
+
continue;
|
|
55
|
+
out = out.split(root).join('');
|
|
56
|
+
out = out.split(root.split('/').join('\\')).join('');
|
|
53
57
|
}
|
|
54
58
|
return out;
|
|
55
59
|
}
|
|
@@ -103,15 +103,18 @@ export async function runCheck(opts, onProgress) {
|
|
|
103
103
|
// to a flat npm install that manufactures nominal false-incompatibles. Fail
|
|
104
104
|
// explicitly instead (design Check step 2).
|
|
105
105
|
if (!fs.existsSync(pnpmPath)) {
|
|
106
|
-
|
|
106
|
+
// Capture-decay verdicts stand without isolation too (carrick#1833).
|
|
107
|
+
const { preGated, probing } = preGate(planPairs(opts, (s) => `@carrick/${s}`), readStubAliasRecords(opts.stubs));
|
|
107
108
|
return {
|
|
108
109
|
success: false,
|
|
109
110
|
workspace_dir: '',
|
|
110
111
|
isolation: 'unavailable',
|
|
111
112
|
install_ok: false,
|
|
112
|
-
install_error: 'vendored pnpm not found; type isolation is unavailable',
|
|
113
113
|
ts_version: tsVersion,
|
|
114
|
-
verdicts: sortVerdicts(
|
|
114
|
+
verdicts: sortVerdicts([
|
|
115
|
+
...unverifiableAll(probing, 'isolation:unavailable', 'type isolation unavailable (pnpm missing); compatibility cannot be verified.'),
|
|
116
|
+
...preGated,
|
|
117
|
+
]),
|
|
115
118
|
degraded_services: opts.stubs.map((s) => ({
|
|
116
119
|
service_name: s.service_name,
|
|
117
120
|
reason: 'isolation unavailable',
|
|
@@ -126,6 +129,7 @@ export async function runCheck(opts, onProgress) {
|
|
|
126
129
|
});
|
|
127
130
|
const scrubCtx = {
|
|
128
131
|
workspaceRoot: ws.workspaceDir,
|
|
132
|
+
workspaceRealRoot: fs.realpathSync(ws.workspaceDir),
|
|
129
133
|
packageLabelOf: ws.packageLabelOf,
|
|
130
134
|
};
|
|
131
135
|
// Generate probes. A pair naming a service with no stub is unverifiable.
|
|
@@ -154,61 +158,25 @@ export async function runCheck(opts, onProgress) {
|
|
|
154
158
|
const spec = opts.pairs.find((p) => p.pair_key === v.pair_key);
|
|
155
159
|
v.pair_id = fnvOfSpec(spec);
|
|
156
160
|
}
|
|
157
|
-
|
|
158
|
-
// member-level `any`/`unknown` recorded by the capture self-check with no
|
|
159
|
-
// failing-external explanation. The probe gates below are WHOLE-type only
|
|
160
|
-
// — `{ orderId: string; metadata: any }` sails through IsAny and the
|
|
161
|
-
// assignment compiles clean — so such pairs must never reach a probe.
|
|
162
|
-
// `any` routes to gate_caught_baked_any, `unknown` to unverifiable; both
|
|
163
|
-
// read as None downstream, never compatible.
|
|
164
|
-
const aliasRecords = readStubAliasRecords(opts.stubs);
|
|
165
|
-
const preGated = [];
|
|
166
|
-
const probing = [];
|
|
167
|
-
for (const plan of plans) {
|
|
168
|
-
const hit = deepDecayOf(plan, aliasRecords);
|
|
169
|
-
if (!hit) {
|
|
170
|
-
probing.push(plan);
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
preGated.push({
|
|
174
|
-
pair_id: plan.pairId,
|
|
175
|
-
pair_key: plan.spec.pair_key,
|
|
176
|
-
// `any` is a confirmed baked top type -> gate_caught_baked_any; `unknown`
|
|
177
|
-
// and `budget_exhausted` (a subtree the capture walk could not finish)
|
|
178
|
-
// are "cannot verify" -> unverifiable. All read as None downstream.
|
|
179
|
-
bucket: hit.kind === 'any' ? 'gate_caught_baked_any' : 'unverifiable',
|
|
180
|
-
gate: `capture:${hit.side}:${hit.kind}`,
|
|
181
|
-
diagnostic: hit.kind === 'budget_exhausted'
|
|
182
|
-
? `the ${hit.side} type is too deep or wide to verify within the ` +
|
|
183
|
-
`capture budget (at '${hit.path}'); compatibility cannot be ` +
|
|
184
|
-
`verified — abstaining so a buried 'any' can never read compatible.`
|
|
185
|
-
: `the ${hit.side} type carries '${hit.kind}' at '${hit.path}' from ` +
|
|
186
|
-
`capture; compatibility cannot be verified (a partially-unresolved ` +
|
|
187
|
-
`type would let an arbitrary shape read compatible).`,
|
|
188
|
-
codes: [],
|
|
189
|
-
resolved: false,
|
|
190
|
-
unresolved_side: hit.side,
|
|
191
|
-
unresolved_reason: hit.kind === 'budget_exhausted'
|
|
192
|
-
? `the ${hit.side} type is too deep or wide to verify at '${hit.path}'`
|
|
193
|
-
: `the ${hit.side} type carries '${hit.kind}' at '${hit.path}'`,
|
|
194
|
-
notes: [],
|
|
195
|
-
});
|
|
196
|
-
}
|
|
161
|
+
const { preGated, probing } = preGate(plans, readStubAliasRecords(opts.stubs));
|
|
197
162
|
writeProbes(ws, probing);
|
|
198
163
|
const errors = [];
|
|
199
164
|
const degraded = [];
|
|
200
165
|
// ---- Install (async, off the event loop) --------------------------------
|
|
201
166
|
progress('installing', 'installing pinned dependencies');
|
|
202
167
|
const install = await runProcess(pnpmPath, ['install'], ws.workspaceDir);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
168
|
+
if (install.code !== 0) {
|
|
169
|
+
const installError = scrubPaths((install.stderr || install.stdout).trim().slice(0, 2000), scrubCtx);
|
|
170
|
+
// `errors` says why the run failed (carrick#1821); each pair the install
|
|
171
|
+
// stopped says it too, because the client reads every pair's own verdict
|
|
172
|
+
// on a failed check (carrick#1833).
|
|
173
|
+
errors.push(`workspace dependency install failed${installError ? `: ${installError}` : ''}`);
|
|
207
174
|
for (const s of opts.stubs) {
|
|
208
175
|
degraded.push({ service_name: s.service_name, reason: 'workspace install failed' });
|
|
209
176
|
}
|
|
210
177
|
const verdicts = sortVerdicts([
|
|
211
|
-
...unverifiableAll(probing, 'install:failed', 'workspace dependency install failed; compatibility cannot be verified.'
|
|
178
|
+
...unverifiableAll(probing, 'install:failed', 'workspace dependency install failed; compatibility cannot be verified.' +
|
|
179
|
+
(installError ? ` Installer output: ${installError}` : '')),
|
|
212
180
|
// Capture-decay verdicts stand regardless of the install outcome.
|
|
213
181
|
...preGated,
|
|
214
182
|
...unresolved,
|
|
@@ -220,7 +188,6 @@ export async function runCheck(opts, onProgress) {
|
|
|
220
188
|
workspace_dir: cleanup ? '' : ws.workspaceDir,
|
|
221
189
|
isolation: 'pnpm',
|
|
222
190
|
install_ok: false,
|
|
223
|
-
install_error: installError,
|
|
224
191
|
ts_version: tsVersion,
|
|
225
192
|
verdicts,
|
|
226
193
|
degraded_services: degraded,
|
|
@@ -249,7 +216,9 @@ export async function runCheck(opts, onProgress) {
|
|
|
249
216
|
});
|
|
250
217
|
}
|
|
251
218
|
const verdicts = sortVerdicts([
|
|
252
|
-
...unverifiableAll(probing, 'tsc:abnormal-termination',
|
|
219
|
+
...unverifiableAll(probing, 'tsc:abnormal-termination', `the type checker terminated abnormally (exit code ${tsc.code ?? 'null'}); ` +
|
|
220
|
+
'compatibility cannot be verified.' +
|
|
221
|
+
(excerpt ? ` Compiler output: ${excerpt}` : '')),
|
|
253
222
|
...preGated,
|
|
254
223
|
...unresolved,
|
|
255
224
|
]);
|
|
@@ -341,6 +310,51 @@ function readStubAliasRecords(stubs) {
|
|
|
341
310
|
}
|
|
342
311
|
return byService;
|
|
343
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Capture-time deep-decay pre-gate (adversarial-review finding 1): a
|
|
315
|
+
* member-level `any`/`unknown` recorded by the capture self-check with no
|
|
316
|
+
* failing-external explanation. The probe gates are WHOLE-type only —
|
|
317
|
+
* `{ orderId: string; metadata: any }` sails through IsAny and the assignment
|
|
318
|
+
* compiles clean — so such pairs must never reach a probe. `any` routes to
|
|
319
|
+
* gate_caught_baked_any, `unknown` to unverifiable; both read as None
|
|
320
|
+
* downstream, never compatible. These verdicts come from the capture alone,
|
|
321
|
+
* so they stand whatever the install or the compiler then does (carrick#1833).
|
|
322
|
+
*/
|
|
323
|
+
function preGate(plans, aliasRecords) {
|
|
324
|
+
const preGated = [];
|
|
325
|
+
const probing = [];
|
|
326
|
+
for (const plan of plans) {
|
|
327
|
+
const hit = deepDecayOf(plan, aliasRecords);
|
|
328
|
+
if (!hit) {
|
|
329
|
+
probing.push(plan);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
preGated.push({
|
|
333
|
+
pair_id: plan.pairId,
|
|
334
|
+
pair_key: plan.spec.pair_key,
|
|
335
|
+
// `any` is a confirmed baked top type -> gate_caught_baked_any; `unknown`
|
|
336
|
+
// and `budget_exhausted` (a subtree the capture walk could not finish)
|
|
337
|
+
// are "cannot verify" -> unverifiable. All read as None downstream.
|
|
338
|
+
bucket: hit.kind === 'any' ? 'gate_caught_baked_any' : 'unverifiable',
|
|
339
|
+
gate: `capture:${hit.side}:${hit.kind}`,
|
|
340
|
+
diagnostic: hit.kind === 'budget_exhausted'
|
|
341
|
+
? `the ${hit.side} type is too deep or wide to verify within the ` +
|
|
342
|
+
`capture budget (at '${hit.path}'); compatibility cannot be ` +
|
|
343
|
+
`verified — abstaining so a buried 'any' can never read compatible.`
|
|
344
|
+
: `the ${hit.side} type carries '${hit.kind}' at '${hit.path}' from ` +
|
|
345
|
+
`capture; compatibility cannot be verified (a partially-unresolved ` +
|
|
346
|
+
`type would let an arbitrary shape read compatible).`,
|
|
347
|
+
codes: [],
|
|
348
|
+
resolved: false,
|
|
349
|
+
unresolved_side: hit.side,
|
|
350
|
+
unresolved_reason: hit.kind === 'budget_exhausted'
|
|
351
|
+
? `the ${hit.side} type is too deep or wide to verify at '${hit.path}'`
|
|
352
|
+
: `the ${hit.side} type carries '${hit.kind}' at '${hit.path}'`,
|
|
353
|
+
notes: [],
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return { preGated, probing };
|
|
357
|
+
}
|
|
344
358
|
/** First side (producer, then consumer) whose capture recorded a deep decay. */
|
|
345
359
|
function deepDecayOf(plan, aliasRecords) {
|
|
346
360
|
for (const side of ['producer', 'consumer']) {
|
|
@@ -63,6 +63,9 @@ export declare function findDisqualifyingTopTypes(root: ts.Type, program: ts.Pro
|
|
|
63
63
|
* names. A walk that runs out of budget reports what it found before it did.
|
|
64
64
|
*/
|
|
65
65
|
export declare function findUnresolvedPlaceholders(root: ts.Type, program: ts.Program, checker: ts.TypeChecker, location: ts.Node): string[];
|
|
66
|
+
/** TypeScript's unresolved-reference placeholder: `TypeFlags.Any` with the
|
|
67
|
+
* internal `intrinsicName === 'error'` (stable since TS 1.x; see `anchors.ts`). */
|
|
68
|
+
export declare function isErrorPlaceholder(t: ts.Type): boolean;
|
|
66
69
|
/** What an anchor's SOURCE program could not resolve (carrick#1164). */
|
|
67
70
|
export interface UnresolvedAtAnchor {
|
|
68
71
|
/** Member paths holding the unresolved-reference placeholder. */
|
|
@@ -87,3 +90,20 @@ export interface UnresolvedAtAnchor {
|
|
|
87
90
|
* one other cause the walk itself can distinguish is its own budget.
|
|
88
91
|
*/
|
|
89
92
|
export declare function provenanceOf(finding: DeepTopType, unresolved?: UnresolvedAtAnchor): TypeProvenance;
|
|
93
|
+
/**
|
|
94
|
+
* The entry for one position at which the emitted tree holds the
|
|
95
|
+
* unresolved-reference placeholder (carrick#1446): `path` is `''` for the
|
|
96
|
+
* alias's own type. `names` are the identifiers the self-check could not find
|
|
97
|
+
* in this alias's statement and the files it reaches.
|
|
98
|
+
*
|
|
99
|
+
* The cause is `unresolved_import`, the word a reader already has for "a
|
|
100
|
+
* reference here did not resolve"; the detail says where it failed to.
|
|
101
|
+
*/
|
|
102
|
+
export declare function unresolvedInTreeProvenance(path: string, names: readonly string[]): TypeProvenance;
|
|
103
|
+
/**
|
|
104
|
+
* The root entry for a literal anchor demoted because its text names a module
|
|
105
|
+
* whose declaration emit was skipped (carrick#1446, carrick#1165): that text is
|
|
106
|
+
* what the index serves for it, and nothing in the emitted tree declares what
|
|
107
|
+
* it imports.
|
|
108
|
+
*/
|
|
109
|
+
export declare function unemittedModuleProvenance(): TypeProvenance;
|