@sun-asterisk/sungen 3.2.2-beta.11 → 3.2.2-beta.14

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 (153) hide show
  1. package/dist/capabilities/discover.d.ts.map +1 -1
  2. package/dist/capabilities/discover.js +26 -3
  3. package/dist/capabilities/discover.js.map +1 -1
  4. package/dist/cli/commands/add-flow.d.ts.map +1 -1
  5. package/dist/cli/commands/add-flow.js +2 -0
  6. package/dist/cli/commands/add-flow.js.map +1 -1
  7. package/dist/cli/commands/add.d.ts.map +1 -1
  8. package/dist/cli/commands/add.js +4 -0
  9. package/dist/cli/commands/add.js.map +1 -1
  10. package/dist/cli/commands/capability.d.ts.map +1 -1
  11. package/dist/cli/commands/capability.js +61 -0
  12. package/dist/cli/commands/capability.js.map +1 -1
  13. package/dist/cli/commands/delivery.d.ts.map +1 -1
  14. package/dist/cli/commands/delivery.js +89 -2
  15. package/dist/cli/commands/delivery.js.map +1 -1
  16. package/dist/cli/commands/generate.d.ts.map +1 -1
  17. package/dist/cli/commands/generate.js +2 -1
  18. package/dist/cli/commands/generate.js.map +1 -1
  19. package/dist/cli/commands/init.d.ts.map +1 -1
  20. package/dist/cli/commands/init.js +27 -2
  21. package/dist/cli/commands/init.js.map +1 -1
  22. package/dist/cli/commands/journey.d.ts.map +1 -1
  23. package/dist/cli/commands/journey.js +19 -1
  24. package/dist/cli/commands/journey.js.map +1 -1
  25. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  26. package/dist/dashboard/snapshot-builder.js +10 -1
  27. package/dist/dashboard/snapshot-builder.js.map +1 -1
  28. package/dist/exporters/csv-exporter.d.ts.map +1 -1
  29. package/dist/exporters/csv-exporter.js +3 -1
  30. package/dist/exporters/csv-exporter.js.map +1 -1
  31. package/dist/exporters/json-exporter.js +3 -1
  32. package/dist/exporters/json-exporter.js.map +1 -1
  33. package/dist/exporters/mobile-app-id.d.ts +22 -0
  34. package/dist/exporters/mobile-app-id.d.ts.map +1 -0
  35. package/dist/exporters/mobile-app-id.js +37 -0
  36. package/dist/exporters/mobile-app-id.js.map +1 -0
  37. package/dist/exporters/spec-parser.d.ts.map +1 -1
  38. package/dist/exporters/spec-parser.js +6 -2
  39. package/dist/exporters/spec-parser.js.map +1 -1
  40. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +3 -0
  41. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  42. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +7 -4
  43. package/dist/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +20 -3
  44. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +4 -1
  45. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +4 -1
  46. package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +3 -0
  47. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  48. package/dist/generators/test-generator/code-generator.js +38 -3
  49. package/dist/generators/test-generator/code-generator.js.map +1 -1
  50. package/dist/generators/test-generator/template-engine.d.ts +3 -0
  51. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  52. package/dist/generators/test-generator/template-engine.js +11 -1
  53. package/dist/generators/test-generator/template-engine.js.map +1 -1
  54. package/dist/generators/test-generator/utils/selector-resolver.d.ts +11 -1
  55. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  56. package/dist/generators/test-generator/utils/selector-resolver.js +33 -0
  57. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  58. package/dist/harness/audit.js +1 -1
  59. package/dist/harness/audit.js.map +1 -1
  60. package/dist/harness/catalog/universal-viewpoints.yaml +15 -0
  61. package/dist/harness/journey.d.ts +20 -0
  62. package/dist/harness/journey.d.ts.map +1 -1
  63. package/dist/harness/journey.js +97 -11
  64. package/dist/harness/journey.js.map +1 -1
  65. package/dist/harness/sensors.d.ts +1 -1
  66. package/dist/harness/sensors.d.ts.map +1 -1
  67. package/dist/harness/sensors.js +32 -9
  68. package/dist/harness/sensors.js.map +1 -1
  69. package/dist/orchestrator/flow-manager.d.ts +11 -0
  70. package/dist/orchestrator/flow-manager.d.ts.map +1 -1
  71. package/dist/orchestrator/flow-manager.js +147 -29
  72. package/dist/orchestrator/flow-manager.js.map +1 -1
  73. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts +59 -0
  74. package/dist/orchestrator/mobile-runtime-scaffolder.d.ts.map +1 -0
  75. package/dist/orchestrator/mobile-runtime-scaffolder.js +298 -0
  76. package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -0
  77. package/dist/orchestrator/project-initializer.d.ts +25 -7
  78. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  79. package/dist/orchestrator/project-initializer.js +115 -62
  80. package/dist/orchestrator/project-initializer.js.map +1 -1
  81. package/dist/orchestrator/screen-manager.d.ts +30 -0
  82. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  83. package/dist/orchestrator/screen-manager.js +205 -22
  84. package/dist/orchestrator/screen-manager.js.map +1 -1
  85. package/dist/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +12 -0
  86. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +32 -4
  87. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +51 -10
  88. package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +23 -2
  89. package/dist/orchestrator/templates/ai-instructions/claude-cmd-delivery.md +6 -0
  90. package/dist/orchestrator/templates/ai-instructions/claude-cmd-locale.md +25 -4
  91. package/dist/orchestrator/templates/ai-instructions/claude-cmd-review.md +8 -1
  92. package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +38 -4
  93. package/dist/orchestrator/templates/ai-instructions/claude-skill-delivery.md +2 -0
  94. package/dist/orchestrator/templates/ai-instructions/claude-skill-locale.md +106 -9
  95. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +33 -3
  96. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +50 -9
  97. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +11 -2
  98. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +6 -0
  99. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-locale.md +25 -4
  100. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +8 -1
  101. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +38 -4
  102. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +2 -0
  103. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-locale.md +106 -9
  104. package/package.json +3 -3
  105. package/src/capabilities/discover.ts +29 -3
  106. package/src/cli/commands/add-flow.ts +2 -0
  107. package/src/cli/commands/add.ts +4 -0
  108. package/src/cli/commands/capability.ts +64 -1
  109. package/src/cli/commands/delivery.ts +89 -2
  110. package/src/cli/commands/generate.ts +2 -1
  111. package/src/cli/commands/init.ts +38 -3
  112. package/src/cli/commands/journey.ts +15 -2
  113. package/src/dashboard/snapshot-builder.ts +10 -1
  114. package/src/exporters/csv-exporter.ts +3 -1
  115. package/src/exporters/json-exporter.ts +3 -1
  116. package/src/exporters/mobile-app-id.ts +34 -0
  117. package/src/exporters/spec-parser.ts +6 -2
  118. package/src/generators/test-generator/adapters/adapter-interface.ts +3 -0
  119. package/src/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +7 -4
  120. package/src/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +20 -3
  121. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +4 -1
  122. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +4 -1
  123. package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +3 -0
  124. package/src/generators/test-generator/code-generator.ts +39 -3
  125. package/src/generators/test-generator/template-engine.ts +14 -1
  126. package/src/generators/test-generator/utils/selector-resolver.ts +51 -1
  127. package/src/harness/audit.ts +1 -1
  128. package/src/harness/catalog/universal-viewpoints.yaml +15 -0
  129. package/src/harness/journey.ts +110 -9
  130. package/src/harness/sensors.ts +25 -8
  131. package/src/orchestrator/flow-manager.ts +165 -29
  132. package/src/orchestrator/mobile-runtime-scaffolder.ts +277 -0
  133. package/src/orchestrator/project-initializer.ts +127 -64
  134. package/src/orchestrator/screen-manager.ts +237 -22
  135. package/src/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +12 -0
  136. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +32 -4
  137. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +51 -10
  138. package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +23 -2
  139. package/src/orchestrator/templates/ai-instructions/claude-cmd-delivery.md +6 -0
  140. package/src/orchestrator/templates/ai-instructions/claude-cmd-locale.md +25 -4
  141. package/src/orchestrator/templates/ai-instructions/claude-cmd-review.md +8 -1
  142. package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +38 -4
  143. package/src/orchestrator/templates/ai-instructions/claude-skill-delivery.md +2 -0
  144. package/src/orchestrator/templates/ai-instructions/claude-skill-locale.md +106 -9
  145. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +33 -3
  146. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +50 -9
  147. package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +11 -2
  148. package/src/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +6 -0
  149. package/src/orchestrator/templates/ai-instructions/copilot-cmd-locale.md +25 -4
  150. package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +8 -1
  151. package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +38 -4
  152. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +2 -0
  153. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-locale.md +106 -9
