@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.
Files changed (135) hide show
  1. package/build/execution/wrapperConfig.d.ts +4 -0
  2. package/build/execution/wrapperConfig.js +14 -2
  3. package/build/prompts/code-reuse.d.ts +5 -1
  4. package/build/prompts/code-reuse.js +16 -10
  5. package/build/prompts/modularization/ui-test-modularization.js +9 -2
  6. package/build/prompts/pom-aware-code-reuse.d.ts +4 -1
  7. package/build/prompts/pom-aware-code-reuse.js +38 -6
  8. package/build/prompts/reuse-hand-off.d.ts +4 -0
  9. package/build/prompts/reuse-hand-off.js +10 -5
  10. package/build/prompts/shared-helper-policy.d.ts +5 -0
  11. package/build/prompts/shared-helper-policy.js +5 -0
  12. package/build/prompts/test-maintenance/driftAnalysisShared.js +2 -0
  13. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +2 -1
  14. package/build/prompts/testbot/testbot-prompts.js +33 -16
  15. package/build/recommendation/answers.d.ts +5 -3
  16. package/build/recommendation/answers.js +5 -2
  17. package/build/recommendation/fakeVerifyContext.d.ts +3 -0
  18. package/build/recommendation/fakeVerifyContext.js +16 -0
  19. package/build/recommendation/runVerifiers.js +2 -0
  20. package/build/recommendation/types.d.ts +12 -0
  21. package/build/recommendation/verifierContracts.d.ts +32 -2
  22. package/build/recommendation/verifierContracts.js +45 -4
  23. package/build/recommendation/verifiers/changedLinesClaimed.d.ts +17 -0
  24. package/build/recommendation/verifiers/changedLinesClaimed.js +88 -0
  25. package/build/recommendation/verifiers/coverage.js +143 -4
  26. package/build/recommendation/verifiers/expectedValueSourced.js +8 -0
  27. package/build/recommendation/verifiers/requirementSourced.js +141 -11
  28. package/build/services/TestGenerationService.js +4 -0
  29. package/build/tools/code-refactor/caller-gate.d.ts +105 -0
  30. package/build/tools/code-refactor/caller-gate.js +174 -0
  31. package/build/tools/code-refactor/changed-helper-report.d.ts +62 -0
  32. package/build/tools/code-refactor/changed-helper-report.js +114 -0
  33. package/build/tools/code-refactor/codeReuseTool.js +131 -62
  34. package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -2
  35. package/build/tools/code-refactor/helper-callers.d.ts +65 -0
  36. package/build/tools/code-refactor/helper-callers.js +85 -0
  37. package/build/tools/code-refactor/modularizationTool.js +10 -0
  38. package/build/tools/code-refactor/pom-pass-state.d.ts +51 -0
  39. package/build/tools/code-refactor/pom-pass-state.js +115 -0
  40. package/build/tools/code-refactor/removed-modules.d.ts +33 -0
  41. package/build/tools/code-refactor/removed-modules.js +61 -0
  42. package/build/tools/code-refactor/retrofit-state.d.ts +3 -2
  43. package/build/tools/code-refactor/retrofit-state.js +11 -12
  44. package/build/tools/code-refactor/reuse-outcome.d.ts +33 -1
  45. package/build/tools/code-refactor/reuse-record-store.d.ts +57 -0
  46. package/build/tools/code-refactor/reuse-record-store.js +140 -0
  47. package/build/tools/code-refactor/reuse-state.d.ts +34 -9
  48. package/build/tools/code-refactor/reuse-state.js +140 -135
  49. package/build/tools/code-refactor/utils-verify-gates.js +42 -18
  50. package/build/tools/code-refactor/verify-gates.d.ts +4 -0
  51. package/build/tools/code-refactor/verify-gates.js +8 -1
  52. package/build/tools/generate-tests/generateContractRestTool.js +7 -7
  53. package/build/tools/generate-tests/generateIntegrationRestTool.js +8 -8
  54. package/build/tools/generate-tests/generateUIRestTool.js +3 -3
  55. package/build/tools/submitReportTool.js +121 -30
  56. package/build/tools/test-management/actionsTool.js +7 -0
  57. package/build/tools/test-management/registerTestPlanTool.d.ts +1 -0
  58. package/build/tools/test-management/registerTestPlanTool.js +71 -23
  59. package/build/types/RepositoryAnalysis.d.ts +20 -20
  60. package/build/types/ReuseOutcome.d.ts +123 -0
  61. package/build/types/TestExecution.d.ts +11 -0
  62. package/build/types/TestExecution.js +19 -0
  63. package/build/types/index.d.ts +1 -1
  64. package/build/utils/branchDiff.d.ts +1 -1
  65. package/build/utils/branchDiff.js +1 -1
  66. package/build/utils/changedRuns.d.ts +13 -0
  67. package/build/utils/changedRuns.js +56 -0
  68. package/build/utils/featureFlags.d.ts +8 -10
  69. package/build/utils/featureFlags.js +23 -21
  70. package/build/utils/normalizeSkyrampImports.d.ts +4 -1
  71. package/build/utils/normalizeSkyrampImports.js +24 -17
  72. package/build/utils/pom-scope/ownership.d.ts +19 -0
  73. package/build/utils/pom-scope/ownership.js +31 -0
  74. package/build/utils/pom-scope/pom-files.d.ts +8 -0
  75. package/build/utils/pom-scope/pom-files.js +8 -0
  76. package/build/utils/pom-scope/scoring.js +13 -1
  77. package/build/utils/pom-scope/strip.d.ts +10 -0
  78. package/build/utils/pom-scope/strip.js +9 -0
  79. package/build/utils/pom-verify/verify.js +13 -2
  80. package/build/utils/reuseRouting.d.ts +30 -2
  81. package/build/utils/reuseRouting.js +36 -15
  82. package/build/utils/testDependencyPolicy.js +4 -16
  83. package/build/utils/utils-verify/action-sites.d.ts +2 -2
  84. package/build/utils/utils-verify/allow.d.ts +1 -1
  85. package/build/utils/utils-verify/allow.js +15 -0
  86. package/build/utils/utils-verify/body-reach.d.ts +2 -2
  87. package/build/utils/utils-verify/body-reach.js +4 -1
  88. package/build/utils/utils-verify/call-sites.d.ts +16 -6
  89. package/build/utils/utils-verify/call-sites.js +12 -6
  90. package/build/utils/utils-verify/changed-helpers.d.ts +38 -0
  91. package/build/utils/utils-verify/changed-helpers.js +91 -0
  92. package/build/utils/utils-verify/changed-lines.d.ts +48 -0
  93. package/build/utils/utils-verify/changed-lines.js +106 -0
  94. package/build/utils/utils-verify/head.d.ts +73 -0
  95. package/build/utils/utils-verify/head.js +156 -0
  96. package/build/utils/utils-verify/importers.d.ts +59 -10
  97. package/build/utils/utils-verify/importers.js +121 -31
  98. package/build/utils/utils-verify/in-house.js +26 -31
  99. package/build/utils/utils-verify/index.d.ts +3 -0
  100. package/build/utils/utils-verify/index.js +3 -0
  101. package/build/utils/utils-verify/language-spec.js +10 -2
  102. package/build/utils/utils-verify/locate.d.ts +3 -0
  103. package/build/utils/utils-verify/locate.js +6 -6
  104. package/build/utils/utils-verify/parse.d.ts +13 -1
  105. package/build/utils/utils-verify/parse.js +122 -49
  106. package/build/utils/utils-verify/retrofit-equivalence.d.ts +0 -5
  107. package/build/utils/utils-verify/retrofit-equivalence.js +2 -34
  108. package/build/utils/utils-verify/stage.js +4 -16
  109. package/build/utils/utils-verify/typecheck.d.ts +25 -4
  110. package/build/utils/utils-verify/typecheck.js +95 -13
  111. package/build/utils/utils-verify/verify.d.ts +33 -8
  112. package/build/utils/utils-verify/verify.js +180 -18
  113. package/build/utils/versions.d.ts +3 -3
  114. package/build/utils/versions.js +1 -1
  115. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1 -0
  116. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +1 -0
  117. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +6 -0
  118. package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +4 -2
  119. package/node_modules/playwright/lib/mcp/skyramp/snapshotLocators.js +246 -0
  120. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +378 -237
  121. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +21 -0
  122. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  123. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-LNgEKtdV.js → codeMirrorModule-Bou8kKzE.js} +1 -1
  124. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-Bwr1eMKC.js → defaultSettingsView-DTp2-8Si.js} +3 -3
  125. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  126. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.BAkLd5DX.js → index.uiPTFncY.js} +1 -1
  127. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.BPopbasy.js → uiMode.GLqsx5cI.js} +1 -1
  128. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  129. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  130. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  131. package/node_modules/playwright/package.json +1 -1
  132. package/package.json +3 -3
  133. package/plugin/prompts/code-reuse/hand-off.md +13 -1
  134. package/plugin/prompts/plan-tests.md +2 -2
  135. 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: the
10
- * execution record already carries `executedAt`, and the file's mtime says when it
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: the
16
- * execution record already carries `executedAt`, and the file's mtime says when it
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 execution = recorded?.status;
74
- if (recorded) {
75
- try {
76
- const mtime = (await stat(file)).mtimeMs;
77
- if (Date.parse(recorded.executedAt) < mtime)
78
- execution = undefined;
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
+ }