@sun-asterisk/sungen 3.2.16-beta.7 → 3.2.16-beta.9
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/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +17 -3
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/gates.js +6 -1
- package/dist/exporters/matrix/gates.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +3 -0
- package/dist/exporters/matrix/types.d.ts.map +1 -1
- package/dist/exporters/matrix/types.js.map +1 -1
- package/dist/exporters/matrix/wording.d.ts +10 -0
- package/dist/exporters/matrix/wording.d.ts.map +1 -1
- package/dist/exporters/matrix/wording.js +52 -0
- package/dist/exporters/matrix/wording.js.map +1 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +11 -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/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 +9 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +5 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +1 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +22 -0
- package/package.json +4 -4
- package/src/exporters/matrix/build.ts +24 -4
- package/src/exporters/matrix/gates.ts +6 -1
- package/src/exporters/matrix/types.ts +3 -0
- package/src/exporters/matrix/wording.ts +47 -0
- package/src/harness/audit.ts +11 -2
- package/src/harness/blindspot.ts +2 -1
- package/src/harness/capability-plan.ts +3 -2
- 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 +9 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +5 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +1 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +22 -0
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) {
|
|
@@ -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}}
|
|
@@ -89,6 +89,11 @@ target × category grid with explicit `—` gaps, dispositions, manifest. CSV mi
|
|
|
89
89
|
flat with a `Level` column (`item`/`variant`) + a requirement-coverage appendix.
|
|
90
90
|
`delivery_item_count` ≠ progress — variants are the execution metric.
|
|
91
91
|
|
|
92
|
+
**Runtime-captured values**: a scenario may produce its own values — `User remember [X] text as
|
|
93
|
+
{{v}}` (compared later) or a capability binding (`@query:`/`@api:`/`@dataFactory:`). These never
|
|
94
|
+
exist in test-data, so Gate D does not ask for them: the capture step renders as an instruction
|
|
95
|
+
(`Note the X text for later (as "v")`) and later references read `the captured v`.
|
|
96
|
+
|
|
92
97
|
**Data fidelity**: invisible test data is made visible, never normalized — `''` → `(empty)`,
|
|
93
98
|
whitespace-only → `(5 spaces)`, padded → `" value "` quoted verbatim. A trim/collapse here would
|
|
94
99
|
silently break the whitespace tests it describes.
|
|
@@ -288,6 +288,7 @@ Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `colu
|
|
|
288
288
|
|
|
289
289
|
| Tag | Effect |
|
|
290
290
|
|---|---|
|
|
291
|
+
| `@spec:<id>` | **Traceability**: this scenario proves requirement `<id>` from `requirements/spec.md` (`@spec:FR-003`). Repeatable — a scenario may prove several. Read by `sungen audit` (requirement coverage) and by delivery (the Coverage sheet's requirement table). Without it the link is only inferred from wording and cannot survive an edit. |
|
|
291
292
|
| `@manual` | Skip in generation |
|
|
292
293
|
| `@auth:role` | Use auth storage state for role |
|
|
293
294
|
| `@no-auth` | Disable inherited auth |
|
|
@@ -4,6 +4,28 @@ description: 'Use when create-test needs to translate spec.md/Figma/UI into .fea
|
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
|
|
8
|
+
## Traceability — tag the requirement each scenario proves
|
|
9
|
+
|
|
10
|
+
Every requirement id in `requirements/spec.md` (`FR-`/`TR-`/`NFR-`) must be reachable from a
|
|
11
|
+
scenario, and the link must be **written down, not inferred**:
|
|
12
|
+
|
|
13
|
+
```gherkin
|
|
14
|
+
@high @spec:FR-003
|
|
15
|
+
Scenario: VP-VAL-002 An email in an invalid format shows the format error
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- Put `@spec:<id>` on the scenario that actually proves the requirement; repeat the tag when one
|
|
19
|
+
scenario proves several (`@spec:FR-003 @spec:FR-004`).
|
|
20
|
+
- One requirement may be proved by several scenarios — tag each of them.
|
|
21
|
+
- A requirement you deliberately do not cover here (performance budget, i18n suite, server-side
|
|
22
|
+
concern) needs no tag; record it later in the delivery map's `requirements:` section with a
|
|
23
|
+
status and reason.
|
|
24
|
+
- Why it matters: `sungen audit` also counts a requirement as covered when its keywords merely
|
|
25
|
+
resemble a scenario title, and reports `SPEC-TRACE-IMPLICIT` for those. Delivery's requirement
|
|
26
|
+
table follows **only** the explicit tag — an untagged requirement reads as an uncovered gap in
|
|
27
|
+
the customer-facing coverage sheet even when a scenario does prove it.
|
|
28
|
+
|
|
7
29
|
## ⚠️ Gotchas — read before generating
|
|
8
30
|
|
|
9
31
|
- **Field-level test-data follows the Data Factory standard — don't hand-invent values.** When the unit has input fields, the field values (valid / boundary / invalid + error codes) come from the **`sungen-data-factory`** catalog via a field-map + `sungen data gen` (create-test step 5.3), not free-form guessing. Weave the standardized boundary/invalid sets into `@cases` with `CHK-*` trace. This skill still owns scenario structure, viewpoints, and non-field oracles.
|