@skyramp/mcp 0.4.3-rc.1 → 0.4.3

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 (37) hide show
  1. package/build/recommendation/answers.d.ts +5 -3
  2. package/build/recommendation/answers.js +5 -2
  3. package/build/recommendation/verifierContracts.d.ts +8 -0
  4. package/build/recommendation/verifierContracts.js +13 -0
  5. package/build/recommendation/verifiers/coverage.js +143 -4
  6. package/build/tools/code-refactor/removed-modules.d.ts +33 -0
  7. package/build/tools/code-refactor/removed-modules.js +61 -0
  8. package/build/tools/code-refactor/reuse-outcome.d.ts +27 -1
  9. package/build/tools/code-refactor/reuse-record-store.d.ts +5 -0
  10. package/build/tools/code-refactor/reuse-record-store.js +15 -1
  11. package/build/tools/code-refactor/reuse-state.d.ts +17 -4
  12. package/build/tools/code-refactor/reuse-state.js +67 -9
  13. package/build/tools/code-refactor/utils-verify-gates.js +39 -15
  14. package/build/tools/submitReportTool.js +47 -18
  15. package/build/tools/test-management/registerTestPlanTool.js +1 -1
  16. package/build/types/ReuseOutcome.d.ts +27 -0
  17. package/build/utils/utils-verify/allow.d.ts +1 -1
  18. package/build/utils/utils-verify/allow.js +15 -0
  19. package/build/utils/utils-verify/changed-lines.d.ts +48 -0
  20. package/build/utils/utils-verify/changed-lines.js +106 -0
  21. package/build/utils/utils-verify/head.d.ts +32 -7
  22. package/build/utils/utils-verify/head.js +55 -14
  23. package/build/utils/utils-verify/typecheck.d.ts +25 -4
  24. package/build/utils/utils-verify/typecheck.js +95 -13
  25. package/build/utils/utils-verify/verify.d.ts +24 -5
  26. package/build/utils/utils-verify/verify.js +156 -8
  27. package/build/utils/versions.d.ts +3 -3
  28. package/build/utils/versions.js +1 -1
  29. package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +4 -2
  30. package/node_modules/playwright/lib/mcp/skyramp/snapshotLocators.js +246 -0
  31. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +221 -233
  32. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +21 -0
  33. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
  34. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.ZYuEFHsi.js → index.uiPTFncY.js} +1 -1
  35. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  36. package/node_modules/playwright/package.json +1 -1
  37. package/package.json +3 -3
@@ -1,4 +1,5 @@
1
1
  import * as path from "path";
2
+ import { readFileSync } from "fs";
2
3
  import { fileURLToPath } from "url";
3
4
  let tsModule;
4
5
  /** The compiler, loaded on first use and once per process. Loaded statically it
@@ -41,8 +42,61 @@ const UNRESOLVED_MODULE_CODES = new Set([2307, 2792]);
41
42
  * exit was to rewrite working code. Every other package still resolves from the
42
43
  * repository first, because every other package is the repository's to pin. */
43
44
  const EXECUTOR_PROVIDED_PACKAGES = ["@skyramp/skyramp"];
