@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.
- package/build/recommendation/answers.d.ts +5 -3
- package/build/recommendation/answers.js +5 -2
- package/build/recommendation/verifierContracts.d.ts +8 -0
- package/build/recommendation/verifierContracts.js +13 -0
- package/build/recommendation/verifiers/coverage.js +143 -4
- package/build/tools/code-refactor/removed-modules.d.ts +33 -0
- package/build/tools/code-refactor/removed-modules.js +61 -0
- package/build/tools/code-refactor/reuse-outcome.d.ts +27 -1
- package/build/tools/code-refactor/reuse-record-store.d.ts +5 -0
- package/build/tools/code-refactor/reuse-record-store.js +15 -1
- package/build/tools/code-refactor/reuse-state.d.ts +17 -4
- package/build/tools/code-refactor/reuse-state.js +67 -9
- package/build/tools/code-refactor/utils-verify-gates.js +39 -15
- package/build/tools/submitReportTool.js +47 -18
- package/build/tools/test-management/registerTestPlanTool.js +1 -1
- package/build/types/ReuseOutcome.d.ts +27 -0
- package/build/utils/utils-verify/allow.d.ts +1 -1
- package/build/utils/utils-verify/allow.js +15 -0
- package/build/utils/utils-verify/changed-lines.d.ts +48 -0
- package/build/utils/utils-verify/changed-lines.js +106 -0
- package/build/utils/utils-verify/head.d.ts +32 -7
- package/build/utils/utils-verify/head.js +55 -14
- package/build/utils/utils-verify/typecheck.d.ts +25 -4
- package/build/utils/utils-verify/typecheck.js +95 -13
- package/build/utils/utils-verify/verify.d.ts +24 -5
- package/build/utils/utils-verify/verify.js +156 -8
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +4 -2
- package/node_modules/playwright/lib/mcp/skyramp/snapshotLocators.js +246 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +221 -233
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +21 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.ZYuEFHsi.js → index.uiPTFncY.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/package.json +1 -1
- 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:
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
//
|
|
798
|
-
//
|
|
799
|
-
//
|
|
800
|
-
//
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
|
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
|
|
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
|
|
57
|
+
export async function readAtHead(file, root) {
|
|
61
58
|
const top = detectGitRoot(root ?? path.dirname(file));
|
|
62
59
|
if (!top)
|
|
63
|
-
return
|
|
60
|
+
return { unknown: true };
|
|
64
61
|
const rel = await gitPath(file, top);
|
|
65
62
|
if (rel === undefined)
|
|
66
|
-
return
|
|
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 (
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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?:
|
|
82
|
+
export declare function typecheckDelivered(files: string[], { mcpRoot, anchor, baseline }?: TypecheckOptions): Promise<UtilsTypecheck>;
|