@ivorycanvas/qamap 0.3.5 → 0.4.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/CHANGELOG.md +49 -0
- package/README.md +37 -23
- package/dist/agent-init.js +2 -2
- package/dist/agent-init.js.map +1 -1
- package/dist/behavior-intent.d.ts +6 -0
- package/dist/behavior-intent.js +183 -0
- package/dist/behavior-intent.js.map +1 -0
- package/dist/behavior-manifest.d.ts +6 -0
- package/dist/behavior-manifest.js +221 -0
- package/dist/behavior-manifest.js.map +1 -0
- package/dist/behavior.d.ts +151 -0
- package/dist/behavior.js +458 -0
- package/dist/behavior.js.map +1 -0
- package/dist/change-intent.d.ts +81 -0
- package/dist/change-intent.js +884 -0
- package/dist/change-intent.js.map +1 -0
- package/dist/cli.js +13 -8
- package/dist/cli.js.map +1 -1
- package/dist/e2e.d.ts +16 -1
- package/dist/e2e.js +290 -25
- package/dist/e2e.js.map +1 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/qa.d.ts +1 -0
- package/dist/qa.js +293 -88
- package/dist/qa.js.map +1 -1
- package/dist/terminal.js +1 -1
- package/dist/terminal.js.map +1 -1
- package/dist/test-plan.d.ts +18 -0
- package/dist/test-plan.js +123 -13
- package/dist/test-plan.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/adoption.md +11 -10
- package/docs/agent-format.md +16 -10
- package/docs/agent-skill.md +8 -5
- package/docs/architecture.md +131 -0
- package/docs/benchmarking.md +23 -4
- package/docs/commands.md +13 -7
- package/docs/e2e-output-examples.md +29 -28
- package/docs/quickstart-demo.md +29 -17
- package/docs/release-validation.md +49 -6
- package/docs/releasing.md +26 -3
- package/docs/roadmap.md +20 -10
- package/package.json +4 -2
- package/schema/qamap-agent.schema.json +122 -3
- package/schema/qamap-behavior.schema.json +339 -0
- package/skills/qamap-pr-qa/SKILL.md +20 -14
package/dist/e2e.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import YAML from "yaml";
|
|
4
|
+
import { analyzeBehaviorGraph, createInferredFlowBehaviorAdapter } from "./behavior.js";
|
|
5
|
+
import { createChangeIntentBehaviorAdapter } from "./behavior-intent.js";
|
|
6
|
+
import { createManifestBehaviorAdapter } from "./behavior-manifest.js";
|
|
7
|
+
import { analyzeChangeIntents } from "./change-intent.js";
|
|
4
8
|
import { buildDomainLanguageSummary } from "./domain-language.js";
|
|
5
9
|
import { defaultDomainManifestPath, loadDomainManifest, matchDomains } from "./domains.js";
|
|
6
10
|
import { analyzeFixtureSource, insightCoversEndpoint } from "./fixture-insight.js";
|
|
@@ -9,7 +13,7 @@ import { buildReverseImportIndex, expandChangedFilesWithImporters, findImporting
|
|
|
9
13
|
import { loadCoreFlowManifest, matchCoreFlows } from "./flows.js";
|
|
10
14
|
import { loadVerificationManifest, matchVerificationManifest } from "./manifest.js";
|
|
11
15
|
import { collectTestSuiteInventory, evaluateFlowCoverageEvidence, summarizeTestSuiteInventory, } from "./test-evidence.js";
|
|
12
|
-
import { collectAddedDiffText, generateTestPlan } from "./test-plan.js";
|
|
16
|
+
import { addedDiffTextFromEvidence, collectAddedDiffEvidence, collectAddedDiffText, generateTestPlan, } from "./test-plan.js";
|
|
13
17
|
import { TOOL_NAME, VERSION } from "./version.js";
|
|
14
18
|
// Human reports render readiness as a stage on a fixed journey instead of a
|
|
15
19
|
// verdict, so a first run reads as "you are at the start", not "you failed".
|
|
@@ -59,15 +63,43 @@ export async function generateE2ePlan(rootInput, options = {}) {
|
|
|
59
63
|
const domains = matchDomains(domainManifest, matchableChangedFiles);
|
|
60
64
|
const verificationManifest = await loadVerificationManifest(coreFlowRoot, { manifestPath: options.manifestPath });
|
|
61
65
|
const verificationManifestMatches = matchVerificationManifest(verificationManifest, matchableChangedFiles);
|
|
62
|
-
const
|
|
66
|
+
const addedDiffEvidence = await collectAddedDiffEvidence(root, {
|
|
63
67
|
base: testPlan.base,
|
|
64
68
|
head: testPlan.head,
|
|
65
69
|
workspaceRoot: testPlan.workspaceRoot,
|
|
66
70
|
includeWorkingTree: options.includeWorkingTree,
|
|
67
71
|
});
|
|
72
|
+
const addedDiffText = addedDiffTextFromEvidence(addedDiffEvidence);
|
|
73
|
+
const changeAnalysis = await analyzeChangeIntents(root, {
|
|
74
|
+
base: testPlan.base,
|
|
75
|
+
head: testPlan.head,
|
|
76
|
+
workspaceRoot: testPlan.workspaceRoot,
|
|
77
|
+
includeWorkingTree: options.includeWorkingTree,
|
|
78
|
+
changedFiles: testPlan.changedFiles,
|
|
79
|
+
addedDiffText,
|
|
80
|
+
addedDiffEvidence,
|
|
81
|
+
});
|
|
68
82
|
const domainLanguage = await buildDomainLanguageSummary(root, testPlan.changedFiles, coreFlows, domains, addedDiffText);
|
|
69
83
|
const workspaceTargets = await buildWorkspaceTargets(root, testPlan);
|
|
70
|
-
const flows = await buildFlows(root, testPlan.changedFiles, recommendedRunner.name, project.type, testSuiteInventory, domainLanguage, addedDiffText);
|
|
84
|
+
const flows = await buildFlows(root, testPlan.changedFiles, recommendedRunner.name, project.type, testSuiteInventory, domainLanguage, addedDiffText, changeAnalysis);
|
|
85
|
+
const behaviorGraph = await analyzeBehaviorGraph({
|
|
86
|
+
root: testPlan.root,
|
|
87
|
+
workspaceRoot: testPlan.workspaceRoot,
|
|
88
|
+
base: testPlan.base,
|
|
89
|
+
head: testPlan.head,
|
|
90
|
+
projectType: project.type,
|
|
91
|
+
surface: behaviorSurfaceForProject(project.type),
|
|
92
|
+
runner: recommendedRunner.name,
|
|
93
|
+
changedFiles: testPlan.changedFiles.map((file) => ({
|
|
94
|
+
path: file.path,
|
|
95
|
+
status: file.status,
|
|
96
|
+
previousPath: file.previousPath,
|
|
97
|
+
})),
|
|
98
|
+
}, [
|
|
99
|
+
createInferredFlowBehaviorAdapter({ flows: flows.map(toInferredBehaviorFlow) }),
|
|
100
|
+
createChangeIntentBehaviorAdapter({ analysis: changeAnalysis }),
|
|
101
|
+
createManifestBehaviorAdapter({ matches: verificationManifestMatches }),
|
|
102
|
+
]);
|
|
71
103
|
const testSuite = summarizeTestSuiteInventory(testSuiteInventory);
|
|
72
104
|
const missingTestability = uniqueStrings([
|
|
73
105
|
...flows.flatMap((flow) => flow.missingTestability),
|
|
@@ -118,16 +150,50 @@ export async function generateE2ePlan(rootInput, options = {}) {
|
|
|
118
150
|
verificationManifestPath: verificationManifest.path,
|
|
119
151
|
verificationManifestMatches,
|
|
120
152
|
domainLanguage,
|
|
153
|
+
changeAnalysis,
|
|
121
154
|
changedFiles: testPlan.changedFiles,
|
|
122
155
|
suggestedCommands: testPlan.suggestedCommands,
|
|
123
156
|
workspaceTargets,
|
|
124
157
|
flows,
|
|
158
|
+
behaviorGraph,
|
|
125
159
|
validationMatrix,
|
|
126
160
|
bootstrap,
|
|
127
161
|
missingTestability,
|
|
128
162
|
setupNotes,
|
|
129
163
|
};
|
|
130
164
|
}
|
|
165
|
+
function behaviorSurfaceForProject(projectType) {
|
|
166
|
+
if (projectType === "web") {
|
|
167
|
+
return "web";
|
|
168
|
+
}
|
|
169
|
+
if (projectType === "expo-react-native" || projectType === "react-native") {
|
|
170
|
+
return "mobile";
|
|
171
|
+
}
|
|
172
|
+
if (projectType === "api-service") {
|
|
173
|
+
return "api";
|
|
174
|
+
}
|
|
175
|
+
if (projectType === "cli") {
|
|
176
|
+
return "cli";
|
|
177
|
+
}
|
|
178
|
+
if (projectType === "design-tokens" || projectType === "data-catalog") {
|
|
179
|
+
return "artifact";
|
|
180
|
+
}
|
|
181
|
+
return "unknown";
|
|
182
|
+
}
|
|
183
|
+
function toInferredBehaviorFlow(flow) {
|
|
184
|
+
return {
|
|
185
|
+
kind: flow.kind ?? "changed-file",
|
|
186
|
+
title: flow.title,
|
|
187
|
+
reason: flow.reason,
|
|
188
|
+
files: flow.files,
|
|
189
|
+
steps: flow.steps,
|
|
190
|
+
entrypoints: flow.entrypoints,
|
|
191
|
+
selectors: flow.selectors,
|
|
192
|
+
coverage: flow.coverage,
|
|
193
|
+
fixtureStatus: flow.fixtureReadiness.status,
|
|
194
|
+
fixtureFiles: flow.fixtureReadiness.mockInsights?.map((insight) => insight.file) ?? [],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
131
197
|
export async function generateE2eDraft(rootInput, options = {}) {
|
|
132
198
|
const root = path.resolve(rootInput);
|
|
133
199
|
const plan = await generateE2ePlan(root, options);
|
|
@@ -1025,6 +1091,10 @@ function draftFileDetails(flow) {
|
|
|
1025
1091
|
setupHints: flow.setupHints.map((hint) => `${hint.title}: ${hint.detail}`).slice(0, 6),
|
|
1026
1092
|
coverageTargets: flow.coverage.map((target) => `${target.priority}: ${target.title}`).slice(0, 7),
|
|
1027
1093
|
manifestUpdatePath: flow.manifestMatch?.updatePath,
|
|
1094
|
+
intentId: flow.intentId,
|
|
1095
|
+
intentConfidence: flow.intentConfidence,
|
|
1096
|
+
lifecycle: flow.lifecycle,
|
|
1097
|
+
qaScenarios: flow.qaScenarios,
|
|
1028
1098
|
};
|
|
1029
1099
|
}
|
|
1030
1100
|
function formatEntrypointHint(entrypoint) {
|
|
@@ -1227,6 +1297,35 @@ function withTerminalPeriod(value) {
|
|
|
1227
1297
|
}
|
|
1228
1298
|
function buildFlowLanguageBrief(flow) {
|
|
1229
1299
|
const actor = inferFlowActor(flow);
|
|
1300
|
+
if (flow.intentId && flow.lifecycle && flow.lifecycle.length > 0) {
|
|
1301
|
+
const trigger = flow.lifecycle.find((stage) => stage.kind === "trigger")?.label ?? `Start the changed ${flow.title} behavior.`;
|
|
1302
|
+
const outcomes = flow.lifecycle
|
|
1303
|
+
.filter((stage) => stage.kind === "observable-outcome")
|
|
1304
|
+
.map((stage) => stripTerminalPunctuation(stage.label));
|
|
1305
|
+
const effects = flow.lifecycle
|
|
1306
|
+
.filter((stage) => stage.kind === "side-effect")
|
|
1307
|
+
.map((stage) => stripTerminalPunctuation(stage.label));
|
|
1308
|
+
const lifecycleSuccessSignal = outcomes.length > 0
|
|
1309
|
+
? outcomes.slice(0, 2).join("; ")
|
|
1310
|
+
: effects.length > 0
|
|
1311
|
+
? `the intended side effect completes: ${effects.slice(0, 2).join("; ")}`
|
|
1312
|
+
: "the observable result matches the commit intent";
|
|
1313
|
+
const repositorySuccessSignal = inferFlowSuccessSignal(flow);
|
|
1314
|
+
const successSignal = repositorySuccessSignal === "the changed journey reaches a visible, stable success state"
|
|
1315
|
+
? lifecycleSuccessSignal
|
|
1316
|
+
: repositorySuccessSignal;
|
|
1317
|
+
const scenarioEdges = (flow.qaScenarios ?? [])
|
|
1318
|
+
.filter((scenario) => scenario.kind !== "primary")
|
|
1319
|
+
.flatMap((scenario) => [scenario.title, ...scenario.edgeCases]);
|
|
1320
|
+
return {
|
|
1321
|
+
actor,
|
|
1322
|
+
trigger,
|
|
1323
|
+
goal: `Complete the intended behavior: ${flow.title}.`,
|
|
1324
|
+
successSignal,
|
|
1325
|
+
reviewQuestion: `Does ${flow.title} follow the inferred lifecycle and produce this outcome: ${successSignal}?`,
|
|
1326
|
+
edgeCases: uniqueStrings(scenarioEdges).slice(0, 6),
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1230
1329
|
const trigger = inferFlowTrigger(flow);
|
|
1231
1330
|
const goal = inferFlowGoal(flow);
|
|
1232
1331
|
const successSignal = inferFlowSuccessSignal(flow);
|
|
@@ -1417,8 +1516,8 @@ function inferFlowSuccessSignal(flow) {
|
|
|
1417
1516
|
return "the changed journey reaches a visible, stable success state";
|
|
1418
1517
|
}
|
|
1419
1518
|
function isVisibleSuccessOutcome(value) {
|
|
1420
|
-
return /\b(?:confirmed|saved|refreshed|succeeded|success|completed|created|updated|deleted|sent|approved|accepted)\b/i.test(value) ||
|
|
1421
|
-
/(
|
|
1519
|
+
return /\b(?:confirmed|saved|scheduled|refreshed|succeeded|success|completed|created|updated|deleted|sent|approved|accepted)\b/i.test(value) ||
|
|
1520
|
+
/(?:완료|성공|예약(?:됨|됐|되)|저장(?:됨|됐|되)|등록(?:됨|됐|되)|제출(?:됨|됐|되)|승인(?:됨|됐|되))/.test(value);
|
|
1422
1521
|
}
|
|
1423
1522
|
function inferFlowReviewQuestion(flow, successSignal) {
|
|
1424
1523
|
if (isApiContractFocusedFlow(flow)) {
|
|
@@ -1519,7 +1618,7 @@ export function formatMarkdownE2ePlan(result) {
|
|
|
1519
1618
|
lines.push("- Includes working tree changes: yes");
|
|
1520
1619
|
}
|
|
1521
1620
|
lines.push(`- Project: ${formatProjectType(result.project.type)}`);
|
|
1522
|
-
lines.push(`-
|
|
1621
|
+
lines.push(`- Automation adapter: ${formatRunnerName(result.recommendedRunner.name)}`);
|
|
1523
1622
|
lines.push(`- Test suite: ${result.testSuite.hasTestSuite ? `${result.testSuite.testFileCount} test file${result.testSuite.testFileCount === 1 ? "" : "s"}` : "not detected"}`);
|
|
1524
1623
|
if (result.testSuite.frameworkSignals.length > 0) {
|
|
1525
1624
|
lines.push(`- Test frameworks: ${result.testSuite.frameworkSignals.join(", ")}`);
|
|
@@ -1544,7 +1643,10 @@ export function formatMarkdownE2ePlan(result) {
|
|
|
1544
1643
|
lines.push(`- Local history: \`${escapeMarkdownInline(result.localHistory.path)}\``);
|
|
1545
1644
|
}
|
|
1546
1645
|
lines.push("");
|
|
1547
|
-
lines.
|
|
1646
|
+
appendChangeIntentMarkdown(lines, result.changeAnalysis);
|
|
1647
|
+
lines.push("## Automation Adapter");
|
|
1648
|
+
lines.push("");
|
|
1649
|
+
lines.push("QAMap selects an output adapter only after deriving runner-independent change intent and QA scenarios.");
|
|
1548
1650
|
lines.push("");
|
|
1549
1651
|
lines.push(result.recommendedRunner.reason);
|
|
1550
1652
|
if (result.project.evidence.length > 0) {
|
|
@@ -1788,6 +1890,53 @@ export function formatMarkdownE2ePlan(result) {
|
|
|
1788
1890
|
}
|
|
1789
1891
|
return lines.join("\n");
|
|
1790
1892
|
}
|
|
1893
|
+
function appendChangeIntentMarkdown(lines, analysis) {
|
|
1894
|
+
lines.push("## Change Intent And Behavior Lifecycle");
|
|
1895
|
+
lines.push("");
|
|
1896
|
+
if (analysis.intents.length === 0) {
|
|
1897
|
+
lines.push("No evidence-backed change intent was inferred. QAMap keeps heuristic flow suggestions review-only.");
|
|
1898
|
+
for (const diagnostic of analysis.diagnostics.slice(0, 3)) {
|
|
1899
|
+
lines.push(`- ${escapeMarkdownInline(diagnostic)}`);
|
|
1900
|
+
}
|
|
1901
|
+
lines.push("");
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
for (const intent of analysis.intents.slice(0, 3)) {
|
|
1905
|
+
lines.push(`### ${escapeMarkdownInline(intent.title)}`);
|
|
1906
|
+
lines.push("");
|
|
1907
|
+
lines.push(`- Confidence: ${intent.confidence}${intent.reviewRequired ? "; human review required" : ""}`);
|
|
1908
|
+
lines.push(`- Summary: ${escapeMarkdownInline(intent.summary)}`);
|
|
1909
|
+
if (intent.commits.length > 0) {
|
|
1910
|
+
lines.push("- Commit evidence:");
|
|
1911
|
+
for (const commit of intent.commits.slice(0, 6)) {
|
|
1912
|
+
lines.push(` - \`${commit.sha.slice(0, 12)}\` ${escapeMarkdownInline(commit.subject)}`);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
if (intent.files.length > 0) {
|
|
1916
|
+
lines.push(`- Source scope: ${intent.files.slice(0, 6).map((file) => `\`${escapeMarkdownInline(file)}\``).join(", ")}`);
|
|
1917
|
+
}
|
|
1918
|
+
lines.push("");
|
|
1919
|
+
lines.push("Behavior lifecycle:");
|
|
1920
|
+
intent.lifecycle.slice(0, 12).forEach((stage, index) => {
|
|
1921
|
+
lines.push(`${index + 1}. **${stage.kind}**: ${escapeMarkdownInline(stage.label)} [${stage.confidence}]`);
|
|
1922
|
+
});
|
|
1923
|
+
lines.push("");
|
|
1924
|
+
lines.push("Required QA scenarios:");
|
|
1925
|
+
for (const scenario of intent.scenarios.slice(0, 4)) {
|
|
1926
|
+
lines.push(`- **${scenario.priority} / ${scenario.kind}**: ${escapeMarkdownInline(scenario.title)}`);
|
|
1927
|
+
for (const step of scenario.steps.slice(0, 3)) {
|
|
1928
|
+
lines.push(` - Step: ${escapeMarkdownInline(step)}`);
|
|
1929
|
+
}
|
|
1930
|
+
for (const assertion of scenario.assertions.slice(0, 3)) {
|
|
1931
|
+
lines.push(` - Assert: ${escapeMarkdownInline(assertion)}`);
|
|
1932
|
+
}
|
|
1933
|
+
if (scenario.edgeCases.length > 0) {
|
|
1934
|
+
lines.push(` - Boundaries: ${scenario.edgeCases.slice(0, 4).map(escapeMarkdownInline).join(", ")}`);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
lines.push("");
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1791
1940
|
function appendVerificationManifestMatchesMarkdown(lines, matches) {
|
|
1792
1941
|
if (matches.length === 0) {
|
|
1793
1942
|
return;
|
|
@@ -1833,7 +1982,7 @@ export function formatMarkdownE2eDraft(result) {
|
|
|
1833
1982
|
lines.push("# QAMap E2E Draft");
|
|
1834
1983
|
lines.push("");
|
|
1835
1984
|
lines.push(`- Root: \`${escapeMarkdownInline(result.root)}\``);
|
|
1836
|
-
lines.push(`-
|
|
1985
|
+
lines.push(`- Automation adapter: ${formatRunnerName(result.runner)}`);
|
|
1837
1986
|
lines.push(`- Output directory: \`${escapeMarkdownInline(result.outputDirectory)}\``);
|
|
1838
1987
|
if (result.dryRun) {
|
|
1839
1988
|
lines.push("- Mode: dry run (no files were written)");
|
|
@@ -1863,6 +2012,7 @@ export function formatMarkdownE2eDraft(result) {
|
|
|
1863
2012
|
lines.push(`- Action categories: ${formatDraftActionKindSummary(result.actionSummary.byKind)}`);
|
|
1864
2013
|
}
|
|
1865
2014
|
lines.push("");
|
|
2015
|
+
appendChangeIntentMarkdown(lines, result.plan.changeAnalysis);
|
|
1866
2016
|
appendVerificationManifestMatchesMarkdown(lines, result.plan.verificationManifestMatches);
|
|
1867
2017
|
lines.push("## Files");
|
|
1868
2018
|
lines.push("");
|
|
@@ -2111,8 +2261,9 @@ async function detectProjectProfile(root, workspaceRoot) {
|
|
|
2111
2261
|
"admin.py",
|
|
2112
2262
|
]);
|
|
2113
2263
|
const projectFilePaths = (await collectProjectFiles(root, 2000)).map((file) => file.path);
|
|
2114
|
-
const
|
|
2115
|
-
const
|
|
2264
|
+
const projectProfileArtifactFiles = projectFilePaths.filter((file) => !isTestLikeFile(file));
|
|
2265
|
+
const hasDesignTokenProject = projectProfileArtifactFiles.some(isDesignTokenFile);
|
|
2266
|
+
const hasDataCatalogProject = projectProfileArtifactFiles.some(isCatalogDataFile);
|
|
2116
2267
|
const hasCliBin = packageJsonHasCliBin(packageJson);
|
|
2117
2268
|
if (hasExpoDependency) {
|
|
2118
2269
|
evidence.push("package.json dependency: expo");
|
|
@@ -2171,15 +2322,15 @@ async function detectProjectProfile(root, workspaceRoot) {
|
|
|
2171
2322
|
if (apiServiceDependency || hasApiServiceConfig || hasDjangoEntrypoint || hasPythonServiceDependency || hasPythonServiceModule) {
|
|
2172
2323
|
return { type: "api-service", evidence };
|
|
2173
2324
|
}
|
|
2325
|
+
if (hasCliBin) {
|
|
2326
|
+
return { type: "cli", evidence };
|
|
2327
|
+
}
|
|
2174
2328
|
if (hasDesignTokenProject) {
|
|
2175
2329
|
return { type: "design-tokens", evidence };
|
|
2176
2330
|
}
|
|
2177
2331
|
if (hasDataCatalogProject) {
|
|
2178
2332
|
return { type: "data-catalog", evidence };
|
|
2179
2333
|
}
|
|
2180
|
-
if (hasCliBin) {
|
|
2181
|
-
return { type: "cli", evidence };
|
|
2182
|
-
}
|
|
2183
2334
|
return {
|
|
2184
2335
|
type: "unknown",
|
|
2185
2336
|
evidence,
|
|
@@ -3057,11 +3208,11 @@ function toCoreFlowChangedFiles(changedFiles, scopedRoot, coreFlowRoot) {
|
|
|
3057
3208
|
previousPath: file.previousPath ? toPosixPath(path.join(relativeRoot, file.previousPath)) : undefined,
|
|
3058
3209
|
}));
|
|
3059
3210
|
}
|
|
3060
|
-
async function buildFlows(root, changedFiles, runner, projectType, testSuiteInventory, domainLanguage, addedDiffText = {}) {
|
|
3211
|
+
async function buildFlows(root, changedFiles, runner, projectType, testSuiteInventory, domainLanguage, addedDiffText = {}, changeAnalysis) {
|
|
3061
3212
|
const files = changedFiles.map((file) => file.path);
|
|
3062
3213
|
const importImpacts = await collectImportImpacts(root, files);
|
|
3063
3214
|
const fixtureContext = await collectFixtureReadinessContext(root, files);
|
|
3064
|
-
const flowResults = await Promise.all(buildFlowCandidates(files, runner, projectType, domainLanguage, importImpacts).map((candidate) => buildFlow(root, runner, candidate, testSuiteInventory, fixtureContext, addedDiffText)));
|
|
3215
|
+
const flowResults = await Promise.all(buildFlowCandidates(files, runner, projectType, domainLanguage, importImpacts, changeAnalysis).map((candidate) => buildFlow(root, runner, candidate, testSuiteInventory, fixtureContext, addedDiffText)));
|
|
3065
3216
|
const flows = flowResults.filter((flow) => Boolean(flow));
|
|
3066
3217
|
return dedupeFlows(flows).slice(0, 4);
|
|
3067
3218
|
}
|
|
@@ -3106,7 +3257,7 @@ async function collectImportImpacts(root, changedFiles) {
|
|
|
3106
3257
|
function describeImportChain(impact) {
|
|
3107
3258
|
return impact.chain.join(" -> ");
|
|
3108
3259
|
}
|
|
3109
|
-
function buildFlowCandidates(files, runner, projectType, domainLanguage, importImpacts = []) {
|
|
3260
|
+
function buildFlowCandidates(files, runner, projectType, domainLanguage, importImpacts = [], changeAnalysis) {
|
|
3110
3261
|
const lowSignalCandidate = importImpacts.length === 0 ? buildLowSignalChangeCandidate(files) : undefined;
|
|
3111
3262
|
if (lowSignalCandidate) {
|
|
3112
3263
|
return [lowSignalCandidate];
|
|
@@ -3311,7 +3462,58 @@ function buildFlowCandidates(files, runner, projectType, domainLanguage, importI
|
|
|
3311
3462
|
],
|
|
3312
3463
|
});
|
|
3313
3464
|
}
|
|
3314
|
-
return candidates;
|
|
3465
|
+
return prioritizeChangeIntentCandidates(changeAnalysis, candidates, projectType);
|
|
3466
|
+
}
|
|
3467
|
+
function prioritizeChangeIntentCandidates(analysis, heuristicCandidates, projectType) {
|
|
3468
|
+
const intentCandidates = (analysis?.intents ?? [])
|
|
3469
|
+
.filter((intent) => intent.confidence !== "low" && intent.files.length > 0)
|
|
3470
|
+
.map((intent) => {
|
|
3471
|
+
const primaryScenario = intent.scenarios.find((scenario) => scenario.kind === "primary") ?? intent.scenarios[0];
|
|
3472
|
+
const steps = uniqueStrings([
|
|
3473
|
+
...(primaryScenario?.steps ?? intent.lifecycle.map((stage) => stage.label)),
|
|
3474
|
+
...(primaryScenario?.assertions ?? []),
|
|
3475
|
+
]);
|
|
3476
|
+
return {
|
|
3477
|
+
kind: intentFlowKind(intent, projectType),
|
|
3478
|
+
title: intent.title,
|
|
3479
|
+
reason: `Commit and diff evidence support this ${intent.confidence}-confidence change intent. ${intent.summary}`,
|
|
3480
|
+
files: intent.files,
|
|
3481
|
+
steps,
|
|
3482
|
+
coverage: intent.scenarios.map((scenario) => ({
|
|
3483
|
+
title: scenario.title,
|
|
3484
|
+
priority: scenario.priority,
|
|
3485
|
+
reason: scenario.rationale,
|
|
3486
|
+
checks: uniqueStrings([...scenario.assertions, ...scenario.edgeCases.map((edgeCase) => `Exercise ${lowercaseFirst(edgeCase)}.`)]),
|
|
3487
|
+
})),
|
|
3488
|
+
intentId: intent.id,
|
|
3489
|
+
intentConfidence: intent.confidence,
|
|
3490
|
+
intentEvidence: intent.evidence,
|
|
3491
|
+
lifecycle: intent.lifecycle,
|
|
3492
|
+
qaScenarios: intent.scenarios,
|
|
3493
|
+
};
|
|
3494
|
+
});
|
|
3495
|
+
if (intentCandidates.length === 0) {
|
|
3496
|
+
return heuristicCandidates;
|
|
3497
|
+
}
|
|
3498
|
+
const intentFiles = new Set(intentCandidates.flatMap((candidate) => candidate.files));
|
|
3499
|
+
const nonOverlapping = heuristicCandidates.filter((candidate) => candidate.files.every((file) => !intentFiles.has(file)) || isVerificationOnlyKind(candidate.kind));
|
|
3500
|
+
return [...intentCandidates, ...nonOverlapping].slice(0, 4);
|
|
3501
|
+
}
|
|
3502
|
+
function intentFlowKind(intent, projectType) {
|
|
3503
|
+
const searchable = `${intent.title} ${intent.keywords.join(" ")} ${intent.lifecycle.map((stage) => stage.label).join(" ")}`.toLowerCase();
|
|
3504
|
+
if (projectType === "cli") {
|
|
3505
|
+
return "command";
|
|
3506
|
+
}
|
|
3507
|
+
if (/\b(?:endpoint|request|response|api|contract)\b/.test(searchable) && projectType === "api-service") {
|
|
3508
|
+
return "api";
|
|
3509
|
+
}
|
|
3510
|
+
if (/\b(?:state|store|persist|sync|toggle|cache|session|notification|reminder)\b/.test(searchable)) {
|
|
3511
|
+
return "state";
|
|
3512
|
+
}
|
|
3513
|
+
if (intent.lifecycle.some((stage) => stage.kind === "observable-outcome") && projectType !== "api-service") {
|
|
3514
|
+
return "ui";
|
|
3515
|
+
}
|
|
3516
|
+
return "domain";
|
|
3315
3517
|
}
|
|
3316
3518
|
function cliCommandChecklistTitle(subject) {
|
|
3317
3519
|
if (/^cli$/i.test(subject.trim())) {
|
|
@@ -3387,13 +3589,14 @@ async function buildFlow(root, runner, candidate, testSuiteInventory, fixtureCon
|
|
|
3387
3589
|
if (files.length === 0) {
|
|
3388
3590
|
return undefined;
|
|
3389
3591
|
}
|
|
3390
|
-
const coverage = buildCoverageTargets(candidate.kind, files, runner);
|
|
3592
|
+
const coverage = candidate.coverage ?? buildCoverageTargets(candidate.kind, files, runner);
|
|
3391
3593
|
const setupHints = await inferFlowSetupHints(root, files, candidate.kind);
|
|
3392
3594
|
const interactionEvidenceApplies = !isVerificationOnlyKind(candidate.kind);
|
|
3393
3595
|
const selectors = interactionEvidenceApplies
|
|
3394
3596
|
? await inferFlowSelectors(root, files, runner, addedDiffText)
|
|
3395
3597
|
: [];
|
|
3396
3598
|
const flow = {
|
|
3599
|
+
kind: candidate.kind,
|
|
3397
3600
|
title: candidate.title,
|
|
3398
3601
|
reason: candidate.reason,
|
|
3399
3602
|
files,
|
|
@@ -3405,6 +3608,11 @@ async function buildFlow(root, runner, candidate, testSuiteInventory, fixtureCon
|
|
|
3405
3608
|
fixtureReadiness: await inferFlowFixtureReadiness(root, files, candidate.kind, setupHints, fixtureContext),
|
|
3406
3609
|
selectors,
|
|
3407
3610
|
missingTestability: interactionEvidenceApplies ? await findFlowTestabilityGaps(root, files, runner) : [],
|
|
3611
|
+
intentId: candidate.intentId,
|
|
3612
|
+
intentConfidence: candidate.intentConfidence,
|
|
3613
|
+
intentEvidence: candidate.intentEvidence,
|
|
3614
|
+
lifecycle: candidate.lifecycle,
|
|
3615
|
+
qaScenarios: candidate.qaScenarios,
|
|
3408
3616
|
};
|
|
3409
3617
|
return {
|
|
3410
3618
|
...flow,
|
|
@@ -4101,11 +4309,11 @@ function normalizeRouteSegment(segment) {
|
|
|
4101
4309
|
if (!stem || /^\([^)]*\)$/.test(stem) || stem.startsWith("_") || stem.startsWith("@")) {
|
|
4102
4310
|
return undefined;
|
|
4103
4311
|
}
|
|
4104
|
-
const routeStem = stem.replace(/^\((?:\.{1,3})\)/, "");
|
|
4312
|
+
const routeStem = stem.replace(/^\+/, "").replace(/^\((?:\.{1,3})\)/, "");
|
|
4105
4313
|
if (!routeStem) {
|
|
4106
4314
|
return undefined;
|
|
4107
4315
|
}
|
|
4108
|
-
if (/^(?:index|page|route|layout|template|loading|error|not-found|404|500)$/i.test(routeStem)) {
|
|
4316
|
+
if (/^(?:index|page|route|server|layout|template|loading|error|not-found|404|500)$/i.test(routeStem)) {
|
|
4109
4317
|
return undefined;
|
|
4110
4318
|
}
|
|
4111
4319
|
const dynamic = dynamicRouteSegmentName(routeStem);
|
|
@@ -4713,7 +4921,7 @@ async function buildDraftFlows(plan) {
|
|
|
4713
4921
|
if (manifestFlows.length === 0 && scenarioFlows.length === 0) {
|
|
4714
4922
|
return baseFlows.map((flow) => ({
|
|
4715
4923
|
...flow,
|
|
4716
|
-
draftSource: "heuristic",
|
|
4924
|
+
draftSource: flow.intentId ? "change-intent" : "heuristic",
|
|
4717
4925
|
}));
|
|
4718
4926
|
}
|
|
4719
4927
|
const combined = prioritizeImportantBaseDraftFlow(dedupeDraftFlowsByOutputPath(dedupeFlows([...manifestFlows, ...scenarioFlows, ...baseFlows]), plan.recommendedRunner.name), baseFlows).slice(0, 4);
|
|
@@ -4747,6 +4955,9 @@ function shouldUseDomainScenariosForDraft(plan) {
|
|
|
4747
4955
|
if (isLowSignalVerificationOnlyChange(files) || isConfigurationOnlyChange(files)) {
|
|
4748
4956
|
return false;
|
|
4749
4957
|
}
|
|
4958
|
+
if (plan.changeAnalysis.intents.some((intent) => intent.confidence !== "low")) {
|
|
4959
|
+
return false;
|
|
4960
|
+
}
|
|
4750
4961
|
return !plan.flows.every(isEvidenceVerificationFocusedFlow);
|
|
4751
4962
|
}
|
|
4752
4963
|
function isEvidenceVerificationFocusedFlow(flow) {
|
|
@@ -4805,9 +5016,10 @@ function uniqueManifestCheckMatches(matches) {
|
|
|
4805
5016
|
function preferredDraftSource(left, right) {
|
|
4806
5017
|
const ranks = {
|
|
4807
5018
|
"verification-manifest": 0,
|
|
4808
|
-
"
|
|
4809
|
-
"
|
|
4810
|
-
|
|
5019
|
+
"change-intent": 1,
|
|
5020
|
+
"core-flow": 2,
|
|
5021
|
+
"domain-language": 3,
|
|
5022
|
+
heuristic: 4,
|
|
4811
5023
|
};
|
|
4812
5024
|
if (!left) {
|
|
4813
5025
|
return right;
|
|
@@ -4884,6 +5096,7 @@ async function buildManifestDraftFlow(plan, match, checkMatches, baseFlows) {
|
|
|
4884
5096
|
...(await inferFlowSetupHints(plan.root, files, "domain")),
|
|
4885
5097
|
]);
|
|
4886
5098
|
const flow = {
|
|
5099
|
+
kind: baseFlow?.kind ?? "domain",
|
|
4887
5100
|
title: match.name,
|
|
4888
5101
|
reason: match.reason,
|
|
4889
5102
|
files,
|
|
@@ -5020,6 +5233,7 @@ async function buildDomainScenarioDraftFlow(plan, scenario, baseFlows) {
|
|
|
5020
5233
|
checks: refinedSteps,
|
|
5021
5234
|
};
|
|
5022
5235
|
const flow = {
|
|
5236
|
+
kind: baseFlow?.kind ?? "domain",
|
|
5023
5237
|
title,
|
|
5024
5238
|
reason,
|
|
5025
5239
|
files,
|
|
@@ -5201,7 +5415,7 @@ function fileOverlapScore(leftFiles, rightFiles) {
|
|
|
5201
5415
|
}
|
|
5202
5416
|
function draftFlowSource(flow) {
|
|
5203
5417
|
const draftFlow = flow;
|
|
5204
|
-
return draftFlow.draftSource ?? "heuristic";
|
|
5418
|
+
return draftFlow.draftSource ?? (flow.intentId ? "change-intent" : "heuristic");
|
|
5205
5419
|
}
|
|
5206
5420
|
function domainScenarioForFlow(flow) {
|
|
5207
5421
|
return flow.domainScenario;
|
|
@@ -5343,6 +5557,7 @@ function buildFallbackFlow(plan) {
|
|
|
5343
5557
|
const fallback = fallbackFlowDefinition(plan.project.type);
|
|
5344
5558
|
const coverage = buildCoverageTargets(fallback.kind, [], plan.recommendedRunner.name);
|
|
5345
5559
|
const flow = {
|
|
5560
|
+
kind: fallback.kind,
|
|
5346
5561
|
title: fallback.title,
|
|
5347
5562
|
reason: fallback.reason,
|
|
5348
5563
|
files: [],
|
|
@@ -5503,6 +5718,7 @@ function buildMaestroDraft(plan, flow) {
|
|
|
5503
5718
|
lines.push(`# Base: ${plan.base}`);
|
|
5504
5719
|
lines.push(`# Head: ${plan.head}`);
|
|
5505
5720
|
appendDraftBriefComments(lines, flow, "maestro", "#");
|
|
5721
|
+
appendIntentDraftComments(lines, flow, "#");
|
|
5506
5722
|
appendExecutionProfileComments(lines, plan.executionProfile, "#");
|
|
5507
5723
|
appendRunnerSetupProposalComments(lines, plan.runnerSetup, "#");
|
|
5508
5724
|
lines.push("# Replace ${APP_ID} with the app id or export APP_ID before running Maestro.");
|
|
@@ -5593,6 +5809,7 @@ function buildPlaywrightDraft(plan, flow, addedDiffText = {}) {
|
|
|
5593
5809
|
lines.push(`// Intent: ${scenario.intent}`);
|
|
5594
5810
|
}
|
|
5595
5811
|
appendDraftBriefComments(lines, flow, "playwright", "//");
|
|
5812
|
+
appendIntentDraftComments(lines, flow, "//");
|
|
5596
5813
|
appendExecutionProfileComments(lines, plan.executionProfile, "//");
|
|
5597
5814
|
appendRunnerSetupProposalComments(lines, plan.runnerSetup, "//");
|
|
5598
5815
|
lines.push("");
|
|
@@ -5707,6 +5924,7 @@ function buildManualDraft(plan, flow) {
|
|
|
5707
5924
|
lines.push(`- Head: \`${plan.head}\``);
|
|
5708
5925
|
lines.push("");
|
|
5709
5926
|
appendManualDraftBrief(lines, flow, "manual");
|
|
5927
|
+
appendManualIntentDraft(lines, flow);
|
|
5710
5928
|
appendManualExecutionProfile(lines, plan.executionProfile);
|
|
5711
5929
|
appendManualRunnerSetupProposal(lines, plan.runnerSetup);
|
|
5712
5930
|
lines.push("");
|
|
@@ -6025,6 +6243,53 @@ function appendDraftBriefComments(lines, flow, runner, commentPrefix) {
|
|
|
6025
6243
|
lines.push(`${commentPrefix} - ${input}`);
|
|
6026
6244
|
}
|
|
6027
6245
|
}
|
|
6246
|
+
function appendIntentDraftComments(lines, flow, commentPrefix) {
|
|
6247
|
+
if (!flow.intentId || !flow.lifecycle || !flow.qaScenarios) {
|
|
6248
|
+
return;
|
|
6249
|
+
}
|
|
6250
|
+
lines.push("");
|
|
6251
|
+
lines.push(`${commentPrefix} Change intent evidence:`);
|
|
6252
|
+
lines.push(`${commentPrefix} - Intent id: ${flow.intentId}`);
|
|
6253
|
+
lines.push(`${commentPrefix} - Confidence: ${flow.intentConfidence ?? "low"}`);
|
|
6254
|
+
for (const evidence of (flow.intentEvidence ?? []).filter((item) => item.kind === "commit").slice(0, 5)) {
|
|
6255
|
+
lines.push(`${commentPrefix} - Commit: ${evidence.value}`);
|
|
6256
|
+
}
|
|
6257
|
+
lines.push(`${commentPrefix} - Behavior lifecycle:`);
|
|
6258
|
+
for (const stage of flow.lifecycle.slice(0, 10)) {
|
|
6259
|
+
lines.push(`${commentPrefix} - ${stage.kind}: ${stage.label}`);
|
|
6260
|
+
}
|
|
6261
|
+
lines.push(`${commentPrefix} - Runner-independent QA scenarios:`);
|
|
6262
|
+
for (const scenario of flow.qaScenarios.slice(0, 4)) {
|
|
6263
|
+
lines.push(`${commentPrefix} - [${scenario.priority}] ${scenario.kind}: ${scenario.title}`);
|
|
6264
|
+
for (const assertion of scenario.assertions.slice(0, 2)) {
|
|
6265
|
+
lines.push(`${commentPrefix} - Assert: ${assertion}`);
|
|
6266
|
+
}
|
|
6267
|
+
}
|
|
6268
|
+
}
|
|
6269
|
+
function appendManualIntentDraft(lines, flow) {
|
|
6270
|
+
if (!flow.intentId || !flow.lifecycle || !flow.qaScenarios) {
|
|
6271
|
+
return;
|
|
6272
|
+
}
|
|
6273
|
+
lines.push("");
|
|
6274
|
+
lines.push("## Change Intent Evidence");
|
|
6275
|
+
lines.push("");
|
|
6276
|
+
lines.push(`- Intent id: \`${flow.intentId}\``);
|
|
6277
|
+
lines.push(`- Confidence: ${flow.intentConfidence ?? "low"}`);
|
|
6278
|
+
for (const evidence of (flow.intentEvidence ?? []).filter((item) => item.kind === "commit").slice(0, 5)) {
|
|
6279
|
+
lines.push(`- Commit: ${evidence.value}`);
|
|
6280
|
+
}
|
|
6281
|
+
lines.push("- Behavior lifecycle:");
|
|
6282
|
+
for (const stage of flow.lifecycle.slice(0, 10)) {
|
|
6283
|
+
lines.push(` - ${stage.kind}: ${stage.label}`);
|
|
6284
|
+
}
|
|
6285
|
+
lines.push("- Runner-independent QA scenarios:");
|
|
6286
|
+
for (const scenario of flow.qaScenarios.slice(0, 4)) {
|
|
6287
|
+
lines.push(` - [${scenario.priority}] ${scenario.kind}: ${scenario.title}`);
|
|
6288
|
+
for (const assertion of scenario.assertions.slice(0, 2)) {
|
|
6289
|
+
lines.push(` - Assert: ${assertion}`);
|
|
6290
|
+
}
|
|
6291
|
+
}
|
|
6292
|
+
}
|
|
6028
6293
|
function appendExecutionProfileComments(lines, profile, commentPrefix) {
|
|
6029
6294
|
lines.push("");
|
|
6030
6295
|
lines.push(`${commentPrefix} Execution profile:`);
|