@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.
- package/dist/capabilities/discover.d.ts.map +1 -1
- package/dist/capabilities/discover.js +26 -3
- package/dist/capabilities/discover.js.map +1 -1
- package/dist/cli/commands/add-flow.d.ts.map +1 -1
- package/dist/cli/commands/add-flow.js +2 -0
- package/dist/cli/commands/add-flow.js.map +1 -1
- package/dist/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/add.js +4 -0
- package/dist/cli/commands/add.js.map +1 -1
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +61 -0
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +89 -2
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +2 -1
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +27 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/journey.d.ts.map +1 -1
- package/dist/cli/commands/journey.js +19 -1
- package/dist/cli/commands/journey.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +10 -1
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +3 -1
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/json-exporter.js +3 -1
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/mobile-app-id.d.ts +22 -0
- package/dist/exporters/mobile-app-id.d.ts.map +1 -0
- package/dist/exporters/mobile-app-id.js +37 -0
- package/dist/exporters/mobile-app-id.js.map +1 -0
- package/dist/exporters/spec-parser.d.ts.map +1 -1
- package/dist/exporters/spec-parser.js +6 -2
- package/dist/exporters/spec-parser.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +3 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +7 -4
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +20 -3
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +4 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +4 -1
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +3 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +38 -3
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +3 -0
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +11 -1
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +11 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +33 -0
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/audit.js +1 -1
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/catalog/universal-viewpoints.yaml +15 -0
- package/dist/harness/journey.d.ts +20 -0
- package/dist/harness/journey.d.ts.map +1 -1
- package/dist/harness/journey.js +97 -11
- package/dist/harness/journey.js.map +1 -1
- package/dist/harness/sensors.d.ts +1 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +32 -9
- package/dist/harness/sensors.js.map +1 -1
- package/dist/orchestrator/flow-manager.d.ts +11 -0
- package/dist/orchestrator/flow-manager.d.ts.map +1 -1
- package/dist/orchestrator/flow-manager.js +147 -29
- package/dist/orchestrator/flow-manager.js.map +1 -1
- package/dist/orchestrator/mobile-runtime-scaffolder.d.ts +59 -0
- package/dist/orchestrator/mobile-runtime-scaffolder.d.ts.map +1 -0
- package/dist/orchestrator/mobile-runtime-scaffolder.js +298 -0
- package/dist/orchestrator/mobile-runtime-scaffolder.js.map +1 -0
- package/dist/orchestrator/project-initializer.d.ts +25 -7
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +115 -62
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/screen-manager.d.ts +30 -0
- package/dist/orchestrator/screen-manager.d.ts.map +1 -1
- package/dist/orchestrator/screen-manager.js +205 -22
- package/dist/orchestrator/screen-manager.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +12 -0
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +32 -4
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +51 -10
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +23 -2
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-delivery.md +6 -0
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-locale.md +25 -4
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-review.md +8 -1
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +38 -4
- package/dist/orchestrator/templates/ai-instructions/claude-skill-delivery.md +2 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-locale.md +106 -9
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +33 -3
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +50 -9
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +11 -2
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +6 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-locale.md +25 -4
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +8 -1
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +38 -4
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +2 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-locale.md +106 -9
- package/package.json +3 -3
- package/src/capabilities/discover.ts +29 -3
- package/src/cli/commands/add-flow.ts +2 -0
- package/src/cli/commands/add.ts +4 -0
- package/src/cli/commands/capability.ts +64 -1
- package/src/cli/commands/delivery.ts +89 -2
- package/src/cli/commands/generate.ts +2 -1
- package/src/cli/commands/init.ts +38 -3
- package/src/cli/commands/journey.ts +15 -2
- package/src/dashboard/snapshot-builder.ts +10 -1
- package/src/exporters/csv-exporter.ts +3 -1
- package/src/exporters/json-exporter.ts +3 -1
- package/src/exporters/mobile-app-id.ts +34 -0
- package/src/exporters/spec-parser.ts +6 -2
- package/src/generators/test-generator/adapters/adapter-interface.ts +3 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/scroll-action.hbs +7 -4
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/toggle-action.hbs +20 -3
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/checked-assertion.hbs +4 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/not-checked-assertion.hbs +4 -1
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +3 -0
- package/src/generators/test-generator/code-generator.ts +39 -3
- package/src/generators/test-generator/template-engine.ts +14 -1
- package/src/generators/test-generator/utils/selector-resolver.ts +51 -1
- package/src/harness/audit.ts +1 -1
- package/src/harness/catalog/universal-viewpoints.yaml +15 -0
- package/src/harness/journey.ts +110 -9
- package/src/harness/sensors.ts +25 -8
- package/src/orchestrator/flow-manager.ts +165 -29
- package/src/orchestrator/mobile-runtime-scaffolder.ts +277 -0
- package/src/orchestrator/project-initializer.ts +127 -64
- package/src/orchestrator/screen-manager.ts +237 -22
- package/src/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +12 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +32 -4
- package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +51 -10
- package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +23 -2
- package/src/orchestrator/templates/ai-instructions/claude-cmd-delivery.md +6 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-locale.md +25 -4
- package/src/orchestrator/templates/ai-instructions/claude-cmd-review.md +8 -1
- package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +38 -4
- package/src/orchestrator/templates/ai-instructions/claude-skill-delivery.md +2 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-locale.md +106 -9
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +33 -3
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +50 -9
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +11 -2
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +6 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-locale.md +25 -4
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +8 -1
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +38 -4
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +2 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-locale.md +106 -9
package/src/harness/audit.ts
CHANGED
|
@@ -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:
|
package/src/harness/journey.ts
CHANGED
|
@@ -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
|
-
//
|
|
120
|
-
|
|
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
|
|
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
|
+
}
|
package/src/harness/sensors.ts
CHANGED
|
@@ -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
|
|
64
|
-
return haystacks.some((h) =>
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
}
|