carrick 0.3.82 → 0.3.83
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 +46 -19
- package/bin/carrick.mjs +45 -2
- package/dist/contract.d.ts +17 -0
- package/dist/contract.js.map +1 -1
- package/dist/hook/apply-patch.d.ts +13 -0
- package/dist/hook/apply-patch.js +100 -0
- package/dist/hook/apply-patch.js.map +1 -0
- package/dist/hook/post-edit.d.ts +30 -1
- package/dist/hook/post-edit.js +95 -24
- package/dist/hook/post-edit.js.map +1 -1
- package/dist/hook/reuse.d.ts +97 -0
- package/dist/hook/reuse.js +245 -0
- package/dist/hook/reuse.js.map +1 -0
- package/dist/hook/stop.d.ts +3 -0
- package/dist/hook/stop.js +76 -0
- package/dist/hook/stop.js.map +1 -0
- package/dist/hook/user-prompt.d.ts +9 -0
- package/dist/hook/user-prompt.js +79 -0
- package/dist/hook/user-prompt.js.map +1 -0
- package/dist/init/codex.d.ts +51 -0
- package/dist/init/codex.js +167 -0
- package/dist/init/codex.js.map +1 -0
- package/dist/init/connect.d.ts +13 -0
- package/dist/init/connect.js +21 -15
- package/dist/init/connect.js.map +1 -1
- package/dist/init/doctor.d.ts +36 -0
- package/dist/init/doctor.js +97 -2
- package/dist/init/doctor.js.map +1 -1
- package/dist/init/files.d.ts +15 -0
- package/dist/init/files.js +20 -1
- package/dist/init/files.js.map +1 -1
- package/dist/init/outdated.d.ts +54 -0
- package/dist/init/outdated.js +175 -0
- package/dist/init/outdated.js.map +1 -0
- package/dist/init/output.d.ts +35 -3
- package/dist/init/output.js +115 -23
- package/dist/init/output.js.map +1 -1
- package/dist/init/projects.d.ts +27 -13
- package/dist/init/projects.js +48 -50
- package/dist/init/projects.js.map +1 -1
- package/dist/init/remove.d.ts +0 -2
- package/dist/init/remove.js +61 -15
- package/dist/init/remove.js.map +1 -1
- package/dist/init/repos.d.ts +34 -0
- package/dist/init/repos.js +77 -0
- package/dist/init/repos.js.map +1 -1
- package/dist/init/run.d.ts +73 -6
- package/dist/init/run.js +357 -94
- package/dist/init/run.js.map +1 -1
- package/dist/init/settings.d.ts +20 -0
- package/dist/init/settings.js +42 -4
- package/dist/init/settings.js.map +1 -1
- package/dist/init/task-skills.d.ts +27 -0
- package/dist/init/task-skills.js +64 -1
- package/dist/init/task-skills.js.map +1 -1
- package/dist/init/workspace-file.d.ts +73 -0
- package/dist/init/workspace-file.js +173 -0
- package/dist/init/workspace-file.js.map +1 -0
- package/package.json +6 -6
- package/plugin/hooks/hooks.json +11 -0
- package/sidecar/dist/src/capture/check-classify.d.ts +10 -1
- package/sidecar/dist/src/capture/check-classify.js +66 -8
- package/sidecar/dist/src/capture/check-deep.d.ts +16 -4
- package/sidecar/dist/src/capture/check-deep.js +21 -17
- package/sidecar/dist/src/capture/check-fields.d.ts +83 -0
- package/sidecar/dist/src/capture/check-fields.js +259 -0
- package/sidecar/dist/src/capture/check-probe.d.ts +21 -1
- package/sidecar/dist/src/capture/check-probe.js +39 -0
- package/sidecar/dist/src/capture/check.js +9 -2
- package/templates/skills/carrick-census.md +18 -9
- package/templates/skills/carrick-drift.md +7 -1
- package/templates/skills/carrick-impact.md +3 -1
- package/templates/skills/carrick-reuse.md +43 -26
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// The repo selection, written where every later command reads it.
|
|
2
|
+
//
|
|
3
|
+
// `carrick init` asks which repos in a folder this install covers
|
|
4
|
+
// (carrick#1338). The answer used to last exactly as long as the run: the
|
|
5
|
+
// proposal, the project and the connection were scoped to it, and then
|
|
6
|
+
// `carrick refresh`, `carrick index`, the editor hooks and the next `init` all
|
|
7
|
+
// went back to deriving every repo in the folder. So a repo somebody had said
|
|
8
|
+
// no to was scanned by the command that ran after the one they said it in
|
|
9
|
+
// (carrick#1344).
|
|
10
|
+
//
|
|
11
|
+
// `carrick-workspace.json` is where that answer belongs. The scanner already
|
|
12
|
+
// reads it (`src/local_mode/workspace.rs`): `exclude` names directories to
|
|
13
|
+
// leave out, `Workspace::load` honours it, and the read path behind
|
|
14
|
+
// `carrick check` asks the same question of the same file, so the folder's
|
|
15
|
+
// hooks stop answering for a repo that is not covered. It sits in the folder
|
|
16
|
+
// holding the repos, which is not itself a repository, so nothing tracked is
|
|
17
|
+
// touched.
|
|
18
|
+
//
|
|
19
|
+
// What this file adds is the record of who wrote what. The list is shared: a
|
|
20
|
+
// user can exclude a repo by hand, and `carrick remove` must not take that
|
|
21
|
+
// away with ours. So every name init adds is also recorded under `carrick`,
|
|
22
|
+
// and the remove path subtracts exactly that list and nothing else. The
|
|
23
|
+
// scanner declares the same key (`WrittenByInit`) so a round trip through its
|
|
24
|
+
// struct cannot drop it.
|
|
25
|
+
import fs from "node:fs";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
/** The file, in the folder that holds the repos. */
|
|
28
|
+
export const WORKSPACE_FILE = "carrick-workspace.json";
|
|
29
|
+
function parse(existing) {
|
|
30
|
+
if (existing === null || existing.trim() === "")
|
|
31
|
+
return {};
|
|
32
|
+
const parsed = JSON.parse(existing);
|
|
33
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
34
|
+
throw new Error(`${WORKSPACE_FILE} is not a JSON object`);
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
function names(value) {
|
|
39
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
40
|
+
}
|
|
41
|
+
/** The half of the document init owns, as it is written. */
|
|
42
|
+
function ours(document) {
|
|
43
|
+
const section = document["carrick"];
|
|
44
|
+
if (typeof section !== "object" || section === null)
|
|
45
|
+
return [];
|
|
46
|
+
return names(section["exclude"]);
|
|
47
|
+
}
|
|
48
|
+
function serialize(document) {
|
|
49
|
+
return `${JSON.stringify(document, null, 2)}\n`;
|
|
50
|
+
}
|
|
51
|
+
/** The file as it is, or null when the folder has none. */
|
|
52
|
+
export function readWorkspaceFile(workspace) {
|
|
53
|
+
try {
|
|
54
|
+
return fs.readFileSync(path.join(workspace, WORKSPACE_FILE), "utf8");
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The repos this folder's workspace file leaves out.
|
|
62
|
+
*
|
|
63
|
+
* Read by `carrick init` before it derives anything: the scanner has already
|
|
64
|
+
* dropped an excluded repo by the time the proposal arrives, so this is the
|
|
65
|
+
* only way the command can say which repo a `--repo` flag is asking for and
|
|
66
|
+
* where the answer was written down. An unreadable or unparseable file
|
|
67
|
+
* excludes nothing here; the scanner reports it.
|
|
68
|
+
*/
|
|
69
|
+
export function excludedRepos(workspace) {
|
|
70
|
+
const existing = readWorkspaceFile(workspace);
|
|
71
|
+
if (existing === null)
|
|
72
|
+
return [];
|
|
73
|
+
try {
|
|
74
|
+
return names(parse(existing)["exclude"]);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The document with these repos excluded, and the record of what we added.
|
|
82
|
+
*
|
|
83
|
+
* Idempotent: a name the file already excludes is left where it is and not
|
|
84
|
+
* claimed, whether a user put it there or an earlier run did. Everything else
|
|
85
|
+
* in the file — a `repos` list, a key this version knows nothing about, the
|
|
86
|
+
* order they are in — is kept as it was found.
|
|
87
|
+
*
|
|
88
|
+
* Throws on a file that is not JSON, exactly as the hook writers do: a file
|
|
89
|
+
* somebody hand-edited into something unparseable is reported, never
|
|
90
|
+
* overwritten.
|
|
91
|
+
*/
|
|
92
|
+
export function withExclusions(existing, exclude) {
|
|
93
|
+
const document = parse(existing);
|
|
94
|
+
const current = names(document["exclude"]);
|
|
95
|
+
const added = exclude.filter((name) => !current.includes(name));
|
|
96
|
+
const claimed = [...ours(document), ...added];
|
|
97
|
+
const next = { ...document, exclude: [...current, ...added] };
|
|
98
|
+
if (claimed.length > 0)
|
|
99
|
+
next["carrick"] = { exclude: claimed };
|
|
100
|
+
const body = serialize(next);
|
|
101
|
+
return { body, changed: body !== existing, added };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The inverse: our exclusions out, everything else kept.
|
|
105
|
+
*
|
|
106
|
+
* `carrick remove` undoes an install, and an install is the only thing this
|
|
107
|
+
* takes back. A name a user excluded by hand stays excluded, and a file that
|
|
108
|
+
* holds their repo list stays a file — it was theirs before init ran. Only a
|
|
109
|
+
* document that is nothing but what init put there is reported as one the
|
|
110
|
+
* caller may delete.
|
|
111
|
+
*/
|
|
112
|
+
export function withoutOurExclusions(existing) {
|
|
113
|
+
const document = parse(existing);
|
|
114
|
+
const removed = ours(document);
|
|
115
|
+
const kept = names(document["exclude"]).filter((name) => !removed.includes(name));
|
|
116
|
+
const next = {};
|
|
117
|
+
for (const [key, value] of Object.entries(document)) {
|
|
118
|
+
if (key === "carrick")
|
|
119
|
+
continue;
|
|
120
|
+
// An emptied list is dropped rather than left as `[]`: it is a key init
|
|
121
|
+
// added, and a file left holding one reads as a decision somebody made.
|
|
122
|
+
if (key === "exclude") {
|
|
123
|
+
if (kept.length > 0)
|
|
124
|
+
next["exclude"] = kept;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
next[key] = value;
|
|
128
|
+
}
|
|
129
|
+
// Nothing of anyone else's left: no keys at all, or an empty `repos` list
|
|
130
|
+
// and nothing beside it.
|
|
131
|
+
const left = Object.keys(next);
|
|
132
|
+
const empty = left.length === 0 || (left.length === 1 && left[0] === "repos" && names(next["repos"]).length === 0);
|
|
133
|
+
return { body: serialize(next), changed: serialize(next) !== existing, removed, empty };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Write the selection into the folder, and say what was recorded.
|
|
137
|
+
*
|
|
138
|
+
* Null when there was nothing to exclude, which is the ordinary case: a single
|
|
139
|
+
* repo is not a choice, and a folder whose repos are all covered has no
|
|
140
|
+
* selection to persist and must not get a file it did not have.
|
|
141
|
+
*/
|
|
142
|
+
export function writeSelection(workspace, exclude) {
|
|
143
|
+
if (exclude.length === 0)
|
|
144
|
+
return null;
|
|
145
|
+
const target = path.join(workspace, WORKSPACE_FILE);
|
|
146
|
+
const written = withExclusions(readWorkspaceFile(workspace), exclude);
|
|
147
|
+
if (written.changed)
|
|
148
|
+
fs.writeFileSync(target, written.body);
|
|
149
|
+
return written;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Take our exclusions back out of the folder's file. Null when there was none.
|
|
153
|
+
*
|
|
154
|
+
* The file goes with them when init is the reason it exists; a file holding a
|
|
155
|
+
* user's own repo list or their own exclusions is rewritten without ours and
|
|
156
|
+
* left where it is.
|
|
157
|
+
*/
|
|
158
|
+
export function removeSelection(workspace) {
|
|
159
|
+
const existing = readWorkspaceFile(workspace);
|
|
160
|
+
if (existing === null)
|
|
161
|
+
return null;
|
|
162
|
+
const cleaned = withoutOurExclusions(existing);
|
|
163
|
+
if (cleaned.removed.length === 0)
|
|
164
|
+
return null;
|
|
165
|
+
const target = path.join(workspace, WORKSPACE_FILE);
|
|
166
|
+
if (cleaned.empty) {
|
|
167
|
+
fs.rmSync(target, { force: true });
|
|
168
|
+
return { file: WORKSPACE_FILE, removed: cleaned.removed, deleted: true };
|
|
169
|
+
}
|
|
170
|
+
fs.writeFileSync(target, cleaned.body);
|
|
171
|
+
return { file: WORKSPACE_FILE, removed: cleaned.removed, deleted: false };
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=workspace-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-file.js","sourceRoot":"","sources":["../../src/init/workspace-file.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carrick",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.83",
|
|
4
4
|
"description": "The API contract index for a TypeScript workspace: what the other services do with the routes and calls in the file you are editing, in your editor and in your agent's context",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
"zod": "^3.23.0"
|
|
58
58
|
},
|
|
59
59
|
"optionalDependencies": {
|
|
60
|
-
"@carrick-tools/cli-darwin-arm64": "0.3.
|
|
61
|
-
"@carrick-tools/cli-darwin-x64": "0.3.
|
|
62
|
-
"@carrick-tools/cli-linux-arm64": "0.3.
|
|
63
|
-
"@carrick-tools/cli-linux-x64": "0.3.
|
|
64
|
-
"@carrick-tools/cli-win32-x64": "0.3.
|
|
60
|
+
"@carrick-tools/cli-darwin-arm64": "0.3.83",
|
|
61
|
+
"@carrick-tools/cli-darwin-x64": "0.3.83",
|
|
62
|
+
"@carrick-tools/cli-linux-arm64": "0.3.83",
|
|
63
|
+
"@carrick-tools/cli-linux-x64": "0.3.83",
|
|
64
|
+
"@carrick-tools/cli-win32-x64": "0.3.83"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@types/node": "^24.13.3",
|
package/plugin/hooks/hooks.json
CHANGED
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
* Seam: node builtins + this bundle only.
|
|
23
23
|
*/
|
|
24
24
|
import type { CheckVerdict } from './api.js';
|
|
25
|
-
import type
|
|
25
|
+
import { type ProbePlan } from './check-probe.js';
|
|
26
26
|
import { type ScrubContext } from './check-scrub.js';
|
|
27
27
|
import type { PairDeepFindings } from './check-deep.js';
|
|
28
|
+
import { type PairFieldReport } from './check-fields.js';
|
|
28
29
|
export interface RawDiagnostic {
|
|
29
30
|
/** Workspace-relative, forward-slash file path (empty for global errors). */
|
|
30
31
|
file: string;
|
|
@@ -55,6 +56,14 @@ export interface ClassifyInput {
|
|
|
55
56
|
* verdict is then not a fact either.
|
|
56
57
|
*/
|
|
57
58
|
deepFindings?: PairDeepFindings;
|
|
59
|
+
/**
|
|
60
|
+
* The differing fields of the two types the judge compared
|
|
61
|
+
* (carrick-tools/carrick-cloud#1118), walked in the same program. Read only
|
|
62
|
+
* on a mismatch, and only to NAME what the judge already decided: it never
|
|
63
|
+
* moves a bucket and never sets `resolved`. Absent when the walk could not
|
|
64
|
+
* run, in which case the tsc text stands alone.
|
|
65
|
+
*/
|
|
66
|
+
fieldReport?: PairFieldReport;
|
|
58
67
|
}
|
|
59
68
|
/** Classify one pair into exactly one bucket, honouring the precedence order. */
|
|
60
69
|
export declare function classifyPair(input: ClassifyInput): CheckVerdict;
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
*
|
|
22
22
|
* Seam: node builtins + this bundle only.
|
|
23
23
|
*/
|
|
24
|
-
import {
|
|
24
|
+
import { decisiveAssignmentLine } from './check-probe.js';
|
|
25
|
+
import { scrubDiagnostic, scrubPaths } from './check-scrub.js';
|
|
26
|
+
import { describeFieldReport } from './check-fields.js';
|
|
25
27
|
const PRIMARY_RE = /^(?<file>(?:[a-zA-Z]:)?[^(]*?)\((?<line>\d+),(?<col>\d+)\): error TS(?<code>\d+): (?<msg>.*)$/;
|
|
26
28
|
/** Parse `tsc --pretty false` output into structured diagnostics. */
|
|
27
29
|
export function parseTscOutput(stdout) {
|
|
@@ -155,31 +157,87 @@ export function classifyPair(input) {
|
|
|
155
157
|
...notAFact(`the ${side} sends a form-encoded body`),
|
|
156
158
|
};
|
|
157
159
|
}
|
|
158
|
-
// 5. Assignment-class error on the
|
|
159
|
-
|
|
160
|
+
// 5. Assignment-class error on the DECISIVE assignment line -> incompatible.
|
|
161
|
+
//
|
|
162
|
+
// On an `http` pair that line is the JSON wire assignment, not the declared
|
|
163
|
+
// one (carrick-tools/carrick-cloud#1119). The two cannot disagree in the
|
|
164
|
+
// direction that matters: the wire comparand short-circuits to the declared
|
|
165
|
+
// type whenever that already assigns, so an error there means the shapes
|
|
166
|
+
// disagree as declared AND as serialised. A pair whose declared forms
|
|
167
|
+
// disagree only over what serialisation changes (a producer `Date` read as
|
|
168
|
+
// the `string` it becomes) has no error on this line and is not a drift.
|
|
169
|
+
const decisiveLine = decisiveAssignmentLine(plan);
|
|
170
|
+
const assignDiag = probeDiags.find((d) => d.line === decisiveLine && ASSIGNMENT_CODES.has(d.code));
|
|
160
171
|
if (assignDiag) {
|
|
172
|
+
const text = scrubDiagnostic(assignDiag.message, scrubCtx, plan.sentEndpoint.alias, plan.expectedEndpoint.alias);
|
|
161
173
|
return {
|
|
162
174
|
...base,
|
|
163
175
|
bucket: 'incompatible',
|
|
164
|
-
diagnostic:
|
|
176
|
+
diagnostic: text + namedFields(input, scrubCtx),
|
|
165
177
|
...factness(input),
|
|
166
178
|
};
|
|
167
179
|
}
|
|
168
|
-
// Any other diagnostic on the assignment line that is not a known
|
|
169
|
-
// code still means the pair could not be cleanly verified.
|
|
170
|
-
|
|
180
|
+
// Any other diagnostic on the DECLARED assignment line that is not a known
|
|
181
|
+
// assignment code still means the pair could not be cleanly verified.
|
|
182
|
+
// A mismatch there with none on the wire line is not one of these: it is the
|
|
183
|
+
// pair the wire rule just cleared, and it falls through to `compatible`.
|
|
184
|
+
const scrub = (text) => scrubDiagnostic(text, scrubCtx, plan.sentEndpoint.alias, plan.expectedEndpoint.alias);
|
|
185
|
+
const otherAssign = probeDiags.find((d) => d.line === plan.assignmentLine && !ASSIGNMENT_CODES.has(d.code));
|
|
171
186
|
if (otherAssign) {
|
|
172
187
|
return {
|
|
173
188
|
...base,
|
|
174
189
|
bucket: 'unverifiable',
|
|
175
190
|
gate: 'assignment:other',
|
|
176
|
-
diagnostic:
|
|
191
|
+
diagnostic: scrub(otherAssign.message),
|
|
192
|
+
...notAFact('the probe raised a diagnostic that is not an assignment mismatch'),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
// The same on the WIRE line: the serialised form was never judged (a type too
|
|
196
|
+
// deep to instantiate the transform over, say). What the DECLARED forms say
|
|
197
|
+
// is then the only judgment there is, and a mismatch in them is still a
|
|
198
|
+
// mismatch — reporting it as unverifiable would hide a real one. The
|
|
199
|
+
// unjudged wire form is named, because it is the reason the usual
|
|
200
|
+
// serialisation allowance did not apply.
|
|
201
|
+
const wireOther = plan.wireAssignmentLine === undefined
|
|
202
|
+
? undefined
|
|
203
|
+
: probeDiags.find((d) => d.line === plan.wireAssignmentLine && !ASSIGNMENT_CODES.has(d.code));
|
|
204
|
+
if (wireOther) {
|
|
205
|
+
const declaredMismatch = probeDiags.find((d) => d.line === plan.assignmentLine && ASSIGNMENT_CODES.has(d.code));
|
|
206
|
+
if (declaredMismatch) {
|
|
207
|
+
return {
|
|
208
|
+
...base,
|
|
209
|
+
bucket: 'incompatible',
|
|
210
|
+
diagnostic: scrub(declaredMismatch.message) +
|
|
211
|
+
' The serialised form of the sent type could not be computed, so this' +
|
|
212
|
+
' compares the types as declared.' +
|
|
213
|
+
namedFields(input, scrubCtx),
|
|
214
|
+
...factness(input),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
...base,
|
|
219
|
+
bucket: 'unverifiable',
|
|
220
|
+
gate: 'assignment:other',
|
|
221
|
+
diagnostic: scrub(wireOther.message),
|
|
177
222
|
...notAFact('the probe raised a diagnostic that is not an assignment mismatch'),
|
|
178
223
|
};
|
|
179
224
|
}
|
|
180
225
|
// 6. No diagnostics -> compatible.
|
|
181
226
|
return { ...base, bucket: 'compatible', ...factness(input) };
|
|
182
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* The field-level sentence appended to a mismatch diagnostic
|
|
230
|
+
* (carrick-tools/carrick-cloud#1118), or `''` when the walk found nothing to
|
|
231
|
+
* name. Scrubbed on the same terms as the tsc text: a printed member type can
|
|
232
|
+
* carry a stub-absolute `import("...")` path.
|
|
233
|
+
*/
|
|
234
|
+
function namedFields(input, scrubCtx) {
|
|
235
|
+
const report = input.fieldReport;
|
|
236
|
+
if (!report)
|
|
237
|
+
return '';
|
|
238
|
+
const text = describeFieldReport(report, input.plan.direction.sent, input.plan.direction.expected);
|
|
239
|
+
return text === '' ? '' : scrubPaths(text, scrubCtx);
|
|
240
|
+
}
|
|
183
241
|
/**
|
|
184
242
|
* Whether a compared pair is a FACT about two known types (carrick#707, R1d).
|
|
185
243
|
*
|
|
@@ -15,6 +15,7 @@
|
|
|
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
17
|
*/
|
|
18
|
+
import ts from 'typescript';
|
|
18
19
|
import type { TypeProvenance } from './api.js';
|
|
19
20
|
import type { ProbePlan } from './check-probe.js';
|
|
20
21
|
/** Deep findings for one pair, per probe side. */
|
|
@@ -22,11 +23,22 @@ export interface PairDeepFindings {
|
|
|
22
23
|
sent: TypeProvenance[];
|
|
23
24
|
expected: TypeProvenance[];
|
|
24
25
|
}
|
|
26
|
+
/** The one compiler program over the assembled probes, shared by every
|
|
27
|
+
* post-judge walk (fact-ness, and the field-level report the mismatch text
|
|
28
|
+
* names). Built once: two `createProgram` calls over the same file set would
|
|
29
|
+
* double the check phase's most expensive step and could not disagree usefully
|
|
30
|
+
* anyway. `undefined` when it cannot be built at all. */
|
|
31
|
+
export interface ProbeProgram {
|
|
32
|
+
program: ts.Program;
|
|
33
|
+
checker: ts.TypeChecker;
|
|
34
|
+
probesDir: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function openProbeProgram(probesDir: string, plans: ProbePlan[]): ProbeProgram | undefined;
|
|
25
37
|
/**
|
|
26
38
|
* Walk both sides of every probe in the assembled workspace.
|
|
27
39
|
*
|
|
28
|
-
* Returns an empty map when the program
|
|
29
|
-
* must never be read as "clean", so the caller treats a missing entry
|
|
30
|
-
* unresolved rather than resolved.
|
|
40
|
+
* Returns an empty map when the program could not be built — absence of
|
|
41
|
+
* findings must never be read as "clean", so the caller treats a missing entry
|
|
42
|
+
* as unresolved rather than resolved.
|
|
31
43
|
*/
|
|
32
|
-
export declare function probeDeepFindings(
|
|
44
|
+
export declare function probeDeepFindings(opened: ProbeProgram | undefined, plans: ProbePlan[]): Map<string, PairDeepFindings>;
|
|
@@ -19,37 +19,41 @@ import ts from 'typescript';
|
|
|
19
19
|
import * as fs from 'node:fs';
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import { findDisqualifyingTopTypes, provenanceOf } from './deep-walk.js';
|
|
22
|
-
|
|
23
|
-
* Walk both sides of every probe in the assembled workspace.
|
|
24
|
-
*
|
|
25
|
-
* Returns an empty map when the program cannot be built — absence of findings
|
|
26
|
-
* must never be read as "clean", so the caller treats a missing entry as
|
|
27
|
-
* unresolved rather than resolved.
|
|
28
|
-
*/
|
|
29
|
-
export function probeDeepFindings(probesDir, plans) {
|
|
30
|
-
const results = new Map();
|
|
22
|
+
export function openProbeProgram(probesDir, plans) {
|
|
31
23
|
if (plans.length === 0)
|
|
32
|
-
return
|
|
24
|
+
return undefined;
|
|
33
25
|
const configPath = path.join(probesDir, 'tsconfig.json');
|
|
34
26
|
if (!fs.existsSync(configPath))
|
|
35
|
-
return
|
|
36
|
-
let program;
|
|
27
|
+
return undefined;
|
|
37
28
|
try {
|
|
38
29
|
const raw = ts.readConfigFile(configPath, (f) => fs.readFileSync(f, 'utf8'));
|
|
39
30
|
if (raw.error)
|
|
40
|
-
return
|
|
31
|
+
return undefined;
|
|
41
32
|
const parsed = ts.parseJsonConfigFileContent(raw.config, ts.sys, probesDir);
|
|
42
33
|
const fileNames = plans
|
|
43
34
|
.map((plan) => path.join(probesDir, 'probes', plan.fileName))
|
|
44
35
|
.filter((f) => fs.existsSync(f));
|
|
45
36
|
if (fileNames.length === 0)
|
|
46
|
-
return
|
|
47
|
-
program = ts.createProgram(fileNames, { ...parsed.options, noEmit: true });
|
|
37
|
+
return undefined;
|
|
38
|
+
const program = ts.createProgram(fileNames, { ...parsed.options, noEmit: true });
|
|
39
|
+
return { program, checker: program.getTypeChecker(), probesDir };
|
|
48
40
|
}
|
|
49
41
|
catch {
|
|
50
|
-
return
|
|
42
|
+
return undefined;
|
|
51
43
|
}
|
|
52
|
-
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Walk both sides of every probe in the assembled workspace.
|
|
47
|
+
*
|
|
48
|
+
* Returns an empty map when the program could not be built — absence of
|
|
49
|
+
* findings must never be read as "clean", so the caller treats a missing entry
|
|
50
|
+
* as unresolved rather than resolved.
|
|
51
|
+
*/
|
|
52
|
+
export function probeDeepFindings(opened, plans) {
|
|
53
|
+
const results = new Map();
|
|
54
|
+
if (!opened)
|
|
55
|
+
return results;
|
|
56
|
+
const { program, checker, probesDir } = opened;
|
|
53
57
|
for (const plan of plans) {
|
|
54
58
|
const file = program.getSourceFile(path.join(probesDir, 'probes', plan.fileName));
|
|
55
59
|
if (!file)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field-level report for a pair the judge called incompatible
|
|
3
|
+
* (carrick-tools/carrick-cloud#1118).
|
|
4
|
+
*
|
|
5
|
+
* `tsc` decides. Its elaboration names ONE field and then elides the rest
|
|
6
|
+
* ("Type 'A' is not assignable to type 'B'. Property 'x' is missing"), which is
|
|
7
|
+
* not enough for a reader deciding what to change: a create endpoint that
|
|
8
|
+
* requires `username` while the client sends `userName` reads as one missing
|
|
9
|
+
* property with no hint that the sent object carries a near-neighbour.
|
|
10
|
+
*
|
|
11
|
+
* This walk enumerates the differing fields of the SAME two types the judge
|
|
12
|
+
* compared, in the SAME program, using the compiler's own assignability
|
|
13
|
+
* relation. It is not a second judge:
|
|
14
|
+
* - it runs only after the bucket is decided and never changes one;
|
|
15
|
+
* - it never contradicts: a difference is only named when the checker itself
|
|
16
|
+
* says the two member types do not assign, and when it finds nothing it
|
|
17
|
+
* adds nothing and the raw tsc text stands alone;
|
|
18
|
+
* - it refuses the shapes where a member list is not an account of the type
|
|
19
|
+
* (a union root, a receiver with an index signature), rather than guessing
|
|
20
|
+
* about them.
|
|
21
|
+
*
|
|
22
|
+
* It reports one thing the judge structurally cannot: an optionality gap in the
|
|
23
|
+
* direction that still assigns (the sending side always provides a field the
|
|
24
|
+
* receiving side declares optional). That is a real drift between two sources
|
|
25
|
+
* — the receiver carries a branch that never runs — and no assignment error can
|
|
26
|
+
* exist for it. It is stated as an observation beside the verdict, never as the
|
|
27
|
+
* verdict.
|
|
28
|
+
*
|
|
29
|
+
* Seam: node builtins + `typescript` + this bundle only.
|
|
30
|
+
*/
|
|
31
|
+
import type { ProbePlan, Side } from './check-probe.js';
|
|
32
|
+
import type { ProbeProgram } from './check-deep.js';
|
|
33
|
+
/** What kind of difference one field path carries. */
|
|
34
|
+
export type FieldDifferenceNature =
|
|
35
|
+
/** The receiving side declares it; the sending side has no such member. */
|
|
36
|
+
'missing_in_sent'
|
|
37
|
+
/** The sending side provides it; the receiving side declares no such member. */
|
|
38
|
+
| 'extra_in_sent'
|
|
39
|
+
/** Optional where it is sent, required where it is read. */
|
|
40
|
+
| 'optional_in_sent'
|
|
41
|
+
/** Always sent, optional where it is read (no assignment error can exist). */
|
|
42
|
+
| 'optional_in_expected'
|
|
43
|
+
/** Both declare it and the member types do not assign. */
|
|
44
|
+
| 'type_differs';
|
|
45
|
+
export interface FieldDifference {
|
|
46
|
+
/** Dotted member path from the compared root (`''` is the root itself). */
|
|
47
|
+
path: string;
|
|
48
|
+
nature: FieldDifferenceNature;
|
|
49
|
+
/** Printed member type on the sending side, for `type_differs`. */
|
|
50
|
+
sentText?: string;
|
|
51
|
+
/** Printed member type on the receiving side, for `type_differs`. */
|
|
52
|
+
expectedText?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface PairFieldReport {
|
|
55
|
+
/** Named differences, capped and ordered deterministically. */
|
|
56
|
+
differences: FieldDifference[];
|
|
57
|
+
/** How many further differences were found beyond the cap. */
|
|
58
|
+
truncated: number;
|
|
59
|
+
/**
|
|
60
|
+
* Whether the sent type's JSON wire form differs from its declared form, so
|
|
61
|
+
* the comparison the reader is being shown is against the serialised shape
|
|
62
|
+
* (a `Date` compared as the string it serialises to).
|
|
63
|
+
*/
|
|
64
|
+
wireApplied: boolean;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Cap on named fields. A mismatch with more differing members than this is
|
|
68
|
+
* better described as two unrelated shapes than as a list, and the text says
|
|
69
|
+
* how many more there are rather than pretending the list is complete.
|
|
70
|
+
*/
|
|
71
|
+
export declare const MAX_NAMED_FIELDS = 8;
|
|
72
|
+
/**
|
|
73
|
+
* Field reports for every plan whose probe the program could read, keyed by
|
|
74
|
+
* pair id. A plan with no entry has no report, which is not a claim that its
|
|
75
|
+
* types agree.
|
|
76
|
+
*/
|
|
77
|
+
export declare function pairFieldReports(opened: ProbeProgram | undefined, plans: ProbePlan[]): Map<string, PairFieldReport>;
|
|
78
|
+
/**
|
|
79
|
+
* The sentence appended to a mismatch diagnostic. Names the two sides as
|
|
80
|
+
* producer and consumer (never the probe's internal sent/expected), so the
|
|
81
|
+
* reader knows which repo to change.
|
|
82
|
+
*/
|
|
83
|
+
export declare function describeFieldReport(report: PairFieldReport, sentSide: Side, expectedSide: Side): string;
|