@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.
- package/build/execution/wrapperConfig.d.ts +4 -0
- package/build/execution/wrapperConfig.js +14 -2
- package/build/prompts/code-reuse.d.ts +5 -1
- package/build/prompts/code-reuse.js +16 -10
- package/build/prompts/modularization/ui-test-modularization.js +9 -2
- package/build/prompts/pom-aware-code-reuse.d.ts +4 -1
- package/build/prompts/pom-aware-code-reuse.js +38 -6
- package/build/prompts/reuse-hand-off.d.ts +4 -0
- package/build/prompts/reuse-hand-off.js +10 -5
- package/build/prompts/shared-helper-policy.d.ts +5 -0
- package/build/prompts/shared-helper-policy.js +5 -0
- package/build/prompts/test-maintenance/driftAnalysisShared.js +2 -0
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +2 -1
- package/build/prompts/testbot/testbot-prompts.js +33 -16
- package/build/recommendation/fakeVerifyContext.d.ts +3 -0
- package/build/recommendation/fakeVerifyContext.js +16 -0
- package/build/recommendation/runVerifiers.js +2 -0
- package/build/recommendation/types.d.ts +12 -0
- package/build/recommendation/verifierContracts.d.ts +24 -2
- package/build/recommendation/verifierContracts.js +32 -4
- package/build/recommendation/verifiers/changedLinesClaimed.d.ts +17 -0
- package/build/recommendation/verifiers/changedLinesClaimed.js +88 -0
- package/build/recommendation/verifiers/expectedValueSourced.js +8 -0
- package/build/recommendation/verifiers/requirementSourced.js +141 -11
- package/build/services/TestGenerationService.js +4 -0
- package/build/tools/code-refactor/caller-gate.d.ts +105 -0
- package/build/tools/code-refactor/caller-gate.js +174 -0
- package/build/tools/code-refactor/changed-helper-report.d.ts +62 -0
- package/build/tools/code-refactor/changed-helper-report.js +114 -0
- package/build/tools/code-refactor/codeReuseTool.js +131 -62
- package/build/tools/code-refactor/enhanceAssertionsTool.js +2 -2
- package/build/tools/code-refactor/helper-callers.d.ts +65 -0
- package/build/tools/code-refactor/helper-callers.js +85 -0
- package/build/tools/code-refactor/modularizationTool.js +10 -0
- package/build/tools/code-refactor/pom-pass-state.d.ts +51 -0
- package/build/tools/code-refactor/pom-pass-state.js +115 -0
- package/build/tools/code-refactor/retrofit-state.d.ts +3 -2
- package/build/tools/code-refactor/retrofit-state.js +11 -12
- package/build/tools/code-refactor/reuse-outcome.d.ts +6 -0
- package/build/tools/code-refactor/reuse-record-store.d.ts +52 -0
- package/build/tools/code-refactor/reuse-record-store.js +126 -0
- package/build/tools/code-refactor/reuse-state.d.ts +18 -6
- package/build/tools/code-refactor/reuse-state.js +78 -131
- package/build/tools/code-refactor/utils-verify-gates.js +3 -3
- package/build/tools/code-refactor/verify-gates.d.ts +4 -0
- package/build/tools/code-refactor/verify-gates.js +8 -1
- package/build/tools/generate-tests/generateContractRestTool.js +7 -7
- package/build/tools/generate-tests/generateIntegrationRestTool.js +8 -8
- package/build/tools/generate-tests/generateUIRestTool.js +3 -3
- package/build/tools/submitReportTool.js +91 -29
- package/build/tools/test-management/actionsTool.js +7 -0
- package/build/tools/test-management/registerTestPlanTool.d.ts +1 -0
- package/build/tools/test-management/registerTestPlanTool.js +70 -22
- package/build/types/RepositoryAnalysis.d.ts +20 -20
- package/build/types/ReuseOutcome.d.ts +96 -0
- package/build/types/TestExecution.d.ts +11 -0
- package/build/types/TestExecution.js +19 -0
- package/build/types/index.d.ts +1 -1
- package/build/utils/branchDiff.d.ts +1 -1
- package/build/utils/branchDiff.js +1 -1
- package/build/utils/changedRuns.d.ts +13 -0
- package/build/utils/changedRuns.js +56 -0
- package/build/utils/featureFlags.d.ts +8 -10
- package/build/utils/featureFlags.js +23 -21
- package/build/utils/normalizeSkyrampImports.d.ts +4 -1
- package/build/utils/normalizeSkyrampImports.js +24 -17
- package/build/utils/pom-scope/ownership.d.ts +19 -0
- package/build/utils/pom-scope/ownership.js +31 -0
- package/build/utils/pom-scope/pom-files.d.ts +8 -0
- package/build/utils/pom-scope/pom-files.js +8 -0
- package/build/utils/pom-scope/scoring.js +13 -1
- package/build/utils/pom-scope/strip.d.ts +10 -0
- package/build/utils/pom-scope/strip.js +9 -0
- package/build/utils/pom-verify/verify.js +13 -2
- package/build/utils/reuseRouting.d.ts +30 -2
- package/build/utils/reuseRouting.js +36 -15
- package/build/utils/testDependencyPolicy.js +4 -16
- package/build/utils/utils-verify/action-sites.d.ts +2 -2
- package/build/utils/utils-verify/body-reach.d.ts +2 -2
- package/build/utils/utils-verify/body-reach.js +4 -1
- package/build/utils/utils-verify/call-sites.d.ts +16 -6
- package/build/utils/utils-verify/call-sites.js +12 -6
- package/build/utils/utils-verify/changed-helpers.d.ts +38 -0
- package/build/utils/utils-verify/changed-helpers.js +91 -0
- package/build/utils/utils-verify/head.d.ts +48 -0
- package/build/utils/utils-verify/head.js +115 -0
- package/build/utils/utils-verify/importers.d.ts +59 -10
- package/build/utils/utils-verify/importers.js +121 -31
- package/build/utils/utils-verify/in-house.js +26 -31
- package/build/utils/utils-verify/index.d.ts +3 -0
- package/build/utils/utils-verify/index.js +3 -0
- package/build/utils/utils-verify/language-spec.js +10 -2
- package/build/utils/utils-verify/locate.d.ts +3 -0
- package/build/utils/utils-verify/locate.js +6 -6
- package/build/utils/utils-verify/parse.d.ts +13 -1
- package/build/utils/utils-verify/parse.js +122 -49
- package/build/utils/utils-verify/retrofit-equivalence.d.ts +0 -5
- package/build/utils/utils-verify/retrofit-equivalence.js +2 -34
- package/build/utils/utils-verify/stage.js +4 -16
- package/build/utils/utils-verify/verify.d.ts +9 -3
- package/build/utils/utils-verify/verify.js +26 -12
- package/node_modules/playwright/lib/dom-analyzer/blueprint.js +1 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +1 -0
- package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +6 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +303 -150
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-LNgEKtdV.js → codeMirrorModule-Bou8kKzE.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-Bwr1eMKC.js → defaultSettingsView-DTp2-8Si.js} +3 -3
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.BAkLd5DX.js → index.ZYuEFHsi.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.BPopbasy.js → uiMode.GLqsx5cI.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +2 -2
- package/plugin/prompts/code-reuse/hand-off.md +13 -1
- package/plugin/prompts/plan-tests.md +2 -2
- package/plugin/skills/enhance-assertions/reference/shared-rules.md +1 -0
|
@@ -305,11 +305,25 @@ export const REQUIREMENT_SOURCED_CONTRACT = {
|
|
|
305
305
|
suggestion: "Cite a change you read in the pull request or in a requirements file, or report this test as something other than a requirement conflict.",
|
|
306
306
|
},
|
|
307
307
|
// The server rendered the prompt, so it holds the title and description the run
|
|
308
|
-
// was given. A requirements file the pull request
|
|
309
|
-
// found by looking, which is reading the
|
|
308
|
+
// was given, and it holds the diff. A requirements file the pull request neither
|
|
309
|
+
// names nor changes is a file the agent found by looking, which is reading the
|
|
310
|
+
// repository, not reading a requirement.
|
|
310
311
|
notNamed: {
|
|
311
|
-
message: "Change `{change}` cites `{source}`, and the pull request names
|
|
312
|
-
suggestion: "Cite a file the title or description names. If
|
|
312
|
+
message: "Change `{change}` cites `{source}`, and the pull request neither names nor changes such a file.",
|
|
313
|
+
suggestion: "Cite a file the title or description names, or one the diff changes. If none does, you read the change in the repository, so set `source` to `diff`.",
|
|
314
|
+
},
|
|
315
|
+
// `{item}` is one top-level list line of the cited file or the description,
|
|
316
|
+
// `{number}` its number, `{source}` that source as the first change citing it
|
|
317
|
+
// spelled it, without its `#` anchor. The check
|
|
318
|
+
// reads only whether some change names the number, not whether a test covers it.
|
|
319
|
+
itemUnlinked: {
|
|
320
|
+
message: '`{source}` lists item {number}, "{item}", and no change in this plan names it.',
|
|
321
|
+
suggestion: "Set `source` to `{source}#{number}` on the change that covers this item, or answer with the reason it needs no test.",
|
|
322
|
+
},
|
|
323
|
+
// `{count}` is the number of top-level list items in `{source}`.
|
|
324
|
+
unnumbered: {
|
|
325
|
+
message: "`{source}` lists {count} items, and no change names one of them by its number.",
|
|
326
|
+
suggestion: "Name the item each change covers in its source, as `{source}#<number>` (`#7,8` for two), so each item can be checked.",
|
|
313
327
|
},
|
|
314
328
|
// Says only what was read: this pull request arrived with no words in it.
|
|
315
329
|
emptyPullRequest: {
|
|
@@ -368,6 +382,20 @@ export const DEFECTS_CONTRACT = {
|
|
|
368
382
|
},
|
|
369
383
|
suggestion: "Which planned test proves each defect the review found, and for one nothing proves, why does it need no test?",
|
|
370
384
|
};
|
|
385
|
+
export const CHANGED_LINES_CLAIMED_CONTRACT = {
|
|
386
|
+
id: "changedLinesClaimed",
|
|
387
|
+
objections: {
|
|
388
|
+
unclaimed: {
|
|
389
|
+
message: "These changed lines are in a file a planned test covers, and no change in the plan names them in its `lines`.",
|
|
390
|
+
suggestion: "Add these lines to the `lines` of the change they belong to, or declare a change for them, with a case that states what a test must see. If the lines change nothing a test can observe, say so as the answer.",
|
|
391
|
+
},
|
|
392
|
+
noCase: {
|
|
393
|
+
message: "These changed lines belong to a change that states no case, so no test is held to what they do.",
|
|
394
|
+
suggestion: "Give the change a case whose `expectedValue` or `derived` states what a test must see after these lines. If the lines change nothing a test can observe, say so as the answer.",
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
suggestion: "Which change owns each run of changed lines in a tested file, and what must a test see because of it?",
|
|
398
|
+
};
|
|
371
399
|
/** Fills a contract sentence's `{placeholder}` spans from the run's or the
|
|
372
400
|
* planned test's own values. A placeholder with no value is left as written rather
|
|
373
401
|
* than blanked, so a missed key shows up as `{knob}` instead of reading as a gap
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { PlanChange, PlanInput, Verifier, VerifyContext } from "../types.js";
|
|
2
|
+
import type { ChangedRun } from "../../utils/changedRuns.js";
|
|
3
|
+
/** Whether a change's `lines` reach this run. Any overlap counts: a run that the
|
|
4
|
+
* claimed range touches is the change's to answer for. */
|
|
5
|
+
export declare function changeCoversRun(change: PlanChange, run: ChangedRun): boolean;
|
|
6
|
+
/** The changed runs in files a planned test names as its changed file, each with
|
|
7
|
+
* the changes that can claim it: a change counts in the repositories of the tests
|
|
8
|
+
* that cite it, and in every repository when no test cites it. */
|
|
9
|
+
export declare function runsInTestedFiles(registration: PlanInput, ctx: VerifyContext): Array<{
|
|
10
|
+
repository: string;
|
|
11
|
+
run: ChangedRun;
|
|
12
|
+
changes: PlanChange[];
|
|
13
|
+
}>;
|
|
14
|
+
/** Every changed run in a file a planned test covers belongs to a change that
|
|
15
|
+
* states what a test must see. The server reads which lines changed; the agent
|
|
16
|
+
* says what they do. One objection per run. */
|
|
17
|
+
export declare const changedLinesClaimed: Verifier;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { normalizeCitedPath } from "./citedPath.js";
|
|
2
|
+
import { CHANGED_LINES_CLAIMED_CONTRACT as CONTRACT } from "../verifierContracts.js";
|
|
3
|
+
/** One `lines` entry read as a file and a line range: `path:12` or `path:12-20`. */
|
|
4
|
+
function readLines(raw) {
|
|
5
|
+
if (typeof raw !== "string")
|
|
6
|
+
return undefined;
|
|
7
|
+
const match = /^(.+):(\d+)(?:-(\d+))?$/.exec(raw.trim());
|
|
8
|
+
if (!match)
|
|
9
|
+
return undefined;
|
|
10
|
+
const file = normalizeCitedPath(match[1]);
|
|
11
|
+
const start = Number(match[2]);
|
|
12
|
+
const end = match[3] === undefined ? start : Number(match[3]);
|
|
13
|
+
return file && end >= start ? { file, start, end } : undefined;
|
|
14
|
+
}
|
|
15
|
+
/** Whether a change's `lines` reach this run. Any overlap counts: a run that the
|
|
16
|
+
* claimed range touches is the change's to answer for. */
|
|
17
|
+
export function changeCoversRun(change, run) {
|
|
18
|
+
const lines = Array.isArray(change?.lines) ? change.lines : [];
|
|
19
|
+
return lines.some((raw) => {
|
|
20
|
+
const range = readLines(raw);
|
|
21
|
+
return (range !== undefined &&
|
|
22
|
+
range.file === run.file &&
|
|
23
|
+
range.start <= run.end &&
|
|
24
|
+
range.end >= run.start);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** Whether a change states what its test must see: some case carries an
|
|
28
|
+
* `expectedValue` or a `derived` rule. */
|
|
29
|
+
function statesAnOutput(change) {
|
|
30
|
+
return (change.testCases ?? []).some((entry) => entry &&
|
|
31
|
+
(entry.expectedValue !== undefined ||
|
|
32
|
+
String(entry.derived ?? "").trim().length > 0));
|
|
33
|
+
}
|
|
34
|
+
/** The changed runs in files a planned test names as its changed file, each with
|
|
35
|
+
* the changes that can claim it: a change counts in the repositories of the tests
|
|
36
|
+
* that cite it, and in every repository when no test cites it. */
|
|
37
|
+
export function runsInTestedFiles(registration, ctx) {
|
|
38
|
+
const touched = new Set();
|
|
39
|
+
const citedIn = new Map();
|
|
40
|
+
for (const plannedTest of registration.plannedTests) {
|
|
41
|
+
const file = normalizeCitedPath(plannedTest.declarations.changedFile);
|
|
42
|
+
if (file)
|
|
43
|
+
touched.add(`${plannedTest.repository}\u0000${file}`);
|
|
44
|
+
for (const id of plannedTest.declarations.changes ?? []) {
|
|
45
|
+
const key = String(id ?? "").trim();
|
|
46
|
+
if (!citedIn.has(key))
|
|
47
|
+
citedIn.set(key, new Set());
|
|
48
|
+
citedIn.get(key)?.add(plannedTest.repository);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return Object.entries(ctx.changedRuns).flatMap(([repository, runs]) => runs
|
|
52
|
+
.filter((run) => touched.has(`${repository}\u0000${run.file}`))
|
|
53
|
+
.map((run) => ({
|
|
54
|
+
repository,
|
|
55
|
+
run,
|
|
56
|
+
changes: registration.changes.filter((change) => {
|
|
57
|
+
const repos = citedIn.get(String(change.id ?? "").trim());
|
|
58
|
+
return ((!repos || repos.has(repository)) && changeCoversRun(change, run));
|
|
59
|
+
}),
|
|
60
|
+
})));
|
|
61
|
+
}
|
|
62
|
+
/** Every changed run in a file a planned test covers belongs to a change that
|
|
63
|
+
* states what a test must see. The server reads which lines changed; the agent
|
|
64
|
+
* says what they do. One objection per run. */
|
|
65
|
+
export const changedLinesClaimed = {
|
|
66
|
+
name: "changedLinesClaimed",
|
|
67
|
+
run(registration, ctx) {
|
|
68
|
+
const objections = [];
|
|
69
|
+
for (const { repository, run, changes } of runsInTestedFiles(registration, ctx)) {
|
|
70
|
+
if (changes.some(statesAnOutput))
|
|
71
|
+
continue;
|
|
72
|
+
const kind = changes.length === 0 ? "unclaimed" : "noCase";
|
|
73
|
+
const where = `${run.file}:${run.start}${run.end > run.start ? `-${run.end}` : ""}`;
|
|
74
|
+
objections.push({
|
|
75
|
+
objectionId: `changedLinesClaimed:${kind}:${repository}/${run.file}:${run.start}`,
|
|
76
|
+
verifier: "changedLinesClaimed",
|
|
77
|
+
message: CONTRACT.objections[kind].message,
|
|
78
|
+
evidence: kind === "unclaimed"
|
|
79
|
+
? `${repository} ${where} (\`${run.firstLine.trim().slice(0, 120)}\`): no change's \`lines\` covers it`
|
|
80
|
+
: `${repository} ${where} (\`${run.firstLine.trim().slice(0, 120)}\`): covered by ${changes
|
|
81
|
+
.map((change) => String(change.id ?? "?"))
|
|
82
|
+
.join(", ")}, and none of them has a case with \`expectedValue\` or \`derived\``,
|
|
83
|
+
suggestion: CONTRACT.objections[kind].suggestion,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return objections;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
@@ -4,6 +4,7 @@ import { EXPECTED_VALUE_SOURCED_CONTRACT as CONTRACT } from "../verifierContract
|
|
|
4
4
|
import { appearsIn, searchable } from "../pullRequestText.js";
|
|
5
5
|
import { escapeRegExp } from "../../utils/regex.js";
|
|
6
6
|
import { renderTestCase } from "./coverage.js";
|
|
7
|
+
import { runsInTestedFiles } from "./changedLinesClaimed.js";
|
|
7
8
|
function readSource(raw) {
|
|
8
9
|
const source = typeof raw === "string" ? raw.trim() : "";
|
|
9
10
|
if (source.length === 0)
|
|
@@ -119,6 +120,8 @@ export const expectedValueSourced = {
|
|
|
119
120
|
const changed = changedPathSet(allChangedFiles(ctx), true);
|
|
120
121
|
const prText = searchable(ctx.pullRequest);
|
|
121
122
|
const raised = new Set();
|
|
123
|
+
// The changes that own a changed run in a file a planned test covers.
|
|
124
|
+
const owners = new Set(runsInTestedFiles(registration, ctx).flatMap(({ changes }) => changes));
|
|
122
125
|
const changes = registration.changes;
|
|
123
126
|
for (const change of changes) {
|
|
124
127
|
const changeId = String(change.id ?? "?");
|
|
@@ -170,6 +173,11 @@ export const expectedValueSourced = {
|
|
|
170
173
|
raise("unsourced", CONTRACT.objections.missingSource, `expected value with no usable \`expectedFrom\`: ${where}`);
|
|
171
174
|
}
|
|
172
175
|
else if (source.kind === "code") {
|
|
176
|
+
// A change read from the diff that names its changed lines has no other
|
|
177
|
+
// source: no requirement exists for the code to disagree with.
|
|
178
|
+
if (/^diff$/i.test(String(change.source ?? "").trim()) &&
|
|
179
|
+
owners.has(change))
|
|
180
|
+
return;
|
|
173
181
|
raise("code", CONTRACT.objections.sourcedFromCode,
|
|
174
182
|
// A derived-only case pins no value, so quoting one read `expected
|
|
175
183
|
// undefined` — a sentence naming nothing the agent can act on.
|
|
@@ -1,47 +1,111 @@
|
|
|
1
|
+
import { allChangedFiles, } from "../types.js";
|
|
1
2
|
import { normalizeCategory } from "../../types/TestRecommendation.js";
|
|
2
3
|
import { REQUIREMENT_SOURCED_CONTRACT, fillPlaceholders, } from "../verifierContracts.js";
|
|
3
|
-
import { normalizeCitedPath } from "./citedPath.js";
|
|
4
|
+
import { changedPathSet, normalizeCitedPath } from "./citedPath.js";
|
|
4
5
|
import { appearsIn, searchable } from "../pullRequestText.js";
|
|
5
6
|
/** Verifier 10. A change names where the agent read it, and the server reads the
|
|
6
7
|
* name back.
|
|
7
8
|
*
|
|
8
9
|
* `source` is the record of where a change came from, and the server holds the
|
|
9
10
|
* pull request's own words, so it reads the claim back against them: a `spec:` path
|
|
10
|
-
* has to name a file the checkout holds AND a file the pull request names, and a
|
|
11
|
+
* has to name a file the checkout holds AND a file the pull request names or changes, and a
|
|
11
12
|
* change read from the pull request has to quote words the pull request carries.
|
|
12
13
|
*
|
|
14
|
+
* A requirements file the pull request names or changes, and the description
|
|
15
|
+
* itself, are read for their lists: every top-level item needs a change that names
|
|
16
|
+
* it by number (`spec:<path>#11`, `pr-description#3`), or the plan answers why it
|
|
17
|
+
* needs no test.
|
|
18
|
+
*
|
|
13
19
|
* A conflict is a disagreement between a requirement and the code. A change read
|
|
14
20
|
* off the diff IS the code, so there is no second side for it to disagree with.
|
|
15
21
|
*
|
|
16
22
|
* The plan tool always supplies the pull request: the testbot fetches its prompt
|
|
17
23
|
* from this same server, so the text is in the process. Both fields blank means
|
|
18
24
|
* the run had no title and no description. */
|
|
19
|
-
const { unsourced: UNSOURCED, unreadable: UNREADABLE, fromDiff: FROM_DIFF, noChange: NO_CHANGE, notNamed: NOT_NAMED, emptyPullRequest: EMPTY_PULL_REQUEST, notQuoted: NOT_QUOTED, unquoted: UNQUOTED, } = REQUIREMENT_SOURCED_CONTRACT.objections;
|
|
25
|
+
const { unsourced: UNSOURCED, unreadable: UNREADABLE, fromDiff: FROM_DIFF, noChange: NO_CHANGE, notNamed: NOT_NAMED, emptyPullRequest: EMPTY_PULL_REQUEST, notQuoted: NOT_QUOTED, unquoted: UNQUOTED, itemUnlinked: ITEM_UNLINKED, unnumbered: UNNUMBERED, } = REQUIREMENT_SOURCED_CONTRACT.objections;
|
|
20
26
|
const CONFLICT_CATEGORY = "requirement_conflict";
|
|
27
|
+
const itemsIn = (anchor) => anchor !== undefined && /^\s*\d+(\s*,\s*\d+)*\s*$/.test(anchor)
|
|
28
|
+
? anchor.split(",").map((n) => Number(n.trim()))
|
|
29
|
+
: [];
|
|
21
30
|
function readSource(raw) {
|
|
22
31
|
const none = (kind) => ({
|
|
23
32
|
kind,
|
|
24
33
|
file: "",
|
|
25
34
|
citedAs: "",
|
|
35
|
+
items: [],
|
|
26
36
|
});
|
|
27
37
|
const source = raw.trim();
|
|
28
38
|
if (source.length === 0)
|
|
29
39
|
return none("unknown");
|
|
30
|
-
if (/^
|
|
40
|
+
if (/^pr-title$/i.test(source))
|
|
31
41
|
return none("pullRequest");
|
|
42
|
+
const description = /^pr-description(?:#(.*))?$/i.exec(source);
|
|
43
|
+
if (description)
|
|
44
|
+
return { ...none("pullRequest"), items: itemsIn(description[1]) };
|
|
32
45
|
if (/^diff$/i.test(source))
|
|
33
46
|
return none("diff");
|
|
34
47
|
const named = /^spec:(.*)$/i.exec(source);
|
|
35
48
|
if (!named)
|
|
36
49
|
return none("unknown");
|
|
37
|
-
const
|
|
38
|
-
|
|
50
|
+
const [path, anchor] = named[1].split(/#/);
|
|
51
|
+
const citedAs = path
|
|
52
|
+
.split(/§/)[0]
|
|
39
53
|
.replace(/(?::\d+)+(?:-\d+)?$/, "")
|
|
40
54
|
.trim();
|
|
41
55
|
const file = normalizeCitedPath(citedAs);
|
|
42
56
|
// `spec:` with nothing after it names no file, so there is nothing to check and
|
|
43
57
|
// nothing to trust either.
|
|
44
|
-
return file
|
|
58
|
+
return file
|
|
59
|
+
? { kind: "spec", file, citedAs, items: itemsIn(anchor) }
|
|
60
|
+
: none("unknown");
|
|
61
|
+
}
|
|
62
|
+
const LIST_LINE = /^([ \t]*)(\d+(?=[.)])|[-*+•])[.)]?\s+(.*)$/;
|
|
63
|
+
/** Only a prose file holds a list of requirements. A YAML or JSON file is a list of
|
|
64
|
+
* settings, and each of its lines would read as one. */
|
|
65
|
+
const PROSE_FILE = /(^|\/)[^./]+$|\.(md|markdown|txt|rst|adoc)$/i;
|
|
66
|
+
/** The top-level items of a list: its numbered or bulleted lines at the smallest
|
|
67
|
+
* indent. A nested bullet is not an item of its own. Lines inside a fenced code
|
|
68
|
+
* block or a leading front-matter block are not items, and neither is a line with
|
|
69
|
+
* no letter or digit (a `- - -` rule). A text with no list has no items.
|
|
70
|
+
*
|
|
71
|
+
* An item's number is the one the text writes (`11.`), so a plan can cite it as the
|
|
72
|
+
* reader sees it, and a bulleted list is numbered by position from 1. A text whose
|
|
73
|
+
* numbers repeat (two numbered lists) or that mixes numbered and bulleted items
|
|
74
|
+
* has no numbering a plan can cite, so it gives no items. */
|
|
75
|
+
function topLevelListItems(text) {
|
|
76
|
+
const lines = text
|
|
77
|
+
.replace(/^\uFEFF/, "")
|
|
78
|
+
.replace(/^---\r?\n[\s\S]*?\r?\n---(\r?\n|$)/, "")
|
|
79
|
+
.split(/\r\n?|\n/);
|
|
80
|
+
// The marker that opened the fence, so ``` is closed by ``` and not by ~~~.
|
|
81
|
+
let fence;
|
|
82
|
+
const listed = lines.flatMap((line) => {
|
|
83
|
+
const marker = /^\s*(```|~~~)/.exec(line)?.[1];
|
|
84
|
+
if (marker && (fence === undefined || fence === marker))
|
|
85
|
+
fence = fence === undefined ? marker : undefined;
|
|
86
|
+
const match = fence !== undefined || marker ? null : LIST_LINE.exec(line);
|
|
87
|
+
if (!match || !/[\p{L}\p{N}]/u.test(match[3]))
|
|
88
|
+
return [];
|
|
89
|
+
return [
|
|
90
|
+
{ indent: match[1].length, written: match[2], text: match[3].trim() },
|
|
91
|
+
];
|
|
92
|
+
});
|
|
93
|
+
const top = Math.min(...listed.map((line) => line.indent));
|
|
94
|
+
const items = listed.filter((line) => line.indent === top);
|
|
95
|
+
const written = items.map((item) => Number.parseInt(item.written, 10));
|
|
96
|
+
if (written.every(Number.isInteger)) {
|
|
97
|
+
// Numbers that repeat mean two numbered lists; no single number names an item.
|
|
98
|
+
if (new Set(written).size !== written.length)
|
|
99
|
+
return [];
|
|
100
|
+
return items.map((item, index) => ({
|
|
101
|
+
number: written[index],
|
|
102
|
+
text: item.text,
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
// A mix of numbered and bulleted lines has no numbering a reader can cite.
|
|
106
|
+
if (written.some(Number.isInteger))
|
|
107
|
+
return [];
|
|
108
|
+
return items.map((item, index) => ({ number: index + 1, text: item.text }));
|
|
45
109
|
}
|
|
46
110
|
/** A source that stands for a requirement: the pull request, or a requirements file. */
|
|
47
111
|
const statesARequirement = (source) => source.kind === "pullRequest" || source.kind === "spec";
|
|
@@ -54,6 +118,12 @@ export const requirementSourced = {
|
|
|
54
118
|
const sourceById = new Map();
|
|
55
119
|
const quoteById = new Map();
|
|
56
120
|
const pullRequestText = searchable(ctx.pullRequest);
|
|
121
|
+
const changedPaths = changedPathSet(allChangedFiles(ctx), false);
|
|
122
|
+
// The list items the changes name, per requirements file the pull request
|
|
123
|
+
// names or changes, and for the description.
|
|
124
|
+
const descriptionItems = [];
|
|
125
|
+
let descriptionCited = false;
|
|
126
|
+
const itemsBySpec = new Map();
|
|
57
127
|
for (const change of changes) {
|
|
58
128
|
const id = String(change.id ?? "").trim();
|
|
59
129
|
const changeId = id || "(unnamed)";
|
|
@@ -75,6 +145,9 @@ export const requirementSourced = {
|
|
|
75
145
|
continue;
|
|
76
146
|
}
|
|
77
147
|
if (source.kind === "pullRequest") {
|
|
148
|
+
if (/^pr-description/i.test(written))
|
|
149
|
+
descriptionCited = true;
|
|
150
|
+
descriptionItems.push(...source.items);
|
|
78
151
|
if (pullRequestText.length > 0)
|
|
79
152
|
continue;
|
|
80
153
|
objections.push({
|
|
@@ -103,11 +176,17 @@ export const requirementSourced = {
|
|
|
103
176
|
});
|
|
104
177
|
continue;
|
|
105
178
|
}
|
|
106
|
-
// A requirements file the pull request
|
|
107
|
-
// looking through the repository, which is not reading a
|
|
179
|
+
// A requirements file the pull request neither names nor changes is a file the
|
|
180
|
+
// agent found by looking through the repository, which is not reading a
|
|
181
|
+
// requirement.
|
|
108
182
|
if (appearsIn(pullRequestText, source.file) ||
|
|
109
|
-
appearsIn(pullRequestText, source.citedAs)
|
|
183
|
+
appearsIn(pullRequestText, source.citedAs) ||
|
|
184
|
+
changedPaths.has(source.file)) {
|
|
185
|
+
const cited = itemsBySpec.get(source.file) ?? { written, items: [] };
|
|
186
|
+
cited.items.push(...source.items);
|
|
187
|
+
itemsBySpec.set(source.file, cited);
|
|
110
188
|
continue;
|
|
189
|
+
}
|
|
111
190
|
objections.push({
|
|
112
191
|
objectionId: `requirementSourced:notNamed:${changeId}`,
|
|
113
192
|
verifier: "requirementSourced",
|
|
@@ -115,10 +194,61 @@ export const requirementSourced = {
|
|
|
115
194
|
change: changeId,
|
|
116
195
|
source: written,
|
|
117
196
|
}),
|
|
118
|
-
evidence: `change ${changeId} states source \`${written}\`; neither the title nor the description names ${source.file}`,
|
|
197
|
+
evidence: `change ${changeId} states source \`${written}\`; neither the title nor the description names ${source.file}, and the diff does not change it`,
|
|
119
198
|
suggestion: NOT_NAMED.suggestion,
|
|
120
199
|
});
|
|
121
200
|
}
|
|
201
|
+
// Every item of a list needs a change that names it by number. A list no change
|
|
202
|
+
// numbers at all is one objection, not one per item: the plan is not citing the
|
|
203
|
+
// list by item yet, and one answer covers that.
|
|
204
|
+
const objectToUnlinked = (items, linked, where, source) => {
|
|
205
|
+
if (items.length === 0)
|
|
206
|
+
return;
|
|
207
|
+
// A number the list does not hold names nothing.
|
|
208
|
+
if (!linked.some((n) => items.some((item) => item.number === n))) {
|
|
209
|
+
objections.push({
|
|
210
|
+
objectionId: `requirementSourced:unnumbered:${where}`,
|
|
211
|
+
verifier: "requirementSourced",
|
|
212
|
+
message: fillPlaceholders(UNNUMBERED.message, {
|
|
213
|
+
source,
|
|
214
|
+
count: String(items.length),
|
|
215
|
+
}),
|
|
216
|
+
evidence: `${where} lists ${items.length} item(s); no change names one as \`#<number>\``,
|
|
217
|
+
suggestion: fillPlaceholders(UNNUMBERED.suggestion, { source }),
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
for (const item of items) {
|
|
222
|
+
if (linked.includes(item.number))
|
|
223
|
+
continue;
|
|
224
|
+
objections.push({
|
|
225
|
+
objectionId: `requirementSourced:specItem:${where}#${item.number}`,
|
|
226
|
+
verifier: "requirementSourced",
|
|
227
|
+
message: fillPlaceholders(ITEM_UNLINKED.message, {
|
|
228
|
+
source,
|
|
229
|
+
number: String(item.number),
|
|
230
|
+
item: item.text,
|
|
231
|
+
}),
|
|
232
|
+
evidence: `${where} lists item ${item.number}, "${item.text}"; no change names #${item.number}`,
|
|
233
|
+
suggestion: fillPlaceholders(ITEM_UNLINKED.suggestion, {
|
|
234
|
+
source,
|
|
235
|
+
number: String(item.number),
|
|
236
|
+
}),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
for (const [file, { written, items }] of itemsBySpec) {
|
|
241
|
+
if (!PROSE_FILE.test(file))
|
|
242
|
+
continue;
|
|
243
|
+
const text = ctx.readSpecFile(file);
|
|
244
|
+
if (text === undefined)
|
|
245
|
+
continue;
|
|
246
|
+
objectToUnlinked(topLevelListItems(text), items, file, `spec:${written.slice(5).split("#")[0]}`);
|
|
247
|
+
}
|
|
248
|
+
// The description's own list is read the same way, when a change cites the
|
|
249
|
+
// description. Its sentences are not read.
|
|
250
|
+
if (descriptionCited)
|
|
251
|
+
objectToUnlinked(topLevelListItems(ctx.pullRequest.description), descriptionItems, "pr-description", "pr-description");
|
|
122
252
|
for (const plannedTest of plannedTests) {
|
|
123
253
|
if (normalizeCategory(plannedTest.scenario.category) !== CONFLICT_CATEGORY)
|
|
124
254
|
continue;
|
|
@@ -14,6 +14,7 @@ import { stageGeneratedPaths, resolveOutputDir } from "../utils/gitStaging.js";
|
|
|
14
14
|
import { isInsideExecutorWorkDir, executorWorkDirRefusal, generationTargets, } from "../utils/executorWorkDir.js";
|
|
15
15
|
import { getTestsRepoDir } from "../utils/AnalysisStateManager.js";
|
|
16
16
|
import { recordReuseHandOff } from "../tools/code-refactor/reuse-state.js";
|
|
17
|
+
import { isPomFirstTarget } from "../utils/reuseRouting.js";
|
|
17
18
|
import { recordAssertionBaseline } from "../tools/code-refactor/assertion-state.js";
|
|
18
19
|
import { persistGeneratedTest } from "../utils/generatedTestRecord.js";
|
|
19
20
|
import { buildModularizeFirstNextSteps, isModularizeFirstHandOffTarget, reuseCodeArgsClause, } from "../prompts/reuse-hand-off.js";
|
|
@@ -166,6 +167,8 @@ export class TestGenerationService {
|
|
|
166
167
|
// and the report can mark it — without ever owing it for a test that was not
|
|
167
168
|
// told to run it. Keyed by the files this call wrote: a test generated into
|
|
168
169
|
// the same directory without codeReuse owes nothing.
|
|
170
|
+
// Both reuse settings on: the page-object pass runs first (isPomFirstTarget).
|
|
171
|
+
const pomFirst = isPomFirstTarget(params.language, reuseTestType);
|
|
169
172
|
await recordReuseHandOff(written, testType, params.language, effectiveFramework);
|
|
170
173
|
// Browser tests: the assertion enhancer anchors new assertions to
|
|
171
174
|
// selectors present in the test file, and after modularize → reuse
|
|
@@ -182,6 +185,7 @@ ${buildModularizeFirstNextSteps({
|
|
|
182
185
|
language: params.language,
|
|
183
186
|
framework: effectiveFramework,
|
|
184
187
|
enhanceFirst,
|
|
188
|
+
pomFirst,
|
|
185
189
|
})}
|
|
186
190
|
`;
|
|
187
191
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { UnifiedAnalysisState } from "../../utils/AnalysisStateManager.js";
|
|
2
|
+
import { type TestExecutionStatus } from "../../types/TestExecution.js";
|
|
3
|
+
import type { HelperChangeKind } from "../../types/ReuseOutcome.js";
|
|
4
|
+
import type { ChangedHelperView } from "./helper-callers.js";
|
|
5
|
+
/** One changed helper as a caller meets it. */
|
|
6
|
+
export interface CalledChange {
|
|
7
|
+
/** Absolute path of the module it lives in — with several modules in scope, a
|
|
8
|
+
* helper NAME alone does not say which one this caller met. */
|
|
9
|
+
utilsFile: string;
|
|
10
|
+
helper: string;
|
|
11
|
+
change: HelperChangeKind;
|
|
12
|
+
}
|
|
13
|
+
/** The execution recorded for one caller of a changed helper. */
|
|
14
|
+
export interface CallerExecution {
|
|
15
|
+
/** Absolute path of the caller. */
|
|
16
|
+
file: string;
|
|
17
|
+
/** Present only when an execution ran the module as it stands. */
|
|
18
|
+
execution?: TestExecutionStatus;
|
|
19
|
+
/** This run delivered or edited the caller, so its own report row carries its
|
|
20
|
+
* execution. Without this, a missing `execution` here reads as "never run" for a
|
|
21
|
+
* file the run did run. */
|
|
22
|
+
deliveredByThisRun?: true;
|
|
23
|
+
}
|
|
24
|
+
/** A caller the run CAN execute that has no result against the changed module.
|
|
25
|
+
*
|
|
26
|
+
* Carries what running it needs, because the gate has it in hand — it reads the
|
|
27
|
+
* inventory entry to decide the file is runnable at all — and a remedy the agent has
|
|
28
|
+
* to go and look values up for is a round spent discovering them. All three are
|
|
29
|
+
* required on an inventory entry (`TestAnalysisResult`), and an owed caller always
|
|
30
|
+
* has one: a caller without an entry is never owed. */
|
|
31
|
+
export interface OwedCaller {
|
|
32
|
+
/** Absolute path of the caller. */
|
|
33
|
+
file: string;
|
|
34
|
+
/** The changed helpers it calls. */
|
|
35
|
+
helpers: CalledChange[];
|
|
36
|
+
language: string;
|
|
37
|
+
framework: string;
|
|
38
|
+
/** Not guessable from the path: SKYR-4435 records a shared module inventoried
|
|
39
|
+
* with `unknown`. */
|
|
40
|
+
testType: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ChangedHelperGateParams {
|
|
43
|
+
views: ChangedHelperView[];
|
|
44
|
+
/** `existingTests` says which callers are inventoried and runnable;
|
|
45
|
+
* `testExecutions` holds the latest run of ANY file — the inventory's
|
|
46
|
+
* `executionAfter` is written for a pre-existing test only, and a caller this
|
|
47
|
+
* run created has its result nowhere else. */
|
|
48
|
+
state: Pick<UnifiedAnalysisState, "existingTests" | "testExecutions">;
|
|
49
|
+
/** Absolute paths of everything this run changed, across every checkout. */
|
|
50
|
+
changedFilesAbs: string[];
|
|
51
|
+
}
|
|
52
|
+
export interface ChangedHelperGateResult {
|
|
53
|
+
/** Per caller of a changed helper, by canonical path, the execution that ran the
|
|
54
|
+
* changed module. */
|
|
55
|
+
executions: Map<string, CallerExecution>;
|
|
56
|
+
/** Callers the run can execute that have no result against the changed module.
|
|
57
|
+
* Empty is the ordinary case. */
|
|
58
|
+
owed: OwedCaller[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The report gate over the callers of a changed helper: each one the run CAN execute
|
|
62
|
+
* must have been run on the module as it stands. A caller that ran and FAILED is
|
|
63
|
+
* disclosed with its result and blocks nothing — the reviewer reads the red and
|
|
64
|
+
* decides, and SKYR-4451 adds the classification that decides for them.
|
|
65
|
+
*
|
|
66
|
+
* This is the retrofit gate's rule turned outward. A retrofit is a caller this run
|
|
67
|
+
* EDITED; a caller here is one it did not touch at all, which is why nothing ran it.
|
|
68
|
+
* The identity check differs accordingly (`freshExecution` is the same rule for
|
|
69
|
+
* both): a retrofit's execution must post-date the CALLER's last edit, and this one's
|
|
70
|
+
* must post-date the MODULE's — the caller is unchanged, and the bytes that changed
|
|
71
|
+
* under it are the module's.
|
|
72
|
+
*
|
|
73
|
+
* FAILS OPEN on every doubt, and the doubts are enumerated rather than inferred. It
|
|
74
|
+
* demands an execution only for a caller it has positive evidence about: the caller
|
|
75
|
+
* set is known, the file was inventoried as a Skyramp-generated test this run did not
|
|
76
|
+
* deliver, and the run is able to execute it. A helper whose callers could not be
|
|
77
|
+
* enumerated demands nothing; a false demand on a helper nobody calls is worse than
|
|
78
|
+
* a missing report line.
|
|
79
|
+
*/
|
|
80
|
+
export declare function changedHelperGate(params: ChangedHelperGateParams): Promise<ChangedHelperGateResult>;
|
|
81
|
+
/**
|
|
82
|
+
* The refusal a set of unexecuted callers makes, and the exact call that clears it.
|
|
83
|
+
*
|
|
84
|
+
* GIVES the remedy rather than describing it. A sentence naming only the path and
|
|
85
|
+
* the phase produced a call missing arguments `skyramp_execute_test` requires, so the
|
|
86
|
+
* agent spent a round discovering which and could not always recover. The
|
|
87
|
+
* `unresolved-module` finding is the model: it hands over the literal header line to
|
|
88
|
+
* write, not a description of one. Here every value the server holds is printed per
|
|
89
|
+
* file, and the one it does not — the repository's own command for the file — is
|
|
90
|
+
* covered by the same instruction `skyramp_actions` gives for a before-run
|
|
91
|
+
* (`runTestCall`), so the two gates never describe the same call two ways.
|
|
92
|
+
*
|
|
93
|
+
* The arguments common to every caller are stated once above the list; a wall of
|
|
94
|
+
* eight calls × six arguments is its own way of being unreadable. A value is never
|
|
95
|
+
* dropped to shorten it.
|
|
96
|
+
*
|
|
97
|
+
* NOTHING HERE IS ABOUT A FAILING CALLER. This refuses on the ABSENCE of a result,
|
|
98
|
+
* which the agent can always remedy with one call per file. A caller that ran and
|
|
99
|
+
* failed is disclosed with its result and refuses nothing — see SKYR-4451.
|
|
100
|
+
*/
|
|
101
|
+
export declare function unrunCallerRefusal(owed: OwedCaller[], ctx: {
|
|
102
|
+
repoRoot: string;
|
|
103
|
+
repository: string;
|
|
104
|
+
stateFile: string;
|
|
105
|
+
}): string | undefined;
|