45
+ /** The PEERS of the executor-provided packages — what their type declarations
46
+ * import and expect the host to install (`@skyramp/skyramp` declares
47
+ * `@playwright/test`, whose `Expect` types its `expect`). Read from each
48
+ * package's own manifest under `mcpRoot`, once per root: the SDK already states
49
+ * the list, and a second copy here would need a test to keep it in step. A peer
50
+ * is resolved from the REPOSITORY first for every file that imports it, the
51
+ * MCP's own copy second — including from inside the MCP's SDK declarations.
52
+ * Otherwise the SDK's copy came from the MCP's install while the spec's own
53
+ * import came from the repository's, and a repository pinned to another
54
+ * Playwright version had two `Locator` identities: every `expect(locator)` in
55
+ * it typed as the base matchers, and four assertion lines that pre-dated the
56
+ * run became blocking `type-error`s against a valid module — which the run then
57
+ * deleted. The customer's `tsc` has one node_modules and never sees the split;
58
+ * the executor runs the repository's Playwright, so the repository's copy is
59
+ * the truth. */
60
+ const peersByRoot = new Map();
61
+ function executorPeerPackages(mcpRoot) {
62
+ let peers = peersByRoot.get(mcpRoot);
63
+ if (peers)
64
+ return peers;
65
+ peers = [];
66
+ for (const pkg of EXECUTOR_PROVIDED_PACKAGES) {
67
+ const manifest = installedManifest(pkg, mcpRoot);
68
+ if (manifest)
69
+ peers.push(...Object.keys(manifest.peerDependencies ?? {}));
70
+ // No manifest reachable from this root: the package resolves from nowhere
71
+ // and its imports are reported as unresolved gaps, so there is no peer to
72
+ // route.
73
+ }
74
+ peersByRoot.set(mcpRoot, peers);
75
+ return peers;
76
+ }
77
+ /** The package's manifest as Node would find it from `from`: the nearest
78
+ * `node_modules/<pkg>` walking UP from there. The MCP's dependencies are not
79
+ * always nested under its own root — `npm install @skyramp/mcp`, testbot's
80
+ * default, hoists them BESIDE `@skyramp/mcp` — and a read at one fixed path
81
+ * found nothing on that layout, left the peer list empty, and brought the two
82
+ * Playwright identities back exactly where customers run. */
83
+ function installedManifest(pkg, from) {
84
+ for (let dir = from;; dir = path.dirname(dir)) {
85
+ try {
86
+ return JSON.parse(readFileSync(path.join(dir, "node_modules", pkg, "package.json"), "utf8"));
87
+ }
88
+ catch {
89
+ if (path.dirname(dir) === dir)
90
+ return undefined;
91
+ }
92
+ }
93
+ }
94
+ const isPackageOrSubpath = (specifier, packages) => packages.some((p) => specifier === p || specifier.startsWith(`${p}/`));
44
95
  function executorProvides(specifier) {
45
- return EXECUTOR_PROVIDED_PACKAGES.some((p) => specifier === p || specifier.startsWith(`${p}/`));
96
+ return isPackageOrSubpath(specifier, EXECUTOR_PROVIDED_PACKAGES);
97
+ }
98
+ function executorPeer(specifier, mcpRoot) {
99
+ return isPackageOrSubpath(specifier, executorPeerPackages(mcpRoot));
46
100
  }
