@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +19 -14
  3. package/dist/behavior-intent.d.ts +6 -0
  4. package/dist/behavior-intent.js +172 -0
  5. package/dist/behavior-intent.js.map +1 -0
  6. package/dist/behavior-manifest.d.ts +6 -0
  7. package/dist/behavior-manifest.js +221 -0
  8. package/dist/behavior-manifest.js.map +1 -0
  9. package/dist/behavior.d.ts +143 -0
  10. package/dist/behavior.js +458 -0
  11. package/dist/behavior.js.map +1 -0
  12. package/dist/change-intent.d.ts +71 -0
  13. package/dist/change-intent.js +744 -0
  14. package/dist/change-intent.js.map +1 -0
  15. package/dist/cli.js +6 -5
  16. package/dist/cli.js.map +1 -1
  17. package/dist/e2e.d.ts +16 -1
  18. package/dist/e2e.js +329 -24
  19. package/dist/e2e.js.map +1 -1
  20. package/dist/index.d.ts +9 -1
  21. package/dist/index.js +4 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/manifest.js +138 -4
  24. package/dist/manifest.js.map +1 -1
  25. package/dist/qa.d.ts +1 -0
  26. package/dist/qa.js +73 -1
  27. package/dist/qa.js.map +1 -1
  28. package/dist/terminal.js +1 -1
  29. package/dist/terminal.js.map +1 -1
  30. package/dist/version.d.ts +1 -1
  31. package/dist/version.js +1 -1
  32. package/docs/adoption.md +11 -10
  33. package/docs/agent-format.md +9 -7
  34. package/docs/agent-skill.md +5 -2
  35. package/docs/architecture.md +131 -0
  36. package/docs/benchmarking.md +26 -4
  37. package/docs/commands.md +13 -7
  38. package/docs/e2e-output-examples.md +23 -9
  39. package/docs/manifest.md +4 -0
  40. package/docs/quickstart-demo.md +9 -2
  41. package/docs/release-validation.md +50 -37
  42. package/docs/releasing.md +13 -0
  43. package/docs/roadmap.md +20 -10
  44. package/package.json +4 -2
  45. package/schema/qamap-agent.schema.json +42 -1
  46. package/schema/qamap-behavior.schema.json +307 -0
  47. 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
- /(?:완료|성공|저장(?:됨|됐|되)|등록(?:됨|됐|되)|제출(?:됨|됐|되)|승인(?:됨|됐|되))/.test(value);
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(`- Recommended runner: ${formatRunnerName(result.recommendedRunner.name)}`);
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.push("## Recommendation");
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(`- Runner: ${formatRunnerName(result.runner)}`);
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 hasDesignTokenProject = projectFilePaths.some(isDesignTokenFile);
2115
- const hasDataCatalogProject = projectFilePaths.some(isCatalogDataFile);
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
- "core-flow": 1,
4809
- "domain-language": 2,
4810
- heuristic: 3,
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
- return Promise.all(flowMatches.map((match) => buildManifestDraftFlow(plan, match, checkMatches, baseFlows)));
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:`);