carrick 0.3.83 → 0.3.85
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/README.md +31 -11
- package/bin/carrick.mjs +89 -0
- package/dist/auth/credentials.d.ts +9 -0
- package/dist/auth/credentials.js +13 -2
- package/dist/auth/credentials.js.map +1 -1
- package/dist/auth/read.d.ts +4 -4
- package/dist/global-install.d.ts +183 -0
- package/dist/global-install.js +393 -0
- package/dist/global-install.js.map +1 -0
- package/dist/hook/post-edit.js +10 -0
- package/dist/hook/post-edit.js.map +1 -1
- package/dist/hook/session-start.js +34 -0
- package/dist/hook/session-start.js.map +1 -1
- package/dist/hook/stop.js +18 -4
- package/dist/hook/stop.js.map +1 -1
- package/dist/hook/user-prompt.js +16 -4
- package/dist/hook/user-prompt.js.map +1 -1
- package/dist/init/doctor.d.ts +61 -0
- package/dist/init/doctor.js +160 -1
- package/dist/init/doctor.js.map +1 -1
- package/dist/init/hosted.d.ts +30 -5
- package/dist/init/hosted.js +75 -9
- package/dist/init/hosted.js.map +1 -1
- package/dist/init/mcp.d.ts +45 -30
- package/dist/init/mcp.js +47 -88
- package/dist/init/mcp.js.map +1 -1
- package/dist/init/outdated.d.ts +47 -0
- package/dist/init/outdated.js +104 -0
- package/dist/init/outdated.js.map +1 -1
- package/dist/init/output.d.ts +43 -6
- package/dist/init/output.js +90 -18
- package/dist/init/output.js.map +1 -1
- package/dist/init/projects.d.ts +2 -2
- package/dist/init/run.d.ts +89 -25
- package/dist/init/run.js +292 -58
- package/dist/init/run.js.map +1 -1
- package/dist/render.d.ts +16 -0
- package/dist/render.js +57 -6
- package/dist/render.js.map +1 -1
- package/dist/scan.d.ts +26 -0
- package/dist/scan.js +92 -0
- package/dist/scan.js.map +1 -1
- package/dist/update-check.d.ts +1 -0
- package/dist/update-check.js +25 -0
- package/dist/update-check.js.map +1 -0
- package/dist/update.d.ts +128 -0
- package/dist/update.js +398 -0
- package/dist/update.js.map +1 -0
- package/package.json +8 -7
- package/sidecar/dist/src/capture/anchors.js +69 -8
- package/sidecar/dist/src/capture/api.d.ts +27 -2
- package/sidecar/dist/src/capture/check-classify.js +31 -2
- package/sidecar/dist/src/capture/check-fields.d.ts +29 -0
- package/sidecar/dist/src/capture/check-fields.js +58 -6
- package/sidecar/dist/src/capture/check.js +5 -0
- package/sidecar/dist/src/capture/deep-walk.js +4 -1
- package/sidecar/dist/src/capture/index.js +6 -1
- package/sidecar/dist/src/capture/node-builder.d.ts +28 -1
- package/sidecar/dist/src/capture/node-builder.js +89 -3
- package/sidecar/dist/src/capture/self-check.d.ts +6 -1
- package/sidecar/dist/src/capture/self-check.js +65 -11
- package/sidecar/dist/src/capture/unresolved.d.ts +7 -0
- package/sidecar/dist/src/capture/unresolved.js +1 -1
- package/sidecar/dist/src/type-inferrer.d.ts +118 -4
- package/sidecar/dist/src/type-inferrer.js +433 -15
- package/sidecar/dist/src/validators.d.ts +40 -40
- package/sidecar/dist/src/validators.js +6 -1
- package/templates/skills/carrick-census.md +3 -1
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import ts from 'typescript';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
|
-
import { printTypeForDestination, undeclaredNamesIn } from './node-builder.js';
|
|
9
|
+
import { printTypeForDestination, substituteUndeclaredNames, undeclaredNamesIn, } from './node-builder.js';
|
|
10
10
|
import { typeIsOrContainsMachinery } from './machinery.js';
|
|
11
|
-
import { unresolvedAtAnchor } from './unresolved.js';
|
|
11
|
+
import { unresolvedAtAnchor, unresolvedSpecifiersReachableFrom, } from './unresolved.js';
|
|
12
12
|
/** Repo-root-relative source file -> extensionless specifier from entryDir. */
|
|
13
13
|
export function entryRelativeSpecifier(entryDir, repoRoot, sourceFile) {
|
|
14
14
|
const target = path
|
|
@@ -54,12 +54,25 @@ export function resolveAnchor(program, request, args) {
|
|
|
54
54
|
// (a generated model that was never generated). The stub then self-checks
|
|
55
55
|
// such a name as an error placeholder, which no walk flags. A name a
|
|
56
56
|
// sibling symbol anchor imports is resolved by that import.
|
|
57
|
+
// carrick#1377: rewrite what nothing declares to `unknown` in place, so
|
|
58
|
+
// one member typed by a module the checkout does not have stops taking
|
|
59
|
+
// every member around it down with it.
|
|
60
|
+
const rewritten = siblingSpec || !args.placeholder
|
|
61
|
+
? undefined
|
|
62
|
+
: substituteUndeclaredNamesInText(text, program, args.placeholder);
|
|
63
|
+
const aliasBody = rewritten?.text ?? text;
|
|
57
64
|
const undeclaredNames = siblingSpec || !args.placeholder
|
|
58
65
|
? []
|
|
59
|
-
: undeclaredNamesInText(
|
|
66
|
+
: undeclaredNamesInText(aliasBody, program, args.placeholder);
|
|
67
|
+
const unresolved = rewritten?.paths.length
|
|
68
|
+
? {
|
|
69
|
+
paths: rewritten.paths,
|
|
70
|
+
specifiers: unresolvedSpecifiersForLiteral(program, request.source_file, args.repoRoot),
|
|
71
|
+
}
|
|
72
|
+
: undefined;
|
|
60
73
|
return {
|
|
61
74
|
request,
|
|
62
|
-
aliasText: siblingSpec ? `import('${siblingSpec}').${text}` :
|
|
75
|
+
aliasText: siblingSpec ? `import('${siblingSpec}').${text}` : aliasBody,
|
|
63
76
|
// Literal anchors ARE the legacy-text tier (WP3 wiring of the design's
|
|
64
77
|
// structural_fallback): hand-produced type text riding the surface.
|
|
65
78
|
// The self-check still classifies decay; the fidelity metric counts
|
|
@@ -67,6 +80,7 @@ export function resolveAnchor(program, request, args) {
|
|
|
67
80
|
// ratchetable. Demotions are distinguished by failureReason.
|
|
68
81
|
serialization: 'structural_fallback',
|
|
69
82
|
...(undeclaredNames.length > 0 ? { undeclaredNames } : {}),
|
|
83
|
+
...(unresolved ? { unresolved } : {}),
|
|
70
84
|
};
|
|
71
85
|
}
|
|
72
86
|
const sourceAbs = path.join(args.repoRoot, request.source_file);
|
|
@@ -276,7 +290,12 @@ function finishInferAnchor(program, sourceFile, request, located, placeholder, r
|
|
|
276
290
|
if (!printed.text) {
|
|
277
291
|
return demote(printed.failure ?? 'node builder print failed');
|
|
278
292
|
}
|
|
279
|
-
const
|
|
293
|
+
const atAnchor = unresolvedAtAnchor(program, sourceFile, type, located);
|
|
294
|
+
// carrick#1377: a member the print named undeclared and this rewrote to
|
|
295
|
+
// `unknown` is an unresolved position too, whether or not the source type
|
|
296
|
+
// carried the compiler's placeholder at it (a bare name the print reused as
|
|
297
|
+
// written does not). Both lists feed the same labelling.
|
|
298
|
+
const unresolved = mergeUnresolved(atAnchor, printed.substitutedPaths, () => unresolvedSpecifiersReachableFrom(program, sourceFile));
|
|
280
299
|
return {
|
|
281
300
|
request,
|
|
282
301
|
aliasText: printed.text,
|
|
@@ -286,13 +305,55 @@ function finishInferAnchor(program, sourceFile, request, located, placeholder, r
|
|
|
286
305
|
...(unresolved ? { unresolved } : {}),
|
|
287
306
|
};
|
|
288
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* Fold substituted member positions into what the source program could not
|
|
310
|
+
* resolve. `specifiers` is a thunk: the reachable-import walk is only worth
|
|
311
|
+
* running for an anchor that actually substituted something.
|
|
312
|
+
*/
|
|
313
|
+
function mergeUnresolved(atAnchor, substitutedPaths, specifiers) {
|
|
314
|
+
if (!substitutedPaths || substitutedPaths.length === 0)
|
|
315
|
+
return atAnchor;
|
|
316
|
+
const paths = new Set([...(atAnchor?.paths ?? []), ...substitutedPaths]);
|
|
317
|
+
return {
|
|
318
|
+
paths: [...paths],
|
|
319
|
+
specifiers: atAnchor?.specifiers ?? specifiers(),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* The unresolved specifiers a LITERAL anchor's source file reaches, or none
|
|
324
|
+
* when the anchor names no file (its text names a type with nothing behind it
|
|
325
|
+
* and the detail says only that).
|
|
326
|
+
*/
|
|
327
|
+
function unresolvedSpecifiersForLiteral(program, sourceFileRel, repoRoot) {
|
|
328
|
+
if (!sourceFileRel)
|
|
329
|
+
return [];
|
|
330
|
+
const sourceFile = program.getSourceFile(path.join(repoRoot, sourceFileRel));
|
|
331
|
+
return sourceFile ? unresolvedSpecifiersReachableFrom(program, sourceFile) : [];
|
|
332
|
+
}
|
|
289
333
|
/** `undeclaredNamesIn` over type text rather than a built node. */
|
|
290
334
|
function undeclaredNamesInText(text, program, destination) {
|
|
335
|
+
const parsed = parseLiteralAnchor(text);
|
|
336
|
+
return parsed ? undeclaredNamesIn(parsed, program, destination) : [];
|
|
337
|
+
}
|
|
338
|
+
/** `substituteUndeclaredNames` over type text rather than a built node. */
|
|
339
|
+
function substituteUndeclaredNamesInText(text, program, destination) {
|
|
340
|
+
const parsed = parseLiteralAnchor(text);
|
|
341
|
+
if (!parsed)
|
|
342
|
+
return undefined;
|
|
343
|
+
const rewritten = substituteUndeclaredNames(parsed, program, destination);
|
|
344
|
+
if (rewritten.substitutions.length === 0)
|
|
345
|
+
return undefined;
|
|
346
|
+
const printer = ts.createPrinter({ removeComments: true });
|
|
347
|
+
return {
|
|
348
|
+
text: printer.printNode(ts.EmitHint.Unspecified, rewritten.node, parsed.getSourceFile()),
|
|
349
|
+
paths: rewritten.substitutions.map((entry) => entry.path),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/** The type node of `type __LiteralAnchor = <text>;`, or undefined. */
|
|
353
|
+
function parseLiteralAnchor(text) {
|
|
291
354
|
const parsed = ts.createSourceFile('literal-anchor.ts', `type __LiteralAnchor = ${text};`, ts.ScriptTarget.Latest, true);
|
|
292
355
|
const statement = parsed.statements[0];
|
|
293
|
-
|
|
294
|
-
return [];
|
|
295
|
-
return undeclaredNamesIn(statement.type, program, destination);
|
|
356
|
+
return statement && ts.isTypeAliasDeclaration(statement) ? statement.type : undefined;
|
|
296
357
|
}
|
|
297
358
|
/**
|
|
298
359
|
* True when the anchor carries a LINE and nothing else — no payload span, no
|
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
* (Named anchor_origin because `provenance` is taken by the op-level
|
|
14
14
|
* producer-provenance fields in src/eval_output.rs.)
|
|
15
15
|
*/
|
|
16
|
-
export type AnchorOrigin = 'llm-symbol' | 'deterministic-infer' | 'anchor-backfill'
|
|
16
|
+
export type AnchorOrigin = 'llm-symbol' | 'deterministic-infer' | 'anchor-backfill'
|
|
17
|
+
/** No type request reached capture for a manifest alias, so the driver sent
|
|
18
|
+
* an `unknown` placeholder: the surface carries every alias the check will
|
|
19
|
+
* probe, and the check's IsUnknown gate answers for it (cloud#1184). */
|
|
20
|
+
| 'manifest-placeholder';
|
|
17
21
|
/**
|
|
18
22
|
* Serialization tier of a captured alias (design doc, Capture step 5):
|
|
19
23
|
* - emitted: compiler declaration emit of an addressable symbol (best)
|
|
@@ -160,10 +164,13 @@ export type SelfCheckOutcome = 'ok' | 'allowlisted_external' | 'decayed_internal
|
|
|
160
164
|
* - `no_request_body`: the located request read is a validated part the
|
|
161
165
|
* route's validator binds that is not a body (a path parameter, a query), so
|
|
162
166
|
* the route states no request body contract there (carrick#1166).
|
|
167
|
+
* - `projected_value_only`: every read of a call's result takes a member out
|
|
168
|
+
* of it and none reads the value itself, so the site states a part of a
|
|
169
|
+
* payload rather than the payload a caller receives (carrick#1375).
|
|
163
170
|
* - `not_recorded`: the position carries a top type and this layer has no
|
|
164
171
|
* cause for it.
|
|
165
172
|
*/
|
|
166
|
-
export type TypeProvenanceReason = 'declared' | 'unresolved_import' | 'budget_exhausted' | 'no_payload_evidence' | 'machinery_envelope' | 'coerced_input' | 'no_success_payload' | 'no_request_body' | 'not_recorded';
|
|
173
|
+
export type TypeProvenanceReason = 'declared' | 'unresolved_import' | 'budget_exhausted' | 'no_payload_evidence' | 'machinery_envelope' | 'coerced_input' | 'no_success_payload' | 'no_request_body' | 'projected_value_only' | 'not_recorded';
|
|
167
174
|
/**
|
|
168
175
|
* One `any`/`unknown` finding inside a captured or inferred type, with its
|
|
169
176
|
* position and its cause. Sorted by `path` wherever a list is emitted, so the
|
|
@@ -367,6 +374,24 @@ export interface CheckVerdict {
|
|
|
367
374
|
resolved: boolean;
|
|
368
375
|
/** Why `resolved` is false. Absent exactly when `resolved` is true. */
|
|
369
376
|
unresolved_reason?: string;
|
|
377
|
+
/**
|
|
378
|
+
* Statements about THIS comparison that are neither the verdict nor an
|
|
379
|
+
* unresolution (carrick#1341).
|
|
380
|
+
*
|
|
381
|
+
* Two things the check can find are true of a pair it called COMPATIBLE, so
|
|
382
|
+
* neither can ride on `diagnostic`: an optionality gap (the sending side
|
|
383
|
+
* always provides a field the receiving side declares optional — legal,
|
|
384
|
+
* assigns, and still a drift between two sources), and the note that the
|
|
385
|
+
* comparison was made against the JSON wire form rather than the declared
|
|
386
|
+
* one (carrick#1340), which is why a producer `Date` read as a `string` is
|
|
387
|
+
* not reported.
|
|
388
|
+
*
|
|
389
|
+
* Written on EVERY bucket, empty when there is nothing to add. A note never
|
|
390
|
+
* changes `bucket` or `resolved` and is never a substitute for
|
|
391
|
+
* `diagnostic`: a reader that renders notes as a mismatch is reading an
|
|
392
|
+
* observation as a verdict.
|
|
393
|
+
*/
|
|
394
|
+
notes: string[];
|
|
370
395
|
}
|
|
371
396
|
/**
|
|
372
397
|
* A service degraded SERVICE-WIDE: install failure, or a stub-tree diagnostic
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { decisiveAssignmentLine } from './check-probe.js';
|
|
25
25
|
import { scrubDiagnostic, scrubPaths } from './check-scrub.js';
|
|
26
|
-
import { describeFieldReport } from './check-fields.js';
|
|
26
|
+
import { describeFieldReport, fieldReportNotes, } from './check-fields.js';
|
|
27
27
|
const PRIMARY_RE = /^(?<file>(?:[a-zA-Z]:)?[^(]*?)\((?<line>\d+),(?<col>\d+)\): error TS(?<code>\d+): (?<msg>.*)$/;
|
|
28
28
|
/** Parse `tsc --pretty false` output into structured diagnostics. */
|
|
29
29
|
export function parseTscOutput(stdout) {
|
|
@@ -69,7 +69,17 @@ function endpointAliasFor(side, plan) {
|
|
|
69
69
|
export function classifyPair(input) {
|
|
70
70
|
const { plan, probeDiags, poisonReason, scrubCtx } = input;
|
|
71
71
|
const codes = [...new Set(probeDiags.map((d) => d.code))].sort((a, b) => a - b);
|
|
72
|
-
|
|
72
|
+
// `notes` sits on `base` so EVERY bucket carries it, including the
|
|
73
|
+
// compatible one (carrick#1341). The two statements it holds are true of a
|
|
74
|
+
// pair that agreed — an optionality gap assigns, and the wire allowance is
|
|
75
|
+
// exactly what made the pair agree — so a channel only reachable from the
|
|
76
|
+
// mismatch branch would be the same missing channel the ticket is about.
|
|
77
|
+
const base = {
|
|
78
|
+
pair_id: plan.pairId,
|
|
79
|
+
pair_key: plan.spec.pair_key,
|
|
80
|
+
codes,
|
|
81
|
+
notes: pairNotes(input, scrubCtx),
|
|
82
|
+
};
|
|
73
83
|
// Every branch below this point except the last two returns a verdict about
|
|
74
84
|
// a type nobody could read; each states that in one place rather than
|
|
75
85
|
// repeating the reasoning.
|
|
@@ -231,6 +241,25 @@ export function classifyPair(input) {
|
|
|
231
241
|
* name. Scrubbed on the same terms as the tsc text: a printed member type can
|
|
232
242
|
* carry a stub-absolute `import("...")` path.
|
|
233
243
|
*/
|
|
244
|
+
/**
|
|
245
|
+
* The observations this pair states that are not its verdict (carrick#1341):
|
|
246
|
+
* the note that the comparison was made against the serialised form, and each
|
|
247
|
+
* optionality gap the walk found. Empty when the walk did not run or found
|
|
248
|
+
* nothing to add — never a claim that the two sides agree.
|
|
249
|
+
*
|
|
250
|
+
* Scrubbed on the same terms as the diagnostic: a printed member type can
|
|
251
|
+
* carry a stub-absolute `import("...")` path.
|
|
252
|
+
*
|
|
253
|
+
* A note is an OBSERVATION and never moves a bucket. Nothing in this function
|
|
254
|
+
* reaches `bucket`, `resolved` or `unresolved_reason`, and the pair's verdict
|
|
255
|
+
* is decided above before this is read.
|
|
256
|
+
*/
|
|
257
|
+
function pairNotes(input, scrubCtx) {
|
|
258
|
+
const report = input.fieldReport;
|
|
259
|
+
if (!report)
|
|
260
|
+
return [];
|
|
261
|
+
return fieldReportNotes(report, input.plan.direction.sent, input.plan.direction.expected).map((note) => scrubPaths(note, scrubCtx));
|
|
262
|
+
}
|
|
234
263
|
function namedFields(input, scrubCtx) {
|
|
235
264
|
const report = input.fieldReport;
|
|
236
265
|
if (!report)
|
|
@@ -75,6 +75,35 @@ export declare const MAX_NAMED_FIELDS = 8;
|
|
|
75
75
|
* types agree.
|
|
76
76
|
*/
|
|
77
77
|
export declare function pairFieldReports(opened: ProbeProgram | undefined, plans: ProbePlan[]): Map<string, PairFieldReport>;
|
|
78
|
+
/**
|
|
79
|
+
* The one wording for "this comparison was made against the serialised form",
|
|
80
|
+
* shared by the mismatch diagnostic and the `notes` channel so the two can
|
|
81
|
+
* never drift apart (carrick#1341).
|
|
82
|
+
*/
|
|
83
|
+
export declare function wireFormNote(sentSide: Side): string;
|
|
84
|
+
/**
|
|
85
|
+
* The statements this report makes that are NOT a mismatch: what belongs on
|
|
86
|
+
* `CheckVerdict.notes` (carrick#1341).
|
|
87
|
+
*
|
|
88
|
+
* Two of the things the walk can find are true of a pair the judge called
|
|
89
|
+
* COMPATIBLE, and so have no mismatch diagnostic to ride on:
|
|
90
|
+
*
|
|
91
|
+
* - the wire note. Since carrick#1340 a producer `Date` read as a `string` is
|
|
92
|
+
* not a drift, because that is what arrives. A reader comparing the two
|
|
93
|
+
* declared shapes by hand sees `Date` against `string` and concludes the
|
|
94
|
+
* check missed it, so the verdict has to say the comparison was made
|
|
95
|
+
* against the serialised form.
|
|
96
|
+
* - an optionality gap (`optional_in_expected`): the sending side always
|
|
97
|
+
* provides a field the receiving side declares optional. That assigns, so
|
|
98
|
+
* no diagnostic can exist for it, and the two sources still disagree — the
|
|
99
|
+
* receiver carries a branch that never runs.
|
|
100
|
+
*
|
|
101
|
+
* Both are OBSERVATIONS. Nothing here is a verdict, nothing here may move one,
|
|
102
|
+
* and this is never a substitute for a mismatch reason: a caller that finds
|
|
103
|
+
* notes on an incompatible row has one statement made twice, not two
|
|
104
|
+
* statements. Returned in the report's own deterministic order.
|
|
105
|
+
*/
|
|
106
|
+
export declare function fieldReportNotes(report: PairFieldReport, sentSide: Side, expectedSide: Side): string[];
|
|
78
107
|
/**
|
|
79
108
|
* The sentence appended to a mismatch diagnostic. Names the two sides as
|
|
80
109
|
* producer and consumer (never the probe's internal sent/expected), so the
|
|
@@ -75,11 +75,22 @@ export function pairFieldReports(opened, plans) {
|
|
|
75
75
|
if (!declared || !expected)
|
|
76
76
|
continue;
|
|
77
77
|
const wire = declaredConstType(source, checker, 'sentWire');
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
// Whether serialising changes anything observable about the sent type.
|
|
79
|
+
const wireChanges = wire !== undefined &&
|
|
80
|
+
!(isAssignableTo(wire.type, declared.type) && isAssignableTo(declared.type, wire.type));
|
|
81
|
+
// Walk the DECLARED type unless serialising really changed it.
|
|
82
|
+
//
|
|
83
|
+
// The probe declares its wire comparand through a conditional alias that
|
|
84
|
+
// short-circuits back to the declared type whenever that already assigns,
|
|
85
|
+
// and a conditional the checker has not had to resolve carries no members
|
|
86
|
+
// to walk. Reading it unconditionally therefore emptied the report on
|
|
87
|
+
// exactly the pairs that AGREE — the ones whose only statement is an
|
|
88
|
+
// optionality gap or the wire note (carrick#1341). Where serialisation
|
|
89
|
+
// did change the type the wire form is a mapped type with real members,
|
|
90
|
+
// and it stays the thing compared, because that is what the judge judged.
|
|
91
|
+
const compared = wireChanges ? wire.type : declared.type;
|
|
92
|
+
const report = diffReport(compared, expected.type, checker, isAssignableTo, expected.node);
|
|
93
|
+
report.wireApplied = wireChanges;
|
|
83
94
|
results.set(plan.pairId, report);
|
|
84
95
|
}
|
|
85
96
|
return results;
|
|
@@ -221,6 +232,47 @@ function printType(type, ctx) {
|
|
|
221
232
|
const flat = text.replace(/\s+/g, ' ').trim();
|
|
222
233
|
return flat.length > MAX_TYPE_TEXT ? `${flat.slice(0, MAX_TYPE_TEXT - 1)}…` : flat;
|
|
223
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* The one wording for "this comparison was made against the serialised form",
|
|
237
|
+
* shared by the mismatch diagnostic and the `notes` channel so the two can
|
|
238
|
+
* never drift apart (carrick#1341).
|
|
239
|
+
*/
|
|
240
|
+
export function wireFormNote(sentSide) {
|
|
241
|
+
return `The ${sentSide}'s type is compared in the form JSON puts on the wire: a value with a toJSON() method (a Date, for example) travels as what it serialises to.`;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* The statements this report makes that are NOT a mismatch: what belongs on
|
|
245
|
+
* `CheckVerdict.notes` (carrick#1341).
|
|
246
|
+
*
|
|
247
|
+
* Two of the things the walk can find are true of a pair the judge called
|
|
248
|
+
* COMPATIBLE, and so have no mismatch diagnostic to ride on:
|
|
249
|
+
*
|
|
250
|
+
* - the wire note. Since carrick#1340 a producer `Date` read as a `string` is
|
|
251
|
+
* not a drift, because that is what arrives. A reader comparing the two
|
|
252
|
+
* declared shapes by hand sees `Date` against `string` and concludes the
|
|
253
|
+
* check missed it, so the verdict has to say the comparison was made
|
|
254
|
+
* against the serialised form.
|
|
255
|
+
* - an optionality gap (`optional_in_expected`): the sending side always
|
|
256
|
+
* provides a field the receiving side declares optional. That assigns, so
|
|
257
|
+
* no diagnostic can exist for it, and the two sources still disagree — the
|
|
258
|
+
* receiver carries a branch that never runs.
|
|
259
|
+
*
|
|
260
|
+
* Both are OBSERVATIONS. Nothing here is a verdict, nothing here may move one,
|
|
261
|
+
* and this is never a substitute for a mismatch reason: a caller that finds
|
|
262
|
+
* notes on an incompatible row has one statement made twice, not two
|
|
263
|
+
* statements. Returned in the report's own deterministic order.
|
|
264
|
+
*/
|
|
265
|
+
export function fieldReportNotes(report, sentSide, expectedSide) {
|
|
266
|
+
const notes = [];
|
|
267
|
+
if (report.wireApplied)
|
|
268
|
+
notes.push(wireFormNote(sentSide));
|
|
269
|
+
for (const difference of report.differences) {
|
|
270
|
+
if (difference.nature !== 'optional_in_expected')
|
|
271
|
+
continue;
|
|
272
|
+
notes.push(`${describeDifference(difference, sentSide, expectedSide)}.`);
|
|
273
|
+
}
|
|
274
|
+
return notes;
|
|
275
|
+
}
|
|
224
276
|
/**
|
|
225
277
|
* The sentence appended to a mismatch diagnostic. Names the two sides as
|
|
226
278
|
* producer and consumer (never the probe's internal sent/expected), so the
|
|
@@ -229,7 +281,7 @@ function printType(type, ctx) {
|
|
|
229
281
|
export function describeFieldReport(report, sentSide, expectedSide) {
|
|
230
282
|
const parts = [];
|
|
231
283
|
if (report.wireApplied) {
|
|
232
|
-
parts.push(
|
|
284
|
+
parts.push(wireFormNote(sentSide));
|
|
233
285
|
}
|
|
234
286
|
if (report.differences.length > 0) {
|
|
235
287
|
const named = report.differences
|
|
@@ -83,6 +83,9 @@ function unverifiableAll(plans, gate, diagnostic) {
|
|
|
83
83
|
codes: [],
|
|
84
84
|
resolved: false,
|
|
85
85
|
unresolved_reason: diagnostic,
|
|
86
|
+
// Nothing was compared here, so there is nothing to observe about the
|
|
87
|
+
// comparison (carrick#1341). Empty is the honest answer, not a default.
|
|
88
|
+
notes: [],
|
|
86
89
|
}));
|
|
87
90
|
}
|
|
88
91
|
function sortVerdicts(verdicts) {
|
|
@@ -142,6 +145,7 @@ export async function runCheck(opts, onProgress) {
|
|
|
142
145
|
codes: [],
|
|
143
146
|
resolved: false,
|
|
144
147
|
unresolved_reason: 'one side of this pair has no captured type surface',
|
|
148
|
+
notes: [],
|
|
145
149
|
});
|
|
146
150
|
}
|
|
147
151
|
}
|
|
@@ -186,6 +190,7 @@ export async function runCheck(opts, onProgress) {
|
|
|
186
190
|
unresolved_reason: hit.kind === 'budget_exhausted'
|
|
187
191
|
? `the ${hit.side} type is too deep or wide to verify at '${hit.path}'`
|
|
188
192
|
: `the ${hit.side} type carries '${hit.kind}' at '${hit.path}'`,
|
|
193
|
+
notes: [],
|
|
189
194
|
});
|
|
190
195
|
}
|
|
191
196
|
writeProbes(ws, probing);
|
|
@@ -266,7 +266,10 @@ export function provenanceOf(finding, unresolved) {
|
|
|
266
266
|
detail: 'the type is too deep or wide to verify within the capture budget here, so it is reported unverified rather than assumed clean',
|
|
267
267
|
};
|
|
268
268
|
}
|
|
269
|
-
|
|
269
|
+
// `unknown` reads here as well as `any` (carrick#1377): a reference nothing
|
|
270
|
+
// declares is rewritten to `unknown` at its own position, and a reader told
|
|
271
|
+
// the author declared it that way stops looking where the fix is.
|
|
272
|
+
if (unresolved?.paths.includes(finding.path)) {
|
|
270
273
|
return {
|
|
271
274
|
path: finding.path,
|
|
272
275
|
kind: finding.kind,
|
|
@@ -95,7 +95,12 @@ function emptyFidelity() {
|
|
|
95
95
|
total_aliases: 0,
|
|
96
96
|
by_serialization: { emitted: 0, node_builder: 0, structural_fallback: 0 },
|
|
97
97
|
by_self_check: { ok: 0, allowlisted_external: 0, decayed_internal: 0 },
|
|
98
|
-
by_anchor_origin: {
|
|
98
|
+
by_anchor_origin: {
|
|
99
|
+
'llm-symbol': 0,
|
|
100
|
+
'deterministic-infer': 0,
|
|
101
|
+
'anchor-backfill': 0,
|
|
102
|
+
'manifest-placeholder': 0,
|
|
103
|
+
},
|
|
99
104
|
usable_rate: 0,
|
|
100
105
|
};
|
|
101
106
|
}
|
|
@@ -30,9 +30,23 @@ export interface NodeBuilderPrintResult {
|
|
|
30
30
|
failure?: string;
|
|
31
31
|
/**
|
|
32
32
|
* Names the print refers to that do not resolve at the destination in the
|
|
33
|
-
* producer's program (carrick#1165). Present only when there are some
|
|
33
|
+
* producer's program (carrick#1165). Present only when there are some —
|
|
34
|
+
* which, since carrick#1377 rewrites what it can reach, means a reference
|
|
35
|
+
* the substitution could not replace.
|
|
34
36
|
*/
|
|
35
37
|
undeclaredNames?: string[];
|
|
38
|
+
/**
|
|
39
|
+
* Member positions the print named something undeclared at, and where that
|
|
40
|
+
* reference now reads `unknown` (carrick#1377). In the walk's own path
|
|
41
|
+
* notation, so a finding at one of them can be labelled as what it is: a
|
|
42
|
+
* module that did not resolve, not a declared top type.
|
|
43
|
+
*/
|
|
44
|
+
substitutedPaths?: string[];
|
|
45
|
+
}
|
|
46
|
+
/** One reference replaced by `unknown`, with the member position it sat at. */
|
|
47
|
+
export interface UndeclaredSubstitution {
|
|
48
|
+
name: string;
|
|
49
|
+
path: string;
|
|
36
50
|
}
|
|
37
51
|
/**
|
|
38
52
|
* Print `type` as a type node anchored at `destination` (a declaration inside
|
|
@@ -40,6 +54,19 @@ export interface NodeBuilderPrintResult {
|
|
|
40
54
|
* any referenced symbol is not plainly accessible from the destination.
|
|
41
55
|
*/
|
|
42
56
|
export declare function printTypeForDestination(program: ts.Program, type: ts.Type, destination: ts.Node): NodeBuilderPrintResult;
|
|
57
|
+
/**
|
|
58
|
+
* Rewrite every reference in `node` that nothing in the producer's program
|
|
59
|
+
* declares to the `unknown` keyword, and report the member position each sat
|
|
60
|
+
* at (carrick#1377).
|
|
61
|
+
*
|
|
62
|
+
* The positions use the same notation as the capture's deep walk — `sub`,
|
|
63
|
+
* `items<0>.meta`, `[index]`, `()` for a callable return — so a path found
|
|
64
|
+
* here names the same member a self-check finding at that path names.
|
|
65
|
+
*/
|
|
66
|
+
export declare function substituteUndeclaredNames(node: ts.TypeNode, program: ts.Program, destination: ts.Node): {
|
|
67
|
+
node: ts.TypeNode;
|
|
68
|
+
substitutions: UndeclaredSubstitution[];
|
|
69
|
+
};
|
|
43
70
|
/**
|
|
44
71
|
* Bare names in a printed type node that nothing in the producer's program
|
|
45
72
|
* declares (carrick#1165).
|
|
@@ -117,10 +117,96 @@ export function printTypeForDestination(program, type, destination) {
|
|
|
117
117
|
failure: `symbols not accessible from the surface entry: ${[...new Set(inaccessible)].join(', ')}`,
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
|
+
// carrick#1377: a reference nothing declares makes the WHOLE answer
|
|
121
|
+
// unpublishable, so one member typed by a package the checkout does not
|
|
122
|
+
// have used to discard every member around it. Replace what it names with
|
|
123
|
+
// `unknown` in place and print that; the rest of the shape survives, and
|
|
124
|
+
// the positions are reported so the finding at each can be labelled as a
|
|
125
|
+
// module that did not resolve rather than a top type the author declared.
|
|
126
|
+
const substituted = substituteUndeclaredNames(node, program, destination);
|
|
120
127
|
const printer = ts.createPrinter({ removeComments: true });
|
|
121
|
-
const text = printer.printNode(ts.EmitHint.Unspecified, node, destination.getSourceFile());
|
|
122
|
-
|
|
123
|
-
|
|
128
|
+
const text = printer.printNode(ts.EmitHint.Unspecified, substituted.node, destination.getSourceFile());
|
|
129
|
+
// Asked of the REWRITTEN node: the field says what the printed answer
|
|
130
|
+
// names, so anything the substitution could not reach still fills it and
|
|
131
|
+
// still refuses publication.
|
|
132
|
+
const undeclaredNames = undeclaredNamesIn(substituted.node, program, destination);
|
|
133
|
+
const substitutedPaths = substituted.substitutions.map((entry) => entry.path);
|
|
134
|
+
return {
|
|
135
|
+
text,
|
|
136
|
+
inaccessible,
|
|
137
|
+
...(undeclaredNames.length > 0 ? { undeclaredNames } : {}),
|
|
138
|
+
...(substitutedPaths.length > 0 ? { substitutedPaths } : {}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Rewrite every reference in `node` that nothing in the producer's program
|
|
143
|
+
* declares to the `unknown` keyword, and report the member position each sat
|
|
144
|
+
* at (carrick#1377).
|
|
145
|
+
*
|
|
146
|
+
* The positions use the same notation as the capture's deep walk — `sub`,
|
|
147
|
+
* `items<0>.meta`, `[index]`, `()` for a callable return — so a path found
|
|
148
|
+
* here names the same member a self-check finding at that path names.
|
|
149
|
+
*/
|
|
150
|
+
export function substituteUndeclaredNames(node, program, destination) {
|
|
151
|
+
const undeclared = new Set(undeclaredNamesIn(node, program, destination));
|
|
152
|
+
if (undeclared.size === 0) {
|
|
153
|
+
return { node, substitutions: [] };
|
|
154
|
+
}
|
|
155
|
+
const substitutions = [];
|
|
156
|
+
const leftmost = (name) => ts.isIdentifier(name) ? name : leftmost(name.left);
|
|
157
|
+
// The path is carried down the visit rather than reconstructed, because a
|
|
158
|
+
// rewritten node has no parent to walk back up from.
|
|
159
|
+
const rewrite = (current, path) => {
|
|
160
|
+
if ((ts.isTypeReferenceNode(current) && undeclared.has(leftmost(current.typeName).text)) ||
|
|
161
|
+
(ts.isTypeQueryNode(current) && undeclared.has(leftmost(current.exprName).text))) {
|
|
162
|
+
substitutions.push({
|
|
163
|
+
name: ts.isTypeReferenceNode(current)
|
|
164
|
+
? leftmost(current.typeName).text
|
|
165
|
+
: leftmost(current.exprName).text,
|
|
166
|
+
path: path === '' ? '<root>' : path,
|
|
167
|
+
});
|
|
168
|
+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
|
|
169
|
+
}
|
|
170
|
+
return ts.visitEachChild(current, (child) => rewrite(child, childPath(current, child, path)),
|
|
171
|
+
/* context */ undefined);
|
|
172
|
+
};
|
|
173
|
+
const rewritten = rewrite(node, '');
|
|
174
|
+
return { node: rewritten, substitutions };
|
|
175
|
+
}
|
|
176
|
+
/** The deep walk's path for `child` inside `parent`, extending `path`. */
|
|
177
|
+
function childPath(parent, child, path) {
|
|
178
|
+
if (ts.isPropertySignature(parent) && parent.type === child) {
|
|
179
|
+
const name = ts.isIdentifier(parent.name) || ts.isStringLiteral(parent.name)
|
|
180
|
+
? parent.name.text
|
|
181
|
+
: parent.name.getText?.() ?? '';
|
|
182
|
+
return path === '' ? name : `${path}.${name}`;
|
|
183
|
+
}
|
|
184
|
+
if (ts.isArrayTypeNode(parent) && parent.elementType === child) {
|
|
185
|
+
return `${path}<0>`;
|
|
186
|
+
}
|
|
187
|
+
if (ts.isIndexSignatureDeclaration(parent) && parent.type === child) {
|
|
188
|
+
return `${path}[index]`;
|
|
189
|
+
}
|
|
190
|
+
if ((ts.isFunctionTypeNode(parent) ||
|
|
191
|
+
ts.isMethodSignature(parent) ||
|
|
192
|
+
ts.isCallSignatureDeclaration(parent)) &&
|
|
193
|
+
parent.type === child) {
|
|
194
|
+
return `${path}()`;
|
|
195
|
+
}
|
|
196
|
+
if (ts.isTypeReferenceNode(parent) && parent.typeArguments) {
|
|
197
|
+
const index = parent.typeArguments.indexOf(child);
|
|
198
|
+
if (index >= 0)
|
|
199
|
+
return `${path}<${index}>`;
|
|
200
|
+
}
|
|
201
|
+
if (ts.isTupleTypeNode(parent)) {
|
|
202
|
+
const index = parent.elements.indexOf(child);
|
|
203
|
+
if (index >= 0)
|
|
204
|
+
return `${path}<${index}>`;
|
|
205
|
+
}
|
|
206
|
+
// A union or intersection member sits at its parent's position, as the deep
|
|
207
|
+
// walk records it; everything else (a type literal's members, a parenthesis)
|
|
208
|
+
// keeps the path it was reached with.
|
|
209
|
+
return path;
|
|
124
210
|
}
|
|
125
211
|
/**
|
|
126
212
|
* Bare names in a printed type node that nothing in the producer's program
|
|
@@ -22,7 +22,12 @@
|
|
|
22
22
|
* Attribution is per-alias closure: failed specifiers are blamed on an alias
|
|
23
23
|
* only if they occur in a file reachable from that alias's surface statement
|
|
24
24
|
* (import-type seeds, then BFS over relative imports). The spike's
|
|
25
|
-
* file-granularity shortcut is gone
|
|
25
|
+
* file-granularity shortcut is gone -- including on the SURFACE file itself,
|
|
26
|
+
* which holds every alias, so a file-granular bucket there blamed the whole
|
|
27
|
+
* service for one alias's dangling specifier (cloud#1184). A surface
|
|
28
|
+
* diagnostic is attributed by `export type` statement span, exactly as
|
|
29
|
+
* check-poison.ts contains poison; one that no statement covers keeps the
|
|
30
|
+
* service-wide bucket.
|
|
26
31
|
*/
|
|
27
32
|
import ts from 'typescript';
|
|
28
33
|
import type { CaptureAliasRecord } from './api.js';
|