@ivorycanvas/qamap 0.3.4 → 0.4.0
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 +43 -1
- package/README.md +19 -14
- package/dist/behavior-intent.d.ts +6 -0
- package/dist/behavior-intent.js +172 -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 +143 -0
- package/dist/behavior.js +458 -0
- package/dist/behavior.js.map +1 -0
- package/dist/change-intent.d.ts +71 -0
- package/dist/change-intent.js +744 -0
- package/dist/change-intent.js.map +1 -0
- package/dist/cli.js +6 -5
- package/dist/cli.js.map +1 -1
- package/dist/e2e.d.ts +16 -1
- package/dist/e2e.js +329 -24
- package/dist/e2e.js.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/manifest.js +138 -4
- package/dist/manifest.js.map +1 -1
- package/dist/qa.d.ts +1 -0
- package/dist/qa.js +73 -1
- package/dist/qa.js.map +1 -1
- package/dist/terminal.js +1 -1
- package/dist/terminal.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 +9 -7
- package/docs/agent-skill.md +5 -2
- package/docs/architecture.md +131 -0
- package/docs/benchmarking.md +26 -4
- package/docs/commands.md +13 -7
- package/docs/e2e-output-examples.md +23 -9
- package/docs/manifest.md +4 -0
- package/docs/quickstart-demo.md +9 -2
- package/docs/release-validation.md +50 -37
- package/docs/releasing.md +13 -0
- package/docs/roadmap.md +20 -10
- package/package.json +4 -2
- package/schema/qamap-agent.schema.json +42 -1
- package/schema/qamap-behavior.schema.json +307 -0
- package/skills/qamap-pr-qa/SKILL.md +16 -11
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";
|
|
@@ -65,9 +69,35 @@ export async function generateE2ePlan(rootInput, options = {}) {
|
|
|
65
69
|
workspaceRoot: testPlan.workspaceRoot,
|
|
66
70
|
includeWorkingTree: options.includeWorkingTree,
|
|
67
71
|
});
|
|
72
|
+
const changeAnalysis = await analyzeChangeIntents(root, {
|
|
73
|
+
base: testPlan.base,
|
|
74
|
+
head: testPlan.head,
|
|
75
|
+
workspaceRoot: testPlan.workspaceRoot,
|
|
76
|
+
includeWorkingTree: options.includeWorkingTree,
|
|
77
|
+
changedFiles: testPlan.changedFiles,
|
|
78
|
+
addedDiffText,
|
|
79
|
+
});
|
|
68
80
|
const domainLanguage = await buildDomainLanguageSummary(root, testPlan.changedFiles, coreFlows, domains, addedDiffText);
|
|
69
81
|
const workspaceTargets = await buildWorkspaceTargets(root, testPlan);
|
|
70
|
-
const flows = await buildFlows(root, testPlan.changedFiles, recommendedRunner.name, project.type, testSuiteInventory, domainLanguage, addedDiffText);
|
|
82
|
+
const flows = await buildFlows(root, testPlan.changedFiles, recommendedRunner.name, project.type, testSuiteInventory, domainLanguage, addedDiffText, changeAnalysis);
|
|
83
|
+
const behaviorGraph = await analyzeBehaviorGraph({
|
|
84
|
+
root: testPlan.root,
|
|
85
|
+
workspaceRoot: testPlan.workspaceRoot,
|
|
86
|
+
base: testPlan.base,
|
|
87
|
+
head: testPlan.head,
|
|
88
|
+
projectType: project.type,
|
|
89
|
+
surface: behaviorSurfaceForProject(project.type),
|
|
90
|
+
runner: recommendedRunner.name,
|
|
91
|
+
changedFiles: testPlan.changedFiles.map((file) => ({
|
|
92
|
+
path: file.path,
|
|
93
|
+
status: file.status,
|
|
94
|
+
previousPath: file.previousPath,
|
|
95
|
+
})),
|
|
96
|
+
}, [
|
|
97
|
+
createInferredFlowBehaviorAdapter({ flows: flows.map(toInferredBehaviorFlow) }),
|
|
98
|
+
createChangeIntentBehaviorAdapter({ analysis: changeAnalysis }),
|
|
99
|
+
createManifestBehaviorAdapter({ matches: verificationManifestMatches }),
|
|
100
|
+
]);
|
|
71
101
|
const testSuite = summarizeTestSuiteInventory(testSuiteInventory);
|
|
72
102
|
const missingTestability = uniqueStrings([
|
|
73
103
|
...flows.flatMap((flow) => flow.missingTestability),
|
|
@@ -118,16 +148,50 @@ export async function generateE2ePlan(rootInput, options = {}) {
|
|
|
118
148
|
verificationManifestPath: verificationManifest.path,
|
|
119
149
|
verificationManifestMatches,
|
|
120
150
|
domainLanguage,
|
|
151
|
+
changeAnalysis,
|
|
121
152
|
changedFiles: testPlan.changedFiles,
|
|
122
153
|
suggestedCommands: testPlan.suggestedCommands,
|
|
123
154
|
workspaceTargets,
|
|
124
155
|
flows,
|
|
156
|
+
behaviorGraph,
|
|
125
157
|
validationMatrix,
|
|
126
158
|
bootstrap,
|
|
127
159
|
missingTestability,
|
|
128
160
|
setupNotes,
|
|
129
161
|
};
|
|
130
162
|
}
|
|
163
|
+
function behaviorSurfaceForProject(projectType) {
|
|
164
|
+
if (projectType === "web") {
|
|
165
|
+
return "web";
|
|
166
|
+
}
|
|
167
|
+
if (projectType === "expo-react-native" || projectType === "react-native") {
|
|
168
|
+
return "mobile";
|
|
169
|
+
}
|
|
170
|
+
if (projectType === "api-service") {
|
|
171
|
+
return "api";
|
|
172
|
+
}
|
|
173
|
+
if (projectType === "cli") {
|
|
174
|
+
return "cli";
|
|
175
|
+
}
|
|
176
|
+
if (projectType === "design-tokens" || projectType === "data-catalog") {
|
|
177
|
+
return "artifact";
|
|
178
|
+
}
|
|
179
|
+
return "unknown";
|
|
180
|
+
}
|
|
181
|
+
function toInferredBehaviorFlow(flow) {
|
|
182
|
+
return {
|
|
183
|
+
kind: flow.kind ?? "changed-file",
|
|
184
|
+
title: flow.title,
|
|
185
|
+
reason: flow.reason,
|
|
186
|
+
files: flow.files,
|
|
187
|
+
steps: flow.steps,
|
|
188
|
+
entrypoints: flow.entrypoints,
|
|
189
|
+
selectors: flow.selectors,
|
|
190
|
+
coverage: flow.coverage,
|
|
191
|
+
fixtureStatus: flow.fixtureReadiness.status,
|
|
192
|
+
fixtureFiles: flow.fixtureReadiness.mockInsights?.map((insight) => insight.file) ?? [],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
131
195
|
export async function generateE2eDraft(rootInput, options = {}) {
|
|
132
196
|
const root = path.resolve(rootInput);
|
|
133
197
|
const plan = await generateE2ePlan(root, options);
|
|
@@ -1025,6 +1089,10 @@ function draftFileDetails(flow) {
|
|
|
1025
1089
|
setupHints: flow.setupHints.map((hint) => `${hint.title}: ${hint.detail}`).slice(0, 6),
|
|
1026
1090
|
coverageTargets: flow.coverage.map((target) => `${target.priority}: ${target.title}`).slice(0, 7),
|
|
1027
1091
|
manifestUpdatePath: flow.manifestMatch?.updatePath,
|
|
1092
|
+
intentId: flow.intentId,
|
|
1093
|
+
intentConfidence: flow.intentConfidence,
|
|
1094
|
+
lifecycle: flow.lifecycle,
|
|
1095
|
+
qaScenarios: flow.qaScenarios,
|
|
1028
1096
|
};
|
|
1029
1097
|
}
|
|
1030
1098
|
function formatEntrypointHint(entrypoint) {
|
|
@@ -1227,6 +1295,35 @@ function withTerminalPeriod(value) {
|
|
|
1227
1295
|
}
|
|
1228
1296
|
function buildFlowLanguageBrief(flow) {
|
|
1229
1297
|
const actor = inferFlowActor(flow);
|
|
1298
|
+
if (flow.intentId && flow.lifecycle && flow.lifecycle.length > 0) {
|
|
1299
|
+
const trigger = flow.lifecycle.find((stage) => stage.kind === "trigger")?.label ?? `Start the changed ${flow.title} behavior.`;
|
|
1300
|
+
const outcomes = flow.lifecycle
|
|
1301
|
+
.filter((stage) => stage.kind === "observable-outcome")
|
|
1302
|
+
.map((stage) => stripTerminalPunctuation(stage.label));
|
|
1303
|
+
const effects = flow.lifecycle
|
|
1304
|
+
.filter((stage) => stage.kind === "side-effect")
|
|
1305
|
+
.map((stage) => stripTerminalPunctuation(stage.label));
|
|
1306
|
+
const lifecycleSuccessSignal = outcomes.length > 0
|
|
1307
|
+
? outcomes.slice(0, 2).join("; ")
|
|
1308
|
+
: effects.length > 0
|
|
1309
|
+
? `the intended side effect completes: ${effects.slice(0, 2).join("; ")}`
|
|
1310
|
+
: "the observable result matches the commit intent";
|
|
1311
|
+
const repositorySuccessSignal = inferFlowSuccessSignal(flow);
|
|
1312
|
+
const successSignal = repositorySuccessSignal === "the changed journey reaches a visible, stable success state"
|
|
1313
|
+
? lifecycleSuccessSignal
|
|
1314
|
+
: repositorySuccessSignal;
|
|
1315
|
+
const scenarioEdges = (flow.qaScenarios ?? [])
|
|
1316
|
+
.filter((scenario) => scenario.kind !== "primary")
|
|
1317
|
+
.flatMap((scenario) => [scenario.title, ...scenario.edgeCases]);
|
|
1318
|
+
return {
|
|
1319
|
+
actor,
|
|
1320
|
+
trigger,
|
|
1321
|
+
goal: `Complete the intended behavior: ${flow.title}.`,
|
|
1322
|
+
successSignal,
|
|
1323
|
+
reviewQuestion: `Does ${flow.title} follow the inferred lifecycle and produce this outcome: ${successSignal}?`,
|
|
1324
|
+
edgeCases: uniqueStrings(scenarioEdges).slice(0, 6),
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1230
1327
|
const trigger = inferFlowTrigger(flow);
|
|
1231
1328
|
const goal = inferFlowGoal(flow);
|
|
1232
1329
|
const successSignal = inferFlowSuccessSignal(flow);
|
|
@@ -1417,8 +1514,8 @@ function inferFlowSuccessSignal(flow) {
|
|
|
1417
1514
|
return "the changed journey reaches a visible, stable success state";
|
|
1418
1515
|
}
|
|
1419
1516
|
function isVisibleSuccessOutcome(value) {
|
|
1420
|
-
return /\b(?:confirmed|saved|refreshed|succeeded|success|completed|created|updated|deleted|sent|approved|accepted)\b/i.test(value) ||
|
|
1421
|
-
/(
|
|
1517
|
+
return /\b(?:confirmed|saved|scheduled|refreshed|succeeded|success|completed|created|updated|deleted|sent|approved|accepted)\b/i.test(value) ||
|
|
1518
|
+
/(?:완료|성공|예약(?:됨|됐|되)|저장(?:됨|됐|되)|등록(?:됨|됐|되)|제출(?:됨|됐|되)|승인(?:됨|됐|되))/.test(value);
|
|
1422
1519
|
}
|
|
1423
1520
|
function inferFlowReviewQuestion(flow, successSignal) {
|
|
1424
1521
|
if (isApiContractFocusedFlow(flow)) {
|
|
@@ -1519,7 +1616,7 @@ export function formatMarkdownE2ePlan(result) {
|
|
|
1519
1616
|
lines.push("- Includes working tree changes: yes");
|
|
1520
1617
|
}
|
|
1521
1618
|
lines.push(`- Project: ${formatProjectType(result.project.type)}`);
|
|
1522
|
-
lines.push(`-
|
|
1619
|
+
lines.push(`- Automation adapter: ${formatRunnerName(result.recommendedRunner.name)}`);
|
|
1523
1620
|
lines.push(`- Test suite: ${result.testSuite.hasTestSuite ? `${result.testSuite.testFileCount} test file${result.testSuite.testFileCount === 1 ? "" : "s"}` : "not detected"}`);
|
|
1524
1621
|
if (result.testSuite.frameworkSignals.length > 0) {
|
|
1525
1622
|
lines.push(`- Test frameworks: ${result.testSuite.frameworkSignals.join(", ")}`);
|
|
@@ -1544,7 +1641,10 @@ export function formatMarkdownE2ePlan(result) {
|
|
|
1544
1641
|
lines.push(`- Local history: \`${escapeMarkdownInline(result.localHistory.path)}\``);
|
|
1545
1642
|
}
|
|
1546
1643
|
lines.push("");
|
|
1547
|
-
lines.
|
|
1644
|
+
appendChangeIntentMarkdown(lines, result.changeAnalysis);
|
|
1645
|
+
lines.push("## Automation Adapter");
|
|
1646
|
+
lines.push("");
|
|
1647
|
+
lines.push("QAMap selects an output adapter only after deriving runner-independent change intent and QA scenarios.");
|
|
1548
1648
|
lines.push("");
|
|
1549
1649
|
lines.push(result.recommendedRunner.reason);
|
|
1550
1650
|
if (result.project.evidence.length > 0) {
|
|
@@ -1788,6 +1888,53 @@ export function formatMarkdownE2ePlan(result) {
|
|
|
1788
1888
|
}
|
|
1789
1889
|
return lines.join("\n");
|
|
1790
1890
|
}
|
|
1891
|
+
function appendChangeIntentMarkdown(lines, analysis) {
|
|
1892
|
+
lines.push("## Change Intent And Behavior Lifecycle");
|
|
1893
|
+
lines.push("");
|
|
1894
|
+
if (analysis.intents.length === 0) {
|
|
1895
|
+
lines.push("No evidence-backed change intent was inferred. QAMap keeps heuristic flow suggestions review-only.");
|
|
1896
|
+
for (const diagnostic of analysis.diagnostics.slice(0, 3)) {
|
|
1897
|
+
lines.push(`- ${escapeMarkdownInline(diagnostic)}`);
|
|
1898
|
+
}
|
|
1899
|
+
lines.push("");
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
for (const intent of analysis.intents.slice(0, 3)) {
|
|
1903
|
+
lines.push(`### ${escapeMarkdownInline(intent.title)}`);
|
|
1904
|
+
lines.push("");
|
|
1905
|
+
lines.push(`- Confidence: ${intent.confidence}${intent.reviewRequired ? "; human review required" : ""}`);
|
|
1906
|
+
lines.push(`- Summary: ${escapeMarkdownInline(intent.summary)}`);
|
|
1907
|
+
if (intent.commits.length > 0) {
|
|
1908
|
+
lines.push("- Commit evidence:");
|
|
1909
|
+
for (const commit of intent.commits.slice(0, 6)) {
|
|
1910
|
+
lines.push(` - \`${commit.sha.slice(0, 12)}\` ${escapeMarkdownInline(commit.subject)}`);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
if (intent.files.length > 0) {
|
|
1914
|
+
lines.push(`- Source scope: ${intent.files.slice(0, 6).map((file) => `\`${escapeMarkdownInline(file)}\``).join(", ")}`);
|
|
1915
|
+
}
|
|
1916
|
+
lines.push("");
|
|
1917
|
+
lines.push("Behavior lifecycle:");
|
|
1918
|
+
intent.lifecycle.slice(0, 12).forEach((stage, index) => {
|
|
1919
|
+
lines.push(`${index + 1}. **${stage.kind}**: ${escapeMarkdownInline(stage.label)} [${stage.confidence}]`);
|
|
1920
|
+
});
|
|
1921
|
+
lines.push("");
|
|
1922
|
+
lines.push("Required QA scenarios:");
|
|
1923
|
+
for (const scenario of intent.scenarios.slice(0, 4)) {
|
|
1924
|
+
lines.push(`- **${scenario.priority} / ${scenario.kind}**: ${escapeMarkdownInline(scenario.title)}`);
|
|
1925
|
+
for (const step of scenario.steps.slice(0, 3)) {
|
|
1926
|
+
lines.push(` - Step: ${escapeMarkdownInline(step)}`);
|
|
1927
|
+
}
|
|
1928
|
+
for (const assertion of scenario.assertions.slice(0, 3)) {
|
|
1929
|
+
lines.push(` - Assert: ${escapeMarkdownInline(assertion)}`);
|
|
1930
|
+
}
|
|
1931
|
+
if (scenario.edgeCases.length > 0) {
|
|
1932
|
+
lines.push(` - Boundaries: ${scenario.edgeCases.slice(0, 4).map(escapeMarkdownInline).join(", ")}`);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
lines.push("");
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1791
1938
|
function appendVerificationManifestMatchesMarkdown(lines, matches) {
|
|
1792
1939
|
if (matches.length === 0) {
|
|
1793
1940
|
return;
|
|
@@ -1833,7 +1980,7 @@ export function formatMarkdownE2eDraft(result) {
|
|
|
1833
1980
|
lines.push("# QAMap E2E Draft");
|
|
1834
1981
|
lines.push("");
|
|
1835
1982
|
lines.push(`- Root: \`${escapeMarkdownInline(result.root)}\``);
|
|
1836
|
-
lines.push(`-
|
|
1983
|
+
lines.push(`- Automation adapter: ${formatRunnerName(result.runner)}`);
|
|
1837
1984
|
lines.push(`- Output directory: \`${escapeMarkdownInline(result.outputDirectory)}\``);
|
|
1838
1985
|
if (result.dryRun) {
|
|
1839
1986
|
lines.push("- Mode: dry run (no files were written)");
|
|
@@ -1863,6 +2010,7 @@ export function formatMarkdownE2eDraft(result) {
|
|
|
1863
2010
|
lines.push(`- Action categories: ${formatDraftActionKindSummary(result.actionSummary.byKind)}`);
|
|
1864
2011
|
}
|
|
1865
2012
|
lines.push("");
|
|
2013
|
+
appendChangeIntentMarkdown(lines, result.plan.changeAnalysis);
|
|
1866
2014
|
appendVerificationManifestMatchesMarkdown(lines, result.plan.verificationManifestMatches);
|
|
1867
2015
|
lines.push("## Files");
|
|
1868
2016
|
lines.push("");
|
|
@@ -2111,8 +2259,9 @@ async function detectProjectProfile(root, workspaceRoot) {
|
|
|
2111
2259
|
"admin.py",
|
|
2112
2260
|
]);
|
|
2113
2261
|
const projectFilePaths = (await collectProjectFiles(root, 2000)).map((file) => file.path);
|
|
2114
|
-
const
|
|
2115
|
-
const
|
|
2262
|
+
const projectProfileArtifactFiles = projectFilePaths.filter((file) => !isTestLikeFile(file));
|
|
2263
|
+
const hasDesignTokenProject = projectProfileArtifactFiles.some(isDesignTokenFile);
|
|
2264
|
+
const hasDataCatalogProject = projectProfileArtifactFiles.some(isCatalogDataFile);
|
|
2116
2265
|
const hasCliBin = packageJsonHasCliBin(packageJson);
|
|
2117
2266
|
if (hasExpoDependency) {
|
|
2118
2267
|
evidence.push("package.json dependency: expo");
|
|
@@ -2171,15 +2320,15 @@ async function detectProjectProfile(root, workspaceRoot) {
|
|
|
2171
2320
|
if (apiServiceDependency || hasApiServiceConfig || hasDjangoEntrypoint || hasPythonServiceDependency || hasPythonServiceModule) {
|
|
2172
2321
|
return { type: "api-service", evidence };
|
|
2173
2322
|
}
|
|
2323
|
+
if (hasCliBin) {
|
|
2324
|
+
return { type: "cli", evidence };
|
|
2325
|
+
}
|
|
2174
2326
|
if (hasDesignTokenProject) {
|
|
2175
2327
|
return { type: "design-tokens", evidence };
|
|
2176
2328
|
}
|
|
2177
2329
|
if (hasDataCatalogProject) {
|
|
2178
2330
|
return { type: "data-catalog", evidence };
|
|
2179
2331
|
}
|
|
2180
|
-
if (hasCliBin) {
|
|
2181
|
-
return { type: "cli", evidence };
|
|
2182
|
-
}
|
|
2183
2332
|
return {
|
|
2184
2333
|
type: "unknown",
|
|
2185
2334
|
evidence,
|
|
@@ -3057,11 +3206,11 @@ function toCoreFlowChangedFiles(changedFiles, scopedRoot, coreFlowRoot) {
|
|
|
3057
3206
|
previousPath: file.previousPath ? toPosixPath(path.join(relativeRoot, file.previousPath)) : undefined,
|
|
3058
3207
|
}));
|
|
3059
3208
|
}
|
|
3060
|
-
async function buildFlows(root, changedFiles, runner, projectType, testSuiteInventory, domainLanguage, addedDiffText = {}) {
|
|
3209
|
+
async function buildFlows(root, changedFiles, runner, projectType, testSuiteInventory, domainLanguage, addedDiffText = {}, changeAnalysis) {
|
|
3061
3210
|
const files = changedFiles.map((file) => file.path);
|
|
3062
3211
|
const importImpacts = await collectImportImpacts(root, files);
|
|
3063
3212
|
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)));
|
|
3213
|
+
const flowResults = await Promise.all(buildFlowCandidates(files, runner, projectType, domainLanguage, importImpacts, changeAnalysis).map((candidate) => buildFlow(root, runner, candidate, testSuiteInventory, fixtureContext, addedDiffText)));
|
|
3065
3214
|
const flows = flowResults.filter((flow) => Boolean(flow));
|
|
3066
3215
|
return dedupeFlows(flows).slice(0, 4);
|
|
3067
3216
|
}
|
|
@@ -3106,7 +3255,7 @@ async function collectImportImpacts(root, changedFiles) {
|
|
|
3106
3255
|
function describeImportChain(impact) {
|
|
3107
3256
|
return impact.chain.join(" -> ");
|
|
3108
3257
|
}
|
|
3109
|
-
function buildFlowCandidates(files, runner, projectType, domainLanguage, importImpacts = []) {
|
|
3258
|
+
function buildFlowCandidates(files, runner, projectType, domainLanguage, importImpacts = [], changeAnalysis) {
|
|
3110
3259
|
const lowSignalCandidate = importImpacts.length === 0 ? buildLowSignalChangeCandidate(files) : undefined;
|
|
3111
3260
|
if (lowSignalCandidate) {
|
|
3112
3261
|
return [lowSignalCandidate];
|
|
@@ -3311,7 +3460,58 @@ function buildFlowCandidates(files, runner, projectType, domainLanguage, importI
|
|
|
3311
3460
|
],
|
|
3312
3461
|
});
|
|
3313
3462
|
}
|
|
3314
|
-
return candidates;
|
|
3463
|
+
return prioritizeChangeIntentCandidates(changeAnalysis, candidates, projectType);
|
|
3464
|
+
}
|
|
3465
|
+
function prioritizeChangeIntentCandidates(analysis, heuristicCandidates, projectType) {
|
|
3466
|
+
const intentCandidates = (analysis?.intents ?? [])
|
|
3467
|
+
.filter((intent) => intent.confidence !== "low" && intent.files.length > 0)
|
|
3468
|
+
.map((intent) => {
|
|
3469
|
+
const primaryScenario = intent.scenarios.find((scenario) => scenario.kind === "primary") ?? intent.scenarios[0];
|
|
3470
|
+
const steps = uniqueStrings([
|
|
3471
|
+
...(primaryScenario?.steps ?? intent.lifecycle.map((stage) => stage.label)),
|
|
3472
|
+
...(primaryScenario?.assertions ?? []),
|
|
3473
|
+
]);
|
|
3474
|
+
return {
|
|
3475
|
+
kind: intentFlowKind(intent, projectType),
|
|
3476
|
+
title: intent.title,
|
|
3477
|
+
reason: `Commit and diff evidence support this ${intent.confidence}-confidence change intent. ${intent.summary}`,
|
|
3478
|
+
files: intent.files,
|
|
3479
|
+
steps,
|
|
3480
|
+
coverage: intent.scenarios.map((scenario) => ({
|
|
3481
|
+
title: scenario.title,
|
|
3482
|
+
priority: scenario.priority,
|
|
3483
|
+
reason: scenario.rationale,
|
|
3484
|
+
checks: uniqueStrings([...scenario.assertions, ...scenario.edgeCases.map((edgeCase) => `Exercise ${lowercaseFirst(edgeCase)}.`)]),
|
|
3485
|
+
})),
|
|
3486
|
+
intentId: intent.id,
|
|
3487
|
+
intentConfidence: intent.confidence,
|
|
3488
|
+
intentEvidence: intent.evidence,
|
|
3489
|
+
lifecycle: intent.lifecycle,
|
|
3490
|
+
qaScenarios: intent.scenarios,
|
|
3491
|
+
};
|
|
3492
|
+
});
|
|
3493
|
+
if (intentCandidates.length === 0) {
|
|
3494
|
+
return heuristicCandidates;
|
|
3495
|
+
}
|
|
3496
|
+
const intentFiles = new Set(intentCandidates.flatMap((candidate) => candidate.files));
|
|
3497
|
+
const nonOverlapping = heuristicCandidates.filter((candidate) => candidate.files.every((file) => !intentFiles.has(file)) || isVerificationOnlyKind(candidate.kind));
|
|
3498
|
+
return [...intentCandidates, ...nonOverlapping].slice(0, 4);
|
|
3499
|
+
}
|
|
3500
|
+
function intentFlowKind(intent, projectType) {
|
|
3501
|
+
const searchable = `${intent.title} ${intent.keywords.join(" ")} ${intent.lifecycle.map((stage) => stage.label).join(" ")}`.toLowerCase();
|
|
3502
|
+
if (projectType === "cli") {
|
|
3503
|
+
return "command";
|
|
3504
|
+
}
|
|
3505
|
+
if (/\b(?:endpoint|request|response|api|contract)\b/.test(searchable) && projectType === "api-service") {
|
|
3506
|
+
return "api";
|
|
3507
|
+
}
|
|
3508
|
+
if (/\b(?:state|store|persist|sync|toggle|cache|session|notification|reminder)\b/.test(searchable)) {
|
|
3509
|
+
return "state";
|
|
3510
|
+
}
|
|
3511
|
+
if (intent.lifecycle.some((stage) => stage.kind === "observable-outcome") && projectType !== "api-service") {
|
|
3512
|
+
return "ui";
|
|
3513
|
+
}
|
|
3514
|
+
return "domain";
|
|
3315
3515
|
}
|
|
3316
3516
|
function cliCommandChecklistTitle(subject) {
|
|
3317
3517
|
if (/^cli$/i.test(subject.trim())) {
|
|
@@ -3387,13 +3587,14 @@ async function buildFlow(root, runner, candidate, testSuiteInventory, fixtureCon
|
|
|
3387
3587
|
if (files.length === 0) {
|
|
3388
3588
|
return undefined;
|
|
3389
3589
|
}
|
|
3390
|
-
const coverage = buildCoverageTargets(candidate.kind, files, runner);
|
|
3590
|
+
const coverage = candidate.coverage ?? buildCoverageTargets(candidate.kind, files, runner);
|
|
3391
3591
|
const setupHints = await inferFlowSetupHints(root, files, candidate.kind);
|
|
3392
3592
|
const interactionEvidenceApplies = !isVerificationOnlyKind(candidate.kind);
|
|
3393
3593
|
const selectors = interactionEvidenceApplies
|
|
3394
3594
|
? await inferFlowSelectors(root, files, runner, addedDiffText)
|
|
3395
3595
|
: [];
|
|
3396
3596
|
const flow = {
|
|
3597
|
+
kind: candidate.kind,
|
|
3397
3598
|
title: candidate.title,
|
|
3398
3599
|
reason: candidate.reason,
|
|
3399
3600
|
files,
|
|
@@ -3405,6 +3606,11 @@ async function buildFlow(root, runner, candidate, testSuiteInventory, fixtureCon
|
|
|
3405
3606
|
fixtureReadiness: await inferFlowFixtureReadiness(root, files, candidate.kind, setupHints, fixtureContext),
|
|
3406
3607
|
selectors,
|
|
3407
3608
|
missingTestability: interactionEvidenceApplies ? await findFlowTestabilityGaps(root, files, runner) : [],
|
|
3609
|
+
intentId: candidate.intentId,
|
|
3610
|
+
intentConfidence: candidate.intentConfidence,
|
|
3611
|
+
intentEvidence: candidate.intentEvidence,
|
|
3612
|
+
lifecycle: candidate.lifecycle,
|
|
3613
|
+
qaScenarios: candidate.qaScenarios,
|
|
3408
3614
|
};
|
|
3409
3615
|
return {
|
|
3410
3616
|
...flow,
|
|
@@ -4101,11 +4307,11 @@ function normalizeRouteSegment(segment) {
|
|
|
4101
4307
|
if (!stem || /^\([^)]*\)$/.test(stem) || stem.startsWith("_") || stem.startsWith("@")) {
|
|
4102
4308
|
return undefined;
|
|
4103
4309
|
}
|
|
4104
|
-
const routeStem = stem.replace(/^\((?:\.{1,3})\)/, "");
|
|
4310
|
+
const routeStem = stem.replace(/^\+/, "").replace(/^\((?:\.{1,3})\)/, "");
|
|
4105
4311
|
if (!routeStem) {
|
|
4106
4312
|
return undefined;
|
|
4107
4313
|
}
|
|
4108
|
-
if (/^(?:index|page|route|layout|template|loading|error|not-found|404|500)$/i.test(routeStem)) {
|
|
4314
|
+
if (/^(?:index|page|route|server|layout|template|loading|error|not-found|404|500)$/i.test(routeStem)) {
|
|
4109
4315
|
return undefined;
|
|
4110
4316
|
}
|
|
4111
4317
|
const dynamic = dynamicRouteSegmentName(routeStem);
|
|
@@ -4713,7 +4919,7 @@ async function buildDraftFlows(plan) {
|
|
|
4713
4919
|
if (manifestFlows.length === 0 && scenarioFlows.length === 0) {
|
|
4714
4920
|
return baseFlows.map((flow) => ({
|
|
4715
4921
|
...flow,
|
|
4716
|
-
draftSource: "heuristic",
|
|
4922
|
+
draftSource: flow.intentId ? "change-intent" : "heuristic",
|
|
4717
4923
|
}));
|
|
4718
4924
|
}
|
|
4719
4925
|
const combined = prioritizeImportantBaseDraftFlow(dedupeDraftFlowsByOutputPath(dedupeFlows([...manifestFlows, ...scenarioFlows, ...baseFlows]), plan.recommendedRunner.name), baseFlows).slice(0, 4);
|
|
@@ -4747,6 +4953,9 @@ function shouldUseDomainScenariosForDraft(plan) {
|
|
|
4747
4953
|
if (isLowSignalVerificationOnlyChange(files) || isConfigurationOnlyChange(files)) {
|
|
4748
4954
|
return false;
|
|
4749
4955
|
}
|
|
4956
|
+
if (plan.changeAnalysis.intents.some((intent) => intent.confidence !== "low")) {
|
|
4957
|
+
return false;
|
|
4958
|
+
}
|
|
4750
4959
|
return !plan.flows.every(isEvidenceVerificationFocusedFlow);
|
|
4751
4960
|
}
|
|
4752
4961
|
function isEvidenceVerificationFocusedFlow(flow) {
|
|
@@ -4805,9 +5014,10 @@ function uniqueManifestCheckMatches(matches) {
|
|
|
4805
5014
|
function preferredDraftSource(left, right) {
|
|
4806
5015
|
const ranks = {
|
|
4807
5016
|
"verification-manifest": 0,
|
|
4808
|
-
"
|
|
4809
|
-
"
|
|
4810
|
-
|
|
5017
|
+
"change-intent": 1,
|
|
5018
|
+
"core-flow": 2,
|
|
5019
|
+
"domain-language": 3,
|
|
5020
|
+
heuristic: 4,
|
|
4811
5021
|
};
|
|
4812
5022
|
if (!left) {
|
|
4813
5023
|
return right;
|
|
@@ -4822,7 +5032,43 @@ async function buildManifestDraftFlows(plan, baseFlows) {
|
|
|
4822
5032
|
.filter((match) => match.kind === "flow")
|
|
4823
5033
|
.slice(0, 4);
|
|
4824
5034
|
const checkMatches = plan.verificationManifestMatches.filter((match) => match.kind === "check");
|
|
4825
|
-
|
|
5035
|
+
const flowDrafts = await Promise.all(flowMatches.map((match) => buildManifestDraftFlow(plan, match, checkMatches, baseFlows)));
|
|
5036
|
+
const flowMatchedFiles = new Set(flowMatches.flatMap((match) => match.matchedFiles));
|
|
5037
|
+
const domainDrafts = await Promise.all(plan.verificationManifestMatches
|
|
5038
|
+
.filter((match) => match.kind === "domain" && match.matchedFiles.some((file) => !flowMatchedFiles.has(file)))
|
|
5039
|
+
.slice(0, 4)
|
|
5040
|
+
.map((match) => buildManifestDomainDraftFlow(plan, match, baseFlows)));
|
|
5041
|
+
return [...flowDrafts, ...domainDrafts.filter((flow) => Boolean(flow))].slice(0, 4);
|
|
5042
|
+
}
|
|
5043
|
+
async function buildManifestDomainDraftFlow(plan, match, baseFlows) {
|
|
5044
|
+
const manifestFiles = normalizeScenarioFilesForRoot(plan, match.matchedFiles);
|
|
5045
|
+
const baseFlow = bestOverlappingBaseFlowForManifestMatch(manifestFiles, baseFlows);
|
|
5046
|
+
if (!baseFlow || isVerificationOnlyFlow(baseFlow)) {
|
|
5047
|
+
return undefined;
|
|
5048
|
+
}
|
|
5049
|
+
// A domain path can cover many independent flows. Preserve manifest
|
|
5050
|
+
// provenance on the best overlapping flow without claiming every matched
|
|
5051
|
+
// domain file belongs to that single draft.
|
|
5052
|
+
const files = baseFlow.files;
|
|
5053
|
+
const flow = {
|
|
5054
|
+
...baseFlow,
|
|
5055
|
+
reason: `${match.reason} ${baseFlow.reason}`,
|
|
5056
|
+
files,
|
|
5057
|
+
entrypoints: uniqueEntrypoints([
|
|
5058
|
+
...baseFlow.entrypoints,
|
|
5059
|
+
...(await inferFlowEntrypoints(plan.root, files, plan.recommendedRunner.name)),
|
|
5060
|
+
]),
|
|
5061
|
+
selectors: uniqueSelectors([
|
|
5062
|
+
...baseFlow.selectors,
|
|
5063
|
+
...(await inferFlowSelectors(plan.root, files, plan.recommendedRunner.name)),
|
|
5064
|
+
]),
|
|
5065
|
+
draftSource: "verification-manifest",
|
|
5066
|
+
manifestMatch: match,
|
|
5067
|
+
};
|
|
5068
|
+
return {
|
|
5069
|
+
...flow,
|
|
5070
|
+
languageBrief: buildFlowLanguageBrief(flow),
|
|
5071
|
+
};
|
|
4826
5072
|
}
|
|
4827
5073
|
async function buildManifestDraftFlow(plan, match, checkMatches, baseFlows) {
|
|
4828
5074
|
const relatedChecks = checkMatches.filter((check) => check.id.startsWith(`${match.id}.`));
|
|
@@ -4848,6 +5094,7 @@ async function buildManifestDraftFlow(plan, match, checkMatches, baseFlows) {
|
|
|
4848
5094
|
...(await inferFlowSetupHints(plan.root, files, "domain")),
|
|
4849
5095
|
]);
|
|
4850
5096
|
const flow = {
|
|
5097
|
+
kind: baseFlow?.kind ?? "domain",
|
|
4851
5098
|
title: match.name,
|
|
4852
5099
|
reason: match.reason,
|
|
4853
5100
|
files,
|
|
@@ -4879,6 +5126,12 @@ function bestBaseFlowForManifestMatch(files, baseFlows) {
|
|
|
4879
5126
|
}
|
|
4880
5127
|
return best && best.score > 0 ? best.flow : baseFlows[0];
|
|
4881
5128
|
}
|
|
5129
|
+
function bestOverlappingBaseFlowForManifestMatch(files, baseFlows) {
|
|
5130
|
+
const ranked = baseFlows
|
|
5131
|
+
.map((flow) => ({ flow, score: fileOverlapScore(files, flow.files) }))
|
|
5132
|
+
.sort((left, right) => right.score - left.score);
|
|
5133
|
+
return ranked[0]?.score > 0 ? ranked[0].flow : undefined;
|
|
5134
|
+
}
|
|
4882
5135
|
function coreFlowForManifestMatch(plan, match) {
|
|
4883
5136
|
return plan.coreFlows.find((flow) => flow.name === match.name || flow.id === match.id);
|
|
4884
5137
|
}
|
|
@@ -4978,6 +5231,7 @@ async function buildDomainScenarioDraftFlow(plan, scenario, baseFlows) {
|
|
|
4978
5231
|
checks: refinedSteps,
|
|
4979
5232
|
};
|
|
4980
5233
|
const flow = {
|
|
5234
|
+
kind: baseFlow?.kind ?? "domain",
|
|
4981
5235
|
title,
|
|
4982
5236
|
reason,
|
|
4983
5237
|
files,
|
|
@@ -5159,7 +5413,7 @@ function fileOverlapScore(leftFiles, rightFiles) {
|
|
|
5159
5413
|
}
|
|
5160
5414
|
function draftFlowSource(flow) {
|
|
5161
5415
|
const draftFlow = flow;
|
|
5162
|
-
return draftFlow.draftSource ?? "heuristic";
|
|
5416
|
+
return draftFlow.draftSource ?? (flow.intentId ? "change-intent" : "heuristic");
|
|
5163
5417
|
}
|
|
5164
5418
|
function domainScenarioForFlow(flow) {
|
|
5165
5419
|
return flow.domainScenario;
|
|
@@ -5301,6 +5555,7 @@ function buildFallbackFlow(plan) {
|
|
|
5301
5555
|
const fallback = fallbackFlowDefinition(plan.project.type);
|
|
5302
5556
|
const coverage = buildCoverageTargets(fallback.kind, [], plan.recommendedRunner.name);
|
|
5303
5557
|
const flow = {
|
|
5558
|
+
kind: fallback.kind,
|
|
5304
5559
|
title: fallback.title,
|
|
5305
5560
|
reason: fallback.reason,
|
|
5306
5561
|
files: [],
|
|
@@ -5461,6 +5716,7 @@ function buildMaestroDraft(plan, flow) {
|
|
|
5461
5716
|
lines.push(`# Base: ${plan.base}`);
|
|
5462
5717
|
lines.push(`# Head: ${plan.head}`);
|
|
5463
5718
|
appendDraftBriefComments(lines, flow, "maestro", "#");
|
|
5719
|
+
appendIntentDraftComments(lines, flow, "#");
|
|
5464
5720
|
appendExecutionProfileComments(lines, plan.executionProfile, "#");
|
|
5465
5721
|
appendRunnerSetupProposalComments(lines, plan.runnerSetup, "#");
|
|
5466
5722
|
lines.push("# Replace ${APP_ID} with the app id or export APP_ID before running Maestro.");
|
|
@@ -5551,6 +5807,7 @@ function buildPlaywrightDraft(plan, flow, addedDiffText = {}) {
|
|
|
5551
5807
|
lines.push(`// Intent: ${scenario.intent}`);
|
|
5552
5808
|
}
|
|
5553
5809
|
appendDraftBriefComments(lines, flow, "playwright", "//");
|
|
5810
|
+
appendIntentDraftComments(lines, flow, "//");
|
|
5554
5811
|
appendExecutionProfileComments(lines, plan.executionProfile, "//");
|
|
5555
5812
|
appendRunnerSetupProposalComments(lines, plan.runnerSetup, "//");
|
|
5556
5813
|
lines.push("");
|
|
@@ -5665,6 +5922,7 @@ function buildManualDraft(plan, flow) {
|
|
|
5665
5922
|
lines.push(`- Head: \`${plan.head}\``);
|
|
5666
5923
|
lines.push("");
|
|
5667
5924
|
appendManualDraftBrief(lines, flow, "manual");
|
|
5925
|
+
appendManualIntentDraft(lines, flow);
|
|
5668
5926
|
appendManualExecutionProfile(lines, plan.executionProfile);
|
|
5669
5927
|
appendManualRunnerSetupProposal(lines, plan.runnerSetup);
|
|
5670
5928
|
lines.push("");
|
|
@@ -5983,6 +6241,53 @@ function appendDraftBriefComments(lines, flow, runner, commentPrefix) {
|
|
|
5983
6241
|
lines.push(`${commentPrefix} - ${input}`);
|
|
5984
6242
|
}
|
|
5985
6243
|
}
|
|
6244
|
+
function appendIntentDraftComments(lines, flow, commentPrefix) {
|
|
6245
|
+
if (!flow.intentId || !flow.lifecycle || !flow.qaScenarios) {
|
|
6246
|
+
return;
|
|
6247
|
+
}
|
|
6248
|
+
lines.push("");
|
|
6249
|
+
lines.push(`${commentPrefix} Change intent evidence:`);
|
|
6250
|
+
lines.push(`${commentPrefix} - Intent id: ${flow.intentId}`);
|
|
6251
|
+
lines.push(`${commentPrefix} - Confidence: ${flow.intentConfidence ?? "low"}`);
|
|
6252
|
+
for (const evidence of (flow.intentEvidence ?? []).filter((item) => item.kind === "commit").slice(0, 5)) {
|
|
6253
|
+
lines.push(`${commentPrefix} - Commit: ${evidence.value}`);
|
|
6254
|
+
}
|
|
6255
|
+
lines.push(`${commentPrefix} - Behavior lifecycle:`);
|
|
6256
|
+
for (const stage of flow.lifecycle.slice(0, 10)) {
|
|
6257
|
+
lines.push(`${commentPrefix} - ${stage.kind}: ${stage.label}`);
|
|
6258
|
+
}
|
|
6259
|
+
lines.push(`${commentPrefix} - Runner-independent QA scenarios:`);
|
|
6260
|
+
for (const scenario of flow.qaScenarios.slice(0, 4)) {
|
|
6261
|
+
lines.push(`${commentPrefix} - [${scenario.priority}] ${scenario.kind}: ${scenario.title}`);
|
|
6262
|
+
for (const assertion of scenario.assertions.slice(0, 2)) {
|
|
6263
|
+
lines.push(`${commentPrefix} - Assert: ${assertion}`);
|
|
6264
|
+
}
|
|
6265
|
+
}
|
|
6266
|
+
}
|
|
6267
|
+
function appendManualIntentDraft(lines, flow) {
|
|
6268
|
+
if (!flow.intentId || !flow.lifecycle || !flow.qaScenarios) {
|
|
6269
|
+
return;
|
|
6270
|
+
}
|
|
6271
|
+
lines.push("");
|
|
6272
|
+
lines.push("## Change Intent Evidence");
|
|
6273
|
+
lines.push("");
|
|
6274
|
+
lines.push(`- Intent id: \`${flow.intentId}\``);
|
|
6275
|
+
lines.push(`- Confidence: ${flow.intentConfidence ?? "low"}`);
|
|
6276
|
+
for (const evidence of (flow.intentEvidence ?? []).filter((item) => item.kind === "commit").slice(0, 5)) {
|
|
6277
|
+
lines.push(`- Commit: ${evidence.value}`);
|
|
6278
|
+
}
|
|
6279
|
+
lines.push("- Behavior lifecycle:");
|
|
6280
|
+
for (const stage of flow.lifecycle.slice(0, 10)) {
|
|
6281
|
+
lines.push(` - ${stage.kind}: ${stage.label}`);
|
|
6282
|
+
}
|
|
6283
|
+
lines.push("- Runner-independent QA scenarios:");
|
|
6284
|
+
for (const scenario of flow.qaScenarios.slice(0, 4)) {
|
|
6285
|
+
lines.push(` - [${scenario.priority}] ${scenario.kind}: ${scenario.title}`);
|
|
6286
|
+
for (const assertion of scenario.assertions.slice(0, 2)) {
|
|
6287
|
+
lines.push(` - Assert: ${assertion}`);
|
|
6288
|
+
}
|
|
6289
|
+
}
|
|
6290
|
+
}
|
|
5986
6291
|
function appendExecutionProfileComments(lines, profile, commentPrefix) {
|
|
5987
6292
|
lines.push("");
|
|
5988
6293
|
lines.push(`${commentPrefix} Execution profile:`);
|