@skyramp/mcp 0.4.2 → 0.4.3-rc.1

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 (119) 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/fakeVerifyContext.d.ts +3 -0
  16. package/build/recommendation/fakeVerifyContext.js +16 -0
  17. package/build/recommendation/runVerifiers.js +2 -0
  18. package/build/recommendation/types.d.ts +12 -0
  19. package/build/recommendation/verifierContracts.d.ts +24 -2
  20. package/build/recommendation/verifierContracts.js +32 -4
  21. package/build/recommendation/verifiers/changedLinesClaimed.d.ts +17 -0
  22. package/build/recommendation/verifiers/changedLinesClaimed.js +88 -0
  23. package/build/recommendation/verifiers/expectedValueSourced.js +8 -0
  24. package/build/recommendation/verifiers/requirementSourced.js +141 -11
  25. package/build/services/TestGenerationService.js +4 -0
  26. package/build/tools/code-refactor/caller-gate.d.ts +105 -0
  27. package/build/tools/code-refactor/caller-gate.js +174 -0
  28. package/build/tools/code-refactor/changed-helper-report.d.ts +62 -0
  29. package/build/tools/code-refactor/changed-helper-report.js +114 -0
  30. package/build/tools/code-refactor/codeReuseTool.js +131 -62
  31. package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -2
  32. package/build/tools/code-refactor/helper-callers.d.ts +65 -0
  33. package/build/tools/code-refactor/helper-callers.js +85 -0
  34. package/build/tools/code-refactor/modularizationTool.js +10 -0
  35. package/build/tools/code-refactor/pom-pass-state.d.ts +51 -0
  36. package/build/tools/code-refactor/pom-pass-state.js +115 -0
  37. package/build/tools/code-refactor/retrofit-state.d.ts +3 -2
  38. package/build/tools/code-refactor/retrofit-state.js +11 -12
  39. package/build/tools/code-refactor/reuse-outcome.d.ts +6 -0
  40. package/build/tools/code-refactor/reuse-record-store.d.ts +52 -0
  41. package/build/tools/code-refactor/reuse-record-store.js +126 -0
  42. package/build/tools/code-refactor/reuse-state.d.ts +18 -6
  43. package/build/tools/code-refactor/reuse-state.js +78 -131
  44. package/build/tools/code-refactor/utils-verify-gates.js +3 -3
  45. package/build/tools/code-refactor/verify-gates.d.ts +4 -0
  46. package/build/tools/code-refactor/verify-gates.js +8 -1
  47. package/build/tools/generate-tests/generateContractRestTool.js +7 -7
  48. package/build/tools/generate-tests/generateIntegrationRestTool.js +8 -8
  49. package/build/tools/generate-tests/generateUIRestTool.js +3 -3
  50. package/build/tools/submitReportTool.js +91 -29
  51. package/build/tools/test-management/actionsTool.js +7 -0
  52. package/build/tools/test-management/registerTestPlanTool.d.ts +1 -0
  53. package/build/tools/test-management/registerTestPlanTool.js +70 -22
  54. package/build/types/RepositoryAnalysis.d.ts +20 -20
  55. package/build/types/ReuseOutcome.d.ts +96 -0
  56. package/build/types/TestExecution.d.ts +11 -0
  57. package/build/types/TestExecution.js +19 -0
  58. package/build/types/index.d.ts +1 -1
  59. package/build/utils/branchDiff.d.ts +1 -1
  60. package/build/utils/branchDiff.js +1 -1
  61. package/build/utils/changedRuns.d.ts +13 -0
  62. package/build/utils/changedRuns.js +56 -0
  63. package/build/utils/featureFlags.d.ts +8 -10
  64. package/build/utils/featureFlags.js +23 -21
  65. package/build/utils/normalizeSkyrampImports.d.ts +4 -1
  66. package/build/utils/normalizeSkyrampImports.js +24 -17
  67. package/build/utils/pom-scope/ownership.d.ts +19 -0
  68. package/build/utils/pom-scope/ownership.js +31 -0
  69. package/build/utils/pom-scope/pom-files.d.ts +8 -0
  70. package/build/utils/pom-scope/pom-files.js +8 -0
  71. package/build/utils/pom-scope/scoring.js +13 -1
  72. package/build/utils/pom-scope/strip.d.ts +10 -0
  73. package/build/utils/pom-scope/strip.js +9 -0
  74. package/build/utils/pom-verify/verify.js +13 -2
  75. package/build/utils/reuseRouting.d.ts +30 -2
  76. package/build/utils/reuseRouting.js +36 -15
  77. package/build/utils/testDependencyPolicy.js +4 -16
  78. package/build/utils/utils-verify/action-sites.d.ts +2 -2
  79. package/build/utils/utils-verify/body-reach.d.ts +2 -2
  80. package/build/utils/utils-verify/body-reach.js +4 -1
  81. package/build/utils/utils-verify/call-sites.d.ts +16 -6
  82. package/build/utils/utils-verify/call-sites.js +12 -6
  83. package/build/utils/utils-verify/changed-helpers.d.ts +38 -0
  84. package/build/utils/utils-verify/changed-helpers.js +91 -0
  85. package/build/utils/utils-verify/head.d.ts +48 -0
  86. package/build/utils/utils-verify/head.js +115 -0
  87. package/build/utils/utils-verify/importers.d.ts +59 -10
  88. package/build/utils/utils-verify/importers.js +121 -31
  89. package/build/utils/utils-verify/in-house.js +26 -31
  90. package/build/utils/utils-verify/index.d.ts +3 -0
  91. package/build/utils/utils-verify/index.js +3 -0
  92. package/build/utils/utils-verify/language-spec.js +10 -2
  93. package/build/utils/utils-verify/locate.d.ts +3 -0
  94. package/build/utils/utils-verify/locate.js +6 -6
  95. package/build/utils/utils-verify/parse.d.ts +13 -1
  96. package/build/utils/utils-verify/parse.js +122 -49
  97. package/build/utils/utils-verify/retrofit-equivalence.d.ts +0 -5
  98. package/build/utils/utils-verify/retrofit-equivalence.js +2 -34
  99. package/build/utils/utils-verify/stage.js +4 -16
  100. package/build/utils/utils-verify/verify.d.ts +9 -3
  101. package/build/utils/utils-verify/verify.js +26 -12
  102. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1 -0
  103. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +1 -0
  104. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +6 -0
  105. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +303 -150
  106. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  107. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-LNgEKtdV.js → codeMirrorModule-Bou8kKzE.js} +1 -1
  108. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-Bwr1eMKC.js → defaultSettingsView-DTp2-8Si.js} +3 -3
  109. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.BAkLd5DX.js → index.ZYuEFHsi.js} +1 -1
  110. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  111. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.BPopbasy.js → uiMode.GLqsx5cI.js} +1 -1
  112. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  113. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  114. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  115. package/node_modules/playwright/package.json +1 -1
  116. package/package.json +2 -2
  117. package/plugin/prompts/code-reuse/hand-off.md +13 -1
  118. package/plugin/prompts/plan-tests.md +2 -2
  119. package/plugin/skills/enhance-assertions/reference/shared-rules.md +1 -0
