@skyramp/mcp 0.4.3-rc.1 → 0.4.3-rc.2

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
@@ -380,12 +380,15 @@ function typingAdvisory(r) {
380
380
  ].join("\n");
381
381
  }
382
382
  /** What the compiler run over the delivered files has to add beyond its blocking
383
- * `type-error`s: errors only the repository's tsconfig raises (advised — whether that
384
- * tsconfig is the one their build runs is the customer's call), and the two ways
385
- * the run could fall short — it did not run at all, or an import resolved from
386
- * neither the repository nor the MCP's install, so what that module types went
387
- * unchecked. Both are stated, because a PASSED that says nothing reads as a clean
388
- * compile. Nothing for Python and JavaScript, where `typecheck` is absent. */
383
+ * `type-error`s: the three advisory kinds — a name the compiler could not find
384
+ * (`ambient-type-error`), an error the baseline already had on an unchanged line
385
+ * (`inherited-type-error`), an error only the repository's tsconfig raises
386
+ * (`tsconfig-type-error`; whether that tsconfig is the one their build runs is
387
+ * the customer's call) — and the two ways the run could fall short — it did not
388
+ * run at all, or an import resolved from neither the repository nor the MCP's
389
+ * install, so what that module types went unchecked. All are stated, because a
390
+ * PASSED that says nothing reads as a clean compile. Nothing for Python and
391
+ * JavaScript, where `typecheck` is absent. */
389
392
  function typecheckAdvisory(r) {
390
393
  const tc = r.typecheck;
391
394
  if (!tc)
@@ -395,20 +398,41 @@ function typecheckAdvisory(r) {
395
398
  lines.push("", "", `NOTE — the delivered files were NOT typechecked (${tc.reason ?? "unknown cause"}); only the static shape rules ran, so a type error can still reach the customer's own \`tsc\`. Read the signatures yourself: every parameter typed by what the SDK call accepts, the request options one object literal.`);
396
399
  else if (tc.unresolved.length > 0)
397
400
  lines.push("", "", `NOTE — the typecheck could not resolve ${tc.unresolved.map((m) => `\`${m}\``).join(", ")} from the repository or from the MCP's own install, so whatever those modules type was not checked. If a delivered file imports one of them, confirm the import path and that the package is installed in the repository.`);
398
- const ambient = r.advisories.filter((v) => v.kind === "ambient-type-error");
399
- if (ambient.length > 0) {
400
- const n = ambient.length;
401
- lines.push("", "", `ADVISORY — ${n} name${n === 1 ? "" : "s"} the compiler could not find (does not block):`, ...ambient.map((v) => `- ${show(v.file, r)}:${v.line} · ${v.helper}: ${v.detail}`), "Each is a global (`process`, a test runner's `describe`, `Promise`) whose declarations come from a package the repository installs (`@types/node`, the runner's types) or from its `lib` setting. This run cannot see the repository's installed packages, so it cannot tell whether the name exists there; the repository's own `tsc` decides. Do NOT rewrite the code to dodge this note — never a `globalThis` cast or a hand-written declaration; a `process.env` read is the generated form and stays as it is. Advisory only: continue without re-verifying, and do NOT write a `reuse-verify` marker for it.");
401
+ // Attribution that could not excuse anything, when there was something to
402
+ // excuse: an error the repository already had then blocked as this run's, and
403
+ // the agent must know the block is not a verdict on its own edit.
404
+ const at = r.attribution;
405
+ if (at && !at.ran && tc.errors.length > 0) {
406
+ const unknown = at.headUnknown.map((f) => path.basename(f));
407
+ lines.push("", "", `NOTE — no type error was excused as inherited (${at.reason ?? "unknown cause"}), so an error the repository already had at HEAD blocks above as if this run caused it.${unknown.length > 0
408
+ ? ` Git gave no HEAD answer for ${unknown.join(", ")}, so every error in ${unknown.length === 1 ? "it" : "them"} is judged this run's.`
409
+ : ""} Fix what is yours; for an error on a line you did not write, report it in your submission rather than delete or rewrite the module.`);
402
410
  }
411
+ const ambient = advisoryBlock(r, "ambient-type-error", (n) => `${n} name${n === 1 ? "" : "s"} the compiler could not find`, "Each is a global (`process`, a test runner's `describe`, `Promise`) whose declarations come from a package the repository installs (`@types/node`, the runner's types) or from its `lib` setting. This run cannot see the repository's installed packages, so it cannot tell whether the name exists there; the repository's own `tsc` decides. Do NOT rewrite the code to dodge this note — never a `globalThis` cast or a hand-written declaration; a `process.env` read is the generated form and stays as it is.");
412
+ if (ambient.length > 0)
413
+ lines.push(...ambient);
403
414
  else if (tc.tsconfigNotReaching)
404
415
  lines.push("", "", `NOTE — the nearest tsconfig (${tc.tsconfigNotReaching}) does not include the delivered files, and nor does any project it references, so nothing was checked under the repository's own settings. The errors above are the ones that hold under every configuration. If this repository type-checks its tests, add them to that config's \`include\`.`);
405
- const settings = r.advisories.filter((v) => v.kind === "tsconfig-type-error");
406
- if (settings.length > 0) {
407
- const n = settings.length;
408
- lines.push("", "", `ADVISORY — ${n} error${n === 1 ? "" : "s"} under the repository's own tsconfig (${tc.tsconfig ?? "tsconfig.json"}) (does not block):`, ...settings.map((v) => `- ${show(v.file, r)}:${v.line} · ${v.helper}: ${v.detail}`), "These compile with the strictness flags off and fail only under that tsconfig's settings (`noImplicitAny`, `strictNullChecks`, …). It includes the delivered files, so if the repository's build runs it, these are the errors it will report — fix them the same way as a type-error. Advisory only: continue without re-verifying, and do NOT write a `reuse-verify` marker for it.");
409
- }
416
+ lines.push(...advisoryBlock(r, "inherited-type-error", (n) => `${n} type error${n === 1 ? "" : "s"} the repository already had at HEAD, on ${n === 1 ? "a line" : "lines"} this run did not change`, "These are not the shared module's fault and not this run's to fix: do NOT delete or rewrite the module, restore inline code, or change the repository's installed packages to clear them. The repository's own `tsc` decides whether each one is real."), ...advisoryBlock(r, "tsconfig-type-error", (n) => `${n} error${n === 1 ? "" : "s"} under the repository's own tsconfig (${tc.tsconfig ?? "tsconfig.json"})`, "These compile with the strictness flags off and fail only under that tsconfig's settings (`noImplicitAny`, `strictNullChecks`, …). It includes the delivered files, so if the repository's build runs it, these are the errors it will report — fix them the same way as a type-error."));
410
417
  return lines.join("\n");
411
418
  }
419
+ /** One compiler advisory block: the findings of `kind`, or nothing. Every kind
420
+ * renders the same way — a count in the heading, one line per finding, the
421
+ * kind's own explanation, then the standing instruction that an advisory is
422
+ * neither re-verified nor documented with a marker — so the shape is stated
423
+ * once and a kind supplies only its heading and its explanation. */
424
+ function advisoryBlock(r, kind, heading, explanation) {
425
+ const found = r.advisories.filter((v) => v.kind === kind);
426
+ if (found.length === 0)
427
+ return [];
428
+ return [
429
+ "",
430
+ "",
431
+ `ADVISORY — ${heading(found.length)} (does not block):`,
432
+ ...found.map((v) => `- ${show(v.file, r)}:${v.line} · ${v.helper}: ${v.detail}`),
433
+ `${explanation} Advisory only: continue without re-verifying, and do NOT write a \`reuse-verify\` marker for it.`,
434
+ ];
435
+ }
412
436
  /** Browser helpers whose Playwright action sequences agree — one operation under two
413
437
  * names, the drift a second run compounds. Rendered beside the other
414
438
  * duplicate-detection advisories (inline call sites, un-extracted duplicates), after
@@ -25,7 +25,8 @@ import { budgetExcuseRefusals, citesBudget, countBudgetExcuseRefusal, MAX_BUDGET
25
25
  import { countUiDeliveryRefusal, MAX_UI_DELIVERY_REFUSALS, traceExportAttempts, uiDeliveryRefusals, } from "../playwright/traceExportStore.js";
26
26
  import { getReportLanguage, isEnforcedReportLanguage, findLanguageViolations, findLanguageNearMisses, reportLanguageDisplayName, } from "../utils/reportLanguage.js";
27
27
  import { canonicalTestPath, findAssertionRecordByFileName, rederiveAssertionOutcome, } from "./code-refactor/assertion-state.js";
28
- import { rederiveReuse, deriveReuseFromDelivered, REUSE_SUBMIT_MAX_REFUSALS, reuseChainSkipped, samePath, } from "./code-refactor/reuse-state.js";
28
+ import { canonPath, canonPathEvenIfGone, rederiveReuse, deriveReuseFromDelivered, REUSE_SUBMIT_MAX_REFUSALS, reuseChainSkipped, samePath, } from "./code-refactor/reuse-state.js";
29
+ import { removedModulesFrom, } from "./code-refactor/removed-modules.js";
29
30
  import { retrofitGate, } from "./code-refactor/retrofit-state.js";
30
31
  import { changedHelpersForRow, deriveChangedHelpers, discloseChangedHelpers, } from "./code-refactor/changed-helper-report.js";
31
32
  import { onlyCommentLinesRemoved, stripGateMarkers, } from "./code-refactor/gate-markers.js";
@@ -722,7 +723,7 @@ specPath,
722
723
  * and the row states that reason. Omitting it altogether says the row has no
723
724
  * delivered file to read — a VERIFY row, or one this run never wrote. */
724
725
  deliveredPath) {
725
- const { outcomes, retrofits, changedHelpers, blocking, consumed, evaluated } = ctx;
726
+ const { outcomes, retrofits, changedHelpers, removedModules, blocking, consumed, evaluated, } = ctx;
726
727
  let { handOffs } = ctx;
727
728
  // POM records describe browser specs; a basename collision with an API test's
728
729
  // fileName must not attach them there. A utils-path record is attachable anywhere.
@@ -791,22 +792,49 @@ deliveredPath) {
791
792
  const base = derived || chainSkipped
792
793
  ? { ...(derived ?? {}), ...(chainSkipped ? { chainSkipped } : {}) }
793
794
  : fallback;
794
- // Helpers this run changed under callers it never touched. Attached to the row
795
- // itself rather than inside `helpers`, and outside the record branch, on purpose:
796
- // a run that deleted or renamed the shared module has no helpers row to hang this
797
- // on — verifyUtils locates nothing — and that is the case with the most to
798
- // disclose, since every helper went at once. It is layered OVER the assembled row
799
- // rather than folded into `derived`, so a row that would otherwise be answered by
800
- // the import-derived fallback still gets that answer as well.
801
- const reuse = changedHelperViews.length > 0
802
- ? {
803
- ...(base ?? {}),
804
- // "This run rewrote code you already had, and here is who calls it" — a
805
- // statement the report has never made: it already names a module that was
806
- // CREATED, and nothing named one that was changed.
807
- changedHelpers: discloseChangedHelpers(changedHelperViews, changedHelpers.executions),
808
- }
809
- : base;
795
+ // Two disclosures about a MODULE, layered over the assembled row rather than
796
+ // folded into `derived`, and outside the record branch, on purpose: a run that
797
+ // deleted or renamed the shared module has no helpers row to hang them on —
798
+ // verifyUtils locates nothing — and that is the case with the most to disclose.
799
+ // A row that would otherwise be answered by the import-derived fallback still
800
+ // gets that answer as well.
801
+ // - `changedHelpers`: "this run rewrote code you already had, and here is who
802
+ // calls it" — a statement the report has never made: it already names a
803
+ // module that was CREATED, and nothing named one that was changed.
804
+ // - `removedModules`: a module this run created beside this test and removed
805
+ // again; without it the row reads "nothing to reuse" over a run that built a
806
+ // module, rewired tests onto it and deleted it again. A row gets the modules
807
+ // its own record located — wherever they lived — and those in its own
808
+ // directory IN ITS OWN LANGUAGE, the tests the module was built beside: a
809
+ // Python row beside a TypeScript module was not one of them. Both sides
810
+ // canonical: the module path is a realpath from locateUtilsFiles, the spec
811
+ // path is as supplied, and on a symlinked checkout (macOS /tmp) the two
812
+ // never match raw.
813
+ const rowDir = rowSpecPath ? path.dirname(canonPath(rowSpecPath)) : undefined;
814
+ const rowLanguage = rowSpecPath
815
+ ? utilsSpecForFile(rowSpecPath)?.language
816
+ : undefined;
817
+ const listedByRecord = new Set((found?.utils?.modules ?? []).map((m) => canonPathEvenIfGone(m.file)));
818
+ const removedHere = removedModules.filter((m) => {
819
+ const file = canonPathEvenIfGone(m.file);
820
+ return (listedByRecord.has(file) ||
821
+ (path.dirname(file) === rowDir &&
822
+ rowLanguage !== undefined &&
823
+ utilsSpecForFile(file)?.language === rowLanguage));
824
+ });
825
+ const extras = {
826
+ ...(changedHelperViews.length > 0
827
+ ? {
828
+ changedHelpers: discloseChangedHelpers(changedHelperViews, changedHelpers.executions),
829
+ }
830
+ : {}),
831
+ ...(removedHere.length > 0
832
+ ? {
833
+ removedModules: removedHere.map(({ file: _file, ...entry }) => entry),
834
+ }
835
+ : {}),
836
+ };
837
+ const reuse = Object.keys(extras).length > 0 ? { ...(base ?? {}), ...extras } : base;
810
838
  if (!reuse)
811
839
  return test;
812
840
  // Pre-existing generated tests this spec's reuse pass rewired (SKYR-4276 A4): the
@@ -1978,6 +2006,7 @@ export function registerSubmitReportTool(server) {
1978
2006
  handOffs: stateData.reuseHandOffs,
1979
2007
  retrofits: retrofitViews,
1980
2008
  changedHelpers,
2009
+ removedModules: await removedModulesFrom(stateData.reuseOutcomes),
1981
2010
  blocking: reuseBlocking,
1982
2011
  consumed: consumedDeclines,
1983
2012
  evaluated: evaluatedDeclineFiles,
@@ -587,7 +587,7 @@ const registerTestPlanSchema = {
587
587
  blocker: z
588
588
  .string()
589
589
  .optional()
590
- .describe("What stopped this run from writing the test: a service that is not running, a paired branch that no longer exists, the one credential the run holds. An objection about a change or a defect with no test closes only with this. A reason the change is not worth testing is not a blocker."),
590
+ .describe("What stopped this run from writing the test: a service that is not running, a paired branch that no longer exists, the one credential the run holds. An objection about a change or a defect with no test, or about a value a page and API change refuses that no UI test names, closes only with this. A reason the change is not worth testing is not a blocker."),
591
591
  })
592
592
  .strict())
593
593
  .default([])
@@ -204,6 +204,28 @@ export interface OrphanedHelper {
204
204
  utilsFile: string;
205
205
  helper: string;
206
206
  }
207
+ /** A shared module this run CREATED and then REMOVED before delivery, with the
208
+ * reason the run state holds for it.
209
+ *
210
+ * A run builds a module, rewires tests onto it, and — when its verify pass keeps
211
+ * failing — deletes it and restores the inline code. Nothing then locates the
212
+ * module: the delivered tests import nothing, and every row reads as "nothing to
213
+ * reuse", which is the silence this entry exists to break. An incumbent module the
214
+ * run deleted is disclosed helper by helper under {@link ChangedHelperEntry}
215
+ * (`removed`); this is the module HEAD never held, which that comparison cannot
216
+ * see. Attached to every row in the module's own directory — the tests the module
217
+ * was built beside. */
218
+ export interface RemovedModuleEntry {
219
+ /** REPOSITORY-RELATIVE path (posix) the module had. */
220
+ utilsFile: string;
221
+ /** In a reader's words: that the run created and removed it, and what the last
222
+ * verify pass that had it said. */
223
+ reason: string;
224
+ /** The failures of the last verify pass that had the module, when it failed —
225
+ * the same shape as {@link HelperReuseOutcome.verificationFailures}. Absent when
226
+ * that pass passed. */
227
+ verificationFailures?: ReuseVerificationFailure[];
228
+ }
207
229
  /** Shared-helper reuse for an API test, re-derived from the delivered files at report
208
230
  * time — nothing here is read back from what the agent or the verify pass recorded.
209
231
  * Absent altogether when no utils file exists for the test (the same omission rule
@@ -392,6 +414,11 @@ export interface ReuseOutcome {
392
414
  * rightly absent, which is exactly when a reader most needs to be told. See
393
415
  * {@link ChangedHelperEntry}. */
394
416
  changedHelpers?: ChangedHelperEntry[];
417
+ /** Shared modules this run created beside this test and removed again before
418
+ * delivery — see {@link RemovedModuleEntry}. Top level like `changedHelpers`,
419
+ * and for the same reason: there is no `helpers` row left to hang it on, which
420
+ * is exactly when a reader most needs to be told. Omitted when none. */
421
+ removedModules?: RemovedModuleEntry[];
395
422
  verification?: ReuseVerificationOutcome;
396
423
  /** Why `verification` is `failed` on the page-object path, on the same terms as
397
424
  * {@link HelperReuseOutcome.verificationFailures}: present only when the
@@ -74,7 +74,7 @@ export type UtilsViolationKind = "duplicate-helper" | "body-assertion" | "scenar
74
74
  /** Kinds that only ever advise. Kept OUT of UtilsViolationKind so that type keeps
75
75
  * meaning "a kind the allow grammar and the gate know about" — an advisory can
76
76
  * never fail a verify and never needs (or accepts) an allow marker. */
77
- export declare const UTILS_ADVISORY_KINDS: readonly ["unusable-parameter-type", "fixed-sleep", "module-name", "duplicate-body-reach", "duplicate-action", "tsconfig-type-error", "ambient-type-error", "in-house-duplicate"];
77
+ export declare const UTILS_ADVISORY_KINDS: readonly ["unusable-parameter-type", "fixed-sleep", "module-name", "duplicate-body-reach", "duplicate-action", "tsconfig-type-error", "ambient-type-error", "inherited-type-error", "in-house-duplicate"];
78
78
  export type UtilsAdvisoryKind = (typeof UTILS_ADVISORY_KINDS)[number];
79
79
  export declare const MARKERABLE_UTILS_KINDS: readonly ["frozen-body", "duplicate-helper", "body-assertion", "scenario-name", "assertion-loss", "status-default", "status-restated"];
80
80
  /** A kind the allow grammar accepts. `guard-removed` is a gate kind but never
@@ -50,6 +50,21 @@ export const UTILS_ADVISORY_KINDS = [
50
50
  * never blocked: a block here made an agent rewrite `process.env` into a
51
51
  * `globalThis` cast to get past the gate. */
52
52
  "ambient-type-error",
53
+ /** A compiler error this run INHERITED: the line is unchanged since HEAD AND
54
+ * the file at HEAD already failed the compiler with the same code on that
55
+ * line. Whatever the error is, the customer or an earlier run wrote it, it is
56
+ * not the shared module's fault, and it must never be the reason a valid
57
+ * module is deleted: a run once destroyed its module over four assertion lines
58
+ * that pre-dated the run. Advised, never blocked.
59
+ *
60
+ * Attribution is by CAUSE, not by position: an error the run caused on a line
61
+ * it left alone — a helper rewritten to return nothing under an unchanged line
62
+ * that reads its result — was not there at HEAD and blocks. A file HEAD does
63
+ * not hold (a module or a spec this run generated) has nothing to inherit
64
+ * from, and every error in it blocks, the generator's included: a line any
65
+ * pass of this run wrote is the run's to fix. See inheritedTypeError in
66
+ * changed-lines.ts. */
67
+ "inherited-type-error",
53
68
  /** A helper this run added to one of the repository's own modules performs an
54
69
  * action sequence a helper already in that module performs — the duplicate the
55
70
  * extension exists to avoid, now inside the customer's file. Advisory for the
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The lines of `after` that `before` carries unchanged, as a map from the line's
3
+ * number in `after` to its number in `before` (both 1-based), by a
4
+ * longest-common-subsequence alignment of the two files' lines. A line that only
5
+ * SHIFTED because lines were inserted or removed around it is unchanged; a line
6
+ * that was rewritten, or written new, is absent. Two lines that swapped places
7
+ * read as one unchanged and one changed — the alignment is a subsequence, not a
8
+ * reordering.
9
+ *
10
+ * Lines are broken where the compiler breaks them (see `split`), so a CRLF
11
+ * checkout does not read as a whole-file rewrite and a line number here is the
12
+ * one a diagnostic carries.
13
+ *
14
+ * The alignment table is quadratic in the two line counts, which is nothing for a
15
+ * spec of a few hundred lines. Past a fixed cell budget no alignment is made and
16
+ * the map is EMPTY: every line then reads as this pass's, which is the closed
17
+ * direction — the gate that reads this map excuses an error only on a line it can
18
+ * map, so a file too large to align is judged as before this map existed.
19
+ */
20
+ export declare function alignLines(before: string, after: string): Map<number, number>;
21
+ /** One compiler error the baseline had: the file as delivered, the line in the
22
+ * BASELINE's numbering, the TS code and the flattened message. Code alone let
23
+ * a NEW error hide behind an old one of the same code — `label` changed from
24
+ * returning a string to a boolean is a second TS2322 on the same line. */
25
+ export declare const baselineErrorKey: (file: string, line: number, code: number, message: string) => string;
26
+ /** The baseline's errors as a MULTISET: each excuses at most one current error.
27
+ * A line that had one error at HEAD and has two now carries one the run caused. */
28
+ export declare class BaselineErrors {
29
+ private readonly left;
30
+ add(key: string): void;
31
+ /** Consume one occurrence; false when none is left. */
32
+ take(key: string): boolean;
33
+ }
34
+ /**
35
+ * Why a compiler error at `file:line` is not this run's, or `undefined` when it
36
+ * is. A block is a demand to fix the line named, and a demand the run cannot
37
+ * meet ends in the module being deleted — so the compiler blocks ONLY on an
38
+ * error this run caused, and an error is inherited only by CAUSE: the line is
39
+ * unchanged since HEAD AND the file at HEAD already failed the compiler with the
40
+ * same code and message on that line, once per occurrence. Position alone is
41
+ * not enough — extraction breaks lines it leaves alone (a helper that no longer
42
+ * returns what the next line reads), and those errors are the run's.
43
+ *
44
+ * `unchangedOf` gives each file's unchanged lines mapped to HEAD's numbering (see
45
+ * alignLines), or `undefined` for a file HEAD does not hold: every error in such
46
+ * a file is this run's.
47
+ */
48
+ export declare function inheritedTypeError(unchangedOf: (file: string) => ReadonlyMap<number, number> | undefined, baselineErrors: BaselineErrors): (file: string, line: number, code: number, message: string) => string | undefined;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The lines of `after` that `before` carries unchanged, as a map from the line's
3
+ * number in `after` to its number in `before` (both 1-based), by a
4
+ * longest-common-subsequence alignment of the two files' lines. A line that only
5
+ * SHIFTED because lines were inserted or removed around it is unchanged; a line
6
+ * that was rewritten, or written new, is absent. Two lines that swapped places
7
+ * read as one unchanged and one changed — the alignment is a subsequence, not a
8
+ * reordering.
9
+ *
10
+ * Lines are broken where the compiler breaks them (see `split`), so a CRLF
11
+ * checkout does not read as a whole-file rewrite and a line number here is the
12
+ * one a diagnostic carries.
13
+ *
14
+ * The alignment table is quadratic in the two line counts, which is nothing for a
15
+ * spec of a few hundred lines. Past a fixed cell budget no alignment is made and
16
+ * the map is EMPTY: every line then reads as this pass's, which is the closed
17
+ * direction — the gate that reads this map excuses an error only on a line it can
18
+ * map, so a file too large to align is judged as before this map existed.
19
+ */
20
+ export function alignLines(before, after) {
21
+ const a = split(before);
22
+ const b = split(after);
23
+ const unchanged = new Map();
24
+ if (a.length * b.length > MAX_CELLS)
25
+ return unchanged;
26
+ // lcs[i][j]: length of the LCS of a[i..] and b[j..].
27
+ const w = b.length + 1;
28
+ const lcs = new Uint32Array((a.length + 1) * w);
29
+ for (let i = a.length - 1; i >= 0; i--)
30
+ for (let j = b.length - 1; j >= 0; j--)
31
+ lcs[i * w + j] =
32
+ a[i] === b[j]
33
+ ? lcs[(i + 1) * w + j + 1] + 1
34
+ : Math.max(lcs[(i + 1) * w + j], lcs[i * w + j + 1]);
35
+ // Walk the table: a matched pair is an unchanged line of `after`; a step that
36
+ // advances `after` alone is a line the edit wrote or changed.
37
+ let i = 0;
38
+ let j = 0;
39
+ while (j < b.length) {
40
+ if (i < a.length && a[i] === b[j]) {
41
+ unchanged.set(j + 1, i + 1);
42
+ i++;
43
+ j++;
44
+ }
45
+ else if (i < a.length && lcs[(i + 1) * w + j] >= lcs[i * w + j + 1]) {
46
+ i++;
47
+ }
48
+ else {
49
+ j++;
50
+ }
51
+ }
52
+ return unchanged;
53
+ }
54
+ const MAX_CELLS = 4_000_000;
55
+ /** Lines as the COMPILER counts them — CRLF, LF, a bare CR, U+2028 and U+2029
56
+ * each end a line — so a line number here is the line number a diagnostic
57
+ * carries. Split on LF alone, a U+2028 inside a string literal put every
58
+ * number after it one out of step, and the unchanged check read the wrong line. */
59
+ function split(content) {
60
+ return content.split(/\r\n|\r|\n|\u2028|\u2029/);
61
+ }
62
+ /** One compiler error the baseline had: the file as delivered, the line in the
63
+ * BASELINE's numbering, the TS code and the flattened message. Code alone let
64
+ * a NEW error hide behind an old one of the same code — `label` changed from
65
+ * returning a string to a boolean is a second TS2322 on the same line. */
66
+ export const baselineErrorKey = (file, line, code, message) => `${file}\0${line}\0${code}\0${message}`;
67
+ /** The baseline's errors as a MULTISET: each excuses at most one current error.
68
+ * A line that had one error at HEAD and has two now carries one the run caused. */
69
+ export class BaselineErrors {
70
+ left = new Map();
71
+ add(key) {
72
+ this.left.set(key, (this.left.get(key) ?? 0) + 1);
73
+ }
74
+ /** Consume one occurrence; false when none is left. */
75
+ take(key) {
76
+ const n = this.left.get(key) ?? 0;
77
+ if (n === 0)
78
+ return false;
79
+ this.left.set(key, n - 1);
80
+ return true;
81
+ }
82
+ }
83
+ /**
84
+ * Why a compiler error at `file:line` is not this run's, or `undefined` when it
85
+ * is. A block is a demand to fix the line named, and a demand the run cannot
86
+ * meet ends in the module being deleted — so the compiler blocks ONLY on an
87
+ * error this run caused, and an error is inherited only by CAUSE: the line is
88
+ * unchanged since HEAD AND the file at HEAD already failed the compiler with the
89
+ * same code and message on that line, once per occurrence. Position alone is
90
+ * not enough — extraction breaks lines it leaves alone (a helper that no longer
91
+ * returns what the next line reads), and those errors are the run's.
92
+ *
93
+ * `unchangedOf` gives each file's unchanged lines mapped to HEAD's numbering (see
94
+ * alignLines), or `undefined` for a file HEAD does not hold: every error in such
95
+ * a file is this run's.
96
+ */
97
+ export function inheritedTypeError(unchangedOf, baselineErrors) {
98
+ return (file, line, code, message) => {
99
+ const before = unchangedOf(file)?.get(line);
100
+ if (before === undefined)
101
+ return undefined;
102
+ return baselineErrors.take(baselineErrorKey(file, before, code, message))
103
+ ? `the line is unchanged since HEAD, where it already failed the compiler the same way (line ${before})`
104
+ : undefined;
105
+ };
106
+ }
@@ -12,18 +12,35 @@
12
12
  * file), and a malfunction must neither become a finding nor vanish. Stated once,
