@sun-asterisk/sungen 3.2.15 → 3.2.16-beta.10
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/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +197 -13
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/dashboard/templates/index.html +1 -1
- package/dist/exporters/feature-parser.d.ts +16 -1
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +21 -2
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts +61 -0
- package/dist/exporters/matrix/build.d.ts.map +1 -0
- package/dist/exporters/matrix/build.js +497 -0
- package/dist/exporters/matrix/build.js.map +1 -0
- package/dist/exporters/matrix/export.d.ts +50 -0
- package/dist/exporters/matrix/export.d.ts.map +1 -0
- package/dist/exporters/matrix/export.js +152 -0
- package/dist/exporters/matrix/export.js.map +1 -0
- package/dist/exporters/matrix/fingerprint.d.ts +20 -0
- package/dist/exporters/matrix/fingerprint.d.ts.map +1 -0
- package/dist/exporters/matrix/fingerprint.js +83 -0
- package/dist/exporters/matrix/fingerprint.js.map +1 -0
- package/dist/exporters/matrix/gates.d.ts +21 -0
- package/dist/exporters/matrix/gates.d.ts.map +1 -0
- package/dist/exporters/matrix/gates.js +367 -0
- package/dist/exporters/matrix/gates.js.map +1 -0
- package/dist/exporters/matrix/map-loader.d.ts +22 -0
- package/dist/exporters/matrix/map-loader.d.ts.map +1 -0
- package/dist/exporters/matrix/map-loader.js +193 -0
- package/dist/exporters/matrix/map-loader.js.map +1 -0
- package/dist/exporters/matrix/render-csv.d.ts +9 -0
- package/dist/exporters/matrix/render-csv.d.ts.map +1 -0
- package/dist/exporters/matrix/render-csv.js +92 -0
- package/dist/exporters/matrix/render-csv.js.map +1 -0
- package/dist/exporters/matrix/render-xlsx.d.ts +41 -0
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -0
- package/dist/exporters/matrix/render-xlsx.js +430 -0
- package/dist/exporters/matrix/render-xlsx.js.map +1 -0
- package/dist/exporters/matrix/types.d.ts +199 -0
- package/dist/exporters/matrix/types.d.ts.map +1 -0
- package/dist/exporters/matrix/types.js +15 -0
- package/dist/exporters/matrix/types.js.map +1 -0
- package/dist/exporters/matrix/wording.d.ts +61 -0
- package/dist/exporters/matrix/wording.d.ts.map +1 -0
- package/dist/exporters/matrix/wording.js +221 -0
- package/dist/exporters/matrix/wording.js.map +1 -0
- package/dist/exporters/scenario-merger.d.ts.map +1 -1
- package/dist/exporters/scenario-merger.js +2 -1
- package/dist/exporters/scenario-merger.js.map +1 -1
- package/dist/exporters/spec-parser.d.ts.map +1 -1
- package/dist/exporters/spec-parser.js +2 -1
- package/dist/exporters/spec-parser.js.map +1 -1
- package/dist/exporters/xlsx-report-builder.d.ts +1 -1
- package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
- package/dist/exporters/xlsx-report-builder.js +3 -3
- package/dist/exporters/xlsx-report-builder.js.map +1 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +16 -2
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/blindspot.d.ts.map +1 -1
- package/dist/harness/blindspot.js +2 -1
- package/dist/harness/blindspot.js.map +1 -1
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +3 -2
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/capability.d.ts +25 -0
- package/dist/harness/capability.d.ts.map +1 -1
- package/dist/harness/capability.js +44 -2
- package/dist/harness/capability.js.map +1 -1
- package/dist/harness/feedback.d.ts.map +1 -1
- package/dist/harness/feedback.js +3 -2
- package/dist/harness/feedback.js.map +1 -1
- package/dist/harness/flow-check.d.ts.map +1 -1
- package/dist/harness/flow-check.js +2 -1
- package/dist/harness/flow-check.js.map +1 -1
- package/dist/harness/flow-plan.d.ts.map +1 -1
- package/dist/harness/flow-plan.js +3 -2
- package/dist/harness/flow-plan.js.map +1 -1
- package/dist/harness/intent.d.ts.map +1 -1
- package/dist/harness/intent.js +2 -1
- package/dist/harness/intent.js.map +1 -1
- package/dist/harness/journey.d.ts.map +1 -1
- package/dist/harness/journey.js +3 -2
- package/dist/harness/journey.js.map +1 -1
- package/dist/harness/ledger.d.ts.map +1 -1
- package/dist/harness/ledger.js +3 -2
- package/dist/harness/ledger.js.map +1 -1
- package/dist/harness/manifest.d.ts.map +1 -1
- package/dist/harness/manifest.js +4 -3
- package/dist/harness/manifest.js.map +1 -1
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +16 -3
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/quality-gates.d.ts.map +1 -1
- package/dist/harness/quality-gates.js +2 -1
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/harness/read-text.d.ts +22 -0
- package/dist/harness/read-text.d.ts.map +1 -0
- package/dist/harness/read-text.js +64 -0
- package/dist/harness/read-text.js.map +1 -0
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +3 -2
- package/dist/harness/script-check.js.map +1 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +2 -10
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/spec-coverage.d.ts +5 -0
- package/dist/harness/spec-coverage.d.ts.map +1 -1
- package/dist/harness/spec-coverage.js +17 -7
- package/dist/harness/spec-coverage.js.map +1 -1
- package/dist/harness/trace.d.ts.map +1 -1
- package/dist/harness/trace.js +4 -3
- package/dist/harness/trace.js.map +1 -1
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +2 -1
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +10 -1
- package/dist/orchestrator/templates/ai-src/commands/delivery.md +203 -120
- package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +102 -6
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +1 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +23 -0
- package/package.json +4 -4
- package/src/cli/commands/delivery.ts +227 -15
- package/src/dashboard/templates/index.html +1 -1
- package/src/exporters/feature-parser.ts +21 -2
- package/src/exporters/matrix/build.ts +532 -0
- package/src/exporters/matrix/export.ts +149 -0
- package/src/exporters/matrix/fingerprint.ts +49 -0
- package/src/exporters/matrix/gates.ts +400 -0
- package/src/exporters/matrix/map-loader.ts +163 -0
- package/src/exporters/matrix/render-csv.ts +94 -0
- package/src/exporters/matrix/render-xlsx.ts +420 -0
- package/src/exporters/matrix/types.ts +226 -0
- package/src/exporters/matrix/wording.ts +221 -0
- package/src/exporters/scenario-merger.ts +2 -1
- package/src/exporters/spec-parser.ts +2 -1
- package/src/exporters/xlsx-report-builder.ts +3 -2
- package/src/harness/audit.ts +17 -3
- package/src/harness/blindspot.ts +2 -1
- package/src/harness/capability-plan.ts +3 -2
- package/src/harness/capability.ts +53 -3
- package/src/harness/feedback.ts +3 -2
- package/src/harness/flow-check.ts +2 -1
- package/src/harness/flow-plan.ts +3 -2
- package/src/harness/intent.ts +2 -1
- package/src/harness/journey.ts +3 -2
- package/src/harness/ledger.ts +3 -2
- package/src/harness/manifest.ts +4 -3
- package/src/harness/parse.ts +17 -3
- package/src/harness/quality-gates.ts +2 -1
- package/src/harness/read-text.ts +28 -0
- package/src/harness/script-check.ts +3 -2
- package/src/harness/sensors.ts +2 -1
- package/src/harness/spec-coverage.ts +22 -7
- package/src/harness/trace.ts +4 -3
- package/src/harness/viewpoint-ledger.ts +2 -1
- package/src/orchestrator/templates/ai-src/commands/create-test.md +10 -1
- package/src/orchestrator/templates/ai-src/commands/delivery.md +203 -120
- package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +102 -6
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +1 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +23 -0
|
@@ -15,9 +15,55 @@ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
|
15
15
|
export interface CapabilityProfile {
|
|
16
16
|
platform?: string; // web | mobile | … (the runtime adapter)
|
|
17
17
|
enabled: string[]; // driver ids enabled for this project
|
|
18
|
+
/**
|
|
19
|
+
* Which VERIFICATION layers this project's test scope uses — the recorded "test type"
|
|
20
|
+
* (`ui` = E2E-only; add `api` / `db` when the project also verifies via API/DB). Explicit here wins;
|
|
21
|
+
* when absent it is DERIVED from `enabled` (ui always; api if the api driver is on; db if db is on).
|
|
22
|
+
* Generation and the audit use it so an E2E-only project does not get @api/@query verification it
|
|
23
|
+
* never asked for.
|
|
24
|
+
*/
|
|
25
|
+
verification?: string[];
|
|
18
26
|
source: 'capabilities.yaml' | 'absent';
|
|
19
27
|
}
|
|
20
28
|
|
|
29
|
+
export type VerifyLayer = 'ui' | 'api' | 'db';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Effective verification scope = the recorded "test type". Explicit `verification` wins; otherwise
|
|
33
|
+
* derived from the enabled drivers. `ui` is always in scope (every project observes the UI/E2E result).
|
|
34
|
+
*/
|
|
35
|
+
export function verificationScope(profile: CapabilityProfile): Set<VerifyLayer> {
|
|
36
|
+
if (profile.verification?.length) {
|
|
37
|
+
const s = new Set<VerifyLayer>(['ui']);
|
|
38
|
+
for (const v of profile.verification) if (v === 'api' || v === 'db' || v === 'ui') s.add(v);
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
const s = new Set<VerifyLayer>(['ui']);
|
|
42
|
+
if (profile.enabled.includes('api')) s.add('api');
|
|
43
|
+
if (profile.enabled.includes('db')) s.add('db');
|
|
44
|
+
return s;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface VerifyScopeFinding { layer: 'api' | 'db'; message: string; }
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Flag API/DB verification that is OUT OF the project's recorded scope: a unit whose scenarios use
|
|
51
|
+
* `@api`/`@requires:api` (or `@query`/`@requires:db`) while the project is not API/DB-testing AND the
|
|
52
|
+
* viewpoint never asked for that layer. Deterministic; advisory only (a nudge, never a hard gate).
|
|
53
|
+
*/
|
|
54
|
+
export function verificationScopeFindings(featureText: string, viewpointText: string, profile: CapabilityProfile): VerifyScopeFinding[] {
|
|
55
|
+
const scope = verificationScope(profile);
|
|
56
|
+
const vp = (viewpointText || '').toLowerCase();
|
|
57
|
+
const out: VerifyScopeFinding[] = [];
|
|
58
|
+
if (/@api\b|@requires:\s*api\b/.test(featureText) && !scope.has('api') && !/\bapi\b/.test(vp)) {
|
|
59
|
+
out.push({ layer: 'api', message: 'uses @api / @requires:api verification, but this project is not API-testing (capabilities `enabled` has no `api`) and the viewpoint does not request API verification. Keep the oracle UI-observable, OR record API in scope (capabilities `verification: [ui, api]` or `sungen capability add api`). Reserve API/DB verify for the ≤20% of effects with no UI observation.' });
|
|
60
|
+
}
|
|
61
|
+
if (/@query\b|@requires:\s*db\b/.test(featureText) && !scope.has('db') && !/\b(database|db)\b/.test(vp)) {
|
|
62
|
+
out.push({ layer: 'db', message: 'uses @query / @requires:db verification, but this project is not DB-testing (capabilities `enabled` has no `db`) and the viewpoint does not request DB verification. Keep the oracle UI-observable, OR record DB in scope (capabilities `verification: [ui, db]` or `sungen capability add db`). Reserve API/DB verify for the ≤20% of effects with no UI observation.' });
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
21
67
|
export interface DriverMeta {
|
|
22
68
|
id: string;
|
|
23
69
|
kind: 'platform' | 'capability';
|
|
@@ -41,6 +87,7 @@ export function readCapabilities(cwd: string): CapabilityProfile {
|
|
|
41
87
|
return {
|
|
42
88
|
platform: typeof y.platform === 'string' ? y.platform : undefined,
|
|
43
89
|
enabled: Array.isArray(y.enabled) ? y.enabled.map(String) : [],
|
|
90
|
+
verification: Array.isArray(y.verification) ? y.verification.map(String) : undefined,
|
|
44
91
|
source: 'capabilities.yaml',
|
|
45
92
|
};
|
|
46
93
|
} catch {
|
|
@@ -48,14 +95,17 @@ export function readCapabilities(cwd: string): CapabilityProfile {
|
|
|
48
95
|
}
|
|
49
96
|
}
|
|
50
97
|
|
|
51
|
-
export function writeCapabilities(cwd: string, profile: { platform?: string; enabled: string[] }): string {
|
|
98
|
+
export function writeCapabilities(cwd: string, profile: { platform?: string; enabled: string[]; verification?: string[] }): string {
|
|
52
99
|
const p = capabilitiesPath(cwd);
|
|
53
100
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
54
101
|
const body =
|
|
55
102
|
'# Sungen capability profile — which runtime/drivers this project uses.\n' +
|
|
56
103
|
'# platform: the runtime adapter (web → Playwright, mobile → Appium). No default.\n' +
|
|
57
|
-
'# enabled: drivers turned on (added via `sungen capability add <driver>`).\n
|
|
58
|
-
|
|
104
|
+
'# enabled: drivers turned on (added via `sungen capability add <driver>`).\n' +
|
|
105
|
+
'# verification: the TEST TYPE — which layers this project verifies (`ui` = E2E only; add `api`/`db`\n' +
|
|
106
|
+
'# only if the project actually verifies via API/Database). Absent → derived from `enabled`.\n' +
|
|
107
|
+
'# This keeps an E2E-only project from getting @api/@query verification it never asked for.\n\n' +
|
|
108
|
+
stringifyYaml({ platform: profile.platform, enabled: profile.enabled, ...(profile.verification ? { verification: profile.verification } : {}) });
|
|
59
109
|
fs.writeFileSync(p, body, 'utf-8');
|
|
60
110
|
return p;
|
|
61
111
|
}
|
package/src/harness/feedback.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import * as fs from 'fs';
|
|
14
14
|
import * as path from 'path';
|
|
15
|
+
import { readTextFile } from './read-text';
|
|
15
16
|
|
|
16
17
|
export type FeedbackType = 'test-design' | 'product' | 'other';
|
|
17
18
|
export type FeedbackDecision = 'accept' | 'reject' | 'edit' | 'add' | 'none';
|
|
@@ -36,7 +37,7 @@ function currentAuditScore(screen?: string): number | undefined {
|
|
|
36
37
|
if (!screen) return undefined;
|
|
37
38
|
const p = path.join(process.cwd(), '.sungen', 'reports', `${screen}-audit.json`);
|
|
38
39
|
if (!fs.existsSync(p)) return undefined;
|
|
39
|
-
try { return JSON.parse(
|
|
40
|
+
try { return JSON.parse(readTextFile(p))?.score?.overall; } catch { return undefined; }
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export function recordFeedback(entry: Omit<FeedbackEntry, 'ts' | 'source' | 'auditScore'> & { ts?: string; source?: string }): string {
|
|
@@ -60,7 +61,7 @@ export function recordFeedback(entry: Omit<FeedbackEntry, 'ts' | 'source' | 'aud
|
|
|
60
61
|
export function readFeedback(): FeedbackEntry[] {
|
|
61
62
|
const p = feedbackPath();
|
|
62
63
|
if (!fs.existsSync(p)) return [];
|
|
63
|
-
return
|
|
64
|
+
return readTextFile(p).split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export interface FeedbackSummary {
|
|
@@ -13,6 +13,7 @@ import * as fs from 'fs';
|
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import { loadScenarios } from './parse';
|
|
15
15
|
import { parseScenarios } from './capability-plan';
|
|
16
|
+
import { readTextFile } from './read-text';
|
|
16
17
|
|
|
17
18
|
const KNOWN_TARGETS = ['cart', 'category', 'brand', 'detail', 'checkout', 'wishlist', 'order', 'payment', 'login'];
|
|
18
19
|
|
|
@@ -33,7 +34,7 @@ function featurePath(base: string, kind: 'screens' | 'flows', name: string): str
|
|
|
33
34
|
}
|
|
34
35
|
function featureTags(fp: string): string[] {
|
|
35
36
|
if (!fs.existsSync(fp)) return [];
|
|
36
|
-
for (const line of
|
|
37
|
+
for (const line of readTextFile(fp).split('\n')) {
|
|
37
38
|
const t = line.trim();
|
|
38
39
|
if (t.startsWith('Feature:')) break;
|
|
39
40
|
if (t.startsWith('@')) return t.split(/\s+/).filter((x) => x.startsWith('@'));
|
package/src/harness/flow-plan.ts
CHANGED
|
@@ -12,6 +12,7 @@ import * as path from 'path';
|
|
|
12
12
|
import { parse as parseYaml } from 'yaml';
|
|
13
13
|
import { MANUAL_REASONS, inferReasonCode } from './capability-plan';
|
|
14
14
|
import { buildFlowCheck, Contract } from './flow-check';
|
|
15
|
+
import { readTextFile } from './read-text';
|
|
15
16
|
|
|
16
17
|
const REF_RE = /\[([A-Za-z][\w ]*?):([\w &.-]+?)\]/g;
|
|
17
18
|
|
|
@@ -19,7 +20,7 @@ interface FlowScenario { name: string; manual: boolean; reason: string; tags: st
|
|
|
19
20
|
|
|
20
21
|
function parseFlowScenarios(featurePath: string): FlowScenario[] {
|
|
21
22
|
if (!fs.existsSync(featurePath)) return [];
|
|
22
|
-
const lines =
|
|
23
|
+
const lines = readTextFile(featurePath).split('\n');
|
|
23
24
|
const out: FlowScenario[] = [];
|
|
24
25
|
const idx: number[] = [];
|
|
25
26
|
lines.forEach((l, i) => { if (/^\s*Scenario:/.test(l)) idx.push(i); });
|
|
@@ -50,7 +51,7 @@ function parseFlowScenarios(featurePath: string): FlowScenario[] {
|
|
|
50
51
|
function selectorKeys(selectorsPath: string): Set<string> {
|
|
51
52
|
if (!fs.existsSync(selectorsPath)) return new Set();
|
|
52
53
|
try {
|
|
53
|
-
const y = parseYaml(
|
|
54
|
+
const y = parseYaml(readTextFile(selectorsPath)) || {};
|
|
54
55
|
return new Set(Object.keys(y).map((k) => k.toLowerCase()));
|
|
55
56
|
} catch { return new Set(); }
|
|
56
57
|
}
|
package/src/harness/intent.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import * as fs from 'fs';
|
|
16
16
|
import * as path from 'path';
|
|
17
|
+
import { readTextFile } from './read-text';
|
|
17
18
|
|
|
18
19
|
export type IntentFocus = 'functional' | 'e-commerce' | 'security' | 'smoke';
|
|
19
20
|
|
|
@@ -50,7 +51,7 @@ export function readIntent(projectRoot: string): IntentProfile {
|
|
|
50
51
|
const ctx = path.join(projectRoot, 'qa', 'context.md');
|
|
51
52
|
if (!fs.existsSync(ctx)) return DEFAULT_INTENT;
|
|
52
53
|
let text: string;
|
|
53
|
-
try { text =
|
|
54
|
+
try { text = readTextFile(ctx).toLowerCase(); } catch { return DEFAULT_INTENT; }
|
|
54
55
|
|
|
55
56
|
const grab = (key: string): string | undefined => {
|
|
56
57
|
const m = text.match(new RegExp(`(?:^|\\n)\\s*${key}\\s*:\\s*([a-z0-9-]+)`));
|
package/src/harness/journey.ts
CHANGED
|
@@ -16,6 +16,7 @@ import * as fs from 'fs';
|
|
|
16
16
|
import * as path from 'path';
|
|
17
17
|
import * as crypto from 'crypto';
|
|
18
18
|
import { reportSlug } from './unit-paths';
|
|
19
|
+
import { readTextFile } from './read-text';
|
|
19
20
|
|
|
20
21
|
export type ObStatus = 'satisfied' | 'needs-work' | 'pending' | 'waived';
|
|
21
22
|
|
|
@@ -44,13 +45,13 @@ export interface JourneyReport {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
function readJSON(p: string): any {
|
|
47
|
-
try { return fs.existsSync(p) ? JSON.parse(
|
|
48
|
+
try { return fs.existsSync(p) ? JSON.parse(readTextFile(p)) : null; } catch { return null; }
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
function readLedgerPhases(p: string): string[] {
|
|
51
52
|
if (!fs.existsSync(p)) return [];
|
|
52
53
|
const out: string[] = [];
|
|
53
|
-
for (const line of
|
|
54
|
+
for (const line of readTextFile(p).split('\n')) {
|
|
54
55
|
if (!line.trim()) continue;
|
|
55
56
|
try { const d = JSON.parse(line); if (d.step) out.push(String(d.step)); } catch { /* skip */ }
|
|
56
57
|
}
|
package/src/harness/ledger.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import { reportSlug } from './unit-paths';
|
|
17
|
+
import { readTextFile } from './read-text';
|
|
17
18
|
|
|
18
19
|
export interface LedgerEvent {
|
|
19
20
|
ts: string;
|
|
@@ -75,7 +76,7 @@ export function recordEvent(screen: string, ev: Omit<LedgerEvent, 'ts'> & { ts?:
|
|
|
75
76
|
export function readEvents(screen: string): LedgerEvent[] {
|
|
76
77
|
const p = ledgerPath(screen);
|
|
77
78
|
if (!fs.existsSync(p)) return [];
|
|
78
|
-
return
|
|
79
|
+
return readTextFile(p).split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
export interface LedgerReport {
|
|
@@ -121,7 +122,7 @@ export function buildReport(screen: string, opts: { allRuns?: boolean } = {}): L
|
|
|
121
122
|
const auditPath = path.join(process.cwd(), '.sungen', 'reports', `${reportSlug(screen)}-audit.json`);
|
|
122
123
|
if (fs.existsSync(auditPath)) {
|
|
123
124
|
try {
|
|
124
|
-
const a = JSON.parse(
|
|
125
|
+
const a = JSON.parse(readTextFile(auditPath));
|
|
125
126
|
coveredCritical = a?.gate?.themesCovered ?? null;
|
|
126
127
|
scenarioCount = a?.scenarioCount ?? null;
|
|
127
128
|
} catch { /* ignore */ }
|
package/src/harness/manifest.ts
CHANGED
|
@@ -12,6 +12,7 @@ import * as fs from 'fs';
|
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import { createHash } from 'crypto';
|
|
14
14
|
import { featureBasename, reportSlug } from './unit-paths';
|
|
15
|
+
import { readTextFile } from './read-text';
|
|
15
16
|
|
|
16
17
|
export interface SpecSection { name: string; hash: string }
|
|
17
18
|
export interface ManifestEntry { scenario: string; vpCode?: string; section: string; specHash: string }
|
|
@@ -59,7 +60,7 @@ function matchSection(featureSection: string, specNames: string[]): string | nul
|
|
|
59
60
|
/** Parse spec.md into hashable sections (## and ### headings; "Section:" prefix stripped). */
|
|
60
61
|
export function parseSpecSections(specPath: string): SpecSection[] {
|
|
61
62
|
if (!fs.existsSync(specPath)) return [];
|
|
62
|
-
const lines =
|
|
63
|
+
const lines = readTextFile(specPath).split('\n');
|
|
63
64
|
const sections: { name: string; body: string[] }[] = [];
|
|
64
65
|
let cur: { name: string; body: string[] } | null = null;
|
|
65
66
|
for (const line of lines) {
|
|
@@ -79,7 +80,7 @@ export function parseSpecSections(specPath: string): SpecSection[] {
|
|
|
79
80
|
/** Parse feature into scenario → section mapping using `# --- Section: X ---` comments. */
|
|
80
81
|
function parseFeatureSections(featurePath: string): { scenario: string; vpCode?: string; section: string }[] {
|
|
81
82
|
if (!fs.existsSync(featurePath)) return [];
|
|
82
|
-
const lines =
|
|
83
|
+
const lines = readTextFile(featurePath).split('\n');
|
|
83
84
|
const out: { scenario: string; vpCode?: string; section: string }[] = [];
|
|
84
85
|
let section = '(unsectioned)';
|
|
85
86
|
for (const raw of lines) {
|
|
@@ -164,7 +165,7 @@ export function manifestPath(screenName: string): string {
|
|
|
164
165
|
}
|
|
165
166
|
export function loadManifest(screenName: string): Manifest | null {
|
|
166
167
|
const p = manifestPath(screenName);
|
|
167
|
-
return fs.existsSync(p) ? JSON.parse(
|
|
168
|
+
return fs.existsSync(p) ? JSON.parse(readTextFile(p)) : null;
|
|
168
169
|
}
|
|
169
170
|
export function saveManifest(m: Manifest): string {
|
|
170
171
|
const p = manifestPath(m.screen);
|
package/src/harness/parse.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import { GherkinParser, ParsedScenario, ParsedStep } from '../generators/gherkin-parser';
|
|
10
|
+
import { readTextFile } from './read-text';
|
|
10
11
|
|
|
11
12
|
export type Priority = 'high' | 'normal' | 'low' | 'unknown';
|
|
12
13
|
|
|
@@ -51,9 +52,22 @@ export function idPrefix(id: string): string {
|
|
|
51
52
|
|
|
52
53
|
// ---------- test-viewpoint.md ----------
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* A viewpoint OVERVIEW id may be a bare category (`VP-LOGIC`, `VP-SEC`) — that is
|
|
57
|
+
* the scheme sungen itself prescribes, with the sequence number living on the
|
|
58
|
+
* scenario (`VP-LOGIC-013`). `isIdLike` demands a digit to avoid picking prose out
|
|
59
|
+
* of tables, which silently rejected every category id: a project declaring
|
|
60
|
+
* VP-LOGIC/VP-VAL/VP-SEC/VP-UI/VP-NAV/VP-PERF/VP-I18N parsed as ONE viewpoint
|
|
61
|
+
* (only VP-I18N, for the "18"), traceability collapsed to ~0%, and the audit told
|
|
62
|
+
* QA to re-tag scenarios that were already correct.
|
|
63
|
+
*/
|
|
64
|
+
function isViewpointId(s: string): boolean {
|
|
65
|
+
return isIdLike(s) || /^VP-[A-Z][A-Z0-9]*$/i.test(s);
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
55
69
|
if (!fs.existsSync(filePath)) return [];
|
|
56
|
-
const text =
|
|
70
|
+
const text = readTextFile(filePath);
|
|
57
71
|
const lines = text.split('\n');
|
|
58
72
|
|
|
59
73
|
const entries = new Map<string, ViewpointEntry>();
|
|
@@ -68,7 +82,7 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
|
68
82
|
const cells = line.split('|').map((c) => c.trim()).filter((_, i, a) => i > 0 && i < a.length - 1);
|
|
69
83
|
if (cells.length >= 3) {
|
|
70
84
|
const id = cells[0];
|
|
71
|
-
if (
|
|
85
|
+
if (isViewpointId(id) && !/^-+$/.test(cells[1])) {
|
|
72
86
|
const pr = /high/i.test(cells[1]) ? 'High' : /medium/i.test(cells[1]) ? 'Medium' : /low/i.test(cells[1]) ? 'Low' : 'Unknown';
|
|
73
87
|
entries.set(id.toUpperCase(), { id: id.toUpperCase(), priority: pr as any, reason: cells[2] });
|
|
74
88
|
}
|
|
@@ -85,7 +99,7 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
|
85
99
|
if (/^##\s/.test(line)) { group = undefined; }
|
|
86
100
|
if (group) {
|
|
87
101
|
const m = line.match(/^[-*+]\s+([A-Za-z][A-Za-z0-9.-]*)/);
|
|
88
|
-
if (m &&
|
|
102
|
+
if (m && isViewpointId(m[1])) {
|
|
89
103
|
const id = m[1].toUpperCase();
|
|
90
104
|
const existing = entries.get(id);
|
|
91
105
|
if (existing) existing.group = group;
|
|
@@ -7,6 +7,7 @@ import * as fs from 'fs';
|
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import { ScenarioInfo, loadScenarios, idPrefix } from './parse';
|
|
9
9
|
import { parseManualComments } from '../exporters/scenario-merger';
|
|
10
|
+
import { readTextFile } from './read-text';
|
|
10
11
|
|
|
11
12
|
// ---------- #2 Downstream-scope ----------
|
|
12
13
|
|
|
@@ -202,5 +203,5 @@ export function crossArtifactOwnership(screenDir: string, scenarios: ScenarioInf
|
|
|
202
203
|
|
|
203
204
|
// convenience reader
|
|
204
205
|
export function readText(p: string): string {
|
|
205
|
-
return fs.existsSync(p) ?
|
|
206
|
+
return fs.existsSync(p) ? readTextFile(p) : '';
|
|
206
207
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Read a user-authored text file with line endings NORMALISED to `\n`.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: the harness parses spec.md / test-viewpoint.md / .feature with
|
|
7
|
+
* line-anchored regexes such as `/^\s*Scenario:\s*(.+)$/`. In JavaScript `.` does
|
|
8
|
+
* NOT match `\r`, so on a CRLF file `(.+)$` can never reach the end of the line
|
|
9
|
+
* and the match silently fails. The parser then reports "nothing found" rather
|
|
10
|
+
* than an error, which is far worse than crashing:
|
|
11
|
+
*
|
|
12
|
+
* - `spec-coverage` found 0 requirements → the FR sensor scored a vacuous
|
|
13
|
+
* 100% and never emitted SPEC-UNCOVERED / SPEC-TRACE-IMPLICIT.
|
|
14
|
+
* - `parseViewpointOverview` found 1 of N viewpoints → traceability collapsed
|
|
15
|
+
* to ~0% and the audit told QA to "re-tag" scenarios that were already
|
|
16
|
+
* correct.
|
|
17
|
+
*
|
|
18
|
+
* A Windows-authored spec is completely normal, so every harness parser that
|
|
19
|
+
* reads a user file must go through here.
|
|
20
|
+
*/
|
|
21
|
+
export function readTextFile(filePath: string): string {
|
|
22
|
+
return fs.readFileSync(filePath, 'utf-8').replace(/\r\n?/g, '\n');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** `readTextFile` split into lines — the common case for line-anchored parsers. */
|
|
26
|
+
export function readTextLines(filePath: string): string[] {
|
|
27
|
+
return readTextFile(filePath).split('\n');
|
|
28
|
+
}
|
|
@@ -17,6 +17,7 @@ import * as path from 'path';
|
|
|
17
17
|
import * as os from 'os';
|
|
18
18
|
import { loadScenarios, ScenarioInfo } from './parse';
|
|
19
19
|
import { featureBasename } from './unit-paths';
|
|
20
|
+
import { readTextFile } from './read-text';
|
|
20
21
|
|
|
21
22
|
export interface ScriptCheckResult {
|
|
22
23
|
screen: string;
|
|
@@ -153,7 +154,7 @@ export async function runScriptCheck(screenDir: string, screenName: string, kind
|
|
|
153
154
|
let specTitles: string[] = [];
|
|
154
155
|
let specSrc = '';
|
|
155
156
|
if (committedSpec) {
|
|
156
|
-
specSrc =
|
|
157
|
+
specSrc = readTextFile(committedSpec);
|
|
157
158
|
specTitles = extractTestTitles(specSrc);
|
|
158
159
|
} else {
|
|
159
160
|
findings.push('No generated spec found under specs/generated/ — run `sungen generate` / `/sungen:run-test` first.');
|
|
@@ -195,7 +196,7 @@ export async function runScriptCheck(screenDir: string, screenName: string, kind
|
|
|
195
196
|
const fresh = findSpec(tmp, screenName, kind);
|
|
196
197
|
if (fresh) {
|
|
197
198
|
const a = normalize(specSrc);
|
|
198
|
-
const b = normalize(
|
|
199
|
+
const b = normalize(readTextFile(fresh));
|
|
199
200
|
if (a !== b) {
|
|
200
201
|
drift = 'drift';
|
|
201
202
|
// collect a few differing lines
|
package/src/harness/sensors.ts
CHANGED
|
@@ -10,6 +10,7 @@ import * as fs from 'fs';
|
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { parse as parseYaml } from 'yaml';
|
|
12
12
|
import { ScenarioInfo, ViewpointEntry, idPrefix } from './parse';
|
|
13
|
+
import { readTextFile } from './read-text';
|
|
13
14
|
|
|
14
15
|
// Business-critical category keywords (matched by CONTAINMENT against the VP category, so a
|
|
15
16
|
// compound category like LIST-DISPLAY / ADD-TO-CART / PRODUCT-DISCOVERY classifies correctly).
|
|
@@ -56,7 +57,7 @@ export interface Catalog {
|
|
|
56
57
|
|
|
57
58
|
export function loadCatalog(): Catalog {
|
|
58
59
|
const p = path.join(__dirname, 'catalog', 'universal-viewpoints.yaml');
|
|
59
|
-
return parseYaml(
|
|
60
|
+
return parseYaml(readTextFile(p)) as Catalog;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
// Word-aware keyword match: `\b<kw>(s|es|ed|ing)?\b`, so a keyword matches whole words (plus
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import { ScenarioInfo } from './parse';
|
|
13
|
+
import { readTextFile } from './read-text';
|
|
13
14
|
|
|
14
15
|
export type Modality = 'MUST' | 'SHOULD' | 'MAY';
|
|
15
16
|
|
|
@@ -20,6 +21,11 @@ export interface SpecCoverageResult {
|
|
|
20
21
|
frTotal: number;
|
|
21
22
|
frCovered: number;
|
|
22
23
|
uncoveredMust: { id: string; text: string }[];
|
|
24
|
+
/** Requirements counted as covered ONLY by keyword inference — no scenario
|
|
25
|
+
* cites the id. The audit accepts that (it asks "did you think about this
|
|
26
|
+
* requirement?"), but the link is not machine-checkable: delivery's
|
|
27
|
+
* requirement table and any later refactor cannot follow it. */
|
|
28
|
+
inferredOnly: string[];
|
|
23
29
|
triggerGaps: TriggerGap[]; // per-constraint trigger matrix gaps
|
|
24
30
|
verdict: 'pass' | 'warn' | 'fail';
|
|
25
31
|
}
|
|
@@ -40,8 +46,12 @@ const ACTION_TRIGGER: { trigger: string; re: RegExp }[] = [
|
|
|
40
46
|
];
|
|
41
47
|
|
|
42
48
|
function modalityOf(text: string): Modality {
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
// Specs are written in the project's language — a Vietnamese spec states
|
|
50
|
+
// obligation with PHẢI / KHÔNG ĐƯỢC and recommendation with NÊN. Reading only
|
|
51
|
+
// English keywords silently demoted every requirement to MAY, so the
|
|
52
|
+
// "uncovered MUST" finding could never fire on those projects.
|
|
53
|
+
if (/\bMUST\b/i.test(text) || /\bPHẢI\b/i.test(text) || /KHÔNG ĐƯỢC/i.test(text)) return 'MUST';
|
|
54
|
+
if (/\bSHOULD\b/i.test(text) || /\bNÊN\b/i.test(text)) return 'SHOULD';
|
|
45
55
|
return 'MAY';
|
|
46
56
|
}
|
|
47
57
|
|
|
@@ -54,7 +64,7 @@ function actionTriggersIn(text: string): string[] {
|
|
|
54
64
|
|
|
55
65
|
export function parseSpecClauses(specPath: string): { frs: FrClause[]; valRows: ValRow[] } {
|
|
56
66
|
if (!fs.existsSync(specPath)) return { frs: [], valRows: [] };
|
|
57
|
-
const lines =
|
|
67
|
+
const lines = readTextFile(specPath).split('\n');
|
|
58
68
|
|
|
59
69
|
const frs: FrClause[] = [];
|
|
60
70
|
for (const line of lines) {
|
|
@@ -97,12 +107,13 @@ function scenarioBlocks(featureText: string): string[] {
|
|
|
97
107
|
export function specCoverage(specPath: string, scenarios: ScenarioInfo[], featureText: string): SpecCoverageResult {
|
|
98
108
|
const { frs, valRows } = parseSpecClauses(specPath);
|
|
99
109
|
if (!fs.existsSync(specPath) || (frs.length === 0 && valRows.length === 0)) {
|
|
100
|
-
return { hasSpec: fs.existsSync(specPath), frTotal: 0, frCovered: 0, uncoveredMust: [], triggerGaps: [], verdict: 'pass' };
|
|
110
|
+
return { hasSpec: fs.existsSync(specPath), frTotal: 0, frCovered: 0, uncoveredMust: [], inferredOnly: [], triggerGaps: [], verdict: 'pass' };
|
|
101
111
|
}
|
|
102
112
|
const featLower = featureText.toLowerCase();
|
|
103
113
|
|
|
104
114
|
// FR coverage: explicit @spec:FR / literal FR-id citation, else keyword fallback.
|
|
105
115
|
const uncoveredMust: { id: string; text: string }[] = [];
|
|
116
|
+
const inferredOnly: string[] = [];
|
|
106
117
|
let frCovered = 0;
|
|
107
118
|
for (const fr of frs) {
|
|
108
119
|
const idLower = fr.id.toLowerCase();
|
|
@@ -110,8 +121,12 @@ export function specCoverage(specPath: string, scenarios: ScenarioInfo[], featur
|
|
|
110
121
|
const words = [...new Set((fr.text.toLowerCase().match(/[a-z][a-z-]{4,}/g) || []))]
|
|
111
122
|
.filter((w) => !/must|should|system|screen|users?|value|input|field/.test(w));
|
|
112
123
|
const kwHit = words.length > 0 && scenarios.some((s) => words.filter((w) => s.haystack.includes(w)).length >= Math.min(2, words.length));
|
|
113
|
-
if (cited || kwHit)
|
|
114
|
-
|
|
124
|
+
if (cited || kwHit) {
|
|
125
|
+
frCovered++;
|
|
126
|
+
if (!cited) inferredOnly.push(fr.id); // covered, but nothing links it explicitly
|
|
127
|
+
} else if (fr.modality === 'MUST') {
|
|
128
|
+
uncoveredMust.push({ id: fr.id, text: fr.text.slice(0, 90) });
|
|
129
|
+
}
|
|
115
130
|
}
|
|
116
131
|
|
|
117
132
|
// Per-constraint trigger coverage — the matrix-collapse catch.
|
|
@@ -135,5 +150,5 @@ export function specCoverage(specPath: string, scenarios: ScenarioInfo[], featur
|
|
|
135
150
|
const verdict: SpecCoverageResult['verdict'] =
|
|
136
151
|
uncoveredMust.length > 0 || triggerGaps.length > 0 ? 'fail' : 'pass';
|
|
137
152
|
|
|
138
|
-
return { hasSpec: true, frTotal: frs.length, frCovered, uncoveredMust, triggerGaps, verdict };
|
|
153
|
+
return { hasSpec: true, frTotal: frs.length, frCovered, uncoveredMust, inferredOnly, triggerGaps, verdict };
|
|
139
154
|
}
|
package/src/harness/trace.ts
CHANGED
|
@@ -15,23 +15,24 @@ import * as fs from 'fs';
|
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import { reportSlug } from './unit-paths';
|
|
17
17
|
import { segmentRuns, latestRunEvents, LedgerEvent } from './ledger';
|
|
18
|
+
import { readTextFile } from './read-text';
|
|
18
19
|
|
|
19
20
|
interface ManualItem { scenario: string; reason: string }
|
|
20
21
|
|
|
21
22
|
function readJson(p: string): any | null {
|
|
22
|
-
try { return fs.existsSync(p) ? JSON.parse(
|
|
23
|
+
try { return fs.existsSync(p) ? JSON.parse(readTextFile(p)) : null; } catch { return null; }
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
function readLedger(screen: string): any[] {
|
|
26
27
|
const p = path.join(process.cwd(), '.sungen', 'ledger', `${reportSlug(screen)}.jsonl`);
|
|
27
28
|
if (!fs.existsSync(p)) return [];
|
|
28
|
-
return
|
|
29
|
+
return readTextFile(p).split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/** Parse @manual scenarios + the explanatory comment line above each. */
|
|
32
33
|
function parseManual(featurePath: string): ManualItem[] {
|
|
33
34
|
if (!fs.existsSync(featurePath)) return [];
|
|
34
|
-
const lines =
|
|
35
|
+
const lines = readTextFile(featurePath).split('\n');
|
|
35
36
|
const out: ManualItem[] = [];
|
|
36
37
|
for (let i = 0; i < lines.length; i++) {
|
|
37
38
|
const m = lines[i].match(/^\s*Scenario:\s*(.+)$/);
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import { ScenarioInfo } from './parse';
|
|
13
|
+
import { readTextFile } from './read-text';
|
|
13
14
|
|
|
14
15
|
export interface LedgerItem { id?: string; text: string; covered: boolean }
|
|
15
16
|
|
|
@@ -27,7 +28,7 @@ const GENERIC = new Set(['display', 'shown', 'value', 'field', 'input', 'page',
|
|
|
27
28
|
/** Extract atomic checklist items from a viewpoint file (format-tolerant). */
|
|
28
29
|
export function parseViewpointItems(viewpointPath: string): { id?: string; text: string }[] {
|
|
29
30
|
if (!fs.existsSync(viewpointPath)) return [];
|
|
30
|
-
const lines =
|
|
31
|
+
const lines = readTextFile(viewpointPath).split('\n');
|
|
31
32
|
const items: { id?: string; text: string }[] = [];
|
|
32
33
|
let inFence = false;
|
|
33
34
|
for (const raw of lines) {
|
|
@@ -22,7 +22,7 @@ If `spec_figma.md` exists OR the user provides a Figma URL for the PAT flow:
|
|
|
22
22
|
|
|
23
23
|
## Platform detection (do this FIRST)
|
|
24
24
|
|
|
25
|
-
Read `qa/capabilities.yaml` and check the `platform` field.
|
|
25
|
+
Read `qa/capabilities.yaml` and check the `platform` field — **and the verification scope** (`verification:` if set, else derived from `enabled`: `ui` always; `api`/`db` only if that driver is on). This is the project's recorded **test type**: an **E2E/UI-only** project (no `api`/`db`) must NOT get `@api`/`@query`/`@requires:api|db` verification unless the test-viewpoint explicitly asks for it — keep oracles UI-observable, and cap any in-scope API/DB-in-E2E verification at the **≤20% band** (see `sungen-tc-generation` → "Respect the project's verification scope"). `sungen audit` flags `VERIFICATION-OUT-OF-SCOPE`.
|
|
26
26
|
|
|
27
27
|
- If `platform: mobile` → the visual-capture step (Step 3, "Auto-detect visual source") uses the **`sungen-capture-mobile`** skill (Appium MCP live capture) **instead of** the web `sungen-capture` (figma/live). Scenarios use mobile gesture vocabulary from `sungen-mobile-gestures` (tap, swipe, long-press — not click/hover) and `accessibility-id`-first selectors; mobile-specific steps (permission dialogs, clipboard, geolocation) are valid Gherkin, not errors. The `Background:` is a **navigation recipe** (see `add-screen --reach`), not a URL path. Everything else — viewpoints, `sungen-tc-generation`, the harness/audit loop — is platform-agnostic and unchanged.
|
|
28
28
|
- If `platform: web` or field absent → follow the **web path** (all steps below, unchanged) — Figma/live capture via `sungen-capture` as documented in Step 3.
|
|
@@ -68,6 +68,15 @@ If the unit is **api-first** (`qa/api/<name>/` or `qa/api/flows/<name>/`), the d
|
|
|
68
68
|
If the unit is **api-first** (`qa/api/<name>/` or `qa/api/flows/<name>/`), the design loop differs — **no visual capture, no selectors**; the contract is the named-endpoint catalog. **Follow the `sungen-api-design` + `sungen-api-coverage-model` skills end-to-end** instead of the screen/flow steps: `sungen context --area <name>` (discover endpoints + `fields:`) → **enumerate the Tier-1 case list per endpoint from the coverage model** (contract + not-found/required-matrix + auth + idempotency, expanded mechanically from `fields:`) → generate `@api`/`@cases`/flow/`@concurrent`/`@query` scenarios with **strict assertions** (prove the effect, never status-only) → **`sungen audit --area <name>` gate + reviewer + repair loop to businessDepth ≥ 0.7** → record + trace. Then recommend `/sungen-run-test <name>`. The capture / viewpoint-group / selector steps do **not** apply.
|
|
69
69
|
{{/cap}}
|
|
70
70
|
|
|
71
|
+
## Requirement traceability (before you finish)
|
|
72
|
+
|
|
73
|
+
Cross-check `requirements/spec.md` against the scenarios you wrote: every `FR-`/`TR-`/`NFR-` id
|
|
74
|
+
must either carry a `@spec:<id>` tag on the scenario that proves it, or be a conscious
|
|
75
|
+
out-of-scope decision you state in the summary. `sungen audit` reports `SPEC-TRACE-IMPLICIT` for
|
|
76
|
+
requirements it could only match by keyword — treat that list as a to-do: add the tag to the
|
|
77
|
+
proving scenario, do not leave the link to inference. Delivery's requirement table follows the
|
|
78
|
+
tag only, so an untagged requirement is later reported as an uncovered gap.
|
|
79
|
+
|
|
71
80
|
## Steps
|
|
72
81
|
|
|
73
82
|
{{#cap parallel-subagents}}
|