47
101
  export const AMBIENT_DECLARATION_CODES = new Set([
48
102
  2304, 2311, 2318, 2468, 2503, 2552, 2580, 2581, 2582, 2583, 2584, 2591, 2592,
@@ -211,26 +265,51 @@ function findConfigs(ts, files) {
211
265
  * an error, and its diagnostics are dropped (everything typed by it is `any` and
212
266
  * was not checked — the gap is stated instead).
213
267
  */
214
- function runPass(ts, files, options, mcpRoot) {
268
+ function runPass(ts, files, options, mcpRoot, repoDir, baseline) {
215
269
  const own = new Set(files.map((f) => path.normalize(f)));
216
270
  const unresolved = new Set();
217
271
  const host = ts.createCompilerHost(options, true);
272
+ if (baseline) {
273
+ // A baseline file is read from the content given, never from disk; a hidden
274
+ // one does not exist to the compiler, so an import of it is unresolved and
275
+ // typed `any`, as it was before the run wrote it.
276
+ const contentOf = (f) => baseline.contents.get(path.normalize(f));
277
+ const hidden = (f) => baseline.hidden.has(path.normalize(f));
278
+ const { fileExists, readFile, getSourceFile } = host;
279
+ host.fileExists = (f) => hidden(f) ? false : contentOf(f) !== undefined || fileExists(f);
280
+ host.readFile = (f) => hidden(f) ? undefined : (contentOf(f) ?? readFile(f));
281
+ host.getSourceFile = (f, lang, onError, shouldCreate) => {
282
+ if (hidden(f))
283
+ return undefined;
284
+ const content = contentOf(f);
285
+ return content === undefined
286
+ ? getSourceFile(f, lang, onError, shouldCreate)
287
+ : ts.createSourceFile(f, content, lang, true);
288
+ };
289
+ }
218
290
  // Global types (`@types/*` pulled in automatically) are looked up from the current
219
291
  // directory upward. That must be the repository, not wherever the MCP process
220
292
  // happens to run: the MCP's own `@types/jest` made a bare `expect` compile here
221
293
  // that the customer's `tsc` cannot see.
222
- const repoDir = path.dirname(files[0]);
223
294
  host.getCurrentDirectory = () => repoDir;
224
295
  // A containing file that does not exist is fine for resolution: only its
225
296
  // directory is read, and node_modules is looked up from there.
226
297
  const anchor = path.join(mcpRoot, "__skyramp_typecheck_anchor__.ts");
298
+ // The same device for the repository: a peer imported from INSIDE the MCP's
299
+ // install must still resolve from the repository first, and the containing
300
+ // file's directory would find the MCP's copy.
301
+ const repoAnchor = path.join(repoDir, "__skyramp_typecheck_anchor__.ts");
227
302
  host.resolveModuleNameLiterals = (literals, containingFile, _r, opts) => literals.map((literal) => {
228
303
  const delivered = own.has(path.normalize(containingFile));
229
- // The executor's own packages are read from the MCP's install first;
230
- // everything else from the repository first, the MCP second.
304
+ // The executor's own packages are read from the MCP's install first; their
305
+ // peers from the repository first wherever the import sits, so both sides of
306
+ // an SDK type meet ONE copy; everything else from the repository first, the
307
+ // MCP second.
231
308
  const [first, second] = executorProvides(literal.text)
232
309
  ? [anchor, containingFile]
233
- : [containingFile, anchor];
310
+ : executorPeer(literal.text, mcpRoot)
311
+ ? [repoAnchor, anchor]
312
+ : [containingFile, anchor];
234
313
  let resolved = ts.resolveModuleName(literal.text, first, opts, host);
235
314
  if (!resolved.resolvedModule) {
236
315
  const other = ts.resolveModuleName(literal.text, second, opts, host);
@@ -281,15 +360,17 @@ const findingKey = (f) => `${f.file}\0${f.line}\0${f.code}\0${f.message}`;
281
360
  * every repository whether or not `typescript` is installed there.
282
361
  *
283
362
  * Two passes over the same files. The PERMISSIVE pass has every strictness flag off:
284
- * an error it reports fails under any configuration and is blocking (`errors`). The
285
- * SETTINGS pass runs the repository's own tsconfig, when one reaches the files; what
286
- * it adds is an error only under those settings and is advisory (`settingErrors`).
363
+ * an error it reports fails under any configuration (`errors`) — the verifier
364
+ * blocks on it unless the repository already had it at HEAD, which the same
365
+ * function establishes when called with a `baseline`. The SETTINGS pass runs the
366
+ * repository's own tsconfig, when one reaches the files; what it adds is an error
367
+ * only under those settings and is advisory (`settingErrors`).
287
368
  * The executor transpiles without typechecking, so neither pass has run before this
288
369
  * one, and a delivered file that fails here reaches the customer's own `tsc` first.
289
370
  *
290
371
  * Never throws: a compiler failure is a result that says the check did not run.
291
372
  */
292
- export async function typecheckDelivered(files, mcpRoot = mcpPackageRoot()) {
373
+ export async function typecheckDelivered(files, { mcpRoot = mcpPackageRoot(), anchor, baseline } = {}) {
293
374
  const result = {
294
375
  ran: false,
295
376
  errors: [],
@@ -310,15 +391,16 @@ export async function typecheckDelivered(files, mcpRoot = mcpPackageRoot()) {
310
391
  // import is unresolved here, typed `any`, and its unconditional error then
311
392
  // surfaces only in the settings pass — downgraded to an advisory.
312
393
  const base = (reaching ?? nearest)?.parsed.options ?? {};
313
- const floor = runPass(ts, files, permissiveOptions(ts, base), mcpRoot);
394
+ const repoDir = path.dirname(anchor ?? files[0]);
395
+ const floor = runPass(ts, files, permissiveOptions(ts, base), mcpRoot, repoDir, baseline);
314
396
  result.ran = true;
315
397
  result.errors = floor.findings.filter((f) => !AMBIENT_DECLARATION_CODES.has(f.code));
316
398
  result.ambientErrors = floor.findings.filter((f) => AMBIENT_DECLARATION_CODES.has(f.code));
317
399
  result.unresolved = [...floor.unresolved].sort();
318
- if (reaching) {
400
+ if (reaching && !baseline) {
319
401
  result.tsconfig = reaching.path;
320
402
  const seen = new Set([...floor.findings, ...floor.unresolvedFindings].map(findingKey));
321
- const settings = runPass(ts, files, settingsOptions(reaching.parsed.options), mcpRoot);
403
+ const settings = runPass(ts, files, settingsOptions(reaching.parsed.options), mcpRoot, repoDir);
322
404
  // An import only the repository's resolution refuses is its `tsc`'s error,
323
405
  // not a gap: the permissive pass resolved it, so the module exists.
324
406
  result.settingErrors = [
@@ -24,6 +24,13 @@ export interface UtilsVerifyResult {
24
24
  ok: boolean;
25
25
  /** Utils files located for this spec (absolute). Empty = nothing was written or imported. */
26
26
  utilsFiles: string[];
27
+ /** Those of `utilsFiles` that HEAD holds — an earlier run's or the customer's. */
28
+ incumbentModules: string[];
29
+ /** Those of `utilsFiles` git could not answer for (outside a repository, or a
30
+ * git failure). Neither an incumbent nor this run's: a caller that states
31
+ * "this run created it" must not say so for these. The rest of `utilsFiles`
32
+ * this run created. */
33
+ headUnknownModules: string[];
27
34
  /** Helpers defined across the utils files. */
28
35
  helpers: number;
29
36
  /** Helper names the delivered spec imports from those files. */
@@ -86,6 +93,13 @@ export interface UtilsVerifyResult {
86
93
  * `tsconfig-type-error` advisories; this carries the rest (the tsconfig read, the
87
94
  * modules it could not resolve). Absent for Python and JavaScript. */
88
95
  typecheck?: UtilsTypecheck;
96
+ /** What the attribution of compiler errors to this run established — with
97
+ * `typecheck`, TypeScript only. `ran` is whether the HEAD compile ran, so an
98
+ * error the repository already had could be excused; when it did not, `reason`
99
+ * says why and every error blocked as this run's. `headUnknown` lists the
100
+ * delivered files git gave no HEAD answer for: each is judged as this run's
101
+ * in full, and the pass says so rather than hide it. */
102
+ attribution?: TypeAttribution;
89
103
  /** Modules the spec imports that this pass READ and does not CHECK: the
90
104
  * repository's own modules (tracked at HEAD, no Skyramp header) on a path with no
91
105
  * in-house checks — integration, today — and a module outside the repository.
@@ -110,6 +124,11 @@ export interface UtilsVerifyResult {
110
124
  * Absent when the spec imports no such module. */
111
125
  inHouse?: InHouseVerification;
112
126
  }
127
+ export interface TypeAttribution {
128
+ ran: boolean;
129
+ reason?: string;
130
+ headUnknown: string[];
131
+ }
113
132
  export interface InHouseVerification {
114
133
  /** Absolute paths of the in-house modules read. */
115
134
  modules: string[];
@@ -177,17 +196,17 @@ export interface VerifyUtilsParams {
177
196
  * returned. See status-once.ts.
178
197
  * - `type-error` (TypeScript): the delivered module or spec fails the compiler with
179
198
  * every strictness flag off — an error under every configuration (a path parameter
180
- * typed `unknown`, request options built as a record and handed to `sendRequest`).
181
- * An error only the repository's own tsconfig raises is the `tsconfig-type-error`
182
- * ADVISORY. See typecheckDelivered.
199
+ * typed `unknown`, request options built as a record and handed to `sendRequest`)
200
+ * — and this run CAUSED it: an error the file already had at HEAD on an
201
+ * unchanged line is the `inherited-type-error` ADVISORY, and an error only the
202
+ * repository's own tsconfig raises is the `tsconfig-type-error` ADVISORY. See
203
+ * typecheckDelivered and inheritedTypeError.
183
204
  *
184
205
  * Analyses the utils files' INTERNAL invariants — unlike the POM verifier, which
185
206
  * checks a spec's cross-file references — so it reads the utils files, not the spec,
186
207
  * except to count what the spec imports.
187
208
  */
188
209
  export declare function verifyUtils(params: VerifyUtilsParams): Promise<UtilsVerifyResult>;
189
- /** The compared counts as one phrase, shared by the assertion-loss detail and the
190
- * PASSED line's note so the two cannot drift. */
191
210
  export declare function describeAssertionCount(c: {
192
211
  baseline: number;
193
212
  delivered: number;
@@ -20,7 +20,8 @@ import { describeFixedSleeps, fixedSleepReplacement } from "./fixed-sleep.js";
20
20
  import { singleImporterHelpers } from "./importers.js";
21
21
  import { assertsStatusEquality, defaultedStatusParam, pinnedStatusLiteral, restatedStatusAssertions, } from "./status-once.js";
22
22
  import { STATUS_ONCE_RULE } from "../../prompts/shared-helper-policy.js";
23
- import { contentAtHead } from "./head.js";
23
+ import { addedSinceHead, changedSinceHead, contentAtHead, readAtHead, } from "./head.js";
24
+ import { alignLines, BaselineErrors, baselineErrorKey, inheritedTypeError, } from "./changed-lines.js";
24
25
  import { incumbentSignatureViolations } from "./incumbent.js";
25
26
  import { helperFamilyFor, helperFamilyOf, scenarioNameViolation, utilsSpecFor, utilsSpecForFile, } from "./language-spec.js";
26
27
  /** What the compiler says about a name defined twice, which `helper-redefined`
@@ -52,9 +53,11 @@ const REDECLARATION_CODES = new Set([2393, 2323, 2451]);
52
53
  * returned. See status-once.ts.
53
54
  * - `type-error` (TypeScript): the delivered module or spec fails the compiler with
54
55
  * every strictness flag off — an error under every configuration (a path parameter
55
- * typed `unknown`, request options built as a record and handed to `sendRequest`).
56
- * An error only the repository's own tsconfig raises is the `tsconfig-type-error`
57
- * ADVISORY. See typecheckDelivered.
56
+ * typed `unknown`, request options built as a record and handed to `sendRequest`)
57
+ * — and this run CAUSED it: an error the file already had at HEAD on an
58
+ * unchanged line is the `inherited-type-error` ADVISORY, and an error only the
59
+ * repository's own tsconfig raises is the `tsconfig-type-error` ADVISORY. See
60
+ * typecheckDelivered and inheritedTypeError.
58
61
  *
59
62
  * Analyses the utils files' INTERNAL invariants — unlike the POM verifier, which
60
63
  * checks a spec's cross-file references — so it reads the utils files, not the spec,
@@ -102,6 +105,8 @@ export async function verifyUtils(params) {
102
105
  const result = {
103
106
  ok: true,
104
107
  utilsFiles,
108
+ incumbentModules: [],
109
+ headUnknownModules: [],
105
110
  helpers: 0,
106
111
  imported: 0,
107
112
  importedNames: [],
@@ -136,6 +141,11 @@ export async function verifyUtils(params) {
136
141
  * kept: both the status advisory's scope and the restatement's remediation
137
142
  * turn on it. */
138
143
  const incumbentsOfFile = new Map();
144
+ /** Per utils file, the module at HEAD — `undefined` for a module HEAD does not
145
+ * hold. The compiler's attribution reads it: an error the module already had at
146
+ * HEAD is not this run's. */
147
+ const headContentOfFile = new Map();
148
+ const contentOfFile = new Map();
139
149
  const allHelpers = [];
140
150
  const helpersByFile = new Map();
141
151
  const allAllows = [];
@@ -170,10 +180,17 @@ export async function verifyUtils(params) {
170
180
  // helpers this run writes, so a defaulted status on an incumbent is not advised
171
181
  // — and an incumbent's signature is held to what it was (incumbent.ts). Absent
172
182
  // for a module this run created, or outside git: then every helper is this run's.
173
- const baseline = await contentAtHead(file);
183
+ const atHead = await readAtHead(file);
184
+ const baseline = "content" in atHead ? atHead.content : undefined;
185
+ if (baseline !== undefined)
186
+ result.incumbentModules.push(file);
187
+ else if ("unknown" in atHead)
188
+ result.headUnknownModules.push(file);
174
189
  const headHelpers = baseline === undefined ? [] : parseUtilsFile(baseline, fileSpec);
175
190
  const incumbents = new Set(headHelpers.map((h) => h.name));
176
191
  incumbentsOfFile.set(file, incumbents);
192
+ headContentOfFile.set(file, baseline);
193
+ contentOfFile.set(file, content);
177
194
  pending.push(...incumbentSignatureViolations(headHelpers, helpers, file));
178
195
  for (const h of helpers) {
179
196
  // A static reading of the signature. An unannotated parameter is an error only
@@ -431,8 +448,10 @@ export async function verifyUtils(params) {
431
448
  // The compiler over the delivered files: the shared module(s) and the spec that
432
449
  // imports them. The executor transpiles TypeScript without typechecking it, so
433
450
  // this is the first typecheck the files meet before the customer's own. An error
434
- // under the permissive settings fails under every configuration and blocks; one
435
- // only the repository's tsconfig raises advises (see typecheckDelivered).
451
+ // under the permissive settings fails under every configuration and blocks when
452
+ // this run caused it — one the file already had at HEAD advises (see the
453
+ // attribution below); one only the repository's tsconfig raises advises (see
454
+ // typecheckDelivered).
436
455
  // Over the TypeScript files only, by extension: a JavaScript module is not
437
456
  // typechecked (its spec would be `checkJs`, which no delivered file relies on),
438
457
  // and a JavaScript spec is not compiled beside a TypeScript module.
@@ -444,7 +463,7 @@ export async function verifyUtils(params) {
444
463
  const realSpec = await realpath(params.testFile);
445
464
  const specFile = realSpec && isTs(realSpec) ? realSpec : undefined;
446
465
  const files = specFile ? [...tsUtils, specFile] : tsUtils;
447
- const tc = await typecheckDelivered(files);
466
+ const tc = await typecheckDelivered(files, { anchor: specFile });
448
467
  result.typecheck = tc;
449
468
  const subjectOf = (file, line) => file === specFile
450
469
  ? path.basename(specFile).replace(/\W/g, "_")
@@ -459,11 +478,96 @@ export async function verifyUtils(params) {
459
478
  const redefined = new Set(pending
460
479
  .filter((v) => v.kind === "helper-redefined")
461
480
  .map((v) => `${v.file}\0${v.helper}`));
481
+ // The files as the repository held them BEFORE this run — each at HEAD —
482
+ // compiled once more, so that an error is excused only when the repository
483
+ // already had it on the same line: attribution by CAUSE. A file HEAD does not
484
+ // hold (a module or a spec this run created) is hidden from that compile and
485
+ // every error in it is this run's — the generator's included: a line any
486
+ // pass of this run wrote is the run's to fix, and only what the customer or
487
+ // an earlier run committed is not.
488
+ const baselineContents = new Map();
489
+ const hidden = new Set();
490
+ const attribution = {
491
+ ran: false,
492
+ headUnknown: [...result.headUnknownModules],
493
+ };
494
+ result.attribution = attribution;
495
+ for (const file of tsUtils) {
496
+ const head = headContentOfFile.get(file);
497
+ if (head === undefined)
498
+ hidden.add(file);
499
+ else
500
+ baselineContents.set(file, head);
501
+ }
502
+ if (specFile) {
503
+ const specAtHead = await readAtHead(specFile);
504
+ if ("content" in specAtHead)
505
+ baselineContents.set(specFile, specAtHead.content);
506
+ else {
507
+ hidden.add(specFile);
508
+ if ("unknown" in specAtHead)
509
+ attribution.headUnknown.push(specFile);
510
+ }
511
+ }
512
+ // Every OTHER file the program may reach — a page object, an in-house helper,
513
+ // a config module — as HEAD held it too: one the run changed is served from
514
+ // HEAD, one the run added does not exist. Read from the working tree, a
515
+ // page-object edit put its error into both compiles and it was excused as
516
+ // inherited. Only the extensions the compiler reads; nothing under
517
+ // node_modules, which the run does not edit.
518
+ // Nothing to attribute on a clean compile: the HEAD compile — a full second
519
+ // program build — is skipped, and the skip is stated so the field is never
520
+ // read as "attribution ran and excused nothing".
521
+ const baselineErrors = new BaselineErrors();
522
+ if (tc.errors.length === 0)
523
+ attribution.reason = "the delivered files compiled clean";
524
+ else if (baselineContents.size === 0)
525
+ attribution.reason =
526
+ "no delivered file exists at HEAD, so there is nothing to inherit from";
527
+ else if (!(await addBaselineForChangedFiles(root, baselineContents, hidden)))
528
+ attribution.reason =
529
+ "the files this run changed could not be listed, so no file could be read at HEAD";
530
+ else {
531
+ const before = await typecheckDelivered([...baselineContents.keys()], {
532
+ anchor: specFile,
533
+ baseline: { contents: baselineContents, hidden },
534
+ });
535
+ if (!before.ran) {
536
+ // The delivered compile ran and this one did not: something in the HEAD
537
+ // content or the host. Stated, never silent — the old errors then block
538
+ // as this run's, which is the outcome attribution exists to prevent.
539
+ attribution.reason = `the HEAD compile did not run: ${before.reason ?? "unknown cause"}`;
540
+ logger.warning("The HEAD compile did not run — no type error will be excused as inherited", { testFile: params.testFile, reason: before.reason });
541
+ }
542
+ else {
543
+ attribution.ran = true;
544
+ for (const e of before.errors)
545
+ baselineErrors.add(baselineErrorKey(e.file, e.line, e.code, e.message));
546
+ }
547
+ }
548
+ const unchangedOf = new Map();
549
+ for (const [file, before] of baselineContents) {
550
+ const now = file === specFile ? specContent : contentOfFile.get(file);
551
+ if (now !== undefined)
552
+ unchangedOf.set(file, alignLines(before, now));
553
+ }
554
+ const inherited = inheritedTypeError((file) => unchangedOf.get(file), baselineErrors);
462
555
  for (const e of tc.errors) {
463
556
  const helper = subjectOf(e.file, e.line);
464
557
  if (REDECLARATION_CODES.has(e.code) &&
465
558
  redefined.has(`${e.file}\0${helper}`))
466
559
  continue;
560
+ const why = inherited(e.file, e.line, e.code, e.message);
561
+ if (why !== undefined) {
562
+ result.advisories.push({
563
+ kind: "inherited-type-error",
564
+ file: e.file,
565
+ helper,
566
+ line: e.line,
567
+ detail: `\`TS${e.code}: ${e.message}\` — ${why}`,
568
+ });
569
+ continue;
570
+ }
467
571
  pending.push({
468
572
  kind: "type-error",
469
573
  file: e.file,
@@ -686,6 +790,50 @@ function remediation(helper, literal, incumbent) {
686
790
  }
687
791
  /** The compared counts as one phrase, shared by the assertion-loss detail and the
688
792
  * PASSED line's note so the two cannot drift. */
793
+ /** Extensions the compiler reads as source. */
794
+ const COMPILED_EXTENSIONS = new Set([
795
+ ".ts",
796
+ ".tsx",
797
+ ".mts",
798
+ ".cts",
799
+ ".js",
800
+ ".jsx",
801
+ ".mjs",
802
+ ".cjs",
803
+ ".json",
804
+ ]);
805
+ /**
806
+ * Add to the baseline every compiled file in the repository that differs from
807
+ * HEAD and is not already accounted for: changed files served from HEAD, added
808
+ * files hidden. Returns whether the listing succeeded — a baseline that cannot
809
+ * be completed must excuse NOTHING, since any file it reads from disk may carry
810
+ * the run's own edit; the caller then compiles no baseline and every error
811
+ * blocks, as before attribution existed.
812
+ */
813
+ async function addBaselineForChangedFiles(root, contents, hidden) {
814
+ if (!root)
815
+ return true; // outside git nothing has a HEAD; nothing is excused
816
+ const compiled = (f) => COMPILED_EXTENSIONS.has(path.extname(f)) &&
817
+ !f.split(path.sep).includes("node_modules");
818
+ try {
819
+ for (const file of await changedSinceHead(root, { filter: "M" })) {
820
+ if (!compiled(file) || contents.has(file) || hidden.has(file))
821
+ continue;
822
+ const head = await contentAtHead(file, root);
823
+ if (head === undefined)
824
+ return false; // git did not answer: excuse nothing
825
+ contents.set(file, head);
826
+ }
827
+ for (const file of await addedSinceHead(root))
828
+ if (compiled(file) && !contents.has(file))
829
+ hidden.add(file);
830
+ return true;
831
+ }
832
+ catch (err) {
833
+ logger.warning("Could not list the files this run changed — no type error will be excused as inherited", { root, error: String(err) });
834
+ return false;
835
+ }
836
+ }
689
837
  export function describeAssertionCount(c) {
690
838
  return `${c.baseline} assertions at the modularization hand-out, ${c.delivered} delivered (${c.inSpec} in the test, ${c.inHelpers} in the helpers it imports and calls)`;
691
839
  }
@@ -1,4 +1,4 @@
1
- export declare const SKYRAMP_IMAGE_VERSION = "v1.3.46";
2
- export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.46";
3
- export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.46";
1
+ export declare const SKYRAMP_IMAGE_VERSION = "v1.3.47";
2
+ export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.47";
3
+ export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.47";
4
4
  export declare const WORKER_CONTROL_PORT = 35142;
@@ -1,4 +1,4 @@
1
- export const SKYRAMP_IMAGE_VERSION = "v1.3.46";
1
+ export const SKYRAMP_IMAGE_VERSION = "v1.3.47";
2
2
  export const EXECUTOR_DOCKER_IMAGE = `skyramp/executor:${SKYRAMP_IMAGE_VERSION}`;
3
3
  export const WORKER_DOCKER_IMAGE = `skyramp/worker:${SKYRAMP_IMAGE_VERSION}`;
4
4
  // Control port the Skyramp worker listens on (SDK `CONTAINER_PORT`).
@@ -88,10 +88,12 @@ Requested path: ${outputZip}` }],
88
88
  counter++;
89
89
  outputZip = `${base}_${counter}${ext}`;
90
90
  }
91
- const { jsonl: jsonlContent, actionCount, skipped } = (0, import_skyRampExport.buildJsonlContent)(ctx.trackedActions, "chromium", ctx.harPath);
91
+ const { jsonl: jsonlContent, actionCount, skipped, simplified } = (0, import_skyRampExport.buildJsonlContent)(ctx.trackedActions, "chromium", ctx.harPath);
92
92
  await (0, import_skyRampExport.writeSkyrampZip)(outputZip, jsonlContent, ctx.harPath);
93
93
  const skippedNote = skipped.length ? `
94
94
  Skipped (not exportable): ${[...new Set(skipped)].join(", ")}` : "";
95
+ const simplifiedNote = simplified.length ? `
96
+ Exported with a simplified locator (a part the trace format cannot carry was dropped, so the step may match more elements at replay): ${[...new Set(simplified)].join("; ")}` : "";
95
97
  const dropped = ctx.droppedActions ?? [];
96
98
  const droppedNote = dropped.length ? `
97
99
  Failed & dropped (not recorded): ${summarizeDropped(dropped)}` : "";
@@ -102,7 +104,7 @@ Failed & dropped (not recorded): ${summarizeDropped(dropped)}` : "";
102
104
  Skyramp zip written to ${outputZip}
103
105
 
104
106
  Contains:
105
- - skyramp_playwright.txt (${actionCount} actions)${skippedNote}${droppedNote}`
107
+ - skyramp_playwright.txt (${actionCount} actions)${skippedNote}${simplifiedNote}${droppedNote}`
106
108
  }]
107
109
  };
108
110
  };