carrick 0.3.53
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/LICENSE.md +99 -0
- package/README.md +79 -0
- package/bin/carrick.mjs +168 -0
- package/dist/channel.d.ts +16 -0
- package/dist/channel.js +37 -0
- package/dist/channel.js.map +1 -0
- package/dist/cli.d.ts +29 -0
- package/dist/cli.js +60 -0
- package/dist/cli.js.map +1 -0
- package/dist/contract.d.ts +154 -0
- package/dist/contract.js +141 -0
- package/dist/contract.js.map +1 -0
- package/dist/diagnostics.d.ts +54 -0
- package/dist/diagnostics.js +199 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/hook/post-edit.d.ts +13 -0
- package/dist/hook/post-edit.js +76 -0
- package/dist/hook/post-edit.js.map +1 -0
- package/dist/hook/session-start.d.ts +2 -0
- package/dist/hook/session-start.js +44 -0
- package/dist/hook/session-start.js.map +1 -0
- package/dist/init/identity.d.ts +20 -0
- package/dist/init/identity.js +60 -0
- package/dist/init/identity.js.map +1 -0
- package/dist/init/repos.d.ts +19 -0
- package/dist/init/repos.js +46 -0
- package/dist/init/repos.js.map +1 -0
- package/dist/init/run.d.ts +11 -0
- package/dist/init/run.js +228 -0
- package/dist/init/run.js.map +1 -0
- package/dist/init/settings.d.ts +50 -0
- package/dist/init/settings.js +138 -0
- package/dist/init/settings.js.map +1 -0
- package/dist/log.d.ts +2 -0
- package/dist/log.js +35 -0
- package/dist/log.js.map +1 -0
- package/dist/native.d.ts +48 -0
- package/dist/native.js +127 -0
- package/dist/native.js.map +1 -0
- package/dist/render.d.ts +65 -0
- package/dist/render.js +334 -0
- package/dist/render.js.map +1 -0
- package/dist/root.d.ts +30 -0
- package/dist/root.js +82 -0
- package/dist/root.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +255 -0
- package/dist/server.js.map +1 -0
- package/dist/templates.d.ts +18 -0
- package/dist/templates.js +66 -0
- package/dist/templates.js.map +1 -0
- package/package.json +72 -0
- package/plugin/.claude-plugin/plugin.json +6 -0
- package/plugin/.lsp.json +14 -0
- package/plugin/hooks/hooks.json +27 -0
- package/sidecar/dist/src/bundler.d.ts +141 -0
- package/sidecar/dist/src/bundler.js +680 -0
- package/sidecar/dist/src/capture/anchors.d.ts +61 -0
- package/sidecar/dist/src/capture/anchors.js +1132 -0
- package/sidecar/dist/src/capture/api.d.ts +378 -0
- package/sidecar/dist/src/capture/api.js +10 -0
- package/sidecar/dist/src/capture/augmentations.d.ts +20 -0
- package/sidecar/dist/src/capture/augmentations.js +60 -0
- package/sidecar/dist/src/capture/check-classify.d.ts +58 -0
- package/sidecar/dist/src/capture/check-classify.js +189 -0
- package/sidecar/dist/src/capture/check-deep.d.ts +32 -0
- package/sidecar/dist/src/capture/check-deep.js +91 -0
- package/sidecar/dist/src/capture/check-poison.d.ts +40 -0
- package/sidecar/dist/src/capture/check-poison.js +155 -0
- package/sidecar/dist/src/capture/check-probe.d.ts +70 -0
- package/sidecar/dist/src/capture/check-probe.js +147 -0
- package/sidecar/dist/src/capture/check-scrub.d.ts +31 -0
- package/sidecar/dist/src/capture/check-scrub.js +71 -0
- package/sidecar/dist/src/capture/check-workspace.d.ts +52 -0
- package/sidecar/dist/src/capture/check-workspace.js +218 -0
- package/sidecar/dist/src/capture/check.d.ts +39 -0
- package/sidecar/dist/src/capture/check.js +450 -0
- package/sidecar/dist/src/capture/deep-walk.d.ts +62 -0
- package/sidecar/dist/src/capture/deep-walk.js +243 -0
- package/sidecar/dist/src/capture/index.d.ts +36 -0
- package/sidecar/dist/src/capture/index.js +477 -0
- package/sidecar/dist/src/capture/lockfile.d.ts +48 -0
- package/sidecar/dist/src/capture/lockfile.js +490 -0
- package/sidecar/dist/src/capture/machinery.d.ts +59 -0
- package/sidecar/dist/src/capture/machinery.js +160 -0
- package/sidecar/dist/src/capture/node-builder.d.ts +37 -0
- package/sidecar/dist/src/capture/node-builder.js +123 -0
- package/sidecar/dist/src/capture/paths-rewrite.d.ts +34 -0
- package/sidecar/dist/src/capture/paths-rewrite.js +100 -0
- package/sidecar/dist/src/capture/self-check.d.ts +38 -0
- package/sidecar/dist/src/capture/self-check.js +317 -0
- package/sidecar/dist/src/capture/specifiers.d.ts +39 -0
- package/sidecar/dist/src/capture/specifiers.js +56 -0
- package/sidecar/dist/src/definition-resolver.d.ts +57 -0
- package/sidecar/dist/src/definition-resolver.js +153 -0
- package/sidecar/dist/src/index.d.ts +14 -0
- package/sidecar/dist/src/index.js +564 -0
- package/sidecar/dist/src/monorepo-builder.d.ts +129 -0
- package/sidecar/dist/src/monorepo-builder.js +584 -0
- package/sidecar/dist/src/project-loader.d.ts +130 -0
- package/sidecar/dist/src/project-loader.js +399 -0
- package/sidecar/dist/src/type-inferrer.d.ts +940 -0
- package/sidecar/dist/src/type-inferrer.js +3540 -0
- package/sidecar/dist/src/type-structural-expander.d.ts +61 -0
- package/sidecar/dist/src/type-structural-expander.js +283 -0
- package/sidecar/dist/src/type-text-canonicalizer.d.ts +40 -0
- package/sidecar/dist/src/type-text-canonicalizer.js +297 -0
- package/sidecar/dist/src/types.d.ts +669 -0
- package/sidecar/dist/src/types.js +5 -0
- package/sidecar/dist/src/validators.d.ts +2214 -0
- package/sidecar/dist/src/validators.js +336 -0
- package/sidecar/package.json +6 -0
- package/templates/carrick.json +7 -0
- package/templates/carrick.yml +45 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Probe generation for the v2 check phase ("tsc as the judge").
|
|
3
|
+
*
|
|
4
|
+
* One probe file per matched pair. The probe is a value-level assignment in
|
|
5
|
+
* the data-flow direction, guarded by six top/bottom-type gates on BOTH sides
|
|
6
|
+
* (IsAny/IsUnknown/IsNever). Value-level (not `[X] extends [Y]`) because the
|
|
7
|
+
* conditional-type relation diverges around `any`, and because the compiler's
|
|
8
|
+
* elaborated assignment error is the user-facing mismatch report.
|
|
9
|
+
*
|
|
10
|
+
* GraphQL pairs additionally unwrap the producer's resolver-return ENVELOPE
|
|
11
|
+
* before the assignment (see the `graphql` branch in `buildProbe`): a GraphQL
|
|
12
|
+
* producer's captured type is the resolver function's return type with
|
|
13
|
+
* transport layers (Promise / async-iterator) already peeled at inference
|
|
14
|
+
* time, which for wrapper-returning resolvers is a single-payload envelope
|
|
15
|
+
* (`{ data: Order; errors: string[] }`) — not the SDL field payload the
|
|
16
|
+
* consumer's selection set reads. The v1 checker unwrapped this structurally
|
|
17
|
+
* at compare time (ts_check type-checker `unwrapGraphqlPayload`, deleted in
|
|
18
|
+
* WP8); the type-level port here is its v2-native equivalent.
|
|
19
|
+
*
|
|
20
|
+
* Seam: node builtins + `typescript` + this bundle only. No imports needed here.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* The direction table, keyed on (protocol, type_kind) — one place. Structurally
|
|
24
|
+
* fixes the confirmed HTTP request-body inversion (data flows consumer ->
|
|
25
|
+
* producer for request bodies, so the check is consumer <= producer).
|
|
26
|
+
*
|
|
27
|
+
* | protocol | type_kind | sent | expected |
|
|
28
|
+
* | http, graphql | response | producer | consumer |
|
|
29
|
+
* | http | request | consumer | producer |
|
|
30
|
+
* | socket, pubsub | both | consumer | producer |
|
|
31
|
+
*/
|
|
32
|
+
export function directionFor(protocol, typeKind) {
|
|
33
|
+
if (protocol === 'socket' || protocol === 'pubsub') {
|
|
34
|
+
return { sent: 'consumer', expected: 'producer' };
|
|
35
|
+
}
|
|
36
|
+
if (protocol === 'http' && typeKind === 'request') {
|
|
37
|
+
return { sent: 'consumer', expected: 'producer' };
|
|
38
|
+
}
|
|
39
|
+
// http/graphql response, and any other (http, graphql) shape.
|
|
40
|
+
return { sent: 'producer', expected: 'consumer' };
|
|
41
|
+
}
|
|
42
|
+
/** Deterministic FNV-1a (32-bit) over the pair's stable key. Never a path. */
|
|
43
|
+
export function fnv1a(input) {
|
|
44
|
+
let hash = 0x811c9dc5;
|
|
45
|
+
for (let i = 0; i < input.length; i++) {
|
|
46
|
+
hash ^= input.charCodeAt(i);
|
|
47
|
+
// hash *= 16777619, kept in 32-bit unsigned range.
|
|
48
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
49
|
+
}
|
|
50
|
+
return hash.toString(16).padStart(8, '0');
|
|
51
|
+
}
|
|
52
|
+
/** The pair ID is derived from the pair's semantic identity, not the workspace
|
|
53
|
+
* path, so it is byte-stable across runs. */
|
|
54
|
+
export function pairId(spec) {
|
|
55
|
+
const key = [
|
|
56
|
+
spec.protocol,
|
|
57
|
+
spec.type_kind,
|
|
58
|
+
spec.producer.service_name,
|
|
59
|
+
spec.producer.alias,
|
|
60
|
+
spec.consumer.service_name,
|
|
61
|
+
spec.consumer.alias,
|
|
62
|
+
spec.pair_key,
|
|
63
|
+
].join('|');
|
|
64
|
+
return fnv1a(key);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Build one probe, recording the exact line of every gate and the assignment so
|
|
68
|
+
* the classifier never depends on hard-coded offsets. `packageOf` maps a
|
|
69
|
+
* service name to its workspace package specifier (e.g. `@carrick/orders`).
|
|
70
|
+
*/
|
|
71
|
+
export function buildProbe(spec, packageOf) {
|
|
72
|
+
const id = pairId(spec);
|
|
73
|
+
const direction = directionFor(spec.protocol, spec.type_kind);
|
|
74
|
+
const sentEndpoint = direction.sent === 'producer' ? spec.producer : spec.consumer;
|
|
75
|
+
const expectedEndpoint = direction.expected === 'producer' ? spec.producer : spec.consumer;
|
|
76
|
+
const sentPkg = packageOf(sentEndpoint.service_name);
|
|
77
|
+
const expectedPkg = packageOf(expectedEndpoint.service_name);
|
|
78
|
+
const lines = [];
|
|
79
|
+
const importLines = [];
|
|
80
|
+
const gateLines = new Map();
|
|
81
|
+
const push = (text) => {
|
|
82
|
+
lines.push(text);
|
|
83
|
+
return lines.length; // 1-based line number just written
|
|
84
|
+
};
|
|
85
|
+
importLines.push(push(`import type { ${sentEndpoint.alias} as Sent } from '${sentPkg}';`));
|
|
86
|
+
importLines.push(push(`import type { ${expectedEndpoint.alias} as Expected } from '${expectedPkg}';`));
|
|
87
|
+
push(`type IsAny<T> = 0 extends 1 & T ? true : false;`);
|
|
88
|
+
push(`type IsUnknown<T> = unknown extends T ? (0 extends 1 & T ? false : true) : false;`);
|
|
89
|
+
push(`type IsNever<T> = [T] extends [never] ? true : false;`);
|
|
90
|
+
push(`type Not<T extends boolean> = T extends true ? false : true;`);
|
|
91
|
+
push(`type Assert<T extends true> = T;`);
|
|
92
|
+
gateLines.set(push(`type _G_sent_any = Assert<Not<IsAny<Sent>>>;`), 'sent:any');
|
|
93
|
+
gateLines.set(push(`type _G_sent_unknown = Assert<Not<IsUnknown<Sent>>>;`), 'sent:unknown');
|
|
94
|
+
gateLines.set(push(`type _G_sent_never = Assert<Not<IsNever<Sent>>>;`), 'sent:never');
|
|
95
|
+
gateLines.set(push(`type _G_expected_any = Assert<Not<IsAny<Expected>>>;`), 'expected:any');
|
|
96
|
+
gateLines.set(push(`type _G_expected_unknown = Assert<Not<IsUnknown<Expected>>>;`), 'expected:unknown');
|
|
97
|
+
gateLines.set(push(`type _G_expected_never = Assert<Not<IsNever<Expected>>>;`), 'expected:never');
|
|
98
|
+
push(`declare const sent: Sent;`);
|
|
99
|
+
let assignmentLine;
|
|
100
|
+
if (spec.protocol === 'graphql') {
|
|
101
|
+
// GraphQL selection-set semantics (v1 `unwrapGraphqlPayload` parity): the
|
|
102
|
+
// producer side (always `sent` for graphql — see the direction table) is
|
|
103
|
+
// the resolver's return type. When that return is a single-payload
|
|
104
|
+
// ENVELOPE — exactly one property whose type is payload-shaped (an
|
|
105
|
+
// object, an array of objects, or a union of objects), every sibling a
|
|
106
|
+
// scalar or scalar array (`errors: string[]`) — the consumer's selection
|
|
107
|
+
// reads the payload, not the envelope, so the payload is the comparand.
|
|
108
|
+
//
|
|
109
|
+
// Fail-closed by construction:
|
|
110
|
+
// - `Sent` already assignable to `Expected` short-circuits to `Sent`
|
|
111
|
+
// (plain structural subset selection stays exactly as before);
|
|
112
|
+
// - zero payload-shaped properties (already-bare payloads of scalars)
|
|
113
|
+
// or several (ambiguous envelope, or a bare payload whose own fields
|
|
114
|
+
// include more than one object-shaped property) keep the whole type —
|
|
115
|
+
// the unwrap never fires on anything but an unambiguous envelope;
|
|
116
|
+
// - `GqlObjLike` can never select `any`/`unknown`/`never` (top/bottom
|
|
117
|
+
// types fail both its array and its object arm), and the comparand is
|
|
118
|
+
// re-gated below anyway — the v2 port of v1's comparand re-guard, so
|
|
119
|
+
// a payload decayed to a top type can never read compatible.
|
|
120
|
+
push(`type GqlObjLike<V> = [V] extends [readonly unknown[]] ? ([V] extends [readonly (infer E)[]] ? ([E] extends [readonly unknown[]] ? false : ([E] extends [object] ? true : false)) : false) : ([V] extends [object] ? true : false);`);
|
|
121
|
+
push(`type GqlIsUnion<T, U = T> = [T extends unknown ? ([U] extends [T] ? false : true) : never] extends [false] ? false : true;`);
|
|
122
|
+
push(`type GqlPayloadKeys<T> = { [K in keyof T]-?: [GqlObjLike<T[K]>] extends [true] ? K : never }[keyof T];`);
|
|
123
|
+
push(`type GqlSingleKey<K> = [K] extends [never] ? never : ([GqlIsUnion<K>] extends [false] ? K : never);`);
|
|
124
|
+
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;`);
|
|
125
|
+
push(`type GqlComparand = [Sent] extends [Expected] ? Sent : ([GqlPayloadOf<Sent>] extends [never] ? Sent : GqlPayloadOf<Sent>);`);
|
|
126
|
+
gateLines.set(push(`type _G_comparand_any = Assert<Not<IsAny<GqlComparand>>>;`), 'sent:any');
|
|
127
|
+
gateLines.set(push(`type _G_comparand_unknown = Assert<Not<IsUnknown<GqlComparand>>>;`), 'sent:unknown');
|
|
128
|
+
gateLines.set(push(`type _G_comparand_never = Assert<Not<IsNever<GqlComparand>>>;`), 'sent:never');
|
|
129
|
+
push(`declare const sentComparand: GqlComparand;`);
|
|
130
|
+
assignmentLine = push(`const expected: Expected = sentComparand;`);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
assignmentLine = push(`const expected: Expected = sent;`);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
pairId: id,
|
|
137
|
+
spec,
|
|
138
|
+
fileName: `pair_${id}.ts`,
|
|
139
|
+
direction,
|
|
140
|
+
sentEndpoint,
|
|
141
|
+
expectedEndpoint,
|
|
142
|
+
source: lines.join('\n') + '\n',
|
|
143
|
+
importLines,
|
|
144
|
+
gateLines,
|
|
145
|
+
assignmentLine,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostic scrub for the v2 check phase.
|
|
3
|
+
*
|
|
4
|
+
* Raw tsc diagnostics leak two run-varying / internal things into the most
|
|
5
|
+
* interesting error class (cross-stub conflicts): stub-absolute
|
|
6
|
+
* `import("/tmp/xxxx/packages/orders/types/surface")` paths inside elaboration
|
|
7
|
+
* text, and the probe's `Sent`/`Expected` import aliases in the headline. This
|
|
8
|
+
* pass maps every workspace-absolute path to a stable `@carrick/<service>` (or
|
|
9
|
+
* `pkg@version`) label and rewrites the aliases to the real surface names, so
|
|
10
|
+
* the message is user-facing AND byte-stable across runs with different temp
|
|
11
|
+
* dirs. Real TS text (e.g. TS2741 "... missing in type 'StockLevel'") is
|
|
12
|
+
* preserved intact.
|
|
13
|
+
*
|
|
14
|
+
* Seam: node builtins only; no imports needed.
|
|
15
|
+
*/
|
|
16
|
+
export interface ScrubContext {
|
|
17
|
+
/** Absolute path of the scratch workspace root (fully removed from output). */
|
|
18
|
+
workspaceRoot: string;
|
|
19
|
+
/** Workspace package dir (== sanitized service) -> the @carrick/<dir> label. */
|
|
20
|
+
packageLabelOf: (packageDir: string) => string | undefined;
|
|
21
|
+
}
|
|
22
|
+
/** Replace stub-absolute import paths and any lingering workspace root. */
|
|
23
|
+
export declare function scrubPaths(text: string, ctx: ScrubContext): string;
|
|
24
|
+
/**
|
|
25
|
+
* Rewrite the probe's `Sent`/`Expected` import aliases (which the compiler
|
|
26
|
+
* prints in the headline) to the real surface alias names. Only quoted forms
|
|
27
|
+
* are touched, so real type names in nested elaboration lines are preserved.
|
|
28
|
+
*/
|
|
29
|
+
export declare function rewriteAliases(text: string, sentAlias: string, expectedAlias: string): string;
|
|
30
|
+
/** Full scrub applied to an incompatible pair's diagnostic message. */
|
|
31
|
+
export declare function scrubDiagnostic(text: string, ctx: ScrubContext, sentAlias: string, expectedAlias: string): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostic scrub for the v2 check phase.
|
|
3
|
+
*
|
|
4
|
+
* Raw tsc diagnostics leak two run-varying / internal things into the most
|
|
5
|
+
* interesting error class (cross-stub conflicts): stub-absolute
|
|
6
|
+
* `import("/tmp/xxxx/packages/orders/types/surface")` paths inside elaboration
|
|
7
|
+
* text, and the probe's `Sent`/`Expected` import aliases in the headline. This
|
|
8
|
+
* pass maps every workspace-absolute path to a stable `@carrick/<service>` (or
|
|
9
|
+
* `pkg@version`) label and rewrites the aliases to the real surface names, so
|
|
10
|
+
* the message is user-facing AND byte-stable across runs with different temp
|
|
11
|
+
* dirs. Real TS text (e.g. TS2741 "... missing in type 'StockLevel'") is
|
|
12
|
+
* preserved intact.
|
|
13
|
+
*
|
|
14
|
+
* Seam: node builtins only; no imports needed.
|
|
15
|
+
*/
|
|
16
|
+
const IMPORT_PATH_RE = /import\("([^"]+)"\)/g;
|
|
17
|
+
const PNPM_SEGMENT_RE = /\/\.pnpm\/([^/]+)\/node_modules\//;
|
|
18
|
+
const NODE_MODULES_TAIL_RE = /\/node_modules\/((?:@[^/]+\/)?[^/]+)(?:\/|$)/;
|
|
19
|
+
const PACKAGES_SEGMENT_RE = /\/packages\/([^/]+)\//;
|
|
20
|
+
/** Map one absolute import specifier to a stable, path-free label. */
|
|
21
|
+
function labelForImportPath(absPath, ctx) {
|
|
22
|
+
const normalized = absPath.split('\\').join('/');
|
|
23
|
+
// pnpm isolated store: .../.pnpm/<name>@<ver>/node_modules/<name>/...
|
|
24
|
+
const pnpm = normalized.match(PNPM_SEGMENT_RE);
|
|
25
|
+
if (pnpm)
|
|
26
|
+
return pnpm[1];
|
|
27
|
+
// A stub tree file: .../packages/<dir>/types/surface -> @carrick/<service>
|
|
28
|
+
const pkg = normalized.match(PACKAGES_SEGMENT_RE);
|
|
29
|
+
if (pkg) {
|
|
30
|
+
const label = ctx.packageLabelOf(pkg[1]);
|
|
31
|
+
if (label)
|
|
32
|
+
return label;
|
|
33
|
+
}
|
|
34
|
+
// Any other node_modules path (defensive; isolated linker should not hit).
|
|
35
|
+
const nm = normalized.match(NODE_MODULES_TAIL_RE);
|
|
36
|
+
if (nm)
|
|
37
|
+
return nm[1];
|
|
38
|
+
// Fall back to just stripping the workspace root prefix.
|
|
39
|
+
if (normalized.startsWith(ctx.workspaceRoot.split('\\').join('/'))) {
|
|
40
|
+
return normalized.slice(ctx.workspaceRoot.length).replace(/^\/+/, '');
|
|
41
|
+
}
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
/** Replace stub-absolute import paths and any lingering workspace root. */
|
|
45
|
+
export function scrubPaths(text, ctx) {
|
|
46
|
+
let out = text.replace(IMPORT_PATH_RE, (_m, p1) => {
|
|
47
|
+
return `import("${labelForImportPath(p1, ctx)}")`;
|
|
48
|
+
});
|
|
49
|
+
// Defensive: no raw temp path may survive anywhere in the message.
|
|
50
|
+
if (ctx.workspaceRoot) {
|
|
51
|
+
out = out.split(ctx.workspaceRoot).join('');
|
|
52
|
+
out = out.split(ctx.workspaceRoot.split('/').join('\\')).join('');
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Rewrite the probe's `Sent`/`Expected` import aliases (which the compiler
|
|
58
|
+
* prints in the headline) to the real surface alias names. Only quoted forms
|
|
59
|
+
* are touched, so real type names in nested elaboration lines are preserved.
|
|
60
|
+
*/
|
|
61
|
+
export function rewriteAliases(text, sentAlias, expectedAlias) {
|
|
62
|
+
return text
|
|
63
|
+
.split(`'Sent'`)
|
|
64
|
+
.join(`'${sentAlias}'`)
|
|
65
|
+
.split(`'Expected'`)
|
|
66
|
+
.join(`'${expectedAlias}'`);
|
|
67
|
+
}
|
|
68
|
+
/** Full scrub applied to an incompatible pair's diagnostic message. */
|
|
69
|
+
export function scrubDiagnostic(text, ctx, sentAlias, expectedAlias) {
|
|
70
|
+
return rewriteAliases(scrubPaths(text, ctx), sentAlias, expectedAlias);
|
|
71
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scratch synthetic-monorepo assembly for the v2 check phase.
|
|
3
|
+
*
|
|
4
|
+
* Copies each capture stub package into a temp pnpm workspace
|
|
5
|
+
* (node-linker=isolated, so every stub keeps its OWN node_modules and two
|
|
6
|
+
* versions of one dependency genuinely coexist), adds a `carrick-probes`
|
|
7
|
+
* package that depends on every stub via `workspace:*`, writes the checker
|
|
8
|
+
* tsconfig, and computes semver-dedupe overrides so patch/minor drift on a
|
|
9
|
+
* private-member class does not manufacture a nominal false-incompatible
|
|
10
|
+
* (design Check step 6) while genuinely conflicting majors stay physically
|
|
11
|
+
* duplicated (and thus verdict incompatible, correctly).
|
|
12
|
+
*
|
|
13
|
+
* Seam: node builtins + this bundle only.
|
|
14
|
+
*/
|
|
15
|
+
import type { CheckStubInput } from './api.js';
|
|
16
|
+
import type { ProbePlan } from './check-probe.js';
|
|
17
|
+
export interface AssembledStub {
|
|
18
|
+
serviceName: string;
|
|
19
|
+
/** Workspace package dir under packages/ (== the @carrick/<dir> suffix). */
|
|
20
|
+
packageDir: string;
|
|
21
|
+
/** Full package specifier, e.g. @carrick/orders-engine. */
|
|
22
|
+
packageName: string;
|
|
23
|
+
}
|
|
24
|
+
export interface AssembledWorkspace {
|
|
25
|
+
workspaceDir: string;
|
|
26
|
+
probesDir: string;
|
|
27
|
+
/** Relative (forward-slash) probes dir, e.g. packages/carrick-probes. */
|
|
28
|
+
probesRel: string;
|
|
29
|
+
stubs: AssembledStub[];
|
|
30
|
+
/** service name -> package specifier (for probe imports). */
|
|
31
|
+
packageOf: (serviceName: string) => string;
|
|
32
|
+
/** package dir -> package specifier (for scrub labels). */
|
|
33
|
+
packageLabelOf: (packageDir: string) => string | undefined;
|
|
34
|
+
/** package dir -> service name (for poison attribution). */
|
|
35
|
+
serviceOfPackageDir: (packageDir: string) => string | undefined;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build pnpm exact-selector overrides that collapse semver-compatible drift to
|
|
39
|
+
* the max version in each compat group, leaving conflicting majors untouched.
|
|
40
|
+
* Returns a deterministically key-sorted map, e.g. { "bson@6.8.0": "6.10.1" }.
|
|
41
|
+
*/
|
|
42
|
+
export declare function computeDedupeOverrides(stubs: {
|
|
43
|
+
dependencies: Record<string, string>;
|
|
44
|
+
}[]): Record<string, string>;
|
|
45
|
+
export interface AssembleOptions {
|
|
46
|
+
stubs: CheckStubInput[];
|
|
47
|
+
workspaceRoot?: string;
|
|
48
|
+
}
|
|
49
|
+
/** Create the scratch workspace and copy in the stub packages. */
|
|
50
|
+
export declare function assembleWorkspace(opts: AssembleOptions): AssembledWorkspace;
|
|
51
|
+
/** Write the generated probe files into the assembled probes package. */
|
|
52
|
+
export declare function writeProbes(ws: AssembledWorkspace, plans: ProbePlan[]): void;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scratch synthetic-monorepo assembly for the v2 check phase.
|
|
3
|
+
*
|
|
4
|
+
* Copies each capture stub package into a temp pnpm workspace
|
|
5
|
+
* (node-linker=isolated, so every stub keeps its OWN node_modules and two
|
|
6
|
+
* versions of one dependency genuinely coexist), adds a `carrick-probes`
|
|
7
|
+
* package that depends on every stub via `workspace:*`, writes the checker
|
|
8
|
+
* tsconfig, and computes semver-dedupe overrides so patch/minor drift on a
|
|
9
|
+
* private-member class does not manufacture a nominal false-incompatible
|
|
10
|
+
* (design Check step 6) while genuinely conflicting majors stay physically
|
|
11
|
+
* duplicated (and thus verdict incompatible, correctly).
|
|
12
|
+
*
|
|
13
|
+
* Seam: node builtins + this bundle only.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as os from 'node:os';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
const PROBES_PACKAGE = 'carrick-probes';
|
|
19
|
+
function parseVersion(v) {
|
|
20
|
+
const core = v.replace(/^[^\d]*/, '').split('-')[0].split('+')[0];
|
|
21
|
+
const [maj = '0', min = '0', pat = '0'] = core.split('.');
|
|
22
|
+
return {
|
|
23
|
+
major: Number(maj) || 0,
|
|
24
|
+
minor: Number(min) || 0,
|
|
25
|
+
patch: Number(pat) || 0,
|
|
26
|
+
raw: v,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Semver compat key: same key => dedupe candidates. 0.x is minor-scoped, and
|
|
31
|
+
* 0.0.x is patch-scoped: semver treats every 0.0.x release as its own
|
|
32
|
+
* breaking boundary, so 0.0.3 and 0.0.5 must never collapse onto one
|
|
33
|
+
* physical copy (that would manufacture a false-compatible for by-reference
|
|
34
|
+
* library types).
|
|
35
|
+
*/
|
|
36
|
+
function compatKey(v) {
|
|
37
|
+
if (v.major > 0)
|
|
38
|
+
return `${v.major}`;
|
|
39
|
+
if (v.minor > 0)
|
|
40
|
+
return `0.${v.minor}`;
|
|
41
|
+
return `0.0.${v.patch}`;
|
|
42
|
+
}
|
|
43
|
+
function versionGreater(a, b) {
|
|
44
|
+
if (a.major !== b.major)
|
|
45
|
+
return a.major > b.major;
|
|
46
|
+
if (a.minor !== b.minor)
|
|
47
|
+
return a.minor > b.minor;
|
|
48
|
+
return a.patch > b.patch;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build pnpm exact-selector overrides that collapse semver-compatible drift to
|
|
52
|
+
* the max version in each compat group, leaving conflicting majors untouched.
|
|
53
|
+
* Returns a deterministically key-sorted map, e.g. { "bson@6.8.0": "6.10.1" }.
|
|
54
|
+
*/
|
|
55
|
+
export function computeDedupeOverrides(stubs) {
|
|
56
|
+
const byName = new Map();
|
|
57
|
+
for (const stub of stubs) {
|
|
58
|
+
for (const [name, version] of Object.entries(stub.dependencies ?? {})) {
|
|
59
|
+
if (!byName.has(name))
|
|
60
|
+
byName.set(name, new Set());
|
|
61
|
+
byName.get(name).add(version);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const overrides = {};
|
|
65
|
+
for (const [name, versions] of byName) {
|
|
66
|
+
if (versions.size < 2)
|
|
67
|
+
continue;
|
|
68
|
+
const groups = new Map();
|
|
69
|
+
for (const raw of versions) {
|
|
70
|
+
const parsed = parseVersion(raw);
|
|
71
|
+
const key = compatKey(parsed);
|
|
72
|
+
if (!groups.has(key))
|
|
73
|
+
groups.set(key, []);
|
|
74
|
+
groups.get(key).push(parsed);
|
|
75
|
+
}
|
|
76
|
+
for (const members of groups.values()) {
|
|
77
|
+
if (members.length < 2)
|
|
78
|
+
continue;
|
|
79
|
+
let max = members[0];
|
|
80
|
+
for (const m of members)
|
|
81
|
+
if (versionGreater(m, max))
|
|
82
|
+
max = m;
|
|
83
|
+
for (const m of members) {
|
|
84
|
+
if (m.raw !== max.raw)
|
|
85
|
+
overrides[`${name}@${m.raw}`] = max.raw;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return Object.fromEntries(Object.keys(overrides)
|
|
90
|
+
.sort()
|
|
91
|
+
.map((k) => [k, overrides[k]]));
|
|
92
|
+
}
|
|
93
|
+
function readStubPackageName(stubDir) {
|
|
94
|
+
const pkgPath = path.join(stubDir, 'package.json');
|
|
95
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
96
|
+
if (!pkg.name)
|
|
97
|
+
throw new Error(`stub package.json missing 'name': ${pkgPath}`);
|
|
98
|
+
return pkg.name;
|
|
99
|
+
}
|
|
100
|
+
function readStubDependencies(stubDir) {
|
|
101
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(stubDir, 'package.json'), 'utf8'));
|
|
102
|
+
return pkg.dependencies ?? {};
|
|
103
|
+
}
|
|
104
|
+
/** Package dir under packages/ == the sanitized suffix of @carrick/<suffix>. */
|
|
105
|
+
function packageDirOf(packageName) {
|
|
106
|
+
const slash = packageName.lastIndexOf('/');
|
|
107
|
+
return slash >= 0 ? packageName.slice(slash + 1) : packageName;
|
|
108
|
+
}
|
|
109
|
+
/** Create the scratch workspace and copy in the stub packages. */
|
|
110
|
+
export function assembleWorkspace(opts) {
|
|
111
|
+
const root = opts.workspaceRoot ?? os.tmpdir();
|
|
112
|
+
fs.mkdirSync(root, { recursive: true });
|
|
113
|
+
const workspaceDir = fs.mkdtempSync(path.join(root, 'carrick-check-v2-'));
|
|
114
|
+
const packagesDir = path.join(workspaceDir, 'packages');
|
|
115
|
+
fs.mkdirSync(packagesDir, { recursive: true });
|
|
116
|
+
fs.writeFileSync(path.join(workspaceDir, '.npmrc'), NPMRC);
|
|
117
|
+
fs.writeFileSync(path.join(workspaceDir, 'pnpm-workspace.yaml'), 'packages:\n - "packages/*"\n');
|
|
118
|
+
const assembled = [];
|
|
119
|
+
const dependencySets = [];
|
|
120
|
+
const svcToPkg = new Map();
|
|
121
|
+
const dirToPkg = new Map();
|
|
122
|
+
const dirToSvc = new Map();
|
|
123
|
+
for (const stub of opts.stubs) {
|
|
124
|
+
const packageName = readStubPackageName(stub.stub_dir);
|
|
125
|
+
const packageDir = packageDirOf(packageName);
|
|
126
|
+
const dest = path.join(packagesDir, packageDir);
|
|
127
|
+
fs.cpSync(stub.stub_dir, dest, {
|
|
128
|
+
recursive: true,
|
|
129
|
+
filter: (src) => !src.split(path.sep).includes('node_modules'),
|
|
130
|
+
});
|
|
131
|
+
assembled.push({ serviceName: stub.service_name, packageDir, packageName });
|
|
132
|
+
dependencySets.push({ dependencies: readStubDependencies(stub.stub_dir) });
|
|
133
|
+
svcToPkg.set(stub.service_name, packageName);
|
|
134
|
+
dirToPkg.set(packageDir, packageName);
|
|
135
|
+
dirToSvc.set(packageDir, stub.service_name);
|
|
136
|
+
}
|
|
137
|
+
// Root manifest carries the semver-dedupe overrides.
|
|
138
|
+
const overrides = computeDedupeOverrides(dependencySets);
|
|
139
|
+
fs.writeFileSync(path.join(workspaceDir, 'package.json'), JSON.stringify({
|
|
140
|
+
name: 'carrick-check-workspace',
|
|
141
|
+
version: '0.0.0',
|
|
142
|
+
private: true,
|
|
143
|
+
...(Object.keys(overrides).length > 0 ? { pnpm: { overrides } } : {}),
|
|
144
|
+
}, null, 2) + '\n');
|
|
145
|
+
const probesDir = path.join(packagesDir, PROBES_PACKAGE);
|
|
146
|
+
fs.mkdirSync(path.join(probesDir, 'probes'), { recursive: true });
|
|
147
|
+
const probeDeps = {};
|
|
148
|
+
for (const stub of assembled)
|
|
149
|
+
probeDeps[stub.packageName] = 'workspace:*';
|
|
150
|
+
fs.writeFileSync(path.join(probesDir, 'package.json'), JSON.stringify({
|
|
151
|
+
name: PROBES_PACKAGE,
|
|
152
|
+
version: '0.0.0',
|
|
153
|
+
private: true,
|
|
154
|
+
dependencies: probeDeps,
|
|
155
|
+
}, null, 2) + '\n');
|
|
156
|
+
fs.writeFileSync(path.join(probesDir, 'tsconfig.json'), CHECKER_TSCONFIG);
|
|
157
|
+
return {
|
|
158
|
+
workspaceDir,
|
|
159
|
+
probesDir,
|
|
160
|
+
probesRel: `packages/${PROBES_PACKAGE}`,
|
|
161
|
+
stubs: assembled,
|
|
162
|
+
packageOf: (s) => {
|
|
163
|
+
const pkg = svcToPkg.get(s);
|
|
164
|
+
if (!pkg)
|
|
165
|
+
throw new Error(`no stub for service '${s}'`);
|
|
166
|
+
return pkg;
|
|
167
|
+
},
|
|
168
|
+
packageLabelOf: (dir) => dirToPkg.get(dir),
|
|
169
|
+
serviceOfPackageDir: (dir) => dirToSvc.get(dir),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** Write the generated probe files into the assembled probes package. */
|
|
173
|
+
export function writeProbes(ws, plans) {
|
|
174
|
+
for (const plan of plans) {
|
|
175
|
+
fs.writeFileSync(path.join(ws.probesDir, 'probes', plan.fileName), plan.source);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const NPMRC = [
|
|
179
|
+
// Every stub gets its own node_modules: two versions of one dep coexist and
|
|
180
|
+
// tsc resolves each stub's ref to its own copy (the isolation guarantee).
|
|
181
|
+
'node-linker=isolated',
|
|
182
|
+
// Stubs pin arbitrary library majors; peer conflicts must degrade to
|
|
183
|
+
// unverifiable via the probe gates, never abort the whole install.
|
|
184
|
+
'strict-peer-dependencies=false',
|
|
185
|
+
// Determinism: install exactly the pinned closure, no implicit peer pull-in.
|
|
186
|
+
'auto-install-peers=false',
|
|
187
|
+
// Determinism: the scratch workspace has no committed lockfile, so the
|
|
188
|
+
// transitive closure would otherwise be a pure function of live registry
|
|
189
|
+
// state. prefer-offline resolves from the local store/metadata cache
|
|
190
|
+
// whenever possible (byte-stable across runs on a host), and the explicit
|
|
191
|
+
// resolution-mode pins pnpm's resolver behavior across pnpm versions.
|
|
192
|
+
// Direct deps are exact-pinned by the stubs; only transitives resolve.
|
|
193
|
+
'prefer-offline=true',
|
|
194
|
+
'resolution-mode=highest',
|
|
195
|
+
'',
|
|
196
|
+
].join('\n');
|
|
197
|
+
// skipLibCheck:false is load-bearing (surfaces cross-stub declare-global
|
|
198
|
+
// TS2717 collisions so the poison rule can convert them to honest
|
|
199
|
+
// unverifiables). noUnusedLocals/Parameters:false keep TS6133/6196 off the
|
|
200
|
+
// gate/assignment lines. moduleResolution bundler accepts both node16
|
|
201
|
+
// `.js`-suffixed and extensionless specifiers in one program.
|
|
202
|
+
const CHECKER_TSCONFIG = JSON.stringify({
|
|
203
|
+
compilerOptions: {
|
|
204
|
+
strict: true,
|
|
205
|
+
exactOptionalPropertyTypes: false,
|
|
206
|
+
skipLibCheck: false,
|
|
207
|
+
noUnusedLocals: false,
|
|
208
|
+
noUnusedParameters: false,
|
|
209
|
+
moduleResolution: 'bundler',
|
|
210
|
+
module: 'esnext',
|
|
211
|
+
target: 'es2022',
|
|
212
|
+
lib: ['es2022', 'dom', 'dom.iterable'],
|
|
213
|
+
types: [],
|
|
214
|
+
noEmit: true,
|
|
215
|
+
forceConsistentCasingInFileNames: true,
|
|
216
|
+
},
|
|
217
|
+
include: ['probes/**/*.ts'],
|
|
218
|
+
}, null, 2) + '\n';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 check core: "tsc as the judge"
|
|
3
|
+
* (docs/reference/type-compat-synthetic-monorepo.md, Check phase).
|
|
4
|
+
*
|
|
5
|
+
* Consumes two or more capture stub packages, assembles a scratch synthetic
|
|
6
|
+
* monorepo, installs pinned deps with the vendored pnpm (isolated linker),
|
|
7
|
+
* generates one probe per matched pair, runs the vendored `tsc` CLI, and
|
|
8
|
+
* classifies diagnostics into the four buckets. Deterministic given the stubs
|
|
9
|
+
* and pairs: verdicts are byte-stable across runs (the scrub pass removes the
|
|
10
|
+
* per-run temp path, and the pair IDs / ordering derive from the pair specs).
|
|
11
|
+
*
|
|
12
|
+
* Seam: this file imports only node builtins, `typescript` (for the version
|
|
13
|
+
* string), and the rest of this bundle. The sidecar reaches it only through
|
|
14
|
+
* ./index.js. The judge is the `tsc` CLI (no compiler API), which keeps the
|
|
15
|
+
* check phase TS7-ready by construction.
|
|
16
|
+
*/
|
|
17
|
+
import type { CheckOptions, CheckProgressPhase, CheckResult } from './api.js';
|
|
18
|
+
export type CheckProgress = (phase: CheckProgressPhase, message: string) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Locate a vendored executable the way node locates a package: walk up from
|
|
21
|
+
* the bundle and take the first `node_modules/.bin/<name>` that exists.
|
|
22
|
+
*
|
|
23
|
+
* Where the bins sit depends on how the sidecar arrived. A checkout and the
|
|
24
|
+
* release tarball both keep them in the sidecar's own `node_modules`, so the
|
|
25
|
+
* first candidate hits and nothing changes. An npm install never can: npm
|
|
26
|
+
* strips nested `node_modules` from a published tarball, so the sidecar ships
|
|
27
|
+
* as a plain directory inside the package and its dependencies are installed
|
|
28
|
+
* by the package that carries it — hoisted to the install root, or nested one
|
|
29
|
+
* level up when a version conflict forces it. Assuming one shape is what made
|
|
30
|
+
* an installed `carrick` degrade every pair to unverifiable (carrick#833).
|
|
31
|
+
*
|
|
32
|
+
* `from` is injectable so the layouts can be pinned by test; production always
|
|
33
|
+
* starts at the bundle. Returns the sidecar-local path when nothing exists,
|
|
34
|
+
* which is the caller's "not found" signal and the most useful thing to name.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveVendoredBin(name: string, from?: string): string;
|
|
37
|
+
/** Run the deterministic check phase. Async: install + tsc run off the event
|
|
38
|
+
* loop so the sidecar stays responsive and can emit keepalive frames. */
|
|
39
|
+
export declare function runCheck(opts: CheckOptions, onProgress?: CheckProgress): Promise<CheckResult>;
|