@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
|
@@ -20,8 +20,10 @@ export declare function unknownAnswerObjections(objections: Objection[], acknowl
|
|
|
20
20
|
export declare const NON_ANSWERABLE_PREFIX = "coverage:stateTest:";
|
|
21
21
|
export declare function isNonAnswerable(objectionId: unknown): boolean;
|
|
22
22
|
/** The objections a sentence alone does not close either. A change with no test
|
|
23
|
-
* needs the test,
|
|
24
|
-
*
|
|
23
|
+
* needs the test, a value a page and API change refuses needs a UI test or a
|
|
24
|
+
* maintained spec that names it (the XR02 evidence is at `surfaceReject` in
|
|
25
|
+
* `verifierContracts.ts`), and a defect with no test needs the test — a defect
|
|
26
|
+
* the code already had before this pull request included. Each closes only when the
|
|
25
27
|
* answer also names what stopped the test: a service that is not running, a
|
|
26
28
|
* branch that no longer exists, the one credential the run holds. Measured on run
|
|
27
29
|
* 34176038240: across eight fixtures 45 objections were raised and 45 were
|
|
@@ -29,7 +31,7 @@ export declare function isNonAnswerable(objectionId: unknown): boolean;
|
|
|
29
31
|
* Prose closed every uncovered change, including changes nothing prevented a test
|
|
30
32
|
* from reaching. Runs 34423214322 and 34423217796 then lost three defects the
|
|
31
33
|
* baseline reported: each was listed and closed by a sentence. */
|
|
32
|
-
export declare const BLOCKER_ONLY_PREFIXES: readonly ["coverage:change:", "defects:untested:"];
|
|
34
|
+
export declare const BLOCKER_ONLY_PREFIXES: readonly ["coverage:change:", "coverage:surfaceReject:", "defects:untested:"];
|
|
33
35
|
export declare function needsBlocker(objectionId: unknown): boolean;
|
|
34
36
|
/** The objection raised below carries this prefix and the id it objects to. */
|
|
35
37
|
export declare const REFUSED_ANSWER_PREFIX = "answers:refused:";
|
|
@@ -53,8 +53,10 @@ export function isNonAnswerable(objectionId) {
|
|
|
53
53
|
objectionId.startsWith(NON_ANSWERABLE_PREFIX));
|
|
54
54
|
}
|
|
55
55
|
/** The objections a sentence alone does not close either. A change with no test
|
|
56
|
-
* needs the test,
|
|
57
|
-
*
|
|
56
|
+
* needs the test, a value a page and API change refuses needs a UI test or a
|
|
57
|
+
* maintained spec that names it (the XR02 evidence is at `surfaceReject` in
|
|
58
|
+
* `verifierContracts.ts`), and a defect with no test needs the test — a defect
|
|
59
|
+
* the code already had before this pull request included. Each closes only when the
|
|
58
60
|
* answer also names what stopped the test: a service that is not running, a
|
|
59
61
|
* branch that no longer exists, the one credential the run holds. Measured on run
|
|
60
62
|
* 34176038240: across eight fixtures 45 objections were raised and 45 were
|
|
@@ -64,6 +66,7 @@ export function isNonAnswerable(objectionId) {
|
|
|
64
66
|
* baseline reported: each was listed and closed by a sentence. */
|
|
65
67
|
export const BLOCKER_ONLY_PREFIXES = [
|
|
66
68
|
"coverage:change:",
|
|
69
|
+
"coverage:surfaceReject:",
|
|
67
70
|
"defects:untested:",
|
|
68
71
|
];
|
|
69
72
|
export function needsBlocker(objectionId) {
|
|
@@ -188,6 +188,14 @@ export declare const COVERAGE_CONTRACT: {
|
|
|
188
188
|
readonly message: "No {kind} test cites this change, which is on the {surface}.";
|
|
189
189
|
readonly suggestion: "Add a test on that surface that cites this change, or answer in one line why that surface needs no test.";
|
|
190
190
|
};
|
|
191
|
+
readonly surfaceCase: {
|
|
192
|
+
readonly message: "This change is on the page and the API, and no ui or e2e test that cites it chooses `{case}` on the page.";
|
|
193
|
+
readonly suggestion: "Add a ui or e2e test, or a step in one, that chooses that value on the page and names it in the step, as in `Principal radio`, or answer in one line why the page cannot send it.";
|
|
194
|
+
};
|
|
195
|
+
readonly surfaceReject: {
|
|
196
|
+
readonly message: "This change is on the page and the API, and no ui or e2e test that cites it tries `{case}` on the page or checks that the page no longer offers it.";
|
|
197
|
+
readonly suggestion: "Add a ui or e2e test, or a step in one, that names that value in the step, as in `Intern radio absent`: try it and assert the page refuses it, or assert the page no longer offers it. If an existing UI spec already does this and this run edits it, declare it in `maintains` with its real path and name the value in `differsBy`. If this run cannot write the test, answer it and set `blocker` to what stopped the run; an answer with no blocker leaves this open.";
|
|
198
|
+
};
|
|
191
199
|
};
|
|
192
200
|
readonly suggestion: "Which planned test tests each behaviour this change promises and each file it touches, and for one nothing covers, why does it need none?";
|
|
193
201
|
};
|
|
@@ -202,6 +202,19 @@ export const COVERAGE_CONTRACT = {
|
|
|
202
202
|
message: "No {kind} test cites this change, which is on the {surface}.",
|
|
203
203
|
suggestion: "Add a test on that surface that cites this change, or answer in one line why that surface needs no test.",
|
|
204
204
|
},
|
|
205
|
+
// `{case}` is the rendered case, as in `level="Principal" (accept)`. SKYR-4529: a form offered Lead and Principal, and
|
|
206
|
+
// only an API test sent Principal.
|
|
207
|
+
surfaceCase: {
|
|
208
|
+
message: "This change is on the page and the API, and no ui or e2e test that cites it chooses `{case}` on the page.",
|
|
209
|
+
suggestion: "Add a ui or e2e test, or a step in one, that chooses that value on the page and names it in the step, as in `Principal radio`, or answer in one line why the page cannot send it.",
|
|
210
|
+
},
|
|
211
|
+
// `{case}` is the rendered case, as in `level="Principal" (accept)`. The XR02 run removed Intern from a form; the page
|
|
212
|
+
// objection closed by a sentence, and no generated test checked the option was
|
|
213
|
+
// gone.
|
|
214
|
+
surfaceReject: {
|
|
215
|
+
message: "This change is on the page and the API, and no ui or e2e test that cites it tries `{case}` on the page or checks that the page no longer offers it.",
|
|
216
|
+
suggestion: "Add a ui or e2e test, or a step in one, that names that value in the step, as in `Intern radio absent`: try it and assert the page refuses it, or assert the page no longer offers it. If an existing UI spec already does this and this run edits it, declare it in `maintains` with its real path and name the value in `differsBy`. If this run cannot write the test, answer it and set `blocker` to what stopped the run; an answer with no blocker leaves this open.",
|
|
217
|
+
},
|
|
205
218
|
},
|
|
206
219
|
suggestion: "Which planned test tests each behaviour this change promises and each file it touches, and for one nothing covers, why does it need none?",
|
|
207
220
|
};
|
|
@@ -5,6 +5,7 @@ import { isTestPath } from "../../utils/testFileClassification.js";
|
|
|
5
5
|
import { maintainedFileIsInItsRepository, normalizeCitedPath, } from "./citedPath.js";
|
|
6
6
|
import { COVERAGE_CONTRACT, fillPlaceholders } from "../verifierContracts.js";
|
|
7
7
|
import { isUIPlannedTest, stateRouteKey, subjectStep } from "../subjectStep.js";
|
|
8
|
+
import { normalizeStepMethod } from "../../types/StepMethod.js";
|
|
8
9
|
/** Whether a path can be product code at all, by the deny list route grounding
|
|
9
10
|
* already applies to a cited file. Nothing else is excluded: a file's kind is
|
|
10
11
|
* not this check's judgment to make. */
|
|
@@ -640,9 +641,6 @@ function rejectObjections(registration) {
|
|
|
640
641
|
}
|
|
641
642
|
return objections;
|
|
642
643
|
}
|
|
643
|
-
// ── The surface a change is on ──────────────────────────────────────────────
|
|
644
|
-
/** The test types that reach each surface. An `e2e` test drives the page AND the
|
|
645
|
-
* requests behind it, so it counts for both. */
|
|
646
644
|
const TEST_TYPES_FOR = {
|
|
647
645
|
api: {
|
|
648
646
|
types: new Set(["contract", "integration", "e2e"]),
|
|
@@ -658,7 +656,7 @@ function declaredSurfaces(change) {
|
|
|
658
656
|
return [];
|
|
659
657
|
return [
|
|
660
658
|
...new Set(named.map((surface) => String(surface ?? "").trim())),
|
|
661
|
-
].filter((surface) => surface
|
|
659
|
+
].filter((surface) => Object.hasOwn(TEST_TYPES_FOR, surface));
|
|
662
660
|
}
|
|
663
661
|
/** A change on a page needs a test that opens the page, and a change on the API
|
|
664
662
|
* needs one that calls it. Run 34065056830 cited a page change from an API test
|
|
@@ -698,6 +696,146 @@ function surfaceObjections(registration) {
|
|
|
698
696
|
}
|
|
699
697
|
return objections;
|
|
700
698
|
}
|
|
699
|
+
/** Whether `text` names `value` as a whole word, ignoring case. Letters in any
|
|
700
|
+
* script: an ASCII boundary found "Gr" in "Größe". */
|
|
701
|
+
function namesWord(text, value) {
|
|
702
|
+
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
703
|
+
return new RegExp(`(?<![\\p{L}\\p{N}])${escaped}(?![\\p{L}\\p{N}])`, "iu").test(text);
|
|
704
|
+
}
|
|
705
|
+
/** An `absent` case names no value, so it draws no page objection. */
|
|
706
|
+
function pageObjectionKind(testCase) {
|
|
707
|
+
if (testCase.absent === true)
|
|
708
|
+
return undefined;
|
|
709
|
+
if (testCase.expect === "accept")
|
|
710
|
+
return "surfaceCase";
|
|
711
|
+
if (testCase.expect === "reject")
|
|
712
|
+
return "surfaceReject";
|
|
713
|
+
return undefined;
|
|
714
|
+
}
|
|
715
|
+
/** The value as a UI step would spell it, or undefined when no step can: a number,
|
|
716
|
+
* a boolean, text with no letter and a `{placeholder}` are never spelled in a step,
|
|
717
|
+
* and a page chooses no HTTP verb. */
|
|
718
|
+
function valueAPageSpells(testCase) {
|
|
719
|
+
const param = testCase.param.trim();
|
|
720
|
+
const value = testCase.value;
|
|
721
|
+
if (!param || param.toLowerCase() === METHOD_PARAM)
|
|
722
|
+
return undefined;
|
|
723
|
+
if (typeof value !== "string")
|
|
724
|
+
return undefined;
|
|
725
|
+
const word = value.trim();
|
|
726
|
+
if (!/\p{L}/u.test(word))
|
|
727
|
+
return undefined;
|
|
728
|
+
if (/^(true|false)$/i.test(word) || PLACEHOLDER_VALUE.test(word))
|
|
729
|
+
return undefined;
|
|
730
|
+
return word;
|
|
731
|
+
}
|
|
732
|
+
/** The page steps that choose a value. An `assert` that the radio is visible
|
|
733
|
+
* chose nothing: the XR02 run passed Principal on one, and no test saved at it.
|
|
734
|
+
* An e2e test's HTTP step is not the page either. */
|
|
735
|
+
const CHOOSING_VERBS = new Set([
|
|
736
|
+
"click",
|
|
737
|
+
"type",
|
|
738
|
+
"press",
|
|
739
|
+
"tap",
|
|
740
|
+
]);
|
|
741
|
+
/** A refused value is also met by asserting the page no longer offers it. */
|
|
742
|
+
const REFUSING_VERBS = new Set([
|
|
743
|
+
...CHOOSING_VERBS,
|
|
744
|
+
"assert",
|
|
745
|
+
]);
|
|
746
|
+
/** The text that can name a value on the page: the steps of the UI tests whose verb
|
|
747
|
+
* is in `verbs`, and the edit note of each maintained spec, which is prose with no
|
|
748
|
+
* verb to read. */
|
|
749
|
+
function pageTexts(uiTests, maintained, verbs) {
|
|
750
|
+
return [
|
|
751
|
+
...uiTests.flatMap((plannedTest) => plannedTest.scenario.steps
|
|
752
|
+
.filter((step) => verbs.has(normalizeStepMethod(step.method) ?? ""))
|
|
753
|
+
.flatMap((step) => [step.path, step.description ?? ""])),
|
|
754
|
+
...maintained.map((entry) => entry.differsBy),
|
|
755
|
+
];
|
|
756
|
+
}
|
|
757
|
+
/** A change on the page AND the API needs each accepted value chosen on the page,
|
|
758
|
+
* and each refused value tried or shown absent there, not only sent by an API
|
|
759
|
+
* test. `surfaceObjections` passes the page once any UI test cites the change,
|
|
760
|
+
* whatever value it picks. A UI step is free text (`Senior radio`), so the check
|
|
761
|
+
* reads whether a page step of a citing ui or e2e test that chooses the value (or,
|
|
762
|
+
* for a refused one, asserts it), or the `differsBy` of a citing maintained spec,
|
|
763
|
+
* names the value.
|
|
764
|
+
*
|
|
765
|
+
* A refused value takes a test, a resolving maintained spec, or a blocker, never a
|
|
766
|
+
* sentence: in the XR02 run the page objection for a removed option closed by
|
|
767
|
+
* prose, and only an existing spec checked that the option was gone. */
|
|
768
|
+
function surfaceCaseObjections(registration, ctx) {
|
|
769
|
+
const objections = [];
|
|
770
|
+
const seen = new Set();
|
|
771
|
+
const pageTypes = TEST_TYPES_FOR.page.types;
|
|
772
|
+
for (const change of registration.changes) {
|
|
773
|
+
const id = change.id.trim();
|
|
774
|
+
const surfaces = declaredSurfaces(change);
|
|
775
|
+
if (!id || !surfaces.includes("page") || !surfaces.includes("api"))
|
|
776
|
+
continue;
|
|
777
|
+
const uiTests = registration.plannedTests.filter((plannedTest) => citesChange(plannedTest, id) &&
|
|
778
|
+
pageTypes.has(plannedTest.scenario.testType.toLowerCase()));
|
|
779
|
+
// A maintained spec counts only when its file resolves, the bar
|
|
780
|
+
// `citedBehaviors` sets: the refused half closes by no sentence, so a note on a
|
|
781
|
+
// file nobody can open must not close it either.
|
|
782
|
+
const maintained = (registration.maintains ?? []).filter((entry) => entry.changes.some((cited) => cited.trim() === id) &&
|
|
783
|
+
maintainedFileIsInItsRepository(entry, ctx));
|
|
784
|
+
const citedAtAll = maintained.length > 0 ||
|
|
785
|
+
registration.plannedTests.some((plannedTest) => citesChange(plannedTest, id));
|
|
786
|
+
const texts = {
|
|
787
|
+
surfaceCase: pageTexts(uiTests, maintained, CHOOSING_VERBS),
|
|
788
|
+
surfaceReject: pageTexts(uiTests, maintained, REFUSING_VERBS),
|
|
789
|
+
};
|
|
790
|
+
for (const testCase of change.testCases ?? []) {
|
|
791
|
+
const kind = pageObjectionKind(testCase);
|
|
792
|
+
// An accepted value with no UI test and no maintained spec already draws
|
|
793
|
+
// `coverage:surface:<id>:page`, and a second objection would need a second
|
|
794
|
+
// answer for one gap. A maintained spec alone does not stop that objection,
|
|
795
|
+
// but a sentence citing the spec closed it while a value went untried, so the
|
|
796
|
+
// value is checked then. A refused value is always checked: that sentence is
|
|
797
|
+
// the gap. A change nothing cites draws `coverage:change:<id>`.
|
|
798
|
+
if (!kind || !citedAtAll)
|
|
799
|
+
continue;
|
|
800
|
+
if (kind === "surfaceCase" &&
|
|
801
|
+
uiTests.length === 0 &&
|
|
802
|
+
maintained.length === 0)
|
|
803
|
+
continue;
|
|
804
|
+
const word = valueAPageSpells(testCase);
|
|
805
|
+
if (word === undefined)
|
|
806
|
+
continue;
|
|
807
|
+
if (texts[kind].some((text) => namesWord(text, word)))
|
|
808
|
+
continue;
|
|
809
|
+
const rendered = renderTestCase({
|
|
810
|
+
...testCase,
|
|
811
|
+
param: testCase.param.trim(),
|
|
812
|
+
value: word,
|
|
813
|
+
});
|
|
814
|
+
const objectionId = `coverage:${kind}:${id}:page:${rendered}`;
|
|
815
|
+
// One step closes `Intern` and `intern` alike, so they are one objection.
|
|
816
|
+
const key = objectionId.toLowerCase();
|
|
817
|
+
if (seen.has(key))
|
|
818
|
+
continue;
|
|
819
|
+
seen.add(key);
|
|
820
|
+
const citers = [
|
|
821
|
+
...uiTests.map((plannedTest) => plannedTest.plannedTestId),
|
|
822
|
+
...maintained.map((entry) => entry.file),
|
|
823
|
+
];
|
|
824
|
+
objections.push({
|
|
825
|
+
objectionId,
|
|
826
|
+
verifier: "coverage",
|
|
827
|
+
message: fillPlaceholders(COVERAGE_CONTRACT.objections[kind].message, {
|
|
828
|
+
case: rendered,
|
|
829
|
+
}),
|
|
830
|
+
evidence: citers.length === 0
|
|
831
|
+
? `No ui or e2e test or maintained spec cites ${id}.`
|
|
832
|
+
: `${citers.join(", ")} cite ${id}; no step or maintained edit of theirs names ${word}.`,
|
|
833
|
+
suggestion: COVERAGE_CONTRACT.objections[kind].suggestion,
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return objections;
|
|
838
|
+
}
|
|
701
839
|
/** One repository's changed files, and the name to attribute uncovered ones to.
|
|
702
840
|
* Keying the objection id on the path alone let two repositories that each change
|
|
703
841
|
* `src/App.tsx` share one id, so every id names its repository. */
|
|
@@ -759,6 +897,7 @@ export const coverage = {
|
|
|
759
897
|
...testCaseObjections(registration),
|
|
760
898
|
...rejectObjections(registration),
|
|
761
899
|
...surfaceObjections(registration),
|
|
900
|
+
...surfaceCaseObjections(registration, ctx),
|
|
762
901
|
];
|
|
763
902
|
if (allChangedFiles(ctx).length === 0)
|
|
764
903
|
return objections;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { UnifiedAnalysisState } from "../../utils/AnalysisStateManager.js";
|
|
2
|
+
import type { RemovedModuleEntry } from "../../types/ReuseOutcome.js";
|
|
3
|
+
/**
|
|
4
|
+
* A shared module the run created and then removed — the one thing about a
|
|
5
|
+
* module that cannot be re-derived from the delivered files, because there is no
|
|
6
|
+
* file left to derive from. The verify pass records what it located and how the
|
|
7
|
+
* pass ended (`locatedModules`, in reuse-state.ts beside the record writes); the
|
|
8
|
+
* report reads the records for a run-created module that is no longer on disk
|
|
9
|
+
* (`removedModulesFrom`). This module imports reuse-state.ts and nothing here
|
|
10
|
+
* is imported back, so the two cannot load each other half-initialised.
|
|
11
|
+
*/
|
|
12
|
+
/** A shared module a run created and removed again, read off the records. `file`
|
|
13
|
+
* is the absolute path for the caller's directory match; the rest is the wire
|
|
14
|
+
* entry. */
|
|
15
|
+
export type RemovedModule = RemovedModuleEntry & {
|
|
16
|
+
file: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The modules the run created and then removed, across every record: each module a
|
|
20
|
+
* verify pass located that HEAD did not hold and that is no longer on disk. One
|
|
21
|
+
* entry per module, however many specs' passes located it.
|
|
22
|
+
*
|
|
23
|
+
* The reason is what the record holds and nothing more: the last verify pass that
|
|
24
|
+
* had the module, and how it ended. The run's own motive is not on record — an
|
|
25
|
+
* agent deletes a module after a failed pass, and the failures are the closest
|
|
26
|
+
* thing to its reason — so the entry states the verdict and lets the reader draw
|
|
27
|
+
* the line. A module removed after a PASSED verify is stated as such. Nor does
|
|
28
|
+
* the entry say what the delivered tests import instead: a module the run
|
|
29
|
+
* RENAMED is gone from this path and present under another, and the row's
|
|
30
|
+
* `helpers` field names that one. The entry claims only what it measured — the
|
|
31
|
+
* path is not in the delivery.
|
|
32
|
+
*/
|
|
33
|
+
export declare function removedModulesFrom(outcomes: UnifiedAnalysisState["reuseOutcomes"]): Promise<RemovedModule[]>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { stat } from "fs/promises";
|
|
2
|
+
import { utilsFileLabel } from "./reuse-state.js";
|
|
3
|
+
/**
|
|
4
|
+
* The modules the run created and then removed, across every record: each module a
|
|
5
|
+
* verify pass located that HEAD did not hold and that is no longer on disk. One
|
|
6
|
+
* entry per module, however many specs' passes located it.
|
|
7
|
+
*
|
|
8
|
+
* The reason is what the record holds and nothing more: the last verify pass that
|
|
9
|
+
* had the module, and how it ended. The run's own motive is not on record — an
|
|
10
|
+
* agent deletes a module after a failed pass, and the failures are the closest
|
|
11
|
+
* thing to its reason — so the entry states the verdict and lets the reader draw
|
|
12
|
+
* the line. A module removed after a PASSED verify is stated as such. Nor does
|
|
13
|
+
* the entry say what the delivered tests import instead: a module the run
|
|
14
|
+
* RENAMED is gone from this path and present under another, and the row's
|
|
15
|
+
* `helpers` field names that one. The entry claims only what it measured — the
|
|
16
|
+
* path is not in the delivery.
|
|
17
|
+
*/
|
|
18
|
+
export async function removedModulesFrom(outcomes) {
|
|
19
|
+
// Several specs' records can hold one module; the LATEST pass over it is the
|
|
20
|
+
// verdict it was removed under, and the latest answer on whether this run
|
|
21
|
+
// created it — whichever record holds it. Latest FIRST, then the claim: an
|
|
22
|
+
// older "created" must not outrank a newer "found at HEAD".
|
|
23
|
+
const latest = new Map();
|
|
24
|
+
for (const record of Object.values(outcomes ?? {}))
|
|
25
|
+
for (const m of record.utils?.modules ?? []) {
|
|
26
|
+
const held = latest.get(m.file);
|
|
27
|
+
if (!held || m.lastPass.at > held.lastPass.at)
|
|
28
|
+
latest.set(m.file, m);
|
|
29
|
+
}
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const m of latest.values()) {
|
|
32
|
+
if (!m.createdByThisRun)
|
|
33
|
+
continue;
|
|
34
|
+
if (await exists(m.file))
|
|
35
|
+
continue;
|
|
36
|
+
const { failed, failures, errored } = m.lastPass;
|
|
37
|
+
const verdict = errored
|
|
38
|
+
? "could not run (the verifier threw)"
|
|
39
|
+
: !failed
|
|
40
|
+
? "passed, so the run gave no reason"
|
|
41
|
+
: failures
|
|
42
|
+
? `failed on ${failures.map((f) => `${f.kind} ×${f.count}`).join(", ")}`
|
|
43
|
+
: "failed outside the file's invariants (a staging refusal)";
|
|
44
|
+
out.push({
|
|
45
|
+
file: m.file,
|
|
46
|
+
utilsFile: utilsFileLabel([m.file]),
|
|
47
|
+
reason: `this run created the module, and it is not in the delivery; the last verify pass that had it ${verdict}`,
|
|
48
|
+
...(failures ? { verificationFailures: failures } : {}),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
async function exists(file) {
|
|
54
|
+
try {
|
|
55
|
+
await stat(file);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { VerifyResult } from "../../utils/pom-verify/index.js";
|
|
2
2
|
import { ReuseVerificationOutcome, type InHouseDeclinedEntry, type ReuseSkippedEntry } from "../../types/ReuseOutcome.js";
|
|
3
|
-
import { HelperVerificationOutcome } from "../../types/ReuseOutcome.js";
|
|
3
|
+
import { HelperVerificationOutcome, type ReuseVerificationFailure } from "../../types/ReuseOutcome.js";
|
|
4
4
|
export { ReuseDeclinedBy, ReuseVerificationOutcome, HelperVerificationOutcome, type ReuseOutcome, type ReuseSkippedEntry, } from "../../types/ReuseOutcome.js";
|
|
5
5
|
/** A member the verifier flagged at some point in this run. NOT the same as "a
|
|
6
6
|
* member that was demoted": the workflow allows one remap attempt, so a flagged
|
|
@@ -87,6 +87,32 @@ export interface UtilsReuseRecord {
|
|
|
87
87
|
* repository's own browser helper modules: the files it read, the helpers it
|
|
88
88
|
* declined. Testimony, kept for the report and labelled there as such. */
|
|
89
89
|
inHouseDeclared?: InHouseDeclaration;
|
|
90
|
+
/** Every shared module a verify pass of this spec located, accumulated across
|
|
91
|
+
* passes. A pass that locates a module overwrites its entry; a pass that
|
|
92
|
+
* locates none touches nothing — that is the pass after the run DELETED the
|
|
93
|
+
* module, and the record is the only place the module's existence survives. */
|
|
94
|
+
modules?: LocatedModule[];
|
|
95
|
+
}
|
|
96
|
+
export interface LocatedModule {
|
|
97
|
+
file: string;
|
|
98
|
+
/** HEAD lacked it: this run created it. `false` for an incumbent. ABSENT when
|
|
99
|
+
* git gave no answer — outside a repository, or a git failure — so the report
|
|
100
|
+
* never says "this run created it" on no evidence. */
|
|
101
|
+
createdByThisRun?: boolean;
|
|
102
|
+
/** The last verify pass that had this module: when, whether it failed, and on
|
|
103
|
+
* what. The PASS's failures, filed under each module it had: a failure on the
|
|
104
|
+
* spec side (a type error at a call site, an assertion loss) names no module,
|
|
105
|
+
* and it is still the verdict the module was removed under. A failed pass with
|
|
106
|
+
* no failures failed outside the file's invariants — a staging refusal. */
|
|
107
|
+
lastPass: {
|
|
108
|
+
/** ISO time, so the latest pass wins when several specs' records hold the
|
|
109
|
+
* same module. */
|
|
110
|
+
at: string;
|
|
111
|
+
failed: boolean;
|
|
112
|
+
failures?: ReuseVerificationFailure[];
|
|
113
|
+
/** The pass threw before it could judge anything. `failed` is true too. */
|
|
114
|
+
errored?: true;
|
|
115
|
+
};
|
|
90
116
|
}
|
|
91
117
|
export interface InHouseDeclaration {
|
|
92
118
|
filesRead: string[];
|
|
@@ -47,6 +47,11 @@ export declare function owns(record: ReuseRecord, testFile: string): boolean;
|
|
|
47
47
|
export declare function samePath(a: string, b: string): boolean;
|
|
48
48
|
/** Exported for retrofit-state: the same canonicalisation the records use. */
|
|
49
49
|
export declare function canonPath(p: string): string;
|
|
50
|
+
/** Canonical path for a file that may be GONE: `realpath` fails on a missing
|
|
51
|
+
* file and the fallback leaves a symlinked parent (macOS `/tmp` → `/private/tmp`)
|
|
52
|
+
* unresolved, so a deleted module never matched a canonical spec path. The
|
|
53
|
+
* nearest existing ancestor is canonicalised and the missing tail put back. */
|
|
54
|
+
export declare function canonPathEvenIfGone(p: string): string;
|
|
50
55
|
/** Whether the run state file can be written — the precondition for any debt this
|
|
51
56
|
* module holds, since only a write clears it. */
|
|
52
57
|
export declare function runStateWritable(explicitStateFile?: string): Promise<boolean>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
2
|
import { access, constants as fsConstants } from "fs/promises";
|
|
3
|
-
import { realpathSync } from "fs";
|
|
3
|
+
import { existsSync, realpathSync } from "fs";
|
|
4
4
|
import { logger } from "../../utils/logger.js";
|
|
5
5
|
import { resolveRunStatePath, StateManager, } from "../../utils/AnalysisStateManager.js";
|
|
6
6
|
import { realpath } from "../../utils/utils-verify/index.js";
|
|
@@ -86,6 +86,7 @@ export function freshRecord() {
|
|
|
86
86
|
utils: undefined,
|
|
87
87
|
modularized: undefined,
|
|
88
88
|
pomPassFinished: undefined,
|
|
89
|
+
baseline: undefined,
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
92
|
/** Whether `record` describes `testFile` (or nobody yet). Basename-keyed run state
|
|
@@ -102,6 +103,19 @@ export function samePath(a, b) {
|
|
|
102
103
|
export function canonPath(p) {
|
|
103
104
|
return canon(p);
|
|
104
105
|
}
|
|
106
|
+
/** Canonical path for a file that may be GONE: `realpath` fails on a missing
|
|
107
|
+
* file and the fallback leaves a symlinked parent (macOS `/tmp` → `/private/tmp`)
|
|
108
|
+
* unresolved, so a deleted module never matched a canonical spec path. The
|
|
109
|
+
* nearest existing ancestor is canonicalised and the missing tail put back. */
|
|
110
|
+
export function canonPathEvenIfGone(p) {
|
|
111
|
+
let probe = p;
|
|
112
|
+
const tail = [];
|
|
113
|
+
while (!existsSync(probe) && path.dirname(probe) !== probe) {
|
|
114
|
+
tail.unshift(path.basename(probe));
|
|
115
|
+
probe = path.dirname(probe);
|
|
116
|
+
}
|
|
117
|
+
return path.join(canon(probe), ...tail);
|
|
118
|
+
}
|
|
105
119
|
function canon(p) {
|
|
106
120
|
try {
|
|
107
121
|
return realpathSync(p);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { type ReuseHandOff, type ReuseHandOffSource } from "../../utils/AnalysisStateManager.js";
|
|
2
2
|
import { type VerifyResult } from "../../utils/pom-verify/index.js";
|
|
3
|
-
import { type InHouseDeclaration, type ReuseOutcome, type ReuseRecord } from "./reuse-outcome.js";
|
|
3
|
+
import { type InHouseDeclaration, type LocatedModule, type ReuseOutcome, type ReuseRecord } from "./reuse-outcome.js";
|
|
4
4
|
import { type UtilsVerifyResult, type UtilsViolationKind } from "../../utils/utils-verify/index.js";
|
|
5
5
|
import type { HelperReuseOutcome, ReuseVerificationFailure } from "../../types/ReuseOutcome.js";
|
|
6
6
|
/** The canonical path comparison the records use — declared in the store, re-exported
|
|
7
7
|
* here for the readers that always imported it from this module. */
|
|
8
|
-
export { samePath, canonPath } from "./reuse-record-store.js";
|
|
8
|
+
export { samePath, canonPath, canonPathEvenIfGone, } from "./reuse-record-store.js";
|
|
9
9
|
/**
|
|
10
10
|
* Persistence for the server-derived POM-reuse outcome, keyed by test-file
|
|
11
11
|
* basename (what `newTestsCreated[].fileName` carries, so
|
|
@@ -105,8 +105,21 @@ export declare function recordUtilsVerifyError(testFile: string, explicitStateFi
|
|
|
105
105
|
* repository, or for a file the root does not contain, the basename stands in.
|
|
106
106
|
*/
|
|
107
107
|
export declare function utilsFileLabel(files: string[]): string;
|
|
108
|
-
/**
|
|
109
|
-
*
|
|
108
|
+
/** The record's module list after one verify pass: every module the pass located
|
|
109
|
+
* gets a fresh entry — whether HEAD lacked it, and the pass's failures when it
|
|
110
|
+
* failed — and every module the pass did not locate keeps its entry. A pass that
|
|
111
|
+
* locates nothing is the pass after the run DELETED the module, and the record
|
|
112
|
+
* is the only place the module's existence survives. */
|
|
113
|
+
export declare function locatedModules(prior: LocatedModule[] | undefined, r: UtilsVerifyResult): LocatedModule[];
|
|
114
|
+
/** The record's module list after a verify pass that THREW: every module keeps
|
|
115
|
+
* its entry and is stamped with the failed pass, so a module removed after it
|
|
116
|
+
* never reads as removed after a pass that passed. */
|
|
117
|
+
export declare function modulesAfterVerifyError(prior: LocatedModule[] | undefined): LocatedModule[];
|
|
118
|
+
/** Verdict of one utils verify pass. The verdict is stored — counts and the file
|
|
119
|
+
* list are re-derived from the delivered files at report time — and so are the
|
|
120
|
+
* modules the pass located and the failures of a failed pass: a module the run
|
|
121
|
+
* removes afterwards can be re-derived from nothing, and its row must still say
|
|
122
|
+
* it existed and why it went. */
|
|
110
123
|
export declare function recordUtilsVerifyOutcome(testFile: string, r: UtilsVerifyResult, language: string, framework: string, testType: string | undefined, explicitStateFile?: string,
|
|
111
124
|
/** The agent's declarations from this verify call, when it made any. */
|
|
112
125
|
inHouseDeclared?: InHouseDeclaration): Promise<boolean>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
1
2
|
import * as fsp from "fs/promises";
|
|
2
3
|
import * as path from "path";
|
|
3
4
|
import { readFile, stat } from "fs/promises";
|
|
@@ -18,11 +19,11 @@ const execFileAsync = promisify(execFile);
|
|
|
18
19
|
import { resolvePomCatalogPath } from "../../utils/pom-catalog.js";
|
|
19
20
|
import { parsePomCatalog, findMissedReuse, } from "../../utils/pom-catalog-parse.js";
|
|
20
21
|
import { ProgrammingLanguage } from "../../types/TestTypes.js";
|
|
21
|
-
import { canonPath, freshRecord, owns, readHandOff, readReuseRecord, readRunState, runStateWritable, samePath, updateReuseOutcome, } from "./reuse-record-store.js";
|
|
22
|
+
import { canonPath, canonPathEvenIfGone, freshRecord, owns, readHandOff, readReuseRecord, readRunState, runStateWritable, samePath, updateReuseOutcome, } from "./reuse-record-store.js";
|
|
22
23
|
import { combineRederivations, pomFirstHandOff } from "./pom-pass-state.js";
|
|
23
24
|
/** The canonical path comparison the records use — declared in the store, re-exported
|
|
24
25
|
* here for the readers that always imported it from this module. */
|
|
25
|
-
export { samePath, canonPath } from "./reuse-record-store.js";
|
|
26
|
+
export { samePath, canonPath, canonPathEvenIfGone, } from "./reuse-record-store.js";
|
|
26
27
|
/**
|
|
27
28
|
* Persistence for the server-derived POM-reuse outcome, keyed by test-file
|
|
28
29
|
* basename (what `newTestsCreated[].fileName` carries, so
|
|
@@ -184,7 +185,15 @@ export async function recordUtilsReuseIssued(testFile, language, framework, test
|
|
|
184
185
|
* run must not leave a debt nothing can pay — and cleared by the next successful pass. */
|
|
185
186
|
export async function recordUtilsVerifyError(testFile, explicitStateFile) {
|
|
186
187
|
await updateReuseOutcome(testFile, (existing) => owns(existing, testFile) && existing.utils
|
|
187
|
-
? {
|
|
188
|
+
? {
|
|
189
|
+
utils: {
|
|
190
|
+
...existing.utils,
|
|
191
|
+
verifyError: true,
|
|
192
|
+
...(existing.utils.modules
|
|
193
|
+
? { modules: modulesAfterVerifyError(existing.utils.modules) }
|
|
194
|
+
: {}),
|
|
195
|
+
},
|
|
196
|
+
}
|
|
188
197
|
: {}, explicitStateFile);
|
|
189
198
|
}
|
|
190
199
|
/**
|
|
@@ -202,12 +211,17 @@ export function utilsFileLabel(files) {
|
|
|
202
211
|
return files.map(repoRelativeLabel).join(", ");
|
|
203
212
|
}
|
|
204
213
|
function repoRelativeLabel(file) {
|
|
205
|
-
|
|
214
|
+
// Canonical even when GONE: a module the run REMOVED has no file to stat, and
|
|
215
|
+
// its label must still name where in the repository it was.
|
|
216
|
+
const canonFile = canonPathEvenIfGone(file);
|
|
217
|
+
let probe = path.dirname(canonFile);
|
|
218
|
+
while (!existsSync(probe) && path.dirname(probe) !== probe)
|
|
219
|
+
probe = path.dirname(probe);
|
|
220
|
+
const root = detectGitRoot(probe);
|
|
206
221
|
if (!root)
|
|
207
222
|
return path.basename(file);
|
|
208
|
-
// Both sides canonical: git reports the REAL root
|
|
209
|
-
|
|
210
|
-
const rel = path.relative(canonPath(root), canonPath(file));
|
|
223
|
+
// Both sides canonical: git reports the REAL root.
|
|
224
|
+
const rel = path.relative(canonPath(root), canonFile);
|
|
211
225
|
// Outside the root means a leading `..` SEGMENT — not any name starting with two
|
|
212
226
|
// dots (`..generated/SkyrampUtils.py` is a valid in-repository path).
|
|
213
227
|
const outside = rel === ".." || rel.startsWith(`..${path.sep}`);
|
|
@@ -215,8 +229,50 @@ function repoRelativeLabel(file) {
|
|
|
215
229
|
return path.basename(file);
|
|
216
230
|
return rel.split(path.sep).join("/");
|
|
217
231
|
}
|
|
218
|
-
/**
|
|
219
|
-
*
|
|
232
|
+
/** The record's module list after one verify pass: every module the pass located
|
|
233
|
+
* gets a fresh entry — whether HEAD lacked it, and the pass's failures when it
|
|
234
|
+
* failed — and every module the pass did not locate keeps its entry. A pass that
|
|
235
|
+
* locates nothing is the pass after the run DELETED the module, and the record
|
|
236
|
+
* is the only place the module's existence survives. */
|
|
237
|
+
export function locatedModules(prior, r) {
|
|
238
|
+
const incumbents = new Set(r.incumbentModules);
|
|
239
|
+
const unknown = new Set(r.headUnknownModules);
|
|
240
|
+
const modules = new Map((prior ?? []).map((m) => [m.file, m]));
|
|
241
|
+
const failures = r.ok ? [] : verificationFailures(r);
|
|
242
|
+
const lastPass = {
|
|
243
|
+
at: new Date().toISOString(),
|
|
244
|
+
failed: !r.ok,
|
|
245
|
+
...(failures.length > 0 ? { failures } : {}),
|
|
246
|
+
};
|
|
247
|
+
for (const file of r.utilsFiles) {
|
|
248
|
+
// Git gave no answer this pass: an earlier pass's answer stands. A transient
|
|
249
|
+
// failure on the last pass before a deletion must not erase the claim.
|
|
250
|
+
const createdByThisRun = unknown.has(file)
|
|
251
|
+
? modules.get(file)?.createdByThisRun
|
|
252
|
+
: !incumbents.has(file);
|
|
253
|
+
modules.set(file, {
|
|
254
|
+
file,
|
|
255
|
+
...(createdByThisRun === undefined ? {} : { createdByThisRun }),
|
|
256
|
+
lastPass,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return [...modules.values()];
|
|
260
|
+
}
|
|
261
|
+
/** The record's module list after a verify pass that THREW: every module keeps
|
|
262
|
+
* its entry and is stamped with the failed pass, so a module removed after it
|
|
263
|
+
* never reads as removed after a pass that passed. */
|
|
264
|
+
export function modulesAfterVerifyError(prior) {
|
|
265
|
+
const at = new Date().toISOString();
|
|
266
|
+
return (prior ?? []).map((m) => ({
|
|
267
|
+
...m,
|
|
268
|
+
lastPass: { at, failed: true, errored: true },
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
/** Verdict of one utils verify pass. The verdict is stored — counts and the file
|
|
272
|
+
* list are re-derived from the delivered files at report time — and so are the
|
|
273
|
+
* modules the pass located and the failures of a failed pass: a module the run
|
|
274
|
+
* removes afterwards can be re-derived from nothing, and its row must still say
|
|
275
|
+
* it existed and why it went. */
|
|
220
276
|
export async function recordUtilsVerifyOutcome(testFile, r, language, framework, testType, explicitStateFile,
|
|
221
277
|
/** The agent's declarations from this verify call, when it made any. */
|
|
222
278
|
inHouseDeclared) {
|
|
@@ -234,6 +290,7 @@ inHouseDeclared) {
|
|
|
234
290
|
return updateReuseOutcome(testFile, (existing) => {
|
|
235
291
|
const mine = owns(existing, testFile);
|
|
236
292
|
const prior = mine ? existing.utils : undefined;
|
|
293
|
+
const modules = locatedModules(prior?.modules, r);
|
|
237
294
|
return {
|
|
238
295
|
...(mine ? {} : freshRecord()),
|
|
239
296
|
testFilePath: testFile,
|
|
@@ -255,6 +312,7 @@ inHouseDeclared) {
|
|
|
255
312
|
...((inHouseDeclared ?? prior?.inHouseDeclared)
|
|
256
313
|
? { inHouseDeclared: inHouseDeclared ?? prior?.inHouseDeclared }
|
|
257
314
|
: {}),
|
|
315
|
+
...(modules.length > 0 ? { modules } : {}),
|
|
258
316
|
},
|
|
259
317
|
};
|
|
260
318
|
}, explicitStateFile);
|