@@ -146,7 +146,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
146
146
  const capGate = scoringCap?.gateProvider as
147
147
  ((i: { scenarios: ScenarioInfo[]; viewpoints: ViewpointEntry[]; catalog: Catalog; focus: typeof intent.focus; cwd: string; screenName: string; threshold: number; businessCriticalMethods?: string[] }) => { gate: GateResult; depth: DepthResult }) | undefined;
148
148
  const provided = capGate?.({ scenarios, viewpoints, catalog, focus: intent.focus, cwd: projectRootFromScreenDir(screenDir), screenName: catalogScreenName, threshold: depthThresholdFor(intent.focus), businessCriticalMethods: intent.businessCriticalMethods });
149
- const gate = provided?.gate ?? viewpointGate(scenarios, viewpoints, catalog);
149
+ const gate = provided?.gate ?? viewpointGate(scenarios, viewpoints, catalog, platform === 'mobile');
150
150
  const depth = provided?.depth ?? assertionDepth(scenarios, dataThemesFor(catalog, gate.pageType), intent.focus);
151
151
 
152
152
  // TQ-4 — deferral-aware coverage credit: an inherently cross-screen theme (cart / detail /
@@ -91,6 +91,21 @@ page_types:
91
91
  keywords: ["success message", "saved", "created", "confirmation"]