@@ -0,0 +1,174 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ import { TestSource } from "../../types/TestAnalysis.js";
4
+ import { freshExecution, } from "../../types/TestExecution.js";
5
+ import { runTestCall } from "../../skills/runTestSkill.js";
6
+ import { canonPath } from "./reuse-state.js";
7
+ /**
8
+ * The report gate over the callers of a changed helper: each one the run CAN execute
9
+ * must have been run on the module as it stands. A caller that ran and FAILED is
10
+ * disclosed with its result and blocks nothing — the reviewer reads the red and
11
+ * decides, and SKYR-4451 adds the classification that decides for them.
12
+ *
13
+ * This is the retrofit gate's rule turned outward. A retrofit is a caller this run
14
+ * EDITED; a caller here is one it did not touch at all, which is why nothing ran it.
15
+ * The identity check differs accordingly (`freshExecution` is the same rule for
16
+ * both): a retrofit's execution must post-date the CALLER's last edit, and this one's
17
+ * must post-date the MODULE's — the caller is unchanged, and the bytes that changed
18
+ * under it are the module's.
19
+ *
20
+ * FAILS OPEN on every doubt, and the doubts are enumerated rather than inferred. It
21
+ * demands an execution only for a caller it has positive evidence about: the caller
22
+ * set is known, the file was inventoried as a Skyramp-generated test this run did not
23
+ * deliver, and the run is able to execute it. A helper whose callers could not be
24
+ * enumerated demands nothing; a false demand on a helper nobody calls is worse than
25
+ * a missing report line.
26
+ */
27
+ export async function changedHelperGate(params) {
28
+ const { views, state } = params;
29
+ const changed = new Set(params.changedFilesAbs.map((f) => canonPath(f)));
30
+ const inventory = new Map((state.existingTests ?? []).map((t) => [canonPath(t.testFile), t]));
31
+ const moduleChangedAt = new Map();
32
+ for (const utilsFile of new Set(views.map((v) => v.utilsFile)))
33
+ moduleChangedAt.set(utilsFile, await moduleMtime(utilsFile));
34
+ const perCaller = new Map();
35
+ for (const v of views) {
36
+ // The gate's predicate, stated once: only a caller set this run established.
37
+ if (!v.callers)
38
+ continue;
39
+ // Set above for every view's module.
40
+ const at = moduleChangedAt.get(v.utilsFile);
41
+ for (const caller of v.callers) {
42
+ const key = canonPath(caller);
43
+ const entry = inventory.get(key);
44
+ let seen = perCaller.get(key);
45
+ if (!seen) {
46
+ // A caller this run delivered or edited is still RECORDED, because the
47
+ // report lists every caller and an omitted execution there reads as "never
48
+ // run". Not inventoried, or the repository's own test: the demand would be
49
+ // one the agent cannot comply with as stated — no entry means no language or
50
+ // type to name, and the repository's own suite is the maintenance gate's
51
+ // (see `skyramp_actions`), not this one's. An unsatisfiable gate is the worse
52
+ // failure.
53
+ if (changed.has(key))
54
+ seen = {
55
+ kind: "delivered",
56
+ file: caller,
57
+ helpers: [],
58
+ changedAt: 0,
59
+ entry,
60
+ };
61
+ else if (entry && entry.source !== TestSource.External)
62
+ seen = {
63
+ kind: "inventoried",
64
+ file: caller,
65
+ helpers: [],
66
+ changedAt: 0,
67
+ entry,
68
+ };
69
+ else
70
+ continue;
71
+ perCaller.set(key, seen);
72
+ }
73
+ seen.helpers.push({
74
+ utilsFile: v.utilsFile,
75
+ helper: v.helper,
76
+ change: v.change,
77
+ });
78
+ seen.changedAt = Math.max(seen.changedAt, at);
79
+ }
80
+ }
81
+ const executions = new Map();
82
+ const owed = [];
83
+ for (const [key, c] of perCaller) {
84
+ // The execution must post-date the MODULE's last write, not the caller's, and
85
+ // its status must be a result — `freshExecution` says both. Read off the
86
+ // per-file record first: a caller this run CREATED has no inventory row, and
87
+ // reading the inventory alone left it with no result on the disclosure while
88
+ // the callers the run edited showed theirs (repo20-SR04, run 35773589796).
89
+ const latest = state.testExecutions?.[key];
90
+ const ran = freshExecution(latest?.phase === "after" ? latest : c.entry?.executionAfter, c.changedAt);
91
+ executions.set(key, {
92
+ file: c.file,
93
+ ...(ran ? { execution: ran } : {}),
94
+ ...(c.kind === "delivered" ? { deliveredByThisRun: true } : {}),
95
+ });
96
+ // A delivered caller owes nothing here: its own row carries its execution, and
97
+ // its own row's checks judge it.
98
+ if (c.kind === "delivered" || ran)
99
+ continue;
100
+ owed.push({
101
+ file: c.file,
102
+ helpers: c.helpers,
103
+ language: c.entry.language,
104
+ framework: c.entry.framework,
105
+ testType: String(c.entry.testType),
106
+ });
107
+ }
108
+ return { executions, owed };
109
+ }
110
+ /**
111
+ * The refusal a set of unexecuted callers makes, and the exact call that clears it.
112
+ *
113
+ * GIVES the remedy rather than describing it. A sentence naming only the path and
114
+ * the phase produced a call missing arguments `skyramp_execute_test` requires, so the
115
+ * agent spent a round discovering which and could not always recover. The
116
+ * `unresolved-module` finding is the model: it hands over the literal header line to
117
+ * write, not a description of one. Here every value the server holds is printed per
118
+ * file, and the one it does not — the repository's own command for the file — is
119
+ * covered by the same instruction `skyramp_actions` gives for a before-run
120
+ * (`runTestCall`), so the two gates never describe the same call two ways.
121
+ *
122
+ * The arguments common to every caller are stated once above the list; a wall of
123
+ * eight calls × six arguments is its own way of being unreadable. A value is never
124
+ * dropped to shorten it.
125
+ *
126
+ * NOTHING HERE IS ABOUT A FAILING CALLER. This refuses on the ABSENCE of a result,
127
+ * which the agent can always remedy with one call per file. A caller that ran and
128
+ * failed is disclosed with its result and refuses nothing — see SKYR-4451.
129
+ */
130
+ export function unrunCallerRefusal(owed, ctx) {
131
+ if (owed.length === 0)
132
+ return undefined;
133
+ const rel = (f) => path.relative(ctx.repoRoot, f) || path.basename(f);
134
+ return (`${owed.length} pre-existing generated test(s) call a shared helper this run CHANGED, and have not been run since the change — ` +
135
+ `Testbot will not ship a helper edit it has not exercised against the tests that already call it. ` +
136
+ `A changed selector or a changed request reaches every caller at once and appears in no calling test's diff.\n` +
137
+ `Run each file below through skyramp_execute_test with phase: "after", stateFile: "${ctx.stateFile}", repository: "${ctx.repository}": ${runTestCall()}. ` +
138
+ `That records the execution and clears this. Each line gives the file's arguments as \`<absolute path> <language> <framework> <test type>\`:\n` +
139
+ owed
140
+ .map((o) => ` - ${rel(o.file)} · calls ${o.helpers.map((h) => h.helper).join(", ")}\n` +
141
+ ` \`${o.file} ${o.language} ${o.framework} ${o.testType}\``)
142
+ .join("\n") +
143
+ `\nDo NOT remove imports, inline a helper back, or restore a caller from HEAD to satisfy this check — un-sharing the helpers is the tamper path, not the fix.`);
144
+ }
145
+ /**
146
+ * When the module last changed, in epoch milliseconds — the "since" every caller's
147
+ * execution is measured against.
148
+ *
149
+ * A DELETED module has no mtime of its own, and answering `0` for it accepted any
150
+ * execution ever recorded — including one from before the deletion, which exercised
151
+ * an import that still resolved and proves nothing about the one that no longer does.
152
+ * Its directory's mtime is the timestamp to use: POSIX updates it when an entry is
153
+ * removed, so it is the moment the module disappeared. Later writes to that directory
154
+ * only push it later, which is stricter rather than wrong, and an execution recorded
155
+ * after the deletion still clears it.
156
+ *
157
+ * Only a module whose directory ALSO cannot be read falls back to `0`, which fails
158
+ * open the way the retrofit gate does: an unreadable timestamp must not demand a
159
+ * re-run of every caller for ever.
160
+ */
161
+ async function moduleMtime(file) {
162
+ try {
163
+ return (await fs.stat(file)).mtimeMs;
164
+ }
165
+ catch {
166
+ /* gone: its directory records when */
167
+ }
168
+ try {
169
+ return (await fs.stat(path.dirname(file))).mtimeMs;
170
+ }
171
+ catch {
172
+ return 0;
173
+ }
174
+ }
@@ -0,0 +1,62 @@
1
+ import type { UnifiedAnalysisState } from "../../utils/AnalysisStateManager.js";
2
+ import type { ChangedHelperEntry } from "../../types/ReuseOutcome.js";
3
+ import { type ChangedHelperView } from "./helper-callers.js";
4
+ import { type CallerExecution } from "./caller-gate.js";
5
+ /** Everything the report needs about the helpers this run changed: the views by the
6
+ * canonical directory of the module, the execution recorded for each caller by its
7
+ * canonical path, and the refusal if any caller the run can execute has not been.
8
+ * Built once per report. */
9
+ export interface ChangedHelperReport {
10
+ byDir: Map<string, ChangedHelperView[]>;
11
+ executions: Map<string, CallerExecution>;
12
+ /** Set when a caller the run CAN execute has not been run against the changed
13
+ * module. Unbounded by design, like the retrofit gate's: the remedy is one tool
14
+ * call the agent can always make, and only callers it can make it for are named. */
15
+ refusal?: string;
16
+ }
17
+ export interface ChangedHelperReportParams {
18
+ /** Every checkout the run may have written to — the same roots the working-tree
19
+ * scan lists changes for. A module is looked for in each. */
20
+ roots: Array<string | undefined>;
21
+ /** Absolute paths of everything this run changed, across every checkout: a caller
22
+ * among them is delivered, and its own row carries its execution. */
23
+ changedFilesAbs: string[];
24
+ state: Pick<UnifiedAnalysisState, "existingTests" | "testExecutions">;
25
+ /** The primary checkout, for the repository-relative names the refusal shows. */
26
+ repoRoot: string;
27
+ /** The section `skyramp_execute_test` records a run under, and the state file it
28
+ * appends to. Both required: a remedy naming an empty repository is a call the
29
+ * tool rejects, which is the round trip the remedy text exists to prevent. */
30
+ repository: string;
31
+ stateFile: string;
32
+ }
33
+ /**
34
+ * The changed-helper set for a whole report: every shared module that differs from
35
+ * HEAD in any root, the helpers changed in it, and the tests beside it that call them.
36
+ *
37
+ * FROM GIT, NOT FROM THE REUSE RECORDS. What makes a file a module is the header it
38
+ * carried at HEAD, and what makes it changed is `git diff`; who changed it does not
39
+ * enter into it. So a helper the maintenance phase repaired with an UPDATE on the
40
+ * module — the path where nothing else runs `incumbent-signature` — is found the same
41
+ * way as one the reuse pass rewrote, and its callers are executed and disclosed the
42
+ * same way. Every git call in the feature is here.
43
+ *
44
+ * Views are keyed by the module's canonical DIRECTORY, and a row looks its own up by
45
+ * the directory of its spec: a module belongs to every row beside it, and to none
46
+ * elsewhere. Never throws: a root git cannot answer for contributes nothing, logged.
47
+ */
48
+ export declare function deriveChangedHelpers(params: ChangedHelperReportParams): Promise<ChangedHelperReport>;
49
+ /** The views a row carries: those of its spec's directory. */
50
+ export declare function changedHelpersForRow(report: ChangedHelperReport, specPath: string): ChangedHelperView[];
51
+ /**
52
+ * What the report states about the helpers this run changed.
53
+ *
54
+ * The DISCLOSURE predicate, separate from the gate's by construction: `callers` is
55
+ * present only when the caller set was established, so the report says "callers
56
+ * unknown" rather than asserting a list it is not sure of. A caller with no
57
+ * execution was not run, which the row shows as an omission rather than as a pass.
58
+ * The module is named per helper, repository-relative, as the orphan entry names
59
+ * its: a delivery can carry several modules, and the same helper name in two of them
60
+ * must not read as one entry.
61
+ */
62
+ export declare function discloseChangedHelpers(views: ChangedHelperView[], executions: ChangedHelperReport["executions"]): ChangedHelperEntry[];
@@ -0,0 +1,114 @@
1
+ import * as path from "path";
2
+ import { logger } from "../../utils/logger.js";
3
+ import { changedSinceHead, contentAtHead, UTILS_FILE_HEADER_RE, UTILS_LANGUAGE_SPECS, } from "../../utils/utils-verify/index.js";
4
+ import { canonPath, utilsFileLabel } from "./reuse-state.js";
5
+ import { changedHelperCallers, } from "./helper-callers.js";
6
+ import { changedHelperGate, unrunCallerRefusal, } from "./caller-gate.js";
7
+ /**
8
+ * The changed-helper set for a whole report: every shared module that differs from
9
+ * HEAD in any root, the helpers changed in it, and the tests beside it that call them.
10
+ *
11
+ * FROM GIT, NOT FROM THE REUSE RECORDS. What makes a file a module is the header it
12
+ * carried at HEAD, and what makes it changed is `git diff`; who changed it does not
13
+ * enter into it. So a helper the maintenance phase repaired with an UPDATE on the
14
+ * module — the path where nothing else runs `incumbent-signature` — is found the same
15
+ * way as one the reuse pass rewrote, and its callers are executed and disclosed the
16
+ * same way. Every git call in the feature is here.
17
+ *
18
+ * Views are keyed by the module's canonical DIRECTORY, and a row looks its own up by
19
+ * the directory of its spec: a module belongs to every row beside it, and to none
20
+ * elsewhere. Never throws: a root git cannot answer for contributes nothing, logged.
21
+ */
22
+ export async function deriveChangedHelpers(params) {
23
+ const byDir = new Map();
24
+ const modulesByDir = new Map();
25
+ const extensions = Object.values(UTILS_LANGUAGE_SPECS).flatMap((s) => s.extensions);
26
+ for (const root of new Set(params.roots.filter((r) => !!r))) {
27
+ let candidates;
28
+ try {
29
+ // Deleted AND modified, renames as deletions: a rename or a stripped header
30
+ // leaves every caller importing a module that is no longer a module.
31
+ candidates = await changedSinceHead(root, { filter: "DM" });
32
+ }
33
+ catch (err) {
34
+ logger.warning("Could not list the files this run changed — shared helpers it changed there will not be reported", { root, error: String(err) });
35
+ continue;
36
+ }
37
+ // `candidates` are canonical (changedSinceHead's contract), so the directory
38
+ // key is taken straight off them.
39
+ for (const file of candidates) {
40
+ if (!extensions.some((e) => file.toLowerCase().endsWith(e)))
41
+ continue;
42
+ // The header AT HEAD is what made it a shared module. A test the run edited or
43
+ // deleted is not one, and neither is a module this run created. Read once, and
44
+ // handed on: the derivation compares against this same content.
45
+ const head = await contentAtHead(file, root);
46
+ if (head === undefined || !UTILS_FILE_HEADER_RE.test(head))
47
+ continue;
48
+ const dir = path.dirname(file);
49
+ modulesByDir.set(dir, [...(modulesByDir.get(dir) ?? []), { file, head }]);
50
+ }
51
+ }
52
+ for (const [dir, modules] of modulesByDir) {
53
+ try {
54
+ const views = await changedHelperCallers(modules);
55
+ if (views.length > 0)
56
+ byDir.set(dir, views);
57
+ }
58
+ catch (err) {
59
+ logger.warning("Could not establish which shared helpers a run changed — its callers will not be named or run", { dir, error: String(err) });
60
+ }
61
+ }
62
+ const views = [...byDir.values()].flat();
63
+ if (views.length === 0)
64
+ return { byDir, executions: new Map() };
65
+ const gate = await changedHelperGate({
66
+ views,
67
+ state: params.state,
68
+ changedFilesAbs: params.changedFilesAbs,
69
+ });
70
+ const refusal = unrunCallerRefusal(gate.owed, {
71
+ repoRoot: params.repoRoot,
72
+ repository: params.repository,
73
+ stateFile: params.stateFile,
74
+ });
75
+ return {
76
+ byDir,
77
+ executions: gate.executions,
78
+ ...(refusal ? { refusal } : {}),
79
+ };
80
+ }
81
+ /** The views a row carries: those of its spec's directory. */
82
+ export function changedHelpersForRow(report, specPath) {
83
+ return report.byDir.get(path.dirname(canonPath(specPath))) ?? [];
84
+ }
85
+ /**
86
+ * What the report states about the helpers this run changed.
87
+ *
88
+ * The DISCLOSURE predicate, separate from the gate's by construction: `callers` is
89
+ * present only when the caller set was established, so the report says "callers
90
+ * unknown" rather than asserting a list it is not sure of. A caller with no
91
+ * execution was not run, which the row shows as an omission rather than as a pass.
92
+ * The module is named per helper, repository-relative, as the orphan entry names
93
+ * its: a delivery can carry several modules, and the same helper name in two of them
94
+ * must not read as one entry.
95
+ */
96
+ export function discloseChangedHelpers(views, executions) {
97
+ return views.map((v) => ({
98
+ utilsFile: utilsFileLabel([v.utilsFile]),
99
+ helper: v.helper,
100
+ change: v.change,
101
+ ...(v.callers
102
+ ? {
103
+ callers: v.callers.map((f) => {
104
+ const e = executions.get(canonPath(f));
105
+ return {
106
+ file: path.basename(f),
107
+ ...(e?.execution ? { execution: e.execution } : {}),
108
+ ...(e?.deliveredByThisRun ? { deliveredByThisRun: true } : {}),
109
+ };
110
+ }),
111
+ }
112
+ : {}),
113
+ }));
114
+ }