carrick 0.3.115 → 0.3.116
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/bundler.d.ts +17 -1
- package/sidecar/dist/src/bundler.js +1 -1
- package/sidecar/dist/src/capture/anchors.js +8 -1
- package/sidecar/dist/src/capture/api.d.ts +7 -2
- package/sidecar/dist/src/capture/check-classify.d.ts +3 -1
- package/sidecar/dist/src/capture/check-classify.js +47 -3
- package/sidecar/dist/src/capture/check-deep.d.ts +11 -1
- package/sidecar/dist/src/capture/check-deep.js +14 -1
- package/sidecar/dist/src/capture/check-probe.d.ts +29 -1
- package/sidecar/dist/src/capture/check-probe.js +50 -1
- package/sidecar/dist/src/capture/check.js +20 -3
- package/sidecar/dist/src/capture/index.d.ts +1 -1
- package/sidecar/dist/src/capture/index.js +35 -5
- package/sidecar/dist/src/capture/status-table.d.ts +17 -0
- package/sidecar/dist/src/capture/status-table.js +26 -0
- package/sidecar/dist/src/index.js +1 -1
- package/sidecar/dist/src/symbol-relation.d.ts +36 -0
- package/sidecar/dist/src/symbol-relation.js +109 -0
- package/sidecar/dist/src/type-inferrer.d.ts +118 -3
- package/sidecar/dist/src/type-inferrer.js +481 -18
- package/sidecar/dist/src/types.d.ts +38 -0
- package/sidecar/dist/src/unwidened.js +22 -0
- package/sidecar/dist/src/validators.d.ts +58 -12
- package/sidecar/dist/src/validators.js +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carrick",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.116",
|
|
4
4
|
"carrickCache": {
|
|
5
5
|
"cache_version": 22,
|
|
6
6
|
"intent_cache_version": 2
|
|
@@ -113,11 +113,11 @@
|
|
|
113
113
|
"zod": "^3.23.0"
|
|
114
114
|
},
|
|
115
115
|
"optionalDependencies": {
|
|
116
|
-
"@carrick-tools/cli-darwin-arm64": "0.3.
|
|
117
|
-
"@carrick-tools/cli-darwin-x64": "0.3.
|
|
118
|
-
"@carrick-tools/cli-linux-arm64": "0.3.
|
|
119
|
-
"@carrick-tools/cli-linux-x64": "0.3.
|
|
120
|
-
"@carrick-tools/cli-win32-x64": "0.3.
|
|
116
|
+
"@carrick-tools/cli-darwin-arm64": "0.3.116",
|
|
117
|
+
"@carrick-tools/cli-darwin-x64": "0.3.116",
|
|
118
|
+
"@carrick-tools/cli-linux-arm64": "0.3.116",
|
|
119
|
+
"@carrick-tools/cli-linux-x64": "0.3.116",
|
|
120
|
+
"@carrick-tools/cli-win32-x64": "0.3.116"
|
|
121
121
|
},
|
|
122
122
|
"devDependencies": {
|
|
123
123
|
"@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.116",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Carrick",
|
|
7
7
|
"email": "hello@carrick.tools"
|
|
@@ -2,8 +2,23 @@
|
|
|
2
2
|
* TypeBundler - the `bundle` action: each requested symbol's declaration text,
|
|
3
3
|
* read from the init'd ts-morph project.
|
|
4
4
|
*/
|
|
5
|
-
import { Project } from 'ts-morph';
|
|
5
|
+
import { Project, SourceFile } from 'ts-morph';
|
|
6
6
|
import type { SymbolRequest, BundleResult } from './types.js';
|
|
7
|
+
import { type ExpandOrigin } from './type-structural-expander.js';
|
|
8
|
+
/** Where a requested symbol is declared: the file and the name it has there. */
|
|
9
|
+
interface DeclarationSite {
|
|
10
|
+
sourceFile: SourceFile;
|
|
11
|
+
name: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Where `name`, as `sourceFile` exports it, is declared (carrick#1605). A name
|
|
15
|
+
* the file declares is its own. Otherwise it is followed the way an import of
|
|
16
|
+
* it is: a model names a type where the consumer imports it from, usually a
|
|
17
|
+
* barrel that only has `export *`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function declarationSite(sourceFile: SourceFile, name: string, label: string, origin: ExpandOrigin): DeclarationSite | {
|
|
20
|
+
reason: string;
|
|
21
|
+
};
|
|
7
22
|
/**
|
|
8
23
|
* Options for TypeBundler construction (legacy)
|
|
9
24
|
*/
|
|
@@ -67,3 +82,4 @@ export declare class TypeBundler {
|
|
|
67
82
|
private log;
|
|
68
83
|
private logError;
|
|
69
84
|
}
|
|
85
|
+
export {};
|
|
@@ -85,7 +85,7 @@ function declaredName(declaration) {
|
|
|
85
85
|
* it is: a model names a type where the consumer imports it from, usually a
|
|
86
86
|
* barrel that only has `export *`.
|
|
87
87
|
*/
|
|
88
|
-
function declarationSite(sourceFile, name, label, origin) {
|
|
88
|
+
export function declarationSite(sourceFile, name, label, origin) {
|
|
89
89
|
if (declaresName(sourceFile, name))
|
|
90
90
|
return { sourceFile, name };
|
|
91
91
|
const declarations = exportedDeclarations(sourceFile, name, new Set());
|
|
@@ -10,7 +10,7 @@ import { printTypeForDestination, substituteUndeclaredNames, undeclaredNamesIn,
|
|
|
10
10
|
import { typeIsOrContainsMachinery } from './machinery.js';
|
|
11
11
|
import { installedPackageSpecifier } from './installed-package.js';
|
|
12
12
|
import { realPath } from './service-config.js';
|
|
13
|
-
import { statusTableBody } from './status-table.js';
|
|
13
|
+
import { statesNoShape, statusTableBody } from './status-table.js';
|
|
14
14
|
import { unresolvedAtAnchor, unresolvedSpecifiersReachableFrom, } from './unresolved.js';
|
|
15
15
|
/**
|
|
16
16
|
* Repo-root-relative source file -> the specifier the surface entry imports
|
|
@@ -320,6 +320,13 @@ function finishInferAnchor(program, sourceFile, request, located, placeholder, r
|
|
|
320
320
|
type = recovered;
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
|
+
// carrick#2116: a narrowed `unknown` (the checker's own empty object type)
|
|
324
|
+
// and `object` state no shape, at the root or as a member of the root
|
|
325
|
+
// union. Printed they read `{}` and `object`, and would be judged as a
|
|
326
|
+
// shape that lacks every field. They are read as the `unknown` they are.
|
|
327
|
+
if (statesNoShape(checker, type)) {
|
|
328
|
+
type = checker.getUnknownType();
|
|
329
|
+
}
|
|
323
330
|
// carrick#766: a LINE-ONLY anchor whose type came back a bare top type has
|
|
324
331
|
// no evidence on either side, so it must abstain.
|
|
325
332
|
//
|
|
@@ -420,7 +420,12 @@ export interface CheckPairSpec {
|
|
|
420
420
|
consumer: CheckPairEndpoint;
|
|
421
421
|
}
|
|
422
422
|
/**
|
|
423
|
-
*
|
|
423
|
+
* Classifier output: the four buckets of pinned decision 7, and one more
|
|
424
|
+
* (carrick#1538).
|
|
425
|
+
* - producer_wider: the published sent type mismatches and the handler's
|
|
426
|
+
* return, read with no literal widened, does not. What travels agrees, so
|
|
427
|
+
* it is stored as compatible with the class beside it, and it carries no
|
|
428
|
+
* diagnostic.
|
|
424
429
|
* - compatible: no diagnostics; the value-level assignment holds.
|
|
425
430
|
* - incompatible: an assignment-class diagnostic (TS2322/2741/...) — real
|
|
426
431
|
* compiler text is the report.
|
|
@@ -429,7 +434,7 @@ export interface CheckPairSpec {
|
|
|
429
434
|
* - gate_caught_baked_any: a side resolved to `any` (the IsAny probe gate
|
|
430
435
|
* fired) — the backstop that stops a baked-any reading as compatible.
|
|
431
436
|
*/
|
|
432
|
-
export type VerdictBucket = 'compatible' | 'incompatible' | 'unverifiable' | 'gate_caught_baked_any';
|
|
437
|
+
export type VerdictBucket = 'compatible' | 'incompatible' | 'unverifiable' | 'gate_caught_baked_any' | 'producer_wider';
|
|
433
438
|
export interface CheckVerdict {
|
|
434
439
|
/** Deterministic FNV-1a hash of the pair (never a temp path). */
|
|
435
440
|
pair_id: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Diagnostic parsing +
|
|
2
|
+
* Diagnostic parsing + bucket classification for the v2 check phase.
|
|
3
3
|
*
|
|
4
4
|
* The judge is the vendored `tsc` CLI with `--pretty false`, run from the
|
|
5
5
|
* workspace root so file locations print workspace-relative (no temp path in
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* raw-text read marked on a side -> unverifiable (text, even agreeing)
|
|
17
17
|
* assignment error, sent union taken in part by a weak expected type
|
|
18
18
|
* -> unverifiable (carrick#1995)
|
|
19
|
+
* assignment error on the published type that the sent side's unwidened
|
|
20
|
+
* reading does not repeat -> producer_wider (carrick#1538)
|
|
19
21
|
* assignment-class error -> incompatible
|
|
20
22
|
* no diagnostics -> compatible [lowest precedence]
|
|
21
23
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Diagnostic parsing +
|
|
2
|
+
* Diagnostic parsing + bucket classification for the v2 check phase.
|
|
3
3
|
*
|
|
4
4
|
* The judge is the vendored `tsc` CLI with `--pretty false`, run from the
|
|
5
5
|
* workspace root so file locations print workspace-relative (no temp path in
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* raw-text read marked on a side -> unverifiable (text, even agreeing)
|
|
17
17
|
* assignment error, sent union taken in part by a weak expected type
|
|
18
18
|
* -> unverifiable (carrick#1995)
|
|
19
|
+
* assignment error on the published type that the sent side's unwidened
|
|
20
|
+
* reading does not repeat -> producer_wider (carrick#1538)
|
|
19
21
|
* assignment-class error -> incompatible
|
|
20
22
|
* no diagnostics -> compatible [lowest precedence]
|
|
21
23
|
*
|
|
@@ -25,7 +27,7 @@
|
|
|
25
27
|
*
|
|
26
28
|
* Seam: node builtins + this bundle only.
|
|
27
29
|
*/
|
|
28
|
-
import { decisiveAssignmentLine } from './check-probe.js';
|
|
30
|
+
import { decisiveAssignmentLine, unwidenedAliasOf, } from './check-probe.js';
|
|
29
31
|
import { scrubDiagnostic, scrubPaths } from './check-scrub.js';
|
|
30
32
|
import { describeFieldReport, fieldReportNotes, } from './check-fields.js';
|
|
31
33
|
const PRIMARY_RE = /^(?<file>(?:[a-zA-Z]:)?[^(]*?)\((?<line>\d+),(?<col>\d+)\): error TS(?<code>\d+): (?<msg>.*)$/;
|
|
@@ -72,7 +74,11 @@ function endpointAliasFor(side, plan) {
|
|
|
72
74
|
/** Classify one pair into exactly one bucket, honouring the precedence order. */
|
|
73
75
|
export function classifyPair(input) {
|
|
74
76
|
const { plan, probeDiags, poisonReason, scrubCtx } = input;
|
|
75
|
-
|
|
77
|
+
// The lines of the unwidened reading (carrick#1538) answer one question, in
|
|
78
|
+
// 5b below. Their diagnostics are no part of what the published pair raised.
|
|
79
|
+
const codes = [
|
|
80
|
+
...new Set(probeDiags.filter((d) => !onUnwidenedLines(plan, d)).map((d) => d.code)),
|
|
81
|
+
].sort((a, b) => a - b);
|
|
76
82
|
// `notes` sits on `base` so EVERY bucket carries it, including the
|
|
77
83
|
// compatible one (carrick#1341). The two statements it holds are true of a
|
|
78
84
|
// pair that agreed — an optionality gap assigns, and the wire allowance is
|
|
@@ -240,6 +246,14 @@ export function classifyPair(input) {
|
|
|
240
246
|
...notAFact(`the ${sent} type is a union of bodies and the ${expected} type, every field optional, accepts only some of them`),
|
|
241
247
|
};
|
|
242
248
|
}
|
|
249
|
+
// 5b. Or the mismatch is the published type's alone (carrick#1538): a
|
|
250
|
+
// literal widened where the handler's return was inferred. The handler's
|
|
251
|
+
// return read with no literal widened fits what the other side expects,
|
|
252
|
+
// so what travels agrees and only the published type is wider. Its own
|
|
253
|
+
// bucket, which carries no diagnostic: a reason is read as a mismatch.
|
|
254
|
+
if (assignDiag && unwidenedReadingFits(input)) {
|
|
255
|
+
return { ...base, bucket: 'producer_wider', ...factness(input) };
|
|
256
|
+
}
|
|
243
257
|
if (assignDiag) {
|
|
244
258
|
const text = scrubDiagnostic(assignDiag.message, scrubCtx, plan.sentEndpoint.alias, plan.expectedEndpoint.alias);
|
|
245
259
|
return {
|
|
@@ -297,6 +311,36 @@ export function classifyPair(input) {
|
|
|
297
311
|
// 6. No diagnostics -> compatible.
|
|
298
312
|
return { ...base, bucket: 'compatible', ...factness(input) };
|
|
299
313
|
}
|
|
314
|
+
function onUnwidenedLines(plan, diagnostic) {
|
|
315
|
+
return plan.unwidenedFromLine !== undefined && diagnostic.line >= plan.unwidenedFromLine;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Whether the sent side's unwidened reading clears a mismatch on its published
|
|
319
|
+
* type (carrick#1538). Every condition must hold, and each absence leaves the
|
|
320
|
+
* published verdict as it is:
|
|
321
|
+
*
|
|
322
|
+
* - the probe carries the reading (its stub ships the unwidened alias);
|
|
323
|
+
* - the compiler raised nothing on its lines: not on the import (the export is
|
|
324
|
+
* there), not on its top-type gates, not on its assignment;
|
|
325
|
+
* - its own declarations typecheck (the alias is not poisoned);
|
|
326
|
+
* - the member-level walk ran over it and found no `any`/`unknown`, which
|
|
327
|
+
* would assign to whatever the other side declares;
|
|
328
|
+
* - the same walk found none on the two published sides either. A mismatch
|
|
329
|
+
* that is not a fact stays the mismatch it was: the reading would be
|
|
330
|
+
* clearing it against a member that accepts anything.
|
|
331
|
+
*/
|
|
332
|
+
function unwidenedReadingFits(input) {
|
|
333
|
+
const { plan, probeDiags, poisonReason } = input;
|
|
334
|
+
if (plan.unwidenedFromLine === undefined)
|
|
335
|
+
return false;
|
|
336
|
+
if (probeDiags.some((d) => onUnwidenedLines(plan, d)))
|
|
337
|
+
return false;
|
|
338
|
+
const sent = plan.sentEndpoint;
|
|
339
|
+
if (poisonReason(sent.service_name, unwidenedAliasOf(sent.alias)))
|
|
340
|
+
return false;
|
|
341
|
+
const walked = input.deepFindings?.sentUnwidened;
|
|
342
|
+
return walked !== undefined && walked.length === 0 && factness(input).resolved;
|
|
343
|
+
}
|
|
300
344
|
/**
|
|
301
345
|
* The field-level sentence appended to a mismatch diagnostic
|
|
302
346
|
* (carrick-tools/carrick-cloud#1118), or `''` when the walk found nothing to
|
|
@@ -14,14 +14,24 @@
|
|
|
14
14
|
* It is used for exactly one thing: setting `CheckVerdict.resolved`. No bucket
|
|
15
15
|
* changes because of it, so a scan's verdicts are identical with and without
|
|
16
16
|
* it — what changes is whether a reader is told the verdict is a fact.
|
|
17
|
+
*
|
|
18
|
+
* One more reading rides the same walk (carrick#1538): the sent side's
|
|
19
|
+
* unwidened alias, where a probe carries it. A member-level `any` there would
|
|
20
|
+
* let that reading assign to anything, so the classifier lets it clear a
|
|
21
|
+
* mismatch only when this walk ran over it and found nothing.
|
|
17
22
|
*/
|
|
18
23
|
import ts from 'typescript';
|
|
19
24
|
import type { TypeProvenance } from './api.js';
|
|
20
|
-
import type
|
|
25
|
+
import { type ProbePlan } from './check-probe.js';
|
|
21
26
|
/** Deep findings for one pair, per probe side. */
|
|
22
27
|
export interface PairDeepFindings {
|
|
23
28
|
sent: TypeProvenance[];
|
|
24
29
|
expected: TypeProvenance[];
|
|
30
|
+
/**
|
|
31
|
+
* Findings on the sent side's unwidened reading (carrick#1538). Absent when
|
|
32
|
+
* the probe carries none, or when its export could not be resolved.
|
|
33
|
+
*/
|
|
34
|
+
sentUnwidened?: TypeProvenance[];
|
|
25
35
|
}
|
|
26
36
|
/** The one compiler program over the assembled probes, shared by every
|
|
27
37
|
* post-judge walk (fact-ness, and the field-level report the mismatch text
|
|
@@ -14,12 +14,18 @@
|
|
|
14
14
|
* It is used for exactly one thing: setting `CheckVerdict.resolved`. No bucket
|
|
15
15
|
* changes because of it, so a scan's verdicts are identical with and without
|
|
16
16
|
* it — what changes is whether a reader is told the verdict is a fact.
|
|
17
|
+
*
|
|
18
|
+
* One more reading rides the same walk (carrick#1538): the sent side's
|
|
19
|
+
* unwidened alias, where a probe carries it. A member-level `any` there would
|
|
20
|
+
* let that reading assign to anything, so the classifier lets it clear a
|
|
21
|
+
* mismatch only when this walk ran over it and found nothing.
|
|
17
22
|
*/
|
|
18
23
|
import ts from 'typescript';
|
|
19
24
|
import * as fs from 'node:fs';
|
|
20
25
|
import * as path from 'node:path';
|
|
21
26
|
import { sidecarCompilerOptions } from './compiler-options.js';
|
|
22
27
|
import { findDisqualifyingTopTypes, provenanceOf } from './deep-walk.js';
|
|
28
|
+
import { UNWIDENED_LOCAL_NAME } from './check-probe.js';
|
|
23
29
|
export function openProbeProgram(probesDir, plans) {
|
|
24
30
|
if (plans.length === 0)
|
|
25
31
|
return undefined;
|
|
@@ -63,7 +69,14 @@ export function probeDeepFindings(opened, plans) {
|
|
|
63
69
|
const expected = walkImportedAlias(file, 'Expected', program, checker);
|
|
64
70
|
if (sent === undefined || expected === undefined)
|
|
65
71
|
continue;
|
|
66
|
-
|
|
72
|
+
const sentUnwidened = plan.unwidenedFromLine === undefined
|
|
73
|
+
? undefined
|
|
74
|
+
: walkImportedAlias(file, UNWIDENED_LOCAL_NAME, program, checker);
|
|
75
|
+
results.set(plan.pairId, {
|
|
76
|
+
sent,
|
|
77
|
+
expected,
|
|
78
|
+
...(sentUnwidened === undefined ? {} : { sentUnwidened }),
|
|
79
|
+
});
|
|
67
80
|
}
|
|
68
81
|
return results;
|
|
69
82
|
}
|
|
@@ -84,7 +84,28 @@ export interface ProbePlan {
|
|
|
84
84
|
* the form that actually travels.
|
|
85
85
|
*/
|
|
86
86
|
wireAssignmentLine?: number;
|
|
87
|
+
/**
|
|
88
|
+
* 1-based line where the second reading of the sent side starts
|
|
89
|
+
* (carrick#1538): the import of the producer's UNWIDENED alias, its
|
|
90
|
+
* top-type gates and one more assignment, all after every other line, so no
|
|
91
|
+
* line above it moves. Present only when the caller named that alias.
|
|
92
|
+
*
|
|
93
|
+
* Nothing from this line on is an import line, a gate line or an assignment
|
|
94
|
+
* line of the plan. The classifier reads these lines for one question only:
|
|
95
|
+
* whether a mismatch on the published type holds for the type the handler
|
|
96
|
+
* sends with no literal widened.
|
|
97
|
+
*/
|
|
98
|
+
unwidenedFromLine?: number;
|
|
87
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* The surface export that carries a response alias's unwidened reading
|
|
102
|
+
* (carrick#1516). The scanner names it when it asks the capture for one
|
|
103
|
+
* (`unwidened_alias` in `src/engine/type_compat_v2.rs`); this is the same
|
|
104
|
+
* name, read back.
|
|
105
|
+
*/
|
|
106
|
+
export declare function unwidenedAliasOf(alias: string): string;
|
|
107
|
+
/** The local name the probe imports the unwidened alias under. */
|
|
108
|
+
export declare const UNWIDENED_LOCAL_NAME = "SentUnwidened";
|
|
88
109
|
/** The assignment line whose diagnostic decides the bucket. */
|
|
89
110
|
/**
|
|
90
111
|
* The JSON wire transform as TypeScript declarations: `JsonWire<T>` is the
|
|
@@ -114,5 +135,12 @@ export declare function decisiveAssignmentLine(plan: ProbePlan): number;
|
|
|
114
135
|
* Build one probe, recording the exact line of every gate and the assignment so
|
|
115
136
|
* the classifier never depends on hard-coded offsets. `packageOf` maps a
|
|
116
137
|
* service name to its workspace package specifier (e.g. `@carrick/orders`).
|
|
138
|
+
*
|
|
139
|
+
* `unwidenedAliasFor` answers, for a producer that is the sending side, the
|
|
140
|
+
* surface export that carries its unwidened reading (carrick#1538), when its
|
|
141
|
+
* stub ships one. The probe of an `http` response then ends with a second
|
|
142
|
+
* assignment of that reading, in the wire form. Without an answer the
|
|
143
|
+
* probe is what it always was, byte for byte. A consumer that sends (a request
|
|
144
|
+
* body) is never asked: the reading is of a handler's return.
|
|
117
145
|
*/
|
|
118
|
-
export declare function buildProbe(spec: CheckPairSpec, packageOf: (serviceName: string) => string): ProbePlan;
|
|
146
|
+
export declare function buildProbe(spec: CheckPairSpec, packageOf: (serviceName: string) => string, unwidenedAliasFor?: (sentProducer: CheckPairEndpoint) => string | undefined): ProbePlan;
|
|
@@ -74,6 +74,17 @@ export function pairId(spec) {
|
|
|
74
74
|
].join('|');
|
|
75
75
|
return fnv1a(key);
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* The surface export that carries a response alias's unwidened reading
|
|
79
|
+
* (carrick#1516). The scanner names it when it asks the capture for one
|
|
80
|
+
* (`unwidened_alias` in `src/engine/type_compat_v2.rs`); this is the same
|
|
81
|
+
* name, read back.
|
|
82
|
+
*/
|
|
83
|
+
export function unwidenedAliasOf(alias) {
|
|
84
|
+
return `${alias}_Unwidened`;
|
|
85
|
+
}
|
|
86
|
+
/** The local name the probe imports the unwidened alias under. */
|
|
87
|
+
export const UNWIDENED_LOCAL_NAME = 'SentUnwidened';
|
|
77
88
|
/** The assignment line whose diagnostic decides the bucket. */
|
|
78
89
|
/**
|
|
79
90
|
* The JSON wire transform as TypeScript declarations: `JsonWire<T>` is the
|
|
@@ -114,8 +125,15 @@ export function decisiveAssignmentLine(plan) {
|
|
|
114
125
|
* Build one probe, recording the exact line of every gate and the assignment so
|
|
115
126
|
* the classifier never depends on hard-coded offsets. `packageOf` maps a
|
|
116
127
|
* service name to its workspace package specifier (e.g. `@carrick/orders`).
|
|
128
|
+
*
|
|
129
|
+
* `unwidenedAliasFor` answers, for a producer that is the sending side, the
|
|
130
|
+
* surface export that carries its unwidened reading (carrick#1538), when its
|
|
131
|
+
* stub ships one. The probe of an `http` response then ends with a second
|
|
132
|
+
* assignment of that reading, in the wire form. Without an answer the
|
|
133
|
+
* probe is what it always was, byte for byte. A consumer that sends (a request
|
|
134
|
+
* body) is never asked: the reading is of a handler's return.
|
|
117
135
|
*/
|
|
118
|
-
export function buildProbe(spec, packageOf) {
|
|
136
|
+
export function buildProbe(spec, packageOf, unwidenedAliasFor) {
|
|
119
137
|
const id = pairId(spec);
|
|
120
138
|
const direction = directionFor(spec.protocol, spec.type_kind);
|
|
121
139
|
const sentEndpoint = direction.sent === 'producer' ? spec.producer : spec.consumer;
|
|
@@ -270,6 +288,36 @@ export function buildProbe(spec, packageOf) {
|
|
|
270
288
|
push(`declare const sentWire: WireSent;`);
|
|
271
289
|
wireAssignmentLine = push(`const expectedWire: Expected = sentWire;`);
|
|
272
290
|
}
|
|
291
|
+
// The unwidened reading (carrick#1538). A handler that returns
|
|
292
|
+
// `cond ? 'a' : 'b'` publishes `string`, because a literal widens where it
|
|
293
|
+
// is inferred, and a consumer that declares `'a' | 'b'` then mismatches a
|
|
294
|
+
// response it reads correctly. The capture keeps the reading with no literal
|
|
295
|
+
// widened beside the published alias, and it is assigned here a second time.
|
|
296
|
+
//
|
|
297
|
+
// Everything sits after the lines above, the import included, and none of it
|
|
298
|
+
// is recorded as an import, gate or assignment line: a missing export, a top
|
|
299
|
+
// type or a mismatch here says only that the second reading did not clear
|
|
300
|
+
// the pair, and the verdict on the published type stands.
|
|
301
|
+
//
|
|
302
|
+
// Only an `http` response gets one. It is the one pair a producer sends on
|
|
303
|
+
// whose decisive assignment a second reading can mirror, here in the wire
|
|
304
|
+
// form. A GraphQL pair compares an unwrapped comparand (above), which a
|
|
305
|
+
// plain assignment of the reading would not mirror, and on every other
|
|
306
|
+
// protocol the consumer is the sender (the direction table).
|
|
307
|
+
let unwidenedFromLine;
|
|
308
|
+
const sentUnwidenedAlias = direction.sent === 'producer' && spec.protocol === 'http'
|
|
309
|
+
? unwidenedAliasFor?.(sentEndpoint)
|
|
310
|
+
: undefined;
|
|
311
|
+
if (sentUnwidenedAlias !== undefined) {
|
|
312
|
+
const local = UNWIDENED_LOCAL_NAME;
|
|
313
|
+
unwidenedFromLine = push(`import type { ${sentUnwidenedAlias} as ${local} } from '${sentPkg}';`);
|
|
314
|
+
push(`type _G_unwidened_any = Assert<Not<IsAny<${local}>>>;`);
|
|
315
|
+
push(`type _G_unwidened_unknown = Assert<Not<IsUnknown<${local}>>>;`);
|
|
316
|
+
push(`type _G_unwidened_never = Assert<Not<IsNever<${local}>>>;`);
|
|
317
|
+
push(`type WireSentUnwidened = [${local}] extends [Expected] ? ${local} : (JsonWireSame<JsonWire<${local}>, ${local}> extends true ? ${local} : JsonWire<${local}>);`);
|
|
318
|
+
push(`declare const sentWireUnwidened: WireSentUnwidened;`);
|
|
319
|
+
push(`const expectedWireUnwidened: Expected = sentWireUnwidened;`);
|
|
320
|
+
}
|
|
273
321
|
return {
|
|
274
322
|
pairId: id,
|
|
275
323
|
spec,
|
|
@@ -282,5 +330,6 @@ export function buildProbe(spec, packageOf) {
|
|
|
282
330
|
gateLines,
|
|
283
331
|
assignmentLine,
|
|
284
332
|
wireAssignmentLine,
|
|
333
|
+
...(unwidenedFromLine === undefined ? {} : { unwidenedFromLine }),
|
|
285
334
|
};
|
|
286
335
|
}
|
|
@@ -19,7 +19,7 @@ import * as fs from 'node:fs';
|
|
|
19
19
|
import * as path from 'node:path';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
import ts from 'typescript';
|
|
22
|
-
import { buildProbe } from './check-probe.js';
|
|
22
|
+
import { buildProbe, unwidenedAliasOf } from './check-probe.js';
|
|
23
23
|
import { classifyPair, parseTscOutput, } from './check-classify.js';
|
|
24
24
|
import { scrubPaths } from './check-scrub.js';
|
|
25
25
|
import { assembleWorkspace, writeProbes, } from './check-workspace.js';
|
|
@@ -190,11 +190,12 @@ export async function runCheck(opts, onProgress) {
|
|
|
190
190
|
packageLabelOf: ws.packageLabelOf,
|
|
191
191
|
};
|
|
192
192
|
// Generate probes. A pair naming a service with no stub is unverifiable.
|
|
193
|
+
const aliasRecords = readStubAliasRecords(opts.stubs);
|
|
193
194
|
const plans = [];
|
|
194
195
|
const unresolved = [];
|
|
195
196
|
for (const spec of opts.pairs) {
|
|
196
197
|
try {
|
|
197
|
-
plans.push(buildProbe(spec, ws.packageOf));
|
|
198
|
+
plans.push(buildProbe(spec, ws.packageOf, unwidenedAliasIn(aliasRecords)));
|
|
198
199
|
}
|
|
199
200
|
catch {
|
|
200
201
|
unresolved.push({
|
|
@@ -215,7 +216,6 @@ export async function runCheck(opts, onProgress) {
|
|
|
215
216
|
const spec = opts.pairs.find((p) => p.pair_key === v.pair_key);
|
|
216
217
|
v.pair_id = fnvOfSpec(spec);
|
|
217
218
|
}
|
|
218
|
-
const aliasRecords = readStubAliasRecords(opts.stubs);
|
|
219
219
|
const { preGated, probing } = preGate(plans, aliasRecords);
|
|
220
220
|
writeProbes(ws, probing);
|
|
221
221
|
const errors = [];
|
|
@@ -386,6 +386,23 @@ function readStubAliasRecords(stubs) {
|
|
|
386
386
|
}
|
|
387
387
|
return byService;
|
|
388
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* The unwidened reading a producer's stub ships beside one of its aliases
|
|
391
|
+
* (carrick#1538), as `buildProbe` asks for it. Read from the same manifest the
|
|
392
|
+
* pre-gate reads. A reading the capture already found a member-level
|
|
393
|
+
* `any`/`unknown` in is not offered: it would assign to whatever the other
|
|
394
|
+
* side declares.
|
|
395
|
+
*/
|
|
396
|
+
function unwidenedAliasIn(aliasRecords) {
|
|
397
|
+
return (sentProducer) => {
|
|
398
|
+
const record = aliasRecords
|
|
399
|
+
.get(sentProducer.service_name)
|
|
400
|
+
?.get(unwidenedAliasOf(sentProducer.alias));
|
|
401
|
+
if (!record || record.any_provenance?.length)
|
|
402
|
+
return undefined;
|
|
403
|
+
return record.alias;
|
|
404
|
+
};
|
|
405
|
+
}
|
|
389
406
|
/**
|
|
390
407
|
* Capture-time deep-decay pre-gate (adversarial-review finding 1): a
|
|
391
408
|
* member-level `any`/`unknown` recorded by the capture self-check with no
|
|
@@ -60,4 +60,4 @@ export declare function surfaceEntryFileName(): string;
|
|
|
60
60
|
/** Same normalization intent as bundle_file_stems on the Rust side. */
|
|
61
61
|
export declare function sanitizeServiceName(name: string): string;
|
|
62
62
|
export declare function captureStub(opts: CaptureStubOptions): CaptureStubResult;
|
|
63
|
-
export { classifyStatusCodes, constituents, httpStatus, statesNoBody, statusKeyCode, statusTableBody, statusTableOf, successEntries, } from './status-table.js';
|
|
63
|
+
export { classifyStatusCodes, constituents, httpStatus, statesNoBody, statesNoShape, statusKeyCode, statusTableBody, statusTableOf, successEntries, } from './status-table.js';
|
|
@@ -266,7 +266,11 @@ export function captureStub(opts) {
|
|
|
266
266
|
// ---- Phase A: analysis program over placeholder entry + anchor sources ----
|
|
267
267
|
let resolved;
|
|
268
268
|
const progress = opts.onProgress ?? (() => { });
|
|
269
|
-
|
|
269
|
+
// The files of a project that declare a module or global augmentation are
|
|
270
|
+
// roots of both programs below: the one its anchors are read in and the one
|
|
271
|
+
// that emits the surface (carrick#2317). Read once per project.
|
|
272
|
+
const augmentations = augmentationReader(bound);
|
|
273
|
+
const analysisCtx = { repoRoot, entryDir: path.dirname(entryPath), entryPath, guard, progress, bound, augmentations };
|
|
270
274
|
try {
|
|
271
275
|
resolved = ownerGroups && emitProject
|
|
272
276
|
? resolveAnchorsByOwner(opts, ownerGroups, emitProject, analysisCtx, errors)
|
|
@@ -275,8 +279,8 @@ export function captureStub(opts) {
|
|
|
275
279
|
catch (err) {
|
|
276
280
|
return fail(stubDir, packageName, [err instanceof Error ? err.message : String(err)]);
|
|
277
281
|
}
|
|
278
|
-
// ---- Augmentation
|
|
279
|
-
const augmentationSources = [...new Set([...
|
|
282
|
+
// ---- Augmentation files of the project that emits the surface ----
|
|
283
|
+
const augmentationSources = [...new Set([...augmentations(parsed), ...(deno?.globals ?? [])])];
|
|
280
284
|
// ---- Phase B: declaration emit of the final entry ----
|
|
281
285
|
const entryLines = ['// Generated by Carrick capture v2. Deleted after emit.'];
|
|
282
286
|
for (const anchor of resolved) {
|
|
@@ -706,6 +710,27 @@ function programHost(options, deno, bound) {
|
|
|
706
710
|
return deno?.host(options);
|
|
707
711
|
return boundedCompilerHost(deno ? deno.host(options) : ts.createCompilerHost(options), bound);
|
|
708
712
|
}
|
|
713
|
+
/**
|
|
714
|
+
* The files a project's config lists that declare a module or global
|
|
715
|
+
* augmentation, for one capture: each project's list is read from disk once,
|
|
716
|
+
* however many programs are built over it.
|
|
717
|
+
*
|
|
718
|
+
* A project's own compile holds these files whether or not anything imports
|
|
719
|
+
* them, so a program that stands in for that compile must hold them too. A
|
|
720
|
+
* file under `node_modules` is the package's business, and a file outside the
|
|
721
|
+
* read bound is not opened, not even to look for the declaration.
|
|
722
|
+
*/
|
|
723
|
+
function augmentationReader(bound) {
|
|
724
|
+
const byProject = new Map();
|
|
725
|
+
return (parsed) => {
|
|
726
|
+
let files = byProject.get(parsed);
|
|
727
|
+
if (!files) {
|
|
728
|
+
files = findAugmentationFiles(parsed.fileNames.filter((f) => !f.includes(`${path.sep}node_modules${path.sep}`) && (!bound || bound.allows(f))));
|
|
729
|
+
byProject.set(parsed, files);
|
|
730
|
+
}
|
|
731
|
+
return files;
|
|
732
|
+
};
|
|
733
|
+
}
|
|
709
734
|
/** Phase A: build the placeholder entry, then resolve every anchor. */
|
|
710
735
|
function resolveAnchors(opts, parsed, ctx, deno) {
|
|
711
736
|
const placeholderLines = ['// Carrick capture v2 analysis placeholder.'];
|
|
@@ -723,7 +748,12 @@ function resolveAnchors(opts, parsed, ctx, deno) {
|
|
|
723
748
|
...parsed.options,
|
|
724
749
|
noEmit: true,
|
|
725
750
|
});
|
|
726
|
-
|
|
751
|
+
// The project's augmentation files are roots: its own compile reads a
|
|
752
|
+
// member one of them declares wherever the member is read, imported or not.
|
|
753
|
+
const rootNames = [
|
|
754
|
+
...new Set([ctx.entryPath, ...anchorSources, ...ctx.augmentations(parsed), ...(deno?.globals ?? [])]),
|
|
755
|
+
];
|
|
756
|
+
const program = ts.createProgram(rootNames, options, programHost(options, deno, ctx.bound));
|
|
727
757
|
// The checker binds every file of the program when it is first asked for,
|
|
728
758
|
// which the first anchor would otherwise do: asked for here, so that the
|
|
729
759
|
// report below says the program is whole before any anchor is read.
|
|
@@ -780,4 +810,4 @@ function resolveAnchors(opts, parsed, ctx, deno) {
|
|
|
780
810
|
}
|
|
781
811
|
// carrick#1841: the response-table reading the inferrer and the v1 bundle
|
|
782
812
|
// share with the capture's symbol anchor.
|
|
783
|
-
export { classifyStatusCodes, constituents, httpStatus, statesNoBody, statusKeyCode, statusTableBody, statusTableOf, successEntries, } from './status-table.js';
|
|
813
|
+
export { classifyStatusCodes, constituents, httpStatus, statesNoBody, statesNoShape, statusKeyCode, statusTableBody, statusTableOf, successEntries, } from './status-table.js';
|
|
@@ -44,6 +44,23 @@ export declare function successEntries(entries: StatusTableEntry[]): StatusTable
|
|
|
44
44
|
* made only of them. A `204: void` row is such a body.
|
|
45
45
|
*/
|
|
46
46
|
export declare function statesNoBody(type: ts.Type): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Whether a payload type states no shape (carrick#2116): at its root, or as
|
|
49
|
+
* a member of its root union, it is
|
|
50
|
+
*
|
|
51
|
+
* - the checker's own empty object type, which is what narrowing `unknown`
|
|
52
|
+
* yields (`if (cached != null) return cached`). It has no property, no
|
|
53
|
+
* signature, no index signature and, unlike every `{}` the source writes,
|
|
54
|
+
* no symbol: an empty object literal carries `__object`, a declared `{}`
|
|
55
|
+
* carries `__type`, an empty interface carries its own; or
|
|
56
|
+
* - the `object` keyword type.
|
|
57
|
+
*
|
|
58
|
+
* Printed, the first reads `{}` like an empty object a handler really sends,
|
|
59
|
+
* so only the source program can tell them apart. A reader answers `unknown`
|
|
60
|
+
* for such a type, as it does for one that is `unknown` at its root. A
|
|
61
|
+
* member's own type is not looked at: `meta: {}` stays a member.
|
|
62
|
+
*/
|
|
63
|
+
export declare function statesNoShape(checker: ts.TypeChecker, type: ts.Type): boolean;
|
|
47
64
|
/**
|
|
48
65
|
* What a response table's success rows carry, read for a consumer:
|
|
49
66
|
* - `body`: the 2xx rows' value types, at least one of which states a body;
|
|
@@ -81,6 +81,32 @@ export function statesNoBody(type) {
|
|
|
81
81
|
const members = type.isUnion() ? type.types : [type];
|
|
82
82
|
return members.every((member) => (member.flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Never)) !== 0);
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Whether a payload type states no shape (carrick#2116): at its root, or as
|
|
86
|
+
* a member of its root union, it is
|
|
87
|
+
*
|
|
88
|
+
* - the checker's own empty object type, which is what narrowing `unknown`
|
|
89
|
+
* yields (`if (cached != null) return cached`). It has no property, no
|
|
90
|
+
* signature, no index signature and, unlike every `{}` the source writes,
|
|
91
|
+
* no symbol: an empty object literal carries `__object`, a declared `{}`
|
|
92
|
+
* carries `__type`, an empty interface carries its own; or
|
|
93
|
+
* - the `object` keyword type.
|
|
94
|
+
*
|
|
95
|
+
* Printed, the first reads `{}` like an empty object a handler really sends,
|
|
96
|
+
* so only the source program can tell them apart. A reader answers `unknown`
|
|
97
|
+
* for such a type, as it does for one that is `unknown` at its root. A
|
|
98
|
+
* member's own type is not looked at: `meta: {}` stays a member.
|
|
99
|
+
*/
|
|
100
|
+
export function statesNoShape(checker, type) {
|
|
101
|
+
const members = type.isUnion() ? type.types : [type];
|
|
102
|
+
return members.some((member) => (member.flags & ts.TypeFlags.NonPrimitive) !== 0 ||
|
|
103
|
+
((member.flags & ts.TypeFlags.Object) !== 0 &&
|
|
104
|
+
member.symbol === undefined &&
|
|
105
|
+
checker.getPropertiesOfType(member).length === 0 &&
|
|
106
|
+
checker.getSignaturesOfType(member, ts.SignatureKind.Call).length === 0 &&
|
|
107
|
+
checker.getSignaturesOfType(member, ts.SignatureKind.Construct).length === 0 &&
|
|
108
|
+
checker.getIndexInfosOfType(member).length === 0));
|
|
109
|
+
}
|
|
84
110
|
/**
|
|
85
111
|
* What a response table's success rows carry, read for a consumer:
|
|
86
112
|
* - `body`: the 2xx rows' value types, at least one of which states a body;
|
|
@@ -322,7 +322,7 @@ function handleInfer(request) {
|
|
|
322
322
|
const results = [...byProject(request.requests, (item) => item.file_path)].map(([key, items]) => projectComponents(key).typeInferrer.infer(items, request.extraction_config, () => {
|
|
323
323
|
done += 1;
|
|
324
324
|
report();
|
|
325
|
-
}));
|
|
325
|
+
}, request.model_symbols));
|
|
326
326
|
const result = results.length === 1
|
|
327
327
|
? results[0]
|
|
328
328
|
: (() => {
|