@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
@@ -0,0 +1,38 @@
1
+ import type { UtilsLanguageSpec } from "./language-spec.js";
2
+ import type { HelperChangeKind } from "../../types/ReuseOutcome.js";
3
+ export interface ChangedHelper {
4
+ helper: string;
5
+ /** 1-based line of the definition as the module stands now; `0` for a removed
6
+ * helper, which has no line in the current file. */
7
+ line: number;
8
+ change: HelperChangeKind;
9
+ }
10
+ /**
11
+ * The helpers a run CHANGED, by comparing the module as it stands against the same
12
+ * module at the incumbent revision.
13
+ *
14
+ * Both sides are parsed with the same reader the verifier uses, so each body is taken
15
+ * to its structural terminator on a comments-and-strings-blanked copy — a brace typed
16
+ * into a selector or a `#` inside a Python string cannot move a helper's end and
17
+ * swallow the helpers after it.
18
+ *
19
+ * Comparison is deliberately INSENSITIVE to formatting and SENSITIVE to literals.
20
+ * Comments are blanked and whitespace OUTSIDE string literals is removed, so a
21
+ * Prettier or Black rewrap — which also inserts a trailing comma where it wraps —
22
+ * reads as no change; the comma before a closing bracket is dropped for the same
23
+ * reason.
24
+ *
25
+ * A string literal is copied through byte for byte — its own whitespace AND its own
26
+ * punctuation. Every normalising rule runs on the non-literal runs only.
27
+ * Whitespace inside a literal is not formatting, it is the value: `.menu .item`
28
+ * matches a descendant and `.menu.item` matches one element carrying both classes,
29
+ * and a filled `"Acme Corp"` is not `"AcmeCorp"`. A normaliser that strips whitespace
30
+ * everywhere reads all three pairs as unchanged, which silently disables this check
31
+ * on the browser selector case it exists for.
32
+ *
33
+ * The residual error is one-directional and chosen: two genuinely different bodies
34
+ * can normalise equal (Python's indentation carries meaning outside literals, and
35
+ * this drops it), which costs a report line. Nothing here can report a helper as
36
+ * changed that is not, which is the error that would cost a false block.
37
+ */
38
+ export declare function changedHelpersBetween(headContent: string, currentContent: string, spec: UtilsLanguageSpec): ChangedHelper[];
@@ -0,0 +1,91 @@
1
+ import { blankComments, parseUtilsFile, stringSpansOf, } from "./parse.js";
2
+ /**
3
+ * The helpers a run CHANGED, by comparing the module as it stands against the same
4
+ * module at the incumbent revision.
5
+ *
6
+ * Both sides are parsed with the same reader the verifier uses, so each body is taken
7
+ * to its structural terminator on a comments-and-strings-blanked copy — a brace typed
8
+ * into a selector or a `#` inside a Python string cannot move a helper's end and
9
+ * swallow the helpers after it.
10
+ *
11
+ * Comparison is deliberately INSENSITIVE to formatting and SENSITIVE to literals.
12
+ * Comments are blanked and whitespace OUTSIDE string literals is removed, so a
13
+ * Prettier or Black rewrap — which also inserts a trailing comma where it wraps —
14
+ * reads as no change; the comma before a closing bracket is dropped for the same
15
+ * reason.
16
+ *
17
+ * A string literal is copied through byte for byte — its own whitespace AND its own
18
+ * punctuation. Every normalising rule runs on the non-literal runs only.
19
+ * Whitespace inside a literal is not formatting, it is the value: `.menu .item`
20
+ * matches a descendant and `.menu.item` matches one element carrying both classes,
21
+ * and a filled `"Acme Corp"` is not `"AcmeCorp"`. A normaliser that strips whitespace
22
+ * everywhere reads all three pairs as unchanged, which silently disables this check
23
+ * on the browser selector case it exists for.
24
+ *
25
+ * The residual error is one-directional and chosen: two genuinely different bodies
26
+ * can normalise equal (Python's indentation carries meaning outside literals, and
27
+ * this drops it), which costs a report line. Nothing here can report a helper as
28
+ * changed that is not, which is the error that would cost a false block.
29
+ */
30
+ export function changedHelpersBetween(headContent, currentContent, spec) {
31
+ const head = byName(parseUtilsFile(headContent, spec), headContent, spec);
32
+ const now = byName(parseUtilsFile(currentContent, spec), currentContent, spec);
33
+ const out = [];
34
+ for (const [name, was] of head) {
35
+ const is = now.get(name);
36
+ if (!is) {
37
+ out.push({ helper: name, line: 0, change: "removed" });
38
+ continue;
39
+ }
40
+ if (was.params !== is.params)
41
+ out.push({ helper: name, line: is.line, change: "signature" });
42
+ else if (was.body !== is.body)
43
+ out.push({ helper: name, line: is.line, change: "body" });
44
+ }
45
+ return out;
46
+ }
47
+ function byName(helpers, content, spec) {
48
+ const norm = (text) => normalize(text, spec);
49
+ // Comments blanked, strings kept, offsets preserved: a reworded comment is not a
50
+ // change to what a caller runs, and a selector literal is.
51
+ const lines = blankComments(content, spec).split("\n");
52
+ const out = new Map();
53
+ for (const h of helpers) {
54
+ // A module that defines one name twice is already a blocking `helper-redefined`
55
+ // finding; keep the first definition rather than letting the second decide.
56
+ if (out.has(h.name))
57
+ continue;
58
+ out.set(h.name, {
59
+ line: h.line,
60
+ params: (h.params ?? []).map(norm).join(","),
61
+ // From startLine, not line: a decorator is part of what the helper does.
62
+ body: norm(lines.slice(h.startLine - 1, h.endLine).join("\n")),
63
+ });
64
+ }
65
+ return out;
66
+ }
67
+ /**
68
+ * Whitespace removed OUTSIDE string literals — the one textual difference a
69
+ * formatter introduces that this treats as no change. Literals are copied through
70
+ * unchanged: their whitespace is the value, not formatting.
71
+ *
72
+ * A formatter's trailing comma is NOT normalised away, deliberately. Four versions
73
+ * of a comma rule were tried and each produced the next defect (`("x",)` is a tuple
74
+ * and `("x")` a string; `[1,,]` has length 2), and what the rule bought was one
75
+ * fewer execution round on a rewrap. Under this feature's own trade — an unneeded
76
+ * run of a caller that is runnable by construction, over a missed change — that is
77
+ * the cheaper side. A rewrapped helper reads as changed and its callers run once.
78
+ */
79
+ function normalize(text, spec) {
80
+ const spans = stringSpansOf(text, spec);
81
+ let out = "";
82
+ let at = 0;
83
+ for (const { start, end } of spans) {
84
+ if (start < at)
85
+ continue; // nested or overlapping: the outer span already carried it
86
+ out += text.slice(at, start).replace(/\s+/g, "");
87
+ out += text.slice(start, end);
88
+ at = end;
89
+ }
90
+ return out + text.slice(at).replace(/\s+/g, "");
91
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The lines of `after` that `before` carries unchanged, as a map from the line's
3
+ * number in `after` to its number in `before` (both 1-based), by a
4
+ * longest-common-subsequence alignment of the two files' lines. A line that only
5
+ * SHIFTED because lines were inserted or removed around it is unchanged; a line
6
+ * that was rewritten, or written new, is absent. Two lines that swapped places
7
+ * read as one unchanged and one changed — the alignment is a subsequence, not a
8
+ * reordering.
9
+ *
10
+ * Lines are broken where the compiler breaks them (see `split`), so a CRLF
11
+ * checkout does not read as a whole-file rewrite and a line number here is the
12
+ * one a diagnostic carries.
13
+ *
14
+ * The alignment table is quadratic in the two line counts, which is nothing for a
15
+ * spec of a few hundred lines. Past a fixed cell budget no alignment is made and
16
+ * the map is EMPTY: every line then reads as this pass's, which is the closed
17
+ * direction — the gate that reads this map excuses an error only on a line it can
18
+ * map, so a file too large to align is judged as before this map existed.
19
+ */
20
+ export declare function alignLines(before: string, after: string): Map<number, number>;
21
+ /** One compiler error the baseline had: the file as delivered, the line in the
22
+ * BASELINE's numbering, the TS code and the flattened message. Code alone let
23
+ * a NEW error hide behind an old one of the same code — `label` changed from
24
+ * returning a string to a boolean is a second TS2322 on the same line. */
25
+ export declare const baselineErrorKey: (file: string, line: number, code: number, message: string) => string;
26
+ /** The baseline's errors as a MULTISET: each excuses at most one current error.
27
+ * A line that had one error at HEAD and has two now carries one the run caused. */
28
+ export declare class BaselineErrors {
29
+ private readonly left;
30
+ add(key: string): void;
31
+ /** Consume one occurrence; false when none is left. */
32
+ take(key: string): boolean;
33
+ }
34
+ /**
35
+ * Why a compiler error at `file:line` is not this run's, or `undefined` when it
36
+ * is. A block is a demand to fix the line named, and a demand the run cannot
37
+ * meet ends in the module being deleted — so the compiler blocks ONLY on an
38
+ * error this run caused, and an error is inherited only by CAUSE: the line is
39
+ * unchanged since HEAD AND the file at HEAD already failed the compiler with the
40
+ * same code and message on that line, once per occurrence. Position alone is
41
+ * not enough — extraction breaks lines it leaves alone (a helper that no longer
42
+ * returns what the next line reads), and those errors are the run's.
43
+ *
44
+ * `unchangedOf` gives each file's unchanged lines mapped to HEAD's numbering (see
45
+ * alignLines), or `undefined` for a file HEAD does not hold: every error in such
46
+ * a file is this run's.
47
+ */
48
+ export declare function inheritedTypeError(unchangedOf: (file: string) => ReadonlyMap<number, number> | undefined, baselineErrors: BaselineErrors): (file: string, line: number, code: number, message: string) => string | undefined;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The lines of `after` that `before` carries unchanged, as a map from the line's
3
+ * number in `after` to its number in `before` (both 1-based), by a
4
+ * longest-common-subsequence alignment of the two files' lines. A line that only
5
+ * SHIFTED because lines were inserted or removed around it is unchanged; a line
6
+ * that was rewritten, or written new, is absent. Two lines that swapped places
7
+ * read as one unchanged and one changed — the alignment is a subsequence, not a
8
+ * reordering.
9
+ *
10
+ * Lines are broken where the compiler breaks them (see `split`), so a CRLF
11
+ * checkout does not read as a whole-file rewrite and a line number here is the
12
+ * one a diagnostic carries.
13
+ *
14
+ * The alignment table is quadratic in the two line counts, which is nothing for a
15
+ * spec of a few hundred lines. Past a fixed cell budget no alignment is made and
16
+ * the map is EMPTY: every line then reads as this pass's, which is the closed
17
+ * direction — the gate that reads this map excuses an error only on a line it can
18
+ * map, so a file too large to align is judged as before this map existed.
19
+ */
20
+ export function alignLines(before, after) {
21
+ const a = split(before);
22
+ const b = split(after);
23
+ const unchanged = new Map();
24
+ if (a.length * b.length > MAX_CELLS)
25
+ return unchanged;
26
+ // lcs[i][j]: length of the LCS of a[i..] and b[j..].
27
+ const w = b.length + 1;
28
+ const lcs = new Uint32Array((a.length + 1) * w);
29
+ for (let i = a.length - 1; i >= 0; i--)
30
+ for (let j = b.length - 1; j >= 0; j--)
31
+ lcs[i * w + j] =
32
+ a[i] === b[j]
33
+ ? lcs[(i + 1) * w + j + 1] + 1
34
+ : Math.max(lcs[(i + 1) * w + j], lcs[i * w + j + 1]);
35
+ // Walk the table: a matched pair is an unchanged line of `after`; a step that
36
+ // advances `after` alone is a line the edit wrote or changed.
37
+ let i = 0;
38
+ let j = 0;
39
+ while (j < b.length) {
40
+ if (i < a.length && a[i] === b[j]) {
41
+ unchanged.set(j + 1, i + 1);
42
+ i++;
43
+ j++;
44
+ }
45
+ else if (i < a.length && lcs[(i + 1) * w + j] >= lcs[i * w + j + 1]) {
46
+ i++;
47
+ }
48
+ else {
49
+ j++;
50
+ }
51
+ }
52
+ return unchanged;
53
+ }
54
+ const MAX_CELLS = 4_000_000;
55
+ /** Lines as the COMPILER counts them — CRLF, LF, a bare CR, U+2028 and U+2029
56
+ * each end a line — so a line number here is the line number a diagnostic
57
+ * carries. Split on LF alone, a U+2028 inside a string literal put every
58
+ * number after it one out of step, and the unchanged check read the wrong line. */
59
+ function split(content) {
60
+ return content.split(/\r\n|\r|\n|\u2028|\u2029/);
61
+ }
62
+ /** One compiler error the baseline had: the file as delivered, the line in the
63
+ * BASELINE's numbering, the TS code and the flattened message. Code alone let
64
+ * a NEW error hide behind an old one of the same code — `label` changed from
65
+ * returning a string to a boolean is a second TS2322 on the same line. */
66
+ export const baselineErrorKey = (file, line, code, message) => `${file}\0${line}\0${code}\0${message}`;
67
+ /** The baseline's errors as a MULTISET: each excuses at most one current error.
68
+ * A line that had one error at HEAD and has two now carries one the run caused. */
69
+ export class BaselineErrors {
70
+ left = new Map();
71
+ add(key) {
72
+ this.left.set(key, (this.left.get(key) ?? 0) + 1);
73
+ }
74
+ /** Consume one occurrence; false when none is left. */
75
+ take(key) {
76
+ const n = this.left.get(key) ?? 0;
77
+ if (n === 0)
78
+ return false;
79
+ this.left.set(key, n - 1);
80
+ return true;
81
+ }
82
+ }
83
+ /**
84
+ * Why a compiler error at `file:line` is not this run's, or `undefined` when it
85
+ * is. A block is a demand to fix the line named, and a demand the run cannot
86
+ * meet ends in the module being deleted — so the compiler blocks ONLY on an
87
+ * error this run caused, and an error is inherited only by CAUSE: the line is
88
+ * unchanged since HEAD AND the file at HEAD already failed the compiler with the
89
+ * same code and message on that line, once per occurrence. Position alone is
90
+ * not enough — extraction breaks lines it leaves alone (a helper that no longer
91
+ * returns what the next line reads), and those errors are the run's.
92
+ *
93
+ * `unchangedOf` gives each file's unchanged lines mapped to HEAD's numbering (see
94
+ * alignLines), or `undefined` for a file HEAD does not hold: every error in such
95
+ * a file is this run's.
96
+ */
97
+ export function inheritedTypeError(unchangedOf, baselineErrors) {
98
+ return (file, line, code, message) => {
99
+ const before = unchangedOf(file)?.get(line);
100
+ if (before === undefined)
101
+ return undefined;
102
+ return baselineErrors.take(baselineErrorKey(file, before, code, message))
103
+ ? `the line is unchanged since HEAD, where it already failed the compiler the same way (line ${before})`
104
+ : undefined;
105
+ };
106
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The repository's committed state, asked of git. ONE reader of a file at HEAD and
3
+ * ONE lister of what changed since, so that every gate comparing a delivered file
4
+ * against its baseline agrees on what "at HEAD" means — and so a fix to the
5
+ * canonicalisation reaches all of them at once. Four readers had grown up
6
+ * independently, and the one that took `path.relative(root, file)` uncanonicalised
7
+ * still carried the symlink fault the others had fixed.
8
+ */
9
+ /** git's own wording for "this path is not in HEAD". Everything else — git missing,
10
+ * a corrupt object store, a permission error — is git NOT ANSWERING, and the two are
11
+ * never treated alike: absence is the ordinary, silent case (this run wrote the
12
+ * file), and a malfunction must neither become a finding nor vanish. Stated once,
13
+ * so two readers cannot drift on which errors mean which. */
14
+ export declare function isAbsentFromHead(err: unknown): boolean;
15
+ /** What git says about a file at HEAD: its content, that HEAD does not hold it,
16
+ * or that git gave no answer — outside a repository, or a git failure. The
17
+ * third is never folded into the second: "absent" is a claim (this run created
18
+ * the file), "unknown" is the absence of one. */
19
+ export type AtHead = {
20
+ content: string;
21
+ } | {
22
+ absent: true;
23
+ } | {
24
+ unknown: true;
25
+ };
26
+ /**
27
+ * The file at HEAD, as one of the three answers above.
28
+ *
29
+ * `root` is any directory of the checkout, and an optimisation for a caller that
30
+ * already holds one. The repository's top level is resolved from it — or from the
31
+ * file's directory, which is still there for a file this run deleted — because
32
+ * `HEAD:<path>` speaks in paths from the top. A file outside the top level is
33
+ * `unknown`: HEAD has no path for it. A git malfunction is logged, as a warning —
34
+ * it must not vanish either.
35
+ */
36
+ export declare function readAtHead(file: string, root?: string): Promise<AtHead>;
37
+ /**
38
+ * The file's content at HEAD, or `undefined` when there is none to read.
39
+ *
40
+ * `undefined` covers the two non-content answers of `readAtHead`, which must both
41
+ * stay silent for a caller that only compares: reporting anything about a file on
42
+ * either would be a claim the evidence does not carry. A caller that STATES
43
+ * whether this run created the file reads `readAtHead` and keeps them apart.
44
+ */
45
+ export declare function contentAtHead(file: string, root?: string): Promise<string | undefined>;
46
+ /**
47
+ * Absolute, CANONICAL paths of the files that differ from HEAD in the repository
48
+ * `root` is in, by git's `--diff-filter` letters (`M` modified, `D` deleted, `DM`
49
+ * both …). Canonical for a deleted file too — its parent resolves — so a caller may
50
+ * key on them directly; that is part of the contract, not an accident.
51
+ *
52
+ * `--no-renames` is load-bearing, not tidiness. With rename detection on — the
53
+ * default since git 2.9 — moving a file reports one `R` and NO `D`, so a caller
54
+ * asking for deletions never hears of a module that was renamed away while every
55
+ * test still imports it by its old name. A rename is a deletion here, because a
56
+ * caller's import does not move with the file.
57
+ *
58
+ * `-z`: NUL-separated and unquoted, so a non-ASCII path stays resolvable.
59
+ *
60
+ * Throws when git cannot answer: each caller says in its own words what it will not
61
+ * do as a result, and none of them may read a failed listing as an empty one.
62
+ */
63
+ export declare function changedSinceHead(root: string, opts: {
64
+ filter: string;
65
+ }): Promise<string[]>;
66
+ /**
67
+ * Absolute, CANONICAL paths of the files in the repository `root` is in that HEAD
68
+ * does not hold: added to the index, or untracked and not ignored. What
69
+ * `changedSinceHead` cannot list — `git diff HEAD` never sees an untracked file —
70
+ * for a caller that must know what did not exist before this run. Throws when git
71
+ * cannot answer, on the same terms.
72
+ */
73
+ export declare function addedSinceHead(root: string): Promise<string[]>;
@@ -0,0 +1,156 @@
1
+ import { execFile } from "child_process";
2
+ import { promisify } from "util";
3
+ import * as path from "path";
4
+ import { detectGitRoot } from "../gitStaging.js";
5
+ import { logger } from "../logger.js";
6
+ import { realpath } from "./locate.js";
7
+ const execFileAsync = promisify(execFile);
8
+ /**
9
+ * The repository's committed state, asked of git. ONE reader of a file at HEAD and
10
+ * ONE lister of what changed since, so that every gate comparing a delivered file
11
+ * against its baseline agrees on what "at HEAD" means — and so a fix to the
12
+ * canonicalisation reaches all of them at once. Four readers had grown up
13
+ * independently, and the one that took `path.relative(root, file)` uncanonicalised
14
+ * still carried the symlink fault the others had fixed.
15
+ */
16
+ /** git's own wording for "this path is not in HEAD". Everything else — git missing,
17
+ * a corrupt object store, a permission error — is git NOT ANSWERING, and the two are
18
+ * never treated alike: absence is the ordinary, silent case (this run wrote the
19
+ * file), and a malfunction must neither become a finding nor vanish. Stated once,
20
+ * so two readers cannot drift on which errors mean which. */
21
+ export function isAbsentFromHead(err) {
22
+ const e = err;
23
+ return /does not exist|exists on disk, but not in|Not a valid object name|bad revision|unknown revision/i.test(`${e?.stderr ?? ""} ${String(err)}`);
24
+ }
25
+ /** Canonical absolute path, for a file that may be GONE: `realpath` falls back to
26
+ * `path.resolve` on a missing file, which leaves a symlinked parent (macOS `/tmp` →
27
+ * `/private/tmp`) unresolved and relativises the path to `../../…` against a
28
+ * canonical root. The parent directory is still there to resolve. */
29
+ async function canonicalEvenIfGone(p) {
30
+ const resolved = await realpath(p);
31
+ if (resolved !== path.resolve(p))
32
+ return resolved; // it exists, and moved
33
+ // Already canonical, or gone: resolving the parent answers both.
34
+ return path.join(await realpath(path.dirname(p)), path.basename(p));
35
+ }
36
+ /** `file` relative to the repository's top level as git spells it (posix, both
37
+ * sides canonical), or undefined when the file is outside it. */
38
+ async function gitPath(file, top) {
39
+ const rel = path.relative(await realpath(top), await canonicalEvenIfGone(file));
40
+ if (rel === "" ||
41
+ rel === ".." ||
42
+ rel.startsWith(`..${path.sep}`) ||
43
+ path.isAbsolute(rel))
44
+ return undefined;
45
+ return rel.split(path.sep).join("/");
46
+ }
47
+ /**
48
+ * The file at HEAD, as one of the three answers above.
49
+ *
50
+ * `root` is any directory of the checkout, and an optimisation for a caller that
51
+ * already holds one. The repository's top level is resolved from it — or from the
52
+ * file's directory, which is still there for a file this run deleted — because
53
+ * `HEAD:<path>` speaks in paths from the top. A file outside the top level is
54
+ * `unknown`: HEAD has no path for it. A git malfunction is logged, as a warning —
55
+ * it must not vanish either.
56
+ */
57
+ export async function readAtHead(file, root) {
58
+ const top = detectGitRoot(root ?? path.dirname(file));
59
+ if (!top)
60
+ return { unknown: true };
61
+ const rel = await gitPath(file, top);
62
+ if (rel === undefined)
63
+ return { unknown: true };
64
+ try {
65
+ const { stdout } = await execFileAsync("git", ["show", `HEAD:${rel}`], {
66
+ cwd: top,
67
+ encoding: "utf8",
68
+ maxBuffer: 16 * 1024 * 1024,
69
+ });
70
+ return { content: stdout };
71
+ }
72
+ catch (err) {
73
+ if (isAbsentFromHead(err))
74
+ return { absent: true };
75
+ logger.warning("Could not read a file at HEAD — whatever compares against it will not run", { file, error: String(err) });
76
+ return { unknown: true };
77
+ }
78
+ }
79
+ /**
80
+ * The file's content at HEAD, or `undefined` when there is none to read.
81
+ *
82
+ * `undefined` covers the two non-content answers of `readAtHead`, which must both
83
+ * stay silent for a caller that only compares: reporting anything about a file on
84
+ * either would be a claim the evidence does not carry. A caller that STATES
85
+ * whether this run created the file reads `readAtHead` and keeps them apart.
86
+ */
87
+ export async function contentAtHead(file, root) {
88
+ const at = await readAtHead(file, root);
89
+ return "content" in at ? at.content : undefined;
90
+ }
91
+ /**
92
+ * Absolute, CANONICAL paths of the files that differ from HEAD in the repository
93
+ * `root` is in, by git's `--diff-filter` letters (`M` modified, `D` deleted, `DM`
94
+ * both …). Canonical for a deleted file too — its parent resolves — so a caller may
95
+ * key on them directly; that is part of the contract, not an accident.
96
+ *
97
+ * `--no-renames` is load-bearing, not tidiness. With rename detection on — the
98
+ * default since git 2.9 — moving a file reports one `R` and NO `D`, so a caller
99
+ * asking for deletions never hears of a module that was renamed away while every
100
+ * test still imports it by its old name. A rename is a deletion here, because a
101
+ * caller's import does not move with the file.
102
+ *
103
+ * `-z`: NUL-separated and unquoted, so a non-ASCII path stays resolvable.
104
+ *
105
+ * Throws when git cannot answer: each caller says in its own words what it will not
106
+ * do as a result, and none of them may read a failed listing as an empty one.
107
+ */
108
+ export async function changedSinceHead(root, opts) {
109
+ const top = detectGitRoot(root);
110
+ if (!top)
111
+ throw new Error(`${root} is not inside a git repository`);
112
+ const { stdout } = await execFileAsync("git", [
113
+ "diff",
114
+ "-z",
115
+ "--name-only",
116
+ `--diff-filter=${opts.filter}`,
117
+ "--no-renames",
118
+ "HEAD",
119
+ ], { cwd: top, encoding: "utf8" });
120
+ const realTop = await realpath(top);
121
+ const out = [];
122
+ for (const rel of stdout.split("\0").filter(Boolean))
123
+ out.push(await canonicalEvenIfGone(path.resolve(realTop, rel)));
124
+ return out;
125
+ }
126
+ /**
127
+ * Absolute, CANONICAL paths of the files in the repository `root` is in that HEAD
128
+ * does not hold: added to the index, or untracked and not ignored. What
129
+ * `changedSinceHead` cannot list — `git diff HEAD` never sees an untracked file —
130
+ * for a caller that must know what did not exist before this run. Throws when git
131
+ * cannot answer, on the same terms.
132
+ */
133
+ export async function addedSinceHead(root) {
134
+ const top = detectGitRoot(root);
135
+ if (!top)
136
+ throw new Error(`${root} is not inside a git repository`);
137
+ const run = async (args) => (await execFileAsync("git", args, { cwd: top, encoding: "utf8" })).stdout
138
+ .split("\0")
139
+ .filter(Boolean);
140
+ const rels = [
141
+ ...(await run([
142
+ "diff",
143
+ "-z",
144
+ "--name-only",
145
+ "--diff-filter=A",
146
+ "--no-renames",
147
+ "HEAD",
148
+ ])),
149
+ ...(await run(["ls-files", "-z", "--others", "--exclude-standard"])),
150
+ ];
151
+ const realTop = await realpath(top);
152
+ const out = [];
153
+ for (const rel of rels)
154
+ out.push(await canonicalEvenIfGone(path.resolve(realTop, rel)));
155
+ return out;
156
+ }
@@ -1,14 +1,30 @@
1
- import { type SiblingFile } from "./call-sites.js";
2
- import type { UtilsHelper } from "./parse.js";
1
+ import { type SiblingScan } from "./call-sites.js";
2
+ import { type UtilsHelper } from "./parse.js";
3
3
  import type { UtilsLanguageSpec } from "./language-spec.js";
4
- /** A helper imported by at most one delivered test. */
5
- export interface SingleImporterHelper {
4
+ /** One helper with the Skyramp-generated tests that import it. */
5
+ export interface HelperImporters {
6
6
  /** Absolute path of the utils file. */
7
7
  file: string;
8
8
  helper: string;
9
9
  line: number;
10
- /** Delivered tests importing it (0 or 1). */
11
- importers: number;
10
+ /** ABSOLUTE PATHS of the delivered tests importing it, in directory order — the
11
+ * spec under verification included. The count alone answers "is this helper
12
+ * shared"; the paths answer "which tests does a change to it reach", which is
13
+ * what a caller has to be named and run. */
14
+ importers: string[];
15
+ }
16
+ /** The importer walk's result. `read` is the walk's OWN verdict on whether it ran:
17
+ * an empty `helpers` means "no helper imports anything" only when `read` is true,
18
+ * and means nothing at all when it is false. A caller that blocks or asserts on
19
+ * the absence of an importer must consult it. */
20
+ export interface HelperImporterScan {
21
+ helpers: HelperImporters[];
22
+ /** Absolute paths of the generated tests the walk read. */
23
+ siblings: string[];
24
+ /** The sibling walk saw the whole directory: the listing succeeded and every
25
+ * candidate file was read. False means the importer lists are short by an unknown
26
+ * amount, not that they are empty. */
27
+ read: boolean;
12
28
  }
13
29
  /** The strings a sibling's source would contain if it imports the module at
14
30
  * `modulePath` (relative to the sibling's directory): the path form for TS/JS
@@ -16,8 +32,21 @@ export interface SingleImporterHelper {
16
32
  * `shared/X` → `shared.X` / `.shared.X`. */
17
33
  export declare function moduleMentionCandidates(modulePath: string): string[];
18
34
  /**
19
- * Count, per utils helper, how many Skyramp-generated tests in the spec's directory
20
- * import it — the spec under verification included.
35
+ * The Skyramp-generated tests in the spec's directory that import each utils helper —
36
+ * the spec under verification included. THE importer walk: the single-importer
37
+ * advisory and the changed-helper caller set both read it, so one definition decides
38
+ * what counts as an importing test.
39
+ *
40
+ * Fails open to an unread scan on any error. That is a silence, not an empty answer;
41
+ * `read` is what tells the two apart.
42
+ *
43
+ * SCOPE, which every caller inherits: siblings are the spec's OWN DIRECTORY. A
44
+ * generated test importing the module from another directory is not found here, so
45
+ * the importer list is a LOWER BOUND on the callers, never the complete set.
46
+ */
47
+ export declare function helperImporters(testFile: string, utilsFiles: string[], helpersByFile: ReadonlyMap<string, UtilsHelper[]>, spec: UtilsLanguageSpec, preReadSiblings?: SiblingScan): Promise<HelperImporterScan>;
48
+ /**
49
+ * Helpers with 0 or 1 importing test — the advisory slice of {@link helperImporters}.
21
50
  *
22
51
  * Findings:
23
52
  * - 0 importers: nothing imports the helper, not even the spec that wrote it — a
@@ -26,6 +55,26 @@ export declare function moduleMentionCandidates(modulePath: string): string[];
26
55
  * TWO delivered tests: on a first run every helper has exactly one importer by
27
56
  * construction (it seeds the utils file), so listing them would be guaranteed
28
57
  * noise on the most common path.
29
- * Fails open to "no findings" on any read error.
58
+ *
59
+ * Reports NOTHING from an incomplete walk. This is a claim, not a gate — it tells the
60
+ * agent a helper is dead and to call it or delete it — and a walk that could not read
61
+ * every candidate cannot support it: the file it failed to read may be the importer.
62
+ * Silence on doubt, so the finding stays trustworthy when it is made.
63
+ */
64
+ export declare function singleImporterHelpers(testFile: string, utilsFiles: string[], helpersByFile: ReadonlyMap<string, UtilsHelper[]>, spec: UtilsLanguageSpec, preReadSiblings?: SiblingScan): Promise<HelperImporters[]>;
65
+ /**
66
+ * `content` prepared for an import scan: the copy `importedNames` should read.
67
+ *
68
+ * Comments go in both languages — an import quoted in prose is not an import, and
69
+ * counting one makes a test a "caller" of a helper it cannot reach, which a gate then
70
+ * blocks on. Python string CONTENTS go too, since a Python import names no string, so
71
+ * the usage example in a docstring stops counting.
72
+ *
73
+ * TS/JS cannot blank every string, because the module specifier IS a string
74
+ * (`from "./skyrampUtils"`). Only the specifiers are kept: a string that directly
75
+ * follows `from` or `require(` stays, and every other string is blanked. Requiring
76
+ * the import to start a line is not enough on its own — a template literal holds
77
+ * whole lines, and the `require` form is bounded by a declaration keyword rather than
78
+ * by the line start — so quoted code kept reading as executable code.
30
79
  */
31
- export declare function singleImporterHelpers(testFile: string, utilsFiles: string[], helpersByFile: ReadonlyMap<string, UtilsHelper[]>, spec: UtilsLanguageSpec, preReadSiblings?: SiblingFile[]): Promise<SingleImporterHelper[]>;
80
+ export declare function importScanSource(content: string, spec: UtilsLanguageSpec): string;