13
13
  * so two readers cannot drift on which errors mean which. */
14
14
  export declare function isAbsentFromHead(err: unknown): boolean;
15
+ /** What git says about a file at HEAD: its content, that HEAD does not hold it,
16
+ * or that git gave no answer — outside a repository, or a git failure. The
17
+ * third is never folded into the second: "absent" is a claim (this run created
18
+ * the file), "unknown" is the absence of one. */
19
+ export type AtHead = {
20
+ content: string;
21
+ } | {
22
+ absent: true;
23
+ } | {
24
+ unknown: true;
25
+ };
15
26
  /**
16
- * The file's content at HEAD, or `undefined` when there is none to read.
17
- *
18
- * `undefined` covers three cases that must all stay silent: outside a repository, the
19
- * file absent from HEAD (this run created it), and git failing to answer. Reporting
20
- * anything about a file on any of them would be a claim the evidence does not carry.
21
- * Only the third is logged, as a warning — a malfunction must not vanish either.
27
+ * The file at HEAD, as one of the three answers above.
22
28
  *
23
29
  * `root` is any directory of the checkout, and an optimisation for a caller that
24
30
  * already holds one. The repository's top level is resolved from it — or from the
25
31
  * file's directory, which is still there for a file this run deleted — because
26
- * `HEAD:<path>` speaks in paths from the top.
32
+ * `HEAD:<path>` speaks in paths from the top. A file outside the top level is
33
+ * `unknown`: HEAD has no path for it. A git malfunction is logged, as a warning —
34
+ * it must not vanish either.
35
+ */
36
+ export declare function readAtHead(file: string, root?: string): Promise<AtHead>;
37
+ /**
38
+ * The file's content at HEAD, or `undefined` when there is none to read.
39
+ *
40
+ * `undefined` covers the two non-content answers of `readAtHead`, which must both
41
+ * stay silent for a caller that only compares: reporting anything about a file on
42
+ * either would be a claim the evidence does not carry. A caller that STATES
43
+ * whether this run created the file reads `readAtHead` and keeps them apart.
27
44
  */