92
92
  template: "User see [Success] message with {{success_text}}"
93
93
 
94
+ # Mobile launcher / home screen (Appium). Display + navigation only — no web form/validation.
95
+ # A `mobile-*` candidate (considered ONLY for mobile targets; never for web). Keywords are
96
+ # launcher-DISTINCTIVE (a tab-bar landing screen) and deliberately exclude generic tokens like
97
+ # "tab"/"navigation"/"home" that a mobile commerce/list screen also carries — so such a screen
98
+ # still selects ecommerce-list, not mobile-home. Both themes are presence/navigation (no `depth:`)
99
+ # — seeing the content and leaving Home (a destination marker OR the Home-only anchor going
100
+ # `is hidden`) IS the assertion; no data-assertion is forced.
101
+ mobile-home:
102
+ detect_keywords: ["bottom navigation", launcher, greeting, "home screen", "launcher screen"]
103
+ must_cover:
104
+ - theme: content-visibility
105
+ keywords: [displayed, render, loads, exposes, shown, visible]
106
+ - theme: navigation-correctness
107
+ keywords: [tap, navigate, "leaves home", "is hidden", opens, navigation]
108
+
94
109
  auth:
95
110
  detect_keywords: [login, logout, password, signin, "sign in", credential]
96
111
  must_cover:
@@ -116,19 +116,31 @@ function computeFresh(projectRoot: string, unit: string): JourneyReport {
116
116
  detail: autoPending ? `${audit.automatableManual.automatable} @manual scenario(s) a driver could automate` : 'no capability-pending automatable cases',
117
117
  });
118
118
 
119
- // Human sign-off: always pending until S5 explicit signoff; carries the review-queue count.
120
- const manualCount = (audit.manualOracle && audit.manualOracle.total) || 0;
119
+ // Needs-you: the review queue. Prefer PRECISE per-scenario anchors from audit's structured
120
+ // depth data each shallow business-critical scenario carries its own VP id — over regex-parsing
121
+ // the aggregate "DEPTH:" finding string (which names no scenario, so points nowhere). Then
122
+ // supplement with the remaining human-judgment findings.
123
+ const shallowBC: { name: string; category?: string }[] = (audit.depth && audit.depth.shallowBusinessCritical) || [];
124
+ for (const s of shallowBC) {
125
+ needsYou.push({
126
+ text: `DEEPEN — "${s.name}": assert data, not just visibility (\`… with {{value}}\` / \`see all … contain {{v}}\`), or defer cross-screen with @manual + reason.`,
127
+ anchor: extractAnchor(s.name),
128
+ });
129
+ }
130
+ const haveStructuredDepth = shallowBC.length > 0;
131
+ for (const f of (audit.findings || [])) {
132
+ if (!isHumanFinding(f)) continue;
133
+ if (haveStructuredDepth && /^\s*DEPTH\b/i.test(f)) continue; // superseded by the per-scenario items above
134
+ needsYou.push({ text: f, anchor: extractAnchor(f) });
135
+ }
136
+
137
+ // Human sign-off: always pending until S5 explicit signoff; carries the now-final review-queue count.
138
+ const manualCount = (audit.manualOracle && (audit.manualOracle.manualTotal ?? audit.manualOracle.total)) || 0;
121
139
  obligations.push({
122
140
  id: 'OB-signoff', title: 'Human sign-off', status: 'pending',
123
- detail: `${needsYou.length} item(s) queued for your review (single sign-off at the end)`,
141
+ detail: `${needsYou.length} item(s) queued for your review · ${manualCount} @manual scenario(s)`,
124
142
  });
