@skyramp/mcp 0.4.2 → 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/execution/wrapperConfig.d.ts +4 -0
- package/build/execution/wrapperConfig.js +14 -2
- package/build/prompts/code-reuse.d.ts +5 -1
- package/build/prompts/code-reuse.js +16 -10
- package/build/prompts/modularization/ui-test-modularization.js +9 -2
- package/build/prompts/pom-aware-code-reuse.d.ts +4 -1
- package/build/prompts/pom-aware-code-reuse.js +38 -6
- package/build/prompts/reuse-hand-off.d.ts +4 -0
- package/build/prompts/reuse-hand-off.js +10 -5
- package/build/prompts/shared-helper-policy.d.ts +5 -0
- package/build/prompts/shared-helper-policy.js +5 -0
- package/build/prompts/test-maintenance/driftAnalysisShared.js +2 -0
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +2 -1
- package/build/prompts/testbot/testbot-prompts.js +33 -16
- package/build/recommendation/answers.d.ts +5 -3
- package/build/recommendation/answers.js +5 -2
- package/build/recommendation/fakeVerifyContext.d.ts +3 -0
- package/build/recommendation/fakeVerifyContext.js +16 -0
- package/build/recommendation/runVerifiers.js +2 -0
- package/build/recommendation/types.d.ts +12 -0
- package/build/recommendation/verifierContracts.d.ts +32 -2
- package/build/recommendation/verifierContracts.js +45 -4
- package/build/recommendation/verifiers/changedLinesClaimed.d.ts +17 -0
- package/build/recommendation/verifiers/changedLinesClaimed.js +88 -0
- package/build/recommendation/verifiers/coverage.js +143 -4
- package/build/recommendation/verifiers/expectedValueSourced.js +8 -0
- package/build/recommendation/verifiers/requirementSourced.js +141 -11
- package/build/services/TestGenerationService.js +4 -0
- package/build/tools/code-refactor/caller-gate.d.ts +105 -0
- package/build/tools/code-refactor/caller-gate.js +174 -0
- package/build/tools/code-refactor/changed-helper-report.d.ts +62 -0
- package/build/tools/code-refactor/changed-helper-report.js +114 -0
- package/build/tools/code-refactor/codeReuseTool.js +131 -62
- package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -2
- package/build/tools/code-refactor/helper-callers.d.ts +65 -0
- package/build/tools/code-refactor/helper-callers.js +85 -0
- package/build/tools/code-refactor/modularizationTool.js +10 -0
- package/build/tools/code-refactor/pom-pass-state.d.ts +51 -0
- package/build/tools/code-refactor/pom-pass-state.js +115 -0
- 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/retrofit-state.d.ts +3 -2
- package/build/tools/code-refactor/retrofit-state.js +11 -12
- package/build/tools/code-refactor/reuse-outcome.d.ts +33 -1
- package/build/tools/code-refactor/reuse-record-store.d.ts +57 -0
- package/build/tools/code-refactor/reuse-record-store.js +140 -0
- package/build/tools/code-refactor/reuse-state.d.ts +34 -9
- package/build/tools/code-refactor/reuse-state.js +140 -135
- package/build/tools/code-refactor/utils-verify-gates.js +42 -18
- package/build/tools/code-refactor/verify-gates.d.ts +4 -0
- package/build/tools/code-refactor/verify-gates.js +8 -1
- package/build/tools/generate-tests/generateContractRestTool.js +7 -7
- package/build/tools/generate-tests/generateIntegrationRestTool.js +8 -8
- package/build/tools/generate-tests/generateUIRestTool.js +3 -3
- package/build/tools/submitReportTool.js +121 -30
- package/build/tools/test-management/actionsTool.js +7 -0
- package/build/tools/test-management/registerTestPlanTool.d.ts +1 -0
- package/build/tools/test-management/registerTestPlanTool.js +71 -23
- package/build/types/RepositoryAnalysis.d.ts +20 -20
- package/build/types/ReuseOutcome.d.ts +123 -0
- package/build/types/TestExecution.d.ts +11 -0
- package/build/types/TestExecution.js +19 -0
- package/build/types/index.d.ts +1 -1
- package/build/utils/branchDiff.d.ts +1 -1
- package/build/utils/branchDiff.js +1 -1
- package/build/utils/changedRuns.d.ts +13 -0
- package/build/utils/changedRuns.js +56 -0
- package/build/utils/featureFlags.d.ts +8 -10
- package/build/utils/featureFlags.js +23 -21
- package/build/utils/normalizeSkyrampImports.d.ts +4 -1
- package/build/utils/normalizeSkyrampImports.js +24 -17
- package/build/utils/pom-scope/ownership.d.ts +19 -0
- package/build/utils/pom-scope/ownership.js +31 -0
- package/build/utils/pom-scope/pom-files.d.ts +8 -0
- package/build/utils/pom-scope/pom-files.js +8 -0
- package/build/utils/pom-scope/scoring.js +13 -1
- package/build/utils/pom-scope/strip.d.ts +10 -0
- package/build/utils/pom-scope/strip.js +9 -0
- package/build/utils/pom-verify/verify.js +13 -2
- package/build/utils/reuseRouting.d.ts +30 -2
- package/build/utils/reuseRouting.js +36 -15
- package/build/utils/testDependencyPolicy.js +4 -16
- package/build/utils/utils-verify/action-sites.d.ts +2 -2
- package/build/utils/utils-verify/allow.d.ts +1 -1
- package/build/utils/utils-verify/allow.js +15 -0
- package/build/utils/utils-verify/body-reach.d.ts +2 -2
- package/build/utils/utils-verify/body-reach.js +4 -1
- package/build/utils/utils-verify/call-sites.d.ts +16 -6
- package/build/utils/utils-verify/call-sites.js +12 -6
- package/build/utils/utils-verify/changed-helpers.d.ts +38 -0
- package/build/utils/utils-verify/changed-helpers.js +91 -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 +73 -0
- package/build/utils/utils-verify/head.js +156 -0
- package/build/utils/utils-verify/importers.d.ts +59 -10
- package/build/utils/utils-verify/importers.js +121 -31
- package/build/utils/utils-verify/in-house.js +26 -31
- package/build/utils/utils-verify/index.d.ts +3 -0
- package/build/utils/utils-verify/index.js +3 -0
- package/build/utils/utils-verify/language-spec.js +10 -2
- package/build/utils/utils-verify/locate.d.ts +3 -0
- package/build/utils/utils-verify/locate.js +6 -6
- package/build/utils/utils-verify/parse.d.ts +13 -1
- package/build/utils/utils-verify/parse.js +122 -49
- package/build/utils/utils-verify/retrofit-equivalence.d.ts +0 -5
- package/build/utils/utils-verify/retrofit-equivalence.js +2 -34
- package/build/utils/utils-verify/stage.js +4 -16
- 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 +33 -8
- package/build/utils/utils-verify/verify.js +180 -18
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +1 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +6 -0
- 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 +378 -237
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +21 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-LNgEKtdV.js → codeMirrorModule-Bou8kKzE.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-Bwr1eMKC.js → defaultSettingsView-DTp2-8Si.js} +3 -3
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.BAkLd5DX.js → index.uiPTFncY.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.BPopbasy.js → uiMode.GLqsx5cI.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +3 -3
- package/plugin/prompts/code-reuse/hand-off.md +13 -1
- package/plugin/prompts/plan-tests.md +2 -2
- package/plugin/skills/enhance-assertions/reference/shared-rules.md +1 -0
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { fixErrorsInstruction } from "../../skills/fixTestImportErrorsSkill.js";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import { logger } from "../../utils/logger.js";
|
|
5
|
+
import { toolError } from "../../utils/utils.js";
|
|
5
6
|
import { ProgrammingLanguage, TestType } from "../../types/TestTypes.js";
|
|
6
7
|
import { ModularizationService, } from "../../services/ModularizationService.js";
|
|
7
8
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
@@ -9,6 +10,7 @@ import { normalizeLanguageParams, resolveParamAliases, } from "../../utils/norma
|
|
|
9
10
|
import { normalizeSkyrampImportsInFile } from "../../utils/normalizeSkyrampImports.js";
|
|
10
11
|
import { stageGeneratedPaths } from "../../utils/gitStaging.js";
|
|
11
12
|
import { recordModularized } from "./reuse-state.js";
|
|
13
|
+
import { pendingPomPass } from "./pom-pass-state.js";
|
|
12
14
|
const modularizationSchema = {
|
|
13
15
|
testFile: z
|
|
14
16
|
.string()
|
|
@@ -82,6 +84,14 @@ After modularization, if errors remain, ${fixErrorsInstruction()}.
|
|
|
82
84
|
if (!params.isTraceBased &&
|
|
83
85
|
[TestType.UI, TestType.E2E, TestType.INTEGRATION].includes(params.testType))
|
|
84
86
|
params.isTraceBased = true;
|
|
87
|
+
// Both reuse settings on: the page-object pass maps the raw steps first, and
|
|
88
|
+
// its gates judge the spec while what it did not substitute is still inline.
|
|
89
|
+
// Checked before anything is recorded, so a refused call leaves no mark.
|
|
90
|
+
const owedPomPass = await pendingPomPass(params.testFile);
|
|
91
|
+
if (owedPomPass) {
|
|
92
|
+
errorResult = toolError(owedPomPass);
|
|
93
|
+
return errorResult;
|
|
94
|
+
}
|
|
85
95
|
normalizeSkyrampImportsInFile(params.testFile);
|
|
86
96
|
// Stage so testbot includes the generated files in its output commit.
|
|
87
97
|
await stageGeneratedPaths(params.testFile);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ReuseHandOff } from "../../utils/AnalysisStateManager.js";
|
|
2
|
+
import type { ReuseRederivation } from "./reuse-state.js";
|
|
3
|
+
/**
|
|
4
|
+
* The page-object pass as run state, for a target both reuse settings serve
|
|
5
|
+
* (isPomFirstTarget): whether the pass has finished for a spec, the mark that
|
|
6
|
+
* finishes it, the checkpoint that holds modularization until it has, and the
|
|
7
|
+
* merge of the two halves one such spec's report row carries.
|
|
8
|
+
*/
|
|
9
|
+
/** Whether a hand-off's chain starts with the page-object pass. Derived from what the
|
|
10
|
+
* hand-off already stores, never recorded beside it: generation writes the hand-off
|
|
11
|
+
* from exactly these three values, and maintenance arms none for such a target. */
|
|
12
|
+
export declare function pomFirstHandOff(handOff: ReuseHandOff): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* With both reuse settings on (isPomFirstTarget), where this spec's page-object pass
|
|
15
|
+
* stands. `undefined` while it is open; `"finished"` once its verify pass PASSED;
|
|
16
|
+
* `"no-layer"` when discovery found no page-object layer to reuse. From then on the
|
|
17
|
+
* reuse tool serves the SkyrampUtils flow for the spec, and the page-object gates
|
|
18
|
+
* have already judged the spec while every step they did not substitute was still raw
|
|
19
|
+
* and inline — the state they were written for.
|
|
20
|
+
*
|
|
21
|
+
* A FAILED verdict keeps the pass open: the page-object report says to fix and
|
|
22
|
+
* re-verify, and `pendingReuseVerification` refuses the tools after it until that
|
|
23
|
+
* happens. A record owned by another spec, or no run state, reads as open; the caller
|
|
24
|
+
* then serves the page-object pass, which is what the spec gets without composition.
|
|
25
|
+
*/
|
|
26
|
+
export declare function pomPassOutcome(testFile: string, explicitStateFile?: string): Promise<"finished" | "no-layer" | undefined>;
|
|
27
|
+
/** Mark the page-object pass finished for a spec (see `pomPassOutcome`). Written by
|
|
28
|
+
* the reuse tool at the two points that finish it: a PASSED page-object verify, and
|
|
29
|
+
* discovery that found no page-object layer. */
|
|
30
|
+
export declare function recordPomPassFinished(testFile: string, explicitStateFile?: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Does this spec owe its page-object pass before `skyramp_modularization`? Only a spec
|
|
33
|
+
* generation handed off with the page-object pass first. Modularizing first would move
|
|
34
|
+
* the raw steps the page-object pass maps into helpers it cannot see, and its gates
|
|
35
|
+
* would then judge a spec with nothing left inline. Fails open without a hand-off, on
|
|
36
|
+
* an unreadable or unwritable state file.
|
|
37
|
+
*/
|
|
38
|
+
export declare function pendingPomPass(testFile: string, explicitStateFile?: string): Promise<string | undefined>;
|
|
39
|
+
/**
|
|
40
|
+
* One row, both halves. The outcome fields are disjoint by construction (the
|
|
41
|
+
* page-object fields sit at the top level, the shared-helper ones under `helpers` and
|
|
42
|
+
* `inHouse`). The two blocking verdicts become ONE, because the report refuses per
|
|
43
|
+
* spec and one `verify: true` call re-checks both halves (the reuse tool's combined
|
|
44
|
+
* verify). Declines are retired only where the half that owns them was measured: the
|
|
45
|
+
* page-object half measures a `kept inline` decline only when its zero-reuse gate
|
|
46
|
+
* walked the candidates, which it does not at non-zero reuse.
|
|
47
|
+
*/
|
|
48
|
+
export declare function combineRederivations(pom: ReuseRederivation, helper: ReuseRederivation, carriesKeptInline: boolean,
|
|
49
|
+
/** The call that re-checks both halves — the shared-helper verify call, which the
|
|
50
|
+
* reuse tool routes to its combined verify for such a spec. */
|
|
51
|
+
combinedVerifyCall: string | undefined): ReuseRederivation;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { logger } from "../../utils/logger.js";
|
|
3
|
+
import { isPomFirstTarget } from "../../utils/reuseRouting.js";
|
|
4
|
+
import { ReuseVerificationOutcome } from "./reuse-outcome.js";
|
|
5
|
+
import { freshRecord, owns, readHandOff, readReuseRecord, runStateWritable, updateReuseOutcome, } from "./reuse-record-store.js";
|
|
6
|
+
/**
|
|
7
|
+
* The page-object pass as run state, for a target both reuse settings serve
|
|
8
|
+
* (isPomFirstTarget): whether the pass has finished for a spec, the mark that
|
|
9
|
+
* finishes it, the checkpoint that holds modularization until it has, and the
|
|
10
|
+
* merge of the two halves one such spec's report row carries.
|
|
11
|
+
*/
|
|
12
|
+
/** Whether a hand-off's chain starts with the page-object pass. Derived from what the
|
|
13
|
+
* hand-off already stores, never recorded beside it: generation writes the hand-off
|
|
14
|
+
* from exactly these three values, and maintenance arms none for such a target. */
|
|
15
|
+
export function pomFirstHandOff(handOff) {
|
|
16
|
+
return isPomFirstTarget(handOff.language, handOff.testType);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* With both reuse settings on (isPomFirstTarget), where this spec's page-object pass
|
|
20
|
+
* stands. `undefined` while it is open; `"finished"` once its verify pass PASSED;
|
|
21
|
+
* `"no-layer"` when discovery found no page-object layer to reuse. From then on the
|
|
22
|
+
* reuse tool serves the SkyrampUtils flow for the spec, and the page-object gates
|
|
23
|
+
* have already judged the spec while every step they did not substitute was still raw
|
|
24
|
+
* and inline — the state they were written for.
|
|
25
|
+
*
|
|
26
|
+
* A FAILED verdict keeps the pass open: the page-object report says to fix and
|
|
27
|
+
* re-verify, and `pendingReuseVerification` refuses the tools after it until that
|
|
28
|
+
* happens. A record owned by another spec, or no run state, reads as open; the caller
|
|
29
|
+
* then serves the page-object pass, which is what the spec gets without composition.
|
|
30
|
+
*/
|
|
31
|
+
export async function pomPassOutcome(testFile, explicitStateFile) {
|
|
32
|
+
try {
|
|
33
|
+
const record = await readReuseRecord(testFile, explicitStateFile);
|
|
34
|
+
if (!record?.pomPassFinished || !owns(record, testFile))
|
|
35
|
+
return undefined;
|
|
36
|
+
return record.verification === ReuseVerificationOutcome.SkippedNoPom
|
|
37
|
+
? "no-layer"
|
|
38
|
+
: "finished";
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Mark the page-object pass finished for a spec (see `pomPassOutcome`). Written by
|
|
45
|
+
* the reuse tool at the two points that finish it: a PASSED page-object verify, and
|
|
46
|
+
* discovery that found no page-object layer. */
|
|
47
|
+
export async function recordPomPassFinished(testFile, explicitStateFile) {
|
|
48
|
+
await updateReuseOutcome(testFile, (existing) => owns(existing, testFile)
|
|
49
|
+
? { pomPassFinished: true }
|
|
50
|
+
: { ...freshRecord(), testFilePath: testFile, pomPassFinished: true }, explicitStateFile);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Does this spec owe its page-object pass before `skyramp_modularization`? Only a spec
|
|
54
|
+
* generation handed off with the page-object pass first. Modularizing first would move
|
|
55
|
+
* the raw steps the page-object pass maps into helpers it cannot see, and its gates
|
|
56
|
+
* would then judge a spec with nothing left inline. Fails open without a hand-off, on
|
|
57
|
+
* an unreadable or unwritable state file.
|
|
58
|
+
*/
|
|
59
|
+
export async function pendingPomPass(testFile, explicitStateFile) {
|
|
60
|
+
try {
|
|
61
|
+
const handOff = (await readHandOff(testFile, explicitStateFile))?.handOff;
|
|
62
|
+
if (!handOff || !pomFirstHandOff(handOff))
|
|
63
|
+
return undefined;
|
|
64
|
+
if ((await pomPassOutcome(testFile, explicitStateFile)) !== undefined)
|
|
65
|
+
return undefined;
|
|
66
|
+
if (!(await runStateWritable(explicitStateFile)))
|
|
67
|
+
return undefined;
|
|
68
|
+
return (`${path.basename(testFile)} takes the page-object pass BEFORE modularization. Call skyramp_reuse_code with ` +
|
|
69
|
+
`{ testFile: "${testFile}", language: "${handOff.language}", framework: "${handOff.framework}", testType: "${handOff.testType}" }, ` +
|
|
70
|
+
`follow its steps to a PASSED verification (or its "No reusable POM layer detected" answer), then call skyramp_modularization again.`);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
logger.warning("Could not check for a pending page-object pass — proceeding", {
|
|
74
|
+
testFile,
|
|
75
|
+
error: String(err),
|
|
76
|
+
});
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* One row, both halves. The outcome fields are disjoint by construction (the
|
|
82
|
+
* page-object fields sit at the top level, the shared-helper ones under `helpers` and
|
|
83
|
+
* `inHouse`). The two blocking verdicts become ONE, because the report refuses per
|
|
84
|
+
* spec and one `verify: true` call re-checks both halves (the reuse tool's combined
|
|
85
|
+
* verify). Declines are retired only where the half that owns them was measured: the
|
|
86
|
+
* page-object half measures a `kept inline` decline only when its zero-reuse gate
|
|
87
|
+
* walked the candidates, which it does not at non-zero reuse.
|
|
88
|
+
*/
|
|
89
|
+
export function combineRederivations(pom, helper, carriesKeptInline,
|
|
90
|
+
/** The call that re-checks both halves — the shared-helper verify call, which the
|
|
91
|
+
* reuse tool routes to its combined verify for such a spec. */
|
|
92
|
+
combinedVerifyCall) {
|
|
93
|
+
const outcome = pom.outcome || helper.outcome
|
|
94
|
+
? { ...(pom.outcome ?? {}), ...(helper.outcome ?? {}) }
|
|
95
|
+
: undefined;
|
|
96
|
+
const verdicts = [pom.blocking, helper.blocking].filter((v) => v !== undefined);
|
|
97
|
+
const blocking = verdicts.length === 0
|
|
98
|
+
? undefined
|
|
99
|
+
: {
|
|
100
|
+
file: verdicts[0].file,
|
|
101
|
+
failures: verdicts.flatMap((v) => v.failures),
|
|
102
|
+
detail: verdicts.map((v) => v.detail).join("\n\n"),
|
|
103
|
+
verifyCall: combinedVerifyCall ?? verdicts[0].verifyCall,
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
outcome,
|
|
107
|
+
...(blocking ? { blocking } : {}),
|
|
108
|
+
carriedConsumed: [
|
|
109
|
+
...(pom.carriedConsumed ?? []),
|
|
110
|
+
...(helper.carriedConsumed ?? []),
|
|
111
|
+
],
|
|
112
|
+
carriedEvaluated: !!helper.carriedEvaluated &&
|
|
113
|
+
(!!pom.carriedEvaluated || !carriesKeptInline),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -6,8 +6,9 @@ import { type StagingResult } from "../../utils/utils-verify/index.js";
|
|
|
6
6
|
* sibling-inline advisory). Staging carries the edit into the output commit, so the
|
|
7
7
|
* report must disclose it — with an execution that ran the edited bytes.
|
|
8
8
|
*
|
|
9
|
-
* Whether an execution counts is answered by TIME, not by a stored hash
|
|
10
|
-
*
|
|
9
|
+
* Whether an execution counts is answered by TIME, not by a stored hash (the rule
|
|
10
|
+
* itself is `freshExecution`, shared with the changed-helper gate): the execution
|
|
11
|
+
* record already carries `executedAt`, and the file's mtime says when it
|
|
11
12
|
* was last edited — an execution older than the last edit ran bytes that no longer
|
|
12
13
|
* exist. That also covers a bare editor write with no tool call in between, which a
|
|
13
14
|
* record-time hash cannot see. (Same identity pattern as VideoRecord's
|
|
@@ -6,14 +6,16 @@ import { stageUtilsArtifacts, } from "../../utils/utils-verify/index.js";
|
|
|
6
6
|
import { importFormViolations, locateUtilsFiles, retrofitMismatches, utilsSpecForFile, } from "../../utils/utils-verify/index.js";
|
|
7
7
|
import { realpath } from "../../utils/utils-verify/index.js";
|
|
8
8
|
import { canonPath, samePath, utilsFileLabel } from "./reuse-state.js";
|
|
9
|
+
import { freshExecution } from "../../types/TestExecution.js";
|
|
9
10
|
/**
|
|
10
11
|
* Retrofit tracking (SKYR-4276 A4): a RETROFIT is a pre-existing Skyramp-generated
|
|
11
12
|
* test the reuse pass edited to import from the shared utils file (STEP 5c / the
|
|
12
13
|
* sibling-inline advisory). Staging carries the edit into the output commit, so the
|
|
13
14
|
* report must disclose it — with an execution that ran the edited bytes.
|
|
14
15
|
*
|
|
15
|
-
* Whether an execution counts is answered by TIME, not by a stored hash
|
|
16
|
-
*
|
|
16
|
+
* Whether an execution counts is answered by TIME, not by a stored hash (the rule
|
|
17
|
+
* itself is `freshExecution`, shared with the changed-helper gate): the execution
|
|
18
|
+
* record already carries `executedAt`, and the file's mtime says when it
|
|
17
19
|
* was last edited — an execution older than the last edit ran bytes that no longer
|
|
18
20
|
* exist. That also covers a bare editor write with no tool call in between, which a
|
|
19
21
|
* record-time hash cannot see. (Same identity pattern as VideoRecord's
|
|
@@ -70,17 +72,14 @@ export async function standingRetrofits(state, changedFilesAbs) {
|
|
|
70
72
|
if (!changed.has(canonPath(file)))
|
|
71
73
|
continue;
|
|
72
74
|
const recorded = state.existingTests?.find((t) => samePath(t.testFile, file))?.executionAfter;
|
|
73
|
-
let
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
/* fail open: an unreadable mtime must not demand a re-run */
|
|
82
|
-
}
|
|
75
|
+
let mtime = 0; // fail open: an unreadable mtime must not demand a re-run
|
|
76
|
+
try {
|
|
77
|
+
mtime = (await stat(file)).mtimeMs;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* see above */
|
|
83
81
|
}
|
|
82
|
+
const execution = freshExecution(recorded, mtime);
|
|
84
83
|
out.push({ file, utilsFile: r.utilsFile, testFile: r.testFile, execution });
|
|
85
84
|
}
|
|
86
85
|
return uncrossAttributions(out);
|
|
@@ -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
|
|
@@ -60,6 +60,12 @@ export interface ReuseRecord {
|
|
|
60
60
|
* enforceable unit, as with every other debt here). Read by the reuse tool on a
|
|
61
61
|
* hand-off target: modularize-first flows owe modularization BEFORE reuse. */
|
|
62
62
|
modularized?: true;
|
|
63
|
+
/** Both reuse settings on (isPomFirstTarget): the page-object pass finished for this
|
|
64
|
+
* spec — its verify pass PASSED, or it found no page-object layer. Sticky: a later
|
|
65
|
+
* page-object verdict (the combined verify of the SkyrampUtils flow re-checks the
|
|
66
|
+
* page-object calls) does not reopen the pass, so routing never flips back to it.
|
|
67
|
+
* Not recoverable from the spec: it is a fact about the run's order. */
|
|
68
|
+
pomPassFinished?: true;
|
|
63
69
|
/** The spec as it stood when `skyramp_modularization` handed out its instructions —
|
|
64
70
|
* after the enhancer, before any code moved (SKYR-4276 A7/A8). The verify pass
|
|
65
71
|
* compares the delivered spec plus the helpers it imports against this. */
|
|
@@ -81,6 +87,32 @@ export interface UtilsReuseRecord {
|
|
|
81
87
|
* repository's own browser helper modules: the files it read, the helpers it
|
|
82
88
|
* declined. Testimony, kept for the report and labelled there as such. */
|
|
83
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
|
+
};
|
|
84
116
|
}
|
|
85
117
|
export interface InHouseDeclaration {
|
|
86
118
|
filesRead: string[];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { StateManager, type ReuseHandOff, type UnifiedAnalysisState } from "../../utils/AnalysisStateManager.js";
|
|
2
|
+
import type { ReuseRecord } from "./reuse-outcome.js";
|
|
3
|
+
/**
|
|
4
|
+
* The run-state store behind every reuse record and hand-off: one read, one
|
|
5
|
+
* read-modify-write, one ownership rule, one canonical path comparison. The reuse
|
|
6
|
+
* checkpoints (reuse-state.ts) and the page-object-pass state (pom-pass-state.ts)
|
|
7
|
+
* both build on these and nothing else touches the file.
|
|
8
|
+
*
|
|
9
|
+
* Every write is best-effort. Reuse observability must never be able to fail a
|
|
10
|
+
* refactor that otherwise succeeded, so a missing/unreadable/unwritable state
|
|
11
|
+
* file degrades to "no reuse summary in the report" — the same outcome as running
|
|
12
|
+
* outside a Testbot run, where there is no run state to record into.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Read-modify-write one spec's outcome. `patch` receives the entry recorded so
|
|
16
|
+
* far, so a caller that needs prior state (the verify pass, which accumulates
|
|
17
|
+
* flagged members) uses the same path as one that does not.
|
|
18
|
+
*
|
|
19
|
+
* Fields the patch omits are preserved: the prompt-generation call knows the
|
|
20
|
+
* candidate count, each verify pass knows the counts and verdict.
|
|
21
|
+
*/
|
|
22
|
+
export declare function updateReuseOutcome(testFile: string, patch: (existing: ReuseRecord) => Partial<ReuseRecord>, explicitStateFile?: string): Promise<boolean>;
|
|
23
|
+
/** The run state as one read — the state manager (for a write-back) and what it
|
|
24
|
+
* holds — or `undefined` when there is no run state to read. */
|
|
25
|
+
export declare function readRunState(explicitStateFile?: string): Promise<{
|
|
26
|
+
manager: StateManager<UnifiedAnalysisState>;
|
|
27
|
+
data: UnifiedAnalysisState;
|
|
28
|
+
} | undefined>;
|
|
29
|
+
/** This spec's hand-off, keyed by realpath, with the run state it was read from —
|
|
30
|
+
* the ONE read every hand-off checkpoint makes. `handOff` is `undefined` when the
|
|
31
|
+
* file was never handed off; the whole result is when there is no run state. */
|
|
32
|
+
export declare function readHandOff(testFile: string, explicitStateFile?: string): Promise<{
|
|
33
|
+
data: UnifiedAnalysisState;
|
|
34
|
+
handOff: ReuseHandOff | undefined;
|
|
35
|
+
} | undefined>;
|
|
36
|
+
/** Read one spec's record, or `undefined` when there is no run state, no record, or the
|
|
37
|
+
* state file cannot be read. Shares `updateReuseOutcome`'s resolve-read-basename plumbing
|
|
38
|
+
* so the two cannot disagree about which key a spec maps to. */
|
|
39
|
+
export declare function readReuseRecord(testFile: string, explicitStateFile?: string): Promise<ReuseRecord | undefined>;
|
|
40
|
+
/** Every field cleared — the patch spread onto a record owned by another spec. */
|
|
41
|
+
export declare function freshRecord(): Partial<ReuseRecord>;
|
|
42
|
+
/** Whether `record` describes `testFile` (or nobody yet). Basename-keyed run state
|
|
43
|
+
* means two specs can share one record; only the owner's history carries over. */
|
|
44
|
+
export declare function owns(record: ReuseRecord, testFile: string): boolean;
|
|
45
|
+
/** Canonical comparison: a record written through a symlinked path (macOS /var →
|
|
46
|
+
* /private/var) must still be recognised as this spec's when checked by realpath. */
|
|
47
|
+
export declare function samePath(a: string, b: string): boolean;
|
|
48
|
+
/** Exported for retrofit-state: the same canonicalisation the records use. */
|
|
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;
|
|
55
|
+
/** Whether the run state file can be written — the precondition for any debt this
|
|
56
|
+
* module holds, since only a write clears it. */
|
|
57
|
+
export declare function runStateWritable(explicitStateFile?: string): Promise<boolean>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { access, constants as fsConstants } from "fs/promises";
|
|
3
|
+
import { existsSync, realpathSync } from "fs";
|
|
4
|
+
import { logger } from "../../utils/logger.js";
|
|
5
|
+
import { resolveRunStatePath, StateManager, } from "../../utils/AnalysisStateManager.js";
|
|
6
|
+
import { realpath } from "../../utils/utils-verify/index.js";
|
|
7
|
+
/**
|
|
8
|
+
* The run-state store behind every reuse record and hand-off: one read, one
|
|
9
|
+
* read-modify-write, one ownership rule, one canonical path comparison. The reuse
|
|
10
|
+
* checkpoints (reuse-state.ts) and the page-object-pass state (pom-pass-state.ts)
|
|
11
|
+
* both build on these and nothing else touches the file.
|
|
12
|
+
*
|
|
13
|
+
* Every write is best-effort. Reuse observability must never be able to fail a
|
|
14
|
+
* refactor that otherwise succeeded, so a missing/unreadable/unwritable state
|
|
15
|
+
* file degrades to "no reuse summary in the report" — the same outcome as running
|
|
16
|
+
* outside a Testbot run, where there is no run state to record into.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Read-modify-write one spec's outcome. `patch` receives the entry recorded so
|
|
20
|
+
* far, so a caller that needs prior state (the verify pass, which accumulates
|
|
21
|
+
* flagged members) uses the same path as one that does not.
|
|
22
|
+
*
|
|
23
|
+
* Fields the patch omits are preserved: the prompt-generation call knows the
|
|
24
|
+
* candidate count, each verify pass knows the counts and verdict.
|
|
25
|
+
*/
|
|
26
|
+
export async function updateReuseOutcome(testFile, patch, explicitStateFile) {
|
|
27
|
+
try {
|
|
28
|
+
const state = await readRunState(explicitStateFile);
|
|
29
|
+
if (!state)
|
|
30
|
+
return false;
|
|
31
|
+
const { manager, data } = state;
|
|
32
|
+
const key = path.basename(testFile);
|
|
33
|
+
const existing = data.reuseOutcomes?.[key] ?? {};
|
|
34
|
+
await manager.appendData({
|
|
35
|
+
...data,
|
|
36
|
+
reuseOutcomes: {
|
|
37
|
+
...data.reuseOutcomes,
|
|
38
|
+
[key]: { ...existing, ...patch(existing) },
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
logger.warning("Could not record POM-reuse outcome — report will omit the reuse summary", {
|
|
45
|
+
testFile,
|
|
46
|
+
error: String(err),
|
|
47
|
+
});
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** The run state as one read — the state manager (for a write-back) and what it
|
|
52
|
+
* holds — or `undefined` when there is no run state to read. */
|
|
53
|
+
export async function readRunState(explicitStateFile) {
|
|
54
|
+
const stateFile = resolveRunStatePath(explicitStateFile);
|
|
55
|
+
if (!stateFile)
|
|
56
|
+
return undefined;
|
|
57
|
+
const manager = StateManager.fromStatePath(stateFile);
|
|
58
|
+
const data = await manager.readData();
|
|
59
|
+
return data ? { manager, data } : undefined;
|
|
60
|
+
}
|
|
61
|
+
/** This spec's hand-off, keyed by realpath, with the run state it was read from —
|
|
62
|
+
* the ONE read every hand-off checkpoint makes. `handOff` is `undefined` when the
|
|
63
|
+
* file was never handed off; the whole result is when there is no run state. */
|
|
64
|
+
export async function readHandOff(testFile, explicitStateFile) {
|
|
65
|
+
const state = await readRunState(explicitStateFile);
|
|
66
|
+
if (!state)
|
|
67
|
+
return undefined;
|
|
68
|
+
return {
|
|
69
|
+
data: state.data,
|
|
70
|
+
handOff: state.data.reuseHandOffs?.[await realpath(testFile)],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Read one spec's record, or `undefined` when there is no run state, no record, or the
|
|
74
|
+
* state file cannot be read. Shares `updateReuseOutcome`'s resolve-read-basename plumbing
|
|
75
|
+
* so the two cannot disagree about which key a spec maps to. */
|
|
76
|
+
export async function readReuseRecord(testFile, explicitStateFile) {
|
|
77
|
+
return (await readRunState(explicitStateFile))?.data.reuseOutcomes?.[path.basename(testFile)];
|
|
78
|
+
}
|
|
79
|
+
/** Every field cleared — the patch spread onto a record owned by another spec. */
|
|
80
|
+
export function freshRecord() {
|
|
81
|
+
return {
|
|
82
|
+
candidatesDetected: undefined,
|
|
83
|
+
verification: undefined,
|
|
84
|
+
flagged: undefined,
|
|
85
|
+
language: undefined,
|
|
86
|
+
utils: undefined,
|
|
87
|
+
modularized: undefined,
|
|
88
|
+
pomPassFinished: undefined,
|
|
89
|
+
baseline: undefined,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Whether `record` describes `testFile` (or nobody yet). Basename-keyed run state
|
|
93
|
+
* means two specs can share one record; only the owner's history carries over. */
|
|
94
|
+
export function owns(record, testFile) {
|
|
95
|
+
return !record.testFilePath || samePath(record.testFilePath, testFile);
|
|
96
|
+
}
|
|
97
|
+
/** Canonical comparison: a record written through a symlinked path (macOS /var →
|
|
98
|
+
* /private/var) must still be recognised as this spec's when checked by realpath. */
|
|
99
|
+
export function samePath(a, b) {
|
|
100
|
+
return canon(a) === canon(b);
|
|
101
|
+
}
|
|
102
|
+
/** Exported for retrofit-state: the same canonicalisation the records use. */
|
|
103
|
+
export function canonPath(p) {
|
|
104
|
+
return canon(p);
|
|
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
|
+
}
|
|
119
|
+
function canon(p) {
|
|
120
|
+
try {
|
|
121
|
+
return realpathSync(p);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return path.resolve(p);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Whether the run state file can be written — the precondition for any debt this
|
|
128
|
+
* module holds, since only a write clears it. */
|
|
129
|
+
export async function runStateWritable(explicitStateFile) {
|
|
130
|
+
const stateFile = resolveRunStatePath(explicitStateFile);
|
|
131
|
+
if (!stateFile)
|
|
132
|
+
return false;
|
|
133
|
+
try {
|
|
134
|
+
await access(stateFile, fsConstants.W_OK);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|