28
45
  export declare function contentAtHead(file: string, root?: string): Promise<string | undefined>;
29
46
  /**
@@ -46,3 +63,11 @@ export declare function contentAtHead(file: string, root?: string): Promise<stri
46
63
  export declare function changedSinceHead(root: string, opts: {
47
64
  filter: string;
48
65
  }): Promise<string[]>;
66
+ /**
67
+ * Absolute, CANONICAL paths of the files in the repository `root` is in that HEAD
68
+ * does not hold: added to the index, or untracked and not ignored. What
69
+ * `changedSinceHead` cannot list — `git diff HEAD` never sees an untracked file —
70
+ * for a caller that must know what did not exist before this run. Throws when git
71
+ * cannot answer, on the same terms.
72
+ */
73
+ export declare function addedSinceHead(root: string): Promise<string[]>;
@@ -45,39 +45,49 @@ async function gitPath(file, top) {
45
45
  return rel.split(path.sep).join("/");
46
46
  }
47
47
  /**
48
- * The file's content at HEAD, or `undefined` when there is none to read.
49
- *
50
- * `undefined` covers three cases that must all stay silent: outside a repository, the
51
- * file absent from HEAD (this run created it), and git failing to answer. Reporting
52
- * anything about a file on any of them would be a claim the evidence does not carry.
53
- * Only the third is logged, as a warning — a malfunction must not vanish either.
48
+ * The file at HEAD, as one of the three answers above.
54
49
  *
55
50
  * `root` is any directory of the checkout, and an optimisation for a caller that
56
51
  * already holds one. The repository's top level is resolved from it — or from the
57
52
  * file's directory, which is still there for a file this run deleted — because
58
- * `HEAD:<path>` speaks in paths from the top.
53
+ * `HEAD:<path>` speaks in paths from the top. A file outside the top level is
54
+ * `unknown`: HEAD has no path for it. A git malfunction is logged, as a warning —
55
+ * it must not vanish either.
59
56
  */