125
143
 
126
- // Needs-you: human-judgment findings (the review-queue seed), with their anchors (titles) intact.
127
- for (const f of (audit.findings || [])) if (isHumanFinding(f)) needsYou.push({ text: f, anchor: extractAnchor(f) });
128
- // Re-stamp the signoff detail now that needsYou is filled.
129
- const signoff = obligations.find((o) => o.id === 'OB-signoff');
130
- if (signoff) signoff.detail = `${needsYou.length} item(s) queued for your review · ${manualCount} @manual scenario(s)`;
131
-
132
144
  // Next suggested = first non-satisfied obligation; if all satisfied but run-test not done → run.
133
145
  const firstGap = obligations.find((o) => o.status !== 'satisfied' && o.id !== 'OB-signoff');
134
146
  let nextSuggested: string;
@@ -331,3 +343,92 @@ export function renderGate(v: GateVerdict): string {
331
343
  L.push('');
332
344
  return L.join('\n');
333
345
  }
346
+
347
+ // ---------------- #409: project-level roll-up across all units ----------------
348
+ //
349
+ // Multi-unit fan-out PARALLELISES generation but human review/sign-off must stay SERIAL — you can't
350
+ // judge N boards at once. This roll-up is that serial surface: one row per unit (gate · score · phase
351
+ // · what it still needs), and a single "review this next" pointer. The per-unit board is unchanged;
352
+ // this only aggregates the same read-only `runJourney` synthesis.
353
+
354
+ export type UnitKind = 'screen' | 'flow' | 'api';
355
+ export interface UnitSummary {
356
+ unit: string;
357
+ kind: UnitKind;
358
+ score: number | null;
359
+ gateStatus: string | null;
360
+ youAreHere: string;
361
+ obligationsOpen: number; // non-satisfied, non-waived (excludes OB-signoff)
362
+ reviewQueue: number;
363
+ signedOff: boolean;
364
+ started: boolean; // has an audit report yet
365
+ }
366
+
367
+ /** Discover every unit under qa/screens, qa/flows, and qa/api top-level areas. */
368
+ export function listUnits(projectRoot: string): { unit: string; kind: UnitKind }[] {
369
+ const out: { unit: string; kind: UnitKind }[] = [];
370
+ const scan = (sub: string, kind: UnitKind) => {
371
+ const d = path.join(projectRoot, 'qa', sub);
372
+ if (!fs.existsSync(d)) return;
373
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
374
+ // 'flows' under qa/api is a container for api-flows (a nested tier) — not a unit itself.
375
+ if (e.isDirectory() && !(sub === 'api' && e.name === 'flows')) out.push({ unit: e.name, kind });
376
+ }
377
+ };
378
+ scan('screens', 'screen');
379
+ scan('flows', 'flow');
380
+ scan('api', 'api');
381
+ return out;
382
+ }
383
+
384
+ /** Roll up every unit's journey into one table of summaries (serial review surface). */
385
+ export function rollupJourney(projectRoot: string): UnitSummary[] {
386
+ return listUnits(projectRoot).map(({ unit, kind }) => {
387
+ const r = runJourney(projectRoot, unit);
388
+ const signedOff = r.obligations.find((o) => o.id === 'OB-signoff')?.status === 'satisfied';
389
+ const obligationsOpen = r.obligations.filter(
390
+ (o) => o.id !== 'OB-signoff' && o.status !== 'satisfied' && o.status !== 'waived',
391
+ ).length;
392
+ return {
393
+ unit, kind, score: r.score, gateStatus: r.gateStatus, youAreHere: r.youAreHere,
394
+ obligationsOpen, reviewQueue: r.needsYou.length, signedOff,
395
+ started: r.generatedFrom.includes('audit'),
396
+ };
397
+ });
398
+ }
399
+
400
+ function unitStatusLabel(r: UnitSummary): string {
401
+ if (!r.started) return '◻️ not started';
402
+ if (r.signedOff) return '✅ signed off';
403
+ if (r.obligationsOpen > 0) return `⏳ ${r.obligationsOpen} obligation(s) open`;
404
+ if (r.reviewQueue > 0) return `🔎 review ${r.reviewQueue} item(s)`;
405
+ return '🟡 ready to sign off';
406
+ }
407
+
408
+ export function renderRollupBoard(rows: UnitSummary[]): string {
409
+ const L: string[] = [];
410
+ L.push('# Journey roll-up — all units');
411
+ L.push('');
412
+ if (!rows.length) {
413
+ L.push('No units found under `qa/screens`, `qa/flows`, or `qa/api`.');
414
+ return L.join('\n');
415
+ }
416
+ L.push('| Unit | Kind | Score | Gate | Phase | Needs |');
417
+ L.push('|---|---|---|---|---|---|');
418
+ for (const r of rows) {
419
+ L.push(`| ${r.unit} | ${r.kind} | ${r.score ?? '—'} | ${r.gateStatus ?? '—'} | ${r.youAreHere} | ${unitStatusLabel(r)} |`);
420
+ }
421
+ L.push('');
422
+ const blocked = rows.filter((r) => r.started && (r.obligationsOpen > 0));
423
+ const toReview = rows.filter((r) => r.started && r.obligationsOpen === 0 && !r.signedOff && r.reviewQueue >= 0);
424
+ const notStarted = rows.filter((r) => !r.started);
425
+ L.push(`**${rows.filter((r) => r.signedOff).length}/${rows.length} signed off** · ${blocked.length} with open obligations · ${notStarted.length} not started.`);
426
+ L.push('');
427
+ L.push('## → Next (review is serial — one unit at a time)');
428
+ const next = blocked[0] || toReview[0] || notStarted[0];
429
+ if (!next) L.push('All units signed off — `sungen delivery` to export.');
430
+ else if (!next.started) L.push(`Generate **${next.unit}** — \`/sungen:create-test ${next.unit}\`.`);
431
+ else L.push(`Review **${next.unit}** (${unitStatusLabel(next)}) — \`sungen journey --screen ${next.unit}\`, then sign off.`);
432
+ L.push('');
433
+ return L.join('\n');
434
+ }
@@ -59,9 +59,17 @@ export function loadCatalog(): Catalog {
59
59
  return parseYaml(fs.readFileSync(p, 'utf-8')) as Catalog;
60
60
  }
