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.
Files changed (73) hide show
  1. package/README.md +46 -19
  2. package/bin/carrick.mjs +45 -2
  3. package/dist/contract.d.ts +17 -0
  4. package/dist/contract.js.map +1 -1
  5. package/dist/hook/apply-patch.d.ts +13 -0
  6. package/dist/hook/apply-patch.js +100 -0
  7. package/dist/hook/apply-patch.js.map +1 -0
  8. package/dist/hook/post-edit.d.ts +30 -1
  9. package/dist/hook/post-edit.js +95 -24
  10. package/dist/hook/post-edit.js.map +1 -1
  11. package/dist/hook/reuse.d.ts +97 -0
  12. package/dist/hook/reuse.js +245 -0
  13. package/dist/hook/reuse.js.map +1 -0
  14. package/dist/hook/stop.d.ts +3 -0
  15. package/dist/hook/stop.js +76 -0
  16. package/dist/hook/stop.js.map +1 -0
  17. package/dist/hook/user-prompt.d.ts +9 -0
  18. package/dist/hook/user-prompt.js +79 -0
  19. package/dist/hook/user-prompt.js.map +1 -0
  20. package/dist/init/codex.d.ts +51 -0
  21. package/dist/init/codex.js +167 -0
  22. package/dist/init/codex.js.map +1 -0
  23. package/dist/init/connect.d.ts +13 -0
  24. package/dist/init/connect.js +21 -15
  25. package/dist/init/connect.js.map +1 -1
  26. package/dist/init/doctor.d.ts +36 -0
  27. package/dist/init/doctor.js +97 -2
  28. package/dist/init/doctor.js.map +1 -1
  29. package/dist/init/files.d.ts +15 -0
  30. package/dist/init/files.js +20 -1
  31. package/dist/init/files.js.map +1 -1
  32. package/dist/init/outdated.d.ts +54 -0
  33. package/dist/init/outdated.js +175 -0
  34. package/dist/init/outdated.js.map +1 -0
  35. package/dist/init/output.d.ts +35 -3
  36. package/dist/init/output.js +115 -23
  37. package/dist/init/output.js.map +1 -1
  38. package/dist/init/projects.d.ts +27 -13
  39. package/dist/init/projects.js +48 -50
  40. package/dist/init/projects.js.map +1 -1
  41. package/dist/init/remove.d.ts +0 -2
  42. package/dist/init/remove.js +61 -15
  43. package/dist/init/remove.js.map +1 -1
  44. package/dist/init/repos.d.ts +34 -0
  45. package/dist/init/repos.js +77 -0
  46. package/dist/init/repos.js.map +1 -1
  47. package/dist/init/run.d.ts +73 -6
  48. package/dist/init/run.js +357 -94
  49. package/dist/init/run.js.map +1 -1
  50. package/dist/init/settings.d.ts +20 -0
  51. package/dist/init/settings.js +42 -4
  52. package/dist/init/settings.js.map +1 -1
  53. package/dist/init/task-skills.d.ts +27 -0
  54. package/dist/init/task-skills.js +64 -1
  55. package/dist/init/task-skills.js.map +1 -1
  56. package/dist/init/workspace-file.d.ts +73 -0
  57. package/dist/init/workspace-file.js +173 -0
  58. package/dist/init/workspace-file.js.map +1 -0
  59. package/package.json +6 -6
  60. package/plugin/hooks/hooks.json +11 -0
  61. package/sidecar/dist/src/capture/check-classify.d.ts +10 -1
  62. package/sidecar/dist/src/capture/check-classify.js +66 -8
  63. package/sidecar/dist/src/capture/check-deep.d.ts +16 -4
  64. package/sidecar/dist/src/capture/check-deep.js +21 -17
  65. package/sidecar/dist/src/capture/check-fields.d.ts +83 -0
  66. package/sidecar/dist/src/capture/check-fields.js +259 -0
  67. package/sidecar/dist/src/capture/check-probe.d.ts +21 -1
  68. package/sidecar/dist/src/capture/check-probe.js +39 -0
  69. package/sidecar/dist/src/capture/check.js +9 -2
  70. package/templates/skills/carrick-census.md +18 -9
  71. package/templates/skills/carrick-drift.md +7 -1
  72. package/templates/skills/carrick-impact.md +3 -1
  73. 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.82",
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.82",
61
- "@carrick-tools/cli-darwin-x64": "0.3.82",
62
- "@carrick-tools/cli-linux-arm64": "0.3.82",
63
- "@carrick-tools/cli-linux-x64": "0.3.82",
64
- "@carrick-tools/cli-win32-x64": "0.3.82"
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",
@@ -22,6 +22,17 @@
22
22
  }
23
23
  ]
24
24
  }
25
+ ],
26
+ "Stop": [
27
+ {
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "carrick hook stop",
32
+ "timeout": 5
33
+ }
34
+ ]
35
+ }
25
36
  ]
26
37
  }
27
38
  }
@@ -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 { ProbePlan } from './check-probe.js';
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 { scrubDiagnostic } from './check-scrub.js';
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 value assignment line -> incompatible.
159
- const assignDiag = probeDiags.find((d) => d.line === plan.assignmentLine && ASSIGNMENT_CODES.has(d.code));
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: scrubDiagnostic(assignDiag.message, scrubCtx, plan.sentEndpoint.alias, plan.expectedEndpoint.alias),
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 assignment
169
- // code still means the pair could not be cleanly verified.
170
- const otherAssign = probeDiags.find((d) => d.line === plan.assignmentLine);
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: scrubDiagnostic(otherAssign.message, scrubCtx, plan.sentEndpoint.alias, plan.expectedEndpoint.alias),
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 cannot be built — absence of findings
29
- * must never be read as "clean", so the caller treats a missing entry as
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(probesDir: string, plans: ProbePlan[]): Map<string, PairDeepFindings>;
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 results;
24
+ return undefined;
33
25
  const configPath = path.join(probesDir, 'tsconfig.json');
34
26
  if (!fs.existsSync(configPath))
35
- return results;
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 results;
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 results;
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 results;
42
+ return undefined;
51
43
  }
52
- const checker = program.getTypeChecker();
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;