60
- export async function contentAtHead(file, root) {
57
+ export async function readAtHead(file, root) {
61
58
  const top = detectGitRoot(root ?? path.dirname(file));
62
59
  if (!top)
63
- return undefined;
60
+ return { unknown: true };
64
61
  const rel = await gitPath(file, top);
65
62
  if (rel === undefined)
66
- return undefined;
63
+ return { unknown: true };
67
64
  try {
68
65
  const { stdout } = await execFileAsync("git", ["show", `HEAD:${rel}`], {
69
66
  cwd: top,
70
67
  encoding: "utf8",
71
68
  maxBuffer: 16 * 1024 * 1024,
72
69
  });
73
- return stdout;
70
+ return { content: stdout };
74
71
  }
75
72
  catch (err) {
76
- if (!isAbsentFromHead(err))
77
- logger.warning("Could not read a file at HEAD — whatever compares against it will not run", { file, error: String(err) });
78
- return undefined;
73
+ if (isAbsentFromHead(err))
74
+ return { absent: true };
75
+ logger.warning("Could not read a file at HEAD — whatever compares against it will not run", { file, error: String(err) });
76
+ return { unknown: true };
79
77
  }
80
78
  }
79
+ /**
80
+ * The file's content at HEAD, or `undefined` when there is none to read.
81
+ *
82
+ * `undefined` covers the two non-content answers of `readAtHead`, which must both
83
+ * stay silent for a caller that only compares: reporting anything about a file on
84
+ * either would be a claim the evidence does not carry. A caller that STATES
85
+ * whether this run created the file reads `readAtHead` and keeps them apart.
86
+ */
87
+ export async function contentAtHead(file, root) {
88
+ const at = await readAtHead(file, root);
89
+ return "content" in at ? at.content : undefined;
90
+ }
81
91
  /**
82
92
  * Absolute, CANONICAL paths of the files that differ from HEAD in the repository
83
93
  * `root` is in, by git's `--diff-filter` letters (`M` modified, `D` deleted, `DM`
@@ -113,3 +123,34 @@ export async function changedSinceHead(root, opts) {
113
123
  out.push(await canonicalEvenIfGone(path.resolve(realTop, rel)));
114
124
  return out;
115
125
  }
126
+ /**
127
+ * Absolute, CANONICAL paths of the files in the repository `root` is in that HEAD
128
+ * does not hold: added to the index, or untracked and not ignored. What
129
+ * `changedSinceHead` cannot list — `git diff HEAD` never sees an untracked file —
130
+ * for a caller that must know what did not exist before this run. Throws when git
131
+ * cannot answer, on the same terms.
132
+ */
133
+ export async function addedSinceHead(root) {
134
+ const top = detectGitRoot(root);
135
+ if (!top)
136
+ throw new Error(`${root} is not inside a git repository`);
137
+ const run = async (args) => (await execFileAsync("git", args, { cwd: top, encoding: "utf8" })).stdout
138
+ .split("\0")
139
+ .filter(Boolean);
140
+ const rels = [
141
+ ...(await run([
142
+ "diff",
143
+ "-z",
144
+ "--name-only",
145
+ "--diff-filter=A",
146
+ "--no-renames",
147
+ "HEAD",
148
+ ])),
149
+ ...(await run(["ls-files", "-z", "--others", "--exclude-standard"])),
150
+ ];
151
+ const realTop = await realpath(top);
152
+ const out = [];
153
+ for (const rel of rels)
154
+ out.push(await canonicalEvenIfGone(path.resolve(realTop, rel)));
155
+ return out;
156
+ }
@@ -44,18 +44,39 @@ export interface UtilsTypecheck {
44
44
  unresolved: string[];
45
45
  }
46
46
  export declare const AMBIENT_DECLARATION_CODES: Set<number>;
47
+ /** The delivered files as the repository held them at HEAD, and the files that
48
+ * did not exist then. With a baseline the check is the permissive pass alone: it
49
+ * exists to say which errors this run inherited, and the settings pass never
50
+ * blocks. */
51
+ export interface TypecheckBaseline {
52
+ contents: ReadonlyMap<string, string>;
53
+ hidden: ReadonlySet<string>;
54
+ }
55
+ export interface TypecheckOptions {
56
+ mcpRoot?: string;
57
+ /** The delivered file whose directory stands for the repository: where global
58
+ * types are looked up from, and where the executor packages' peers resolve
59
+ * from for EVERY file. The spec, when one is delivered — it is the consumer,
60
+ * and in a monorepo its package's Playwright is the one the executor runs,
61
+ * not a hoisted root copy the shared module's directory would find. Defaults
62
+ * to the first file. */
63
+ anchor?: string;
64
+ baseline?: TypecheckBaseline;
65
+ }
47
66
  /**
48
67
  * Typecheck the delivered TypeScript files — the shared module and the spec that
49
68
  * imports it — with the compiler the MCP itself depends on, so the check runs in
50
69
  * every repository whether or not `typescript` is installed there.
51
70
  *
52
71
  * Two passes over the same files. The PERMISSIVE pass has every strictness flag off:
53
- * an error it reports fails under any configuration and is blocking (`errors`). The
54
- * SETTINGS pass runs the repository's own tsconfig, when one reaches the files; what
55
- * it adds is an error only under those settings and is advisory (`settingErrors`).
72
+ * an error it reports fails under any configuration (`errors`) — the verifier
73
+ * blocks on it unless the repository already had it at HEAD, which the same
74
+ * function establishes when called with a `baseline`. The SETTINGS pass runs the
75
+ * repository's own tsconfig, when one reaches the files; what it adds is an error
76
+ * only under those settings and is advisory (`settingErrors`).
56
77
  * The executor transpiles without typechecking, so neither pass has run before this
57
78
  * one, and a delivered file that fails here reaches the customer's own `tsc` first.
58
79
  *
59
80
  * Never throws: a compiler failure is a result that says the check did not run.
60
81
  */
61
- export declare function typecheckDelivered(files: string[], mcpRoot?: string): Promise<UtilsTypecheck>;
82
+ export declare function typecheckDelivered(files: string[], { mcpRoot, anchor, baseline }?: TypecheckOptions): Promise<UtilsTypecheck>;