61
61
 
62
+ // Word-aware keyword match: `\b<kw>(s|es|ed|ing)?\b`, so a keyword matches whole words (plus
63
+ // simple plural/verb suffixes) but NOT as a substring of a larger word — e.g. `form` must not
64
+ // match inside `Format` or `@platform`, while `product` still matches `products`. Multi-word
65
+ // keywords ("add to cart", "is hidden") match the phrase with a boundary at each end.
66
+ const wordRe = (kw: string) => {
67
+ const esc = kw.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
68
+ return new RegExp(`\\b${esc}(?:s|es|ed|ing)?\\b`);
69
+ };
62
70
  const has = (haystacks: string[], kw: string) => {
63
- const k = kw.toLowerCase();
64
- return haystacks.some((h) => h.includes(k));
71
+ const re = wordRe(kw);
72
+ return haystacks.some((h) => re.test(h));
65
73
  };
66
74
 
67
75
  // ---------- Sensor 1: Viewpoint Gate (catalog-driven) ----------
@@ -75,16 +83,21 @@ export interface GateResult {
75
83
  universalGaps: string[];
76
84
  }
77
85
 
78
- export function viewpointGate(scenarios: ScenarioInfo[], viewpoints: ViewpointEntry[], catalog: Catalog): GateResult {
86
+ export function viewpointGate(scenarios: ScenarioInfo[], viewpoints: ViewpointEntry[], catalog: Catalog, isMobile = false): GateResult {
79
87
  const haystacks = [
80
88
  ...scenarios.map((s) => s.haystack),
81
89
  ...viewpoints.map((v) => `${v.id} ${v.reason}`.toLowerCase()),
82
90
  ];
83
91
 
84
- // Detect page-type by detect_keywords hit count
92
+ // Detect page-type by detect_keywords hit count. `mobile-*` page-types are candidates ONLY for
93
+ // mobile targets; web targets never consider them. Mobile targets still consider the web types
94
+ // too, so a mobile commerce/form screen is selected as ecommerce-list/form by keyword fit —
95
+ // `mobile-home` does not crowd out a better-fitting type. With no type matching → pageType stays
96
+ // null → no themes required → the gate passes leniently rather than false-FAILing.
85
97
  let pageType: string | null = null;
86
98
  let best = 0;
87
99
  for (const [pt, def] of Object.entries(catalog.page_types)) {
100
+ if (pt.startsWith('mobile-') && !isMobile) continue;
88
101
  const hits = def.detect_keywords.filter((k) => has(haystacks, k)).length;
89
102
  if (hits > best) { best = hits; pageType = pt; }
90
103
  }
@@ -97,10 +110,14 @@ export function viewpointGate(scenarios: ScenarioInfo[], viewpoints: ViewpointEn
97
110
  // Scenarios that cover this theme by keyword (include @manual — a manual
98
111
  // scenario with a real assertion still covers the viewpoint for design).
99
112
  const coverers = scenarios.filter((s) => t.keywords.some((k) => s.haystack.includes(k.toLowerCase())));
100
- const deep = coverers.some((s) => s.hasDataAssertion);
101
- if (deep) covered++;
102
- else if (coverers.length > 0) gaps.push({ theme: t.theme, keywords: t.keywords, status: 'shallow' });
103
- else gaps.push({ theme: t.theme, keywords: t.keywords, status: 'missing' });
113
+ if (coverers.length === 0) { gaps.push({ theme: t.theme, keywords: t.keywords, status: 'missing' }); continue; }
114
+ // A theme WITHOUT `depth:` is visibility/navigation — landing/seeing/leaving IS the
115
+ // assertion, so mere presence covers it. Only DATA-correctness themes (depth.requires:
116
+ // data-assertion) need a data assertion to count as deeply covered.
117
+ const needsDepth = t.depth?.requires === 'data-assertion';
118
+ if (!needsDepth) { covered++; continue; }
119
+ if (coverers.some((s) => s.hasDataAssertion)) covered++;
120
+ else gaps.push({ theme: t.theme, keywords: t.keywords, status: 'shallow' });
104
121
  }
105
122
  }
106
123
 
@@ -6,11 +6,18 @@
6
6
 
7
7
  import * as fs from 'fs';
8
8
  import * as path from 'path';
9
+ import { readCapabilities } from '../harness/capability';
9
10
 
10
11
  export interface FlowOptions {
11
12
  name: string;
12
13
  path?: string;
13
14
  description?: string;
15
+ /**
16
+ * Mobile navigation recipe for the flow's starting screen. A `>`-separated list of nav steps
17
+ * that take the app from launch to the first screen in the flow. Scaffolded into the Background.
18
+ * Ignored (with a warning) for web projects.
19
+ */
20
+ reach?: string;
14
21
  }
15
22
 
16
23
  export class FlowManager {
@@ -25,6 +32,18 @@ export class FlowManager {
25
32
  async addFlow(options: FlowOptions): Promise<void> {
26
33
  this.validateFlowName(options.name);
27
34
 
35
+ // Detect project platform from qa/capabilities.yaml
36
+ const caps = readCapabilities(this.cwd);
37
+ const isMobile = caps.platform === 'mobile';
38
+
39
+ // --reach is mobile-only; warn and drop when the project is web
40
+ if (options.reach && !isMobile) {
41
+ console.warn(
42
+ `Warning: --reach is a mobile navigation recipe and is ignored for web projects. Dropping it.`
43
+ );
44
+ options = { ...options, reach: undefined };
45
+ }
46
+
28
47
  const flowName = this.normalizeFlowName(options.name);
29
48
  const flowDir = path.join(this.flowsDir, flowName);
30
49
 
@@ -59,28 +78,40 @@ export class FlowManager {
59
78
  process.exit(1);
60
79
  }
61
80
 
62
- fs.writeFileSync(featurePath, this.generateFeatureTemplate(options, flowName), 'utf-8');
81
+ fs.writeFileSync(featurePath, this.generateFeatureTemplate(options, flowName, isMobile), 'utf-8');
63
82
 
64
83
  const startPath = options.path || '/login';
65
- fs.writeFileSync(selectorPath, [
66
- `# ${options.name} Flow Selectors`,
67
- `# Namespace keys by screen to avoid duplicates: "login:submit", "awards:submit"`,
68
- ``,
69
- `# --- Login screen ---`,
70
- `login:`,
71
- ` type: 'page'`,
72
- ` value: '${startPath}'`,
73
- ``,
74
- `# "login:email":`,
75
- `# type: 'testid'`,
76
- `# value: 'email-input'`,
77
- ``,
78
- `# "login:submit":`,
79
- `# type: 'role'`,
80
- `# value: 'button'`,
81
- `# name: 'Login'`,
82
- ``,
83
- ].join('\n'), 'utf-8');
84
+ const selectorLines = isMobile
85
+ ? [
86
+ `# ${options.name} Flow Selectors (mobile)`,
87
+ `# Namespace keys by screen: "home:button", "settings:save"`,
88
+ `# Generated at /sungen:run-test via Appium MCP — accessibility-id preferred.`,
89
+ ``,
90
+ `# "home:element":`,
91
+ `# type: 'accessibility-id'`,
92
+ `# value: 'element-id'`,
93
+ ``,
94
+ ]
95
+ : [
96
+ `# ${options.name} Flow Selectors`,
97
+ `# Namespace keys by screen to avoid duplicates: "login:submit", "awards:submit"`,
98
+ ``,
99
+ `# --- Login screen ---`,
100
+ `login:`,
101
+ ` type: 'page'`,
102
+ ` value: '${startPath}'`,
103
+ ``,
104
+ `# "login:email":`,
105
+ `# type: 'testid'`,
106
+ `# value: 'email-input'`,
107
+ ``,
108
+ `# "login:submit":`,
109
+ `# type: 'role'`,
110
+ `# value: 'button'`,
111
+ `# name: 'Login'`,
112
+ ``,
113
+ ];
114
+ fs.writeFileSync(selectorPath, selectorLines.join('\n'), 'utf-8');
84
115
 
85
116
  fs.writeFileSync(testDataPath, [
86
117
  `# ${options.name} Flow Test Data`,
@@ -90,7 +121,7 @@ export class FlowManager {
90
121
  ].join('\n'), 'utf-8');
91
122
 
92
123
  const specPath = path.join(requirementsDir, 'spec.md');
93
- fs.writeFileSync(specPath, this.generateSpecTemplate(options, flowName), 'utf-8');
124
+ fs.writeFileSync(specPath, this.generateSpecTemplate(options, flowName, isMobile), 'utf-8');
94
125
 
95
126
  const viewpointPath = path.join(requirementsDir, 'test-viewpoint.md');
96
127
  fs.writeFileSync(viewpointPath, this.generateViewpointTemplate(options), 'utf-8');
@@ -104,12 +135,21 @@ export class FlowManager {
104
135
  console.log(` ${path.relative(this.cwd, requirementsUiDir)}/`);
105
136
  console.log('');
106
137
 
107
- console.log('Next steps:');
108
- console.log(` 1. Fill requirements/spec.md with flow specification (screens, transitions, business rules)`);
109
- console.log(` Optionally add UI designs to requirements/ui/`);
110
- console.log(` 2. Generate test cases: /sungen:create-test ${flowName}`);
111
- console.log(` 3. Compile: sungen generate --flow ${flowName}`);
112
- console.log(` 4. Run: npx playwright test\n`);
138
+ if (isMobile) {
139
+ console.log('Next steps:');
140
+ console.log(` 1. Fill requirements/spec.md with flow specification (screens, transitions, business rules)`);
141
+ console.log(` Use sungen-capture-mobile to scan each screen via Appium MCP`);
142
+ console.log(` 2. Generate test cases: /sungen:create-test ${flowName}`);
143
+ console.log(` 3. Compile: sungen generate --flow ${flowName} --framework appium`);
144
+ console.log(` 4. Run: npm run test:mobile\n`);
145
+ } else {
146
+ console.log('Next steps:');
147
+ console.log(` 1. Fill requirements/spec.md with flow specification (screens, transitions, business rules)`);
148
+ console.log(` Optionally add UI designs to requirements/ui/`);
149
+ console.log(` 2. Generate test cases: /sungen:create-test ${flowName}`);
150
+ console.log(` 3. Compile: sungen generate --flow ${flowName}`);
151
+ console.log(` 4. Run: npx playwright test\n`);
152
+ }
113
153
  }
114
154
 
115
155
  private validateFlowName(name: string): void {
@@ -131,7 +171,39 @@ export class FlowManager {
131
171
  .replace(/-+/g, '-');
132
172
  }
133
173
 
134
- private generateSpecTemplate(options: FlowOptions, flowName: string): string {
174
+ private generateSpecTemplate(options: FlowOptions, flowName: string, isMobile: boolean): string {
175
+ if (isMobile) {
176
+ return `# ${options.name} Flow Specification
177
+
178
+ ## Overview
179
+ - **Platform:** mobile (Android + iOS)
180
+ - **Auth Required:** no
181
+ - **Starting screen reach:** ${options.reach || '<!-- nav recipe to reach the first screen, e.g. "tap [Tab] > tap [Row]" -->'}
182
+
183
+ ## Screens in Flow
184
+ <!-- List all screens this flow visits, in order -->
185
+
186
+ | # | Screen | Reach | Description |
187
+ |---|--------|-------|-------------|
188
+ | 1 | [Screen Name] | tap [element] | [What user does here] |
189
+
190
+ ## Flow Steps
191
+
192
+ ### Step 1: [Screen Name]
193
+ - **Action:** [What the user taps]
194
+ - **Expected:** [What should happen]
195
+ - **Transition:** Navigates to Step 2
196
+
197
+ ## Business Rules
198
+ <!-- Rules that affect the flow: permissions, conditions, limits -->
199
+ - [Rule 1]
200
+
201
+ ## Notes
202
+ <!-- Edge cases, known issues, environment-specific behavior -->
203
+ - [Note 1]
204
+ `;
205
+ }
206
+
135
207
  const startPath = options.path || '/login';
136
208
  return `# ${options.name} Flow Specification
137
209
 
@@ -215,9 +287,33 @@ export class FlowManager {
215
287
  ].join('\n');
216
288
  }
217
289
 
218
- private generateFeatureTemplate(options: FlowOptions, flowName: string): string {
290
+ private generateFeatureTemplate(options: FlowOptions, flowName: string, isMobile: boolean): string {
219
291
  const description = options.description || `complete the ${options.name} flow`;
220
292
 
293
+ if (isMobile) {
294
+ const background = this.buildMobileBackground(options.reach);
295
+ return `@flow @platform:mobile
296
+ Feature: ${options.name} Flow
297
+
298
+ As a user
299
+ I want to ${description}
300
+ So that I can accomplish my end-to-end goal
301
+
302
+ # Mobile (Appium) flow — selectors use colon namespace: "home:element", "settings:save".
303
+ # See sungen-mobile-gestures for swipe/long-press/scroll steps.
304
+
305
+ ${background}
306
+ @high
307
+ Scenario: Sample step for ${options.name}
308
+ When User tap [Home:element]
309
+ Then User see [Settings] screen
310
+
311
+ Scenario: Sample next step for ${options.name}
312
+ When User tap [Settings:element]
313
+ Then User see [Result:header] header with {{result_text}}
314
+ `;
315
+ }
316
+
221
317
  return `@flow
222
318
  Feature: ${options.name} Flow
223
319
 
@@ -240,4 +336,44 @@ Feature: ${options.name} Flow
240
336
  Then User see "expected result" text
241
337
  `;
242
338
  }
339
+
340
+ /**
341
+ * Build the Background block for a mobile flow — same structure as screen-manager.
342
+ * The `--reach` steps represent how to reach the FIRST screen of the flow from app launch.
343
+ */
344
+ private buildMobileBackground(reach?: string): string {
345
+ const normalizeStep = (raw: string): string =>
346
+ raw
347
+ .trim()
348
+ .replace(/^(given|when|and|then|but)\s+/i, '')
349
+ .replace(/^user\s+/i, '')
350
+ .trim();
351
+
352
+ const steps = (reach || '')
353
+ .split('>')
354
+ .map(normalizeStep)
355
+ .filter(Boolean);
356
+
357
+ if (steps.length === 0) {
358
+ return [
359
+ ' Background:',
360
+ ' # Navigation recipe — how to reach the first screen of this flow from app launch.',
361
+ ' # Prefill with --reach "tap [Tab] > tap [Row]" at add-flow time.',
362
+ ' Given User is on [Home] screen',
363
+ ' # When User tap [<tab or button that leads to the first screen>]',
364
+ '',
365
+ ].join('\n');
366
+ }
367
+
368
+ const navLines = steps.map(
369
+ (body, i) => ` ${i === 0 ? 'When' : 'And'} User ${body}`,
370
+ );
371
+ return [
372
+ ' Background:',
373
+ ' # Navigation recipe — replayed by capture-mobile to land on the first screen.',
374
+ ' Given User is on [Home] screen',
375
+ ...navLines,
376
+ '',
377
+ ].join('\n');
378
+ }
243
379
  }