@sun-asterisk/sungen 3.2.16-beta.11 → 3.2.16-beta.13
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/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +17 -0
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/index.js +5 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts +9 -0
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +20 -1
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/harness/audit.d.ts +4 -0
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +20 -3
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts +2 -0
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +14 -2
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/viewpoint-baseline.d.ts +49 -0
- package/dist/harness/viewpoint-baseline.d.ts.map +1 -0
- package/dist/harness/viewpoint-baseline.js +141 -0
- package/dist/harness/viewpoint-baseline.js.map +1 -0
- package/dist/orchestrator/assets-drift.d.ts +24 -0
- package/dist/orchestrator/assets-drift.d.ts.map +1 -0
- package/dist/orchestrator/assets-drift.js +80 -0
- package/dist/orchestrator/assets-drift.js.map +1 -0
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +24 -9
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +1 -0
- package/package.json +4 -4
- package/src/cli/commands/audit.ts +18 -1
- package/src/cli/index.ts +6 -0
- package/src/exporters/matrix/build.ts +20 -1
- package/src/harness/audit.ts +22 -4
- package/src/harness/capability-plan.ts +14 -2
- package/src/harness/viewpoint-baseline.ts +128 -0
- package/src/orchestrator/assets-drift.ts +64 -0
- package/src/orchestrator/templates/ai-src/commands/create-test.md +24 -9
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +1 -0
package/src/harness/audit.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import { loadScenarios, parseViewpointOverview, ScenarioInfo, ViewpointEntry } from './parse';
|
|
12
|
+
import { checkViewpointBaseline, ViewpointBaseline } from './viewpoint-baseline';
|
|
12
13
|
import { featureBasename } from './unit-paths';
|
|
13
14
|
import {
|
|
14
15
|
loadCatalog, viewpointGate, assertionDepth, dataThemesFor, depthThresholdFor, coverageBalance, duplicateClusters, traceability, claimProof, taxonomyLint,
|
|
@@ -46,6 +47,7 @@ export interface AuditReport {
|
|
|
46
47
|
flowDepth: FlowDepthResult; // H3 — stateful-flow regression depth (count / teardown / multi-source)
|
|
47
48
|
oracle: OracleStrengthResult; // H4 — facet-oracle strength (weak name-substring vs title/detail/API/DB)
|
|
48
49
|
ledger: LedgerResult; // atomic viewpoint-item coverage (per-bullet status)
|
|
50
|
+
viewpointBaseline: ViewpointBaseline; // is the yardstick still the accepted one? (#557)
|
|
49
51
|
calibration: { // #8 — multi-axis score so a high overall can't hide a weak axis
|
|
50
52
|
axes: Record<string, number>;
|
|
51
53
|
weakest: { axis: string; value: number };
|
|
@@ -73,7 +75,7 @@ export interface AuditReport {
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
/** The catalog-resolution id for a unit dir (relative to qa/): screen · flows/<flow> · api/<area> · api/flows/<flow>. */
|
|
76
|
-
function catalogIdFromScreenDir(screenDir: string): string {
|
|
78
|
+
export function catalogIdFromScreenDir(screenDir: string): string {
|
|
77
79
|
const parts = screenDir.split(path.sep);
|
|
78
80
|
const qa = parts.lastIndexOf('qa');
|
|
79
81
|
if (qa >= 0) {
|
|
@@ -154,6 +156,9 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
154
156
|
const capGate = scoringCap?.gateProvider as
|
|
155
157
|
((i: { scenarios: ScenarioInfo[]; viewpoints: ViewpointEntry[]; catalog: Catalog; focus: typeof intent.focus; cwd: string; screenName: string; threshold: number; businessCriticalMethods?: string[] }) => { gate: GateResult; depth: DepthResult }) | undefined;
|
|
156
158
|
const provided = capGate?.({ scenarios, viewpoints, catalog, focus: intent.focus, cwd: projectRootFromScreenDir(screenDir), screenName: catalogScreenName, threshold: depthThresholdFor(intent.focus), businessCriticalMethods: intent.businessCriticalMethods });
|
|
159
|
+
// Is the declaration this suite is measured against still the one a human accepted?
|
|
160
|
+
const viewpointBaseline = checkViewpointBaseline(
|
|
161
|
+
projectRootFromScreenDir(screenDir), catalogScreenName, viewpointPath);
|
|
157
162
|
const viewpointText = fs.existsSync(viewpointPath) ? readTextFile(viewpointPath) : '';
|
|
158
163
|
const gate = provided?.gate ?? viewpointGate(scenarios, viewpoints, catalog, platform === 'mobile', viewpointText);
|
|
159
164
|
const depth = provided?.depth ?? assertionDepth(scenarios, dataThemesFor(catalog, gate.pageType), intent.focus);
|
|
@@ -234,13 +239,19 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
234
239
|
// claimProof, specFR, atomicLedger — now carry weight. A suite where two
|
|
235
240
|
// thirds of the scenarios do not prove their own title used to score 7.9.
|
|
236
241
|
const specRatio = spec.frTotal ? spec.frCovered / spec.frTotal : 1;
|
|
242
|
+
// 3. Evidence has to be INDEPENDENT of what it judges. `atomicLedger` and
|
|
243
|
+
// `traceability` both measure the suite against test-viewpoint.md, so a run
|
|
244
|
+
// that rewrites that file scores them 100% by construction — which is exactly
|
|
245
|
+
// what a create-test run did, silently dropping the performance viewpoint on
|
|
246
|
+
// the way. While the declaration is unconfirmed, neither axis is evidence.
|
|
247
|
+
const viewpointMoved = viewpointBaseline.status === 'changed';
|
|
237
248
|
const axisDefs: Array<{ key: string; value: number; weight: number; applicable: boolean; critical: boolean }> = [
|
|
238
249
|
{ key: 'coverage', value: coverage, weight: 0.22, applicable: !!gate.pageType && gate.themesTotal > 0, critical: true },
|
|
239
250
|
{ key: 'specFR', value: specRatio, weight: 0.15, applicable: spec.hasSpec && spec.frTotal > 0, critical: true },
|
|
240
|
-
{ key: 'atomicLedger', value: ledger.ratio, weight: 0.13, applicable: ledger.hasViewpoint && ledger.total > 0, critical: true },
|
|
251
|
+
{ key: 'atomicLedger', value: ledger.ratio, weight: 0.13, applicable: ledger.hasViewpoint && ledger.total > 0 && !viewpointMoved, critical: true },
|
|
241
252
|
{ key: 'businessDepth', value: businessDepth, weight: 0.20, applicable: true, critical: true },
|
|
242
253
|
{ key: 'claimProof', value: claim.ratio, weight: 0.15, applicable: claim.withClaims > 0, critical: true },
|
|
243
|
-
{ key: 'traceability', value: traceScore, weight: 0.09, applicable: viewpoints.length > 0, critical: false },
|
|
254
|
+
{ key: 'traceability', value: traceScore, weight: 0.09, applicable: viewpoints.length > 0 && !viewpointMoved, critical: false },
|
|
244
255
|
{ key: 'balance', value: balanceScore, weight: 0.06, applicable: true, critical: false },
|
|
245
256
|
];
|
|
246
257
|
const scored = axisDefs.filter((a) => a.applicable);
|
|
@@ -278,6 +289,13 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
278
289
|
: `this unit supplies no evidence for [${missingEvidence.join(', ')}], and a top mark has to rest on complete evidence`;
|
|
279
290
|
findings.push(`SCORE-CAPPED: overall held at ${cap.toFixed(1)} because ${why}.`);
|
|
280
291
|
}
|
|
292
|
+
if (viewpointBaseline.status === 'changed') {
|
|
293
|
+
const moved = [
|
|
294
|
+
viewpointBaseline.removed?.length ? `removed [${viewpointBaseline.removed.join(', ')}]` : '',
|
|
295
|
+
viewpointBaseline.added?.length ? `added [${viewpointBaseline.added.join(', ')}]` : '',
|
|
296
|
+
].filter(Boolean).join(', ');
|
|
297
|
+
findings.push(`VIEWPOINT-BASELINE-CHANGED: test-viewpoint.md no longer declares what it did when this unit was last accepted — ${moved || 'the declared ids were reordered or replaced'}. atomicLedger + traceability measure the suite AGAINST this file, so they are excluded from the score until the change is confirmed: a generator that rewrites the declaration scores both 100% by construction, and a viewpoint dropped from the file stops being missing from anything. Review the diff (a removed id means that coverage is now unclaimed), then run \`sungen audit --screen ${screenName} --accept-viewpoint\`.`);
|
|
298
|
+
}
|
|
281
299
|
if (gate.pageTypeSource === 'undetermined') {
|
|
282
300
|
findings.push(`PAGE-TYPE-UNDETERMINED: no page type fits this screen with enough confidence (best ${gate.pageTypeEvidence?.hits ?? 0} keyword hit(s)), so NO critical themes were demanded and the coverage axis is excluded from the score — declare it in test-viewpoint.md (\`page-type: <id>\`) to have the theme checklist applied.`);
|
|
283
301
|
}
|
|
@@ -465,7 +483,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
465
483
|
screen: screenName,
|
|
466
484
|
scenarioCount: scenarios.length,
|
|
467
485
|
gate, depth, claim, taxonomy, balance, duplicates, trace, spec,
|
|
468
|
-
taxonomyMismatch, downstream, manualOracle: manualOracleResult, automatableManual: autoManual, flowDepth, oracle, ledger, calibration,
|
|
486
|
+
taxonomyMismatch, downstream, manualOracle: manualOracleResult, automatableManual: autoManual, flowDepth, oracle, ledger, viewpointBaseline, calibration,
|
|
469
487
|
score: {
|
|
470
488
|
overall: Math.round(overall * 10) / 10,
|
|
471
489
|
coverage: Math.round(coverage * 100) / 100,
|
|
@@ -114,6 +114,11 @@ export function inferFromText(reason: string): string | undefined {
|
|
|
114
114
|
return undefined;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/** Every code the text supports, not just the first rule in declaration order. */
|
|
118
|
+
export function inferAllFromText(reason: string): string[] {
|
|
119
|
+
return INFER.filter((r) => r.re.test(reason)).map((r) => r.code);
|
|
120
|
+
}
|
|
121
|
+
|
|
117
122
|
export interface ReasonMismatch { scenario: string; explicit: string; inferred: string }
|
|
118
123
|
|
|
119
124
|
/**
|
|
@@ -150,8 +155,15 @@ export function manualReasonMismatches(featurePath: string): ReasonMismatch[] {
|
|
|
150
155
|
else if (l === '') continue;
|
|
151
156
|
else break;
|
|
152
157
|
}
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
// The block scanned here includes the tester PROCEDURE, so several codes can
|
|
159
|
+
// match at once — "not worth automating" (M8, the author's actual reason) and
|
|
160
|
+
// "the service stays responsive" (M6, a word about latency, not layout). The
|
|
161
|
+
// first rule in declaration order used to win and the correct tag was reported
|
|
162
|
+
// as a mismatch. A tag is only wrong when the text supports some OTHER code and
|
|
163
|
+
// NOT the declared one.
|
|
164
|
+
const codes = inferAllFromText(parts.join(' '));
|
|
165
|
+
const inferred = codes.find((c) => c !== explicit);
|
|
166
|
+
if (inferred && !codes.includes(explicit)) out.push({ scenario: m[1].trim(), explicit, inferred });
|
|
155
167
|
}
|
|
156
168
|
return out;
|
|
157
169
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { parseViewpointOverview } from './parse';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Keep the yardstick from moving with the thing it measures.
|
|
8
|
+
*
|
|
9
|
+
* `test-viewpoint.md` is the project's declaration of WHAT must be tested, and two
|
|
10
|
+
* scored axes measure the suite against it: `traceability` (do scenarios use the
|
|
11
|
+
* declared viewpoint ids?) and `atomicLedger` (is every declared item covered?).
|
|
12
|
+
* Both are only evidence while the declaration is INDEPENDENT of the suite.
|
|
13
|
+
*
|
|
14
|
+
* A `/sungen:create-test` run rewrote a filled test-viewpoint.md, replacing the
|
|
15
|
+
* declared categories with exactly the ones it had just generated. Both axes read
|
|
16
|
+
* 100% by construction — and the performance viewpoint it dropped on the way
|
|
17
|
+
* (4 perf scenarios → 2) went unreported, because VP-PERF was no longer declared
|
|
18
|
+
* for anything to be missing from.
|
|
19
|
+
*
|
|
20
|
+
* So: remember the declaration, and when it changes, say so and stop counting the
|
|
21
|
+
* two axes that depend on it until a human accepts the new baseline. Same rule the
|
|
22
|
+
* score model already applies to an undetermined page type — absent independent
|
|
23
|
+
* evidence scores nothing, in either direction.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Axes whose evidence is `test-viewpoint.md` — excluded while the baseline is unconfirmed. */
|
|
27
|
+
export const VIEWPOINT_DEPENDENT_AXES = ['atomicLedger', 'traceability'] as const;
|
|
28
|
+
|
|
29
|
+
export interface ViewpointBaseline {
|
|
30
|
+
status: 'absent' | 'new' | 'unchanged' | 'changed';
|
|
31
|
+
/** sha1 of the normalized file — '' when the unit has no test-viewpoint.md. */
|
|
32
|
+
hash: string;
|
|
33
|
+
ids: string[];
|
|
34
|
+
/** Only on `changed`: how the declaration moved. */
|
|
35
|
+
added?: string[];
|
|
36
|
+
removed?: string[];
|
|
37
|
+
recordedAt?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface BaselineFile {
|
|
41
|
+
version: 1;
|
|
42
|
+
units: Record<string, { hash: string; ids: string[]; recordedAt: string }>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function baselinePath(projectRoot: string): string {
|
|
46
|
+
return path.join(projectRoot, '.sungen', 'viewpoint-baseline.json');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readBaselineFile(projectRoot: string): BaselineFile {
|
|
50
|
+
const p = baselinePath(projectRoot);
|
|
51
|
+
if (!fs.existsSync(p)) return { version: 1, units: {} };
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf-8')) as BaselineFile;
|
|
54
|
+
return parsed && typeof parsed === 'object' && parsed.units ? parsed : { version: 1, units: {} };
|
|
55
|
+
} catch {
|
|
56
|
+
return { version: 1, units: {} }; // unreadable → treat as unrecorded, never throw
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writeBaselineFile(projectRoot: string, data: BaselineFile): void {
|
|
61
|
+
const p = baselinePath(projectRoot);
|
|
62
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
63
|
+
fs.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`, 'utf-8');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Hash the DECLARATION, not the prose: an author reflowing a sentence must not read
|
|
68
|
+
* as the taxonomy moving. Only the declared viewpoint ids are fingerprinted.
|
|
69
|
+
*/
|
|
70
|
+
function fingerprint(ids: string[]): string {
|
|
71
|
+
return crypto.createHash('sha1').update(ids.join('\n')).digest('hex').slice(0, 12);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Compare the unit's current test-viewpoint declaration to the recorded baseline.
|
|
76
|
+
* Read-only — recording is the caller's decision, so an audit stays a measurement
|
|
77
|
+
* and `--accept-viewpoint` stays the explicit human act. A first sighting reports
|
|
78
|
+
* `new` (nothing to compare against yet).
|
|
79
|
+
*/
|
|
80
|
+
export function checkViewpointBaseline(
|
|
81
|
+
projectRoot: string,
|
|
82
|
+
unitId: string,
|
|
83
|
+
viewpointPath: string,
|
|
84
|
+
): ViewpointBaseline {
|
|
85
|
+
if (!fs.existsSync(viewpointPath)) return { status: 'absent', hash: '', ids: [] };
|
|
86
|
+
const ids = parseViewpointOverview(viewpointPath).map((v) => v.id).sort();
|
|
87
|
+
const hash = fingerprint(ids);
|
|
88
|
+
if (ids.length === 0) return { status: 'absent', hash: '', ids: [] };
|
|
89
|
+
|
|
90
|
+
const file = readBaselineFile(projectRoot);
|
|
91
|
+
const recorded = file.units[unitId];
|
|
92
|
+
if (!recorded) return { status: 'new', hash, ids };
|
|
93
|
+
if (recorded.hash === hash) return { status: 'unchanged', hash, ids, recordedAt: recorded.recordedAt };
|
|
94
|
+
|
|
95
|
+
const before = new Set(recorded.ids ?? []);
|
|
96
|
+
const now = new Set(ids);
|
|
97
|
+
return {
|
|
98
|
+
status: 'changed',
|
|
99
|
+
hash,
|
|
100
|
+
ids,
|
|
101
|
+
added: ids.filter((i) => !before.has(i)),
|
|
102
|
+
removed: (recorded.ids ?? []).filter((i) => !now.has(i)),
|
|
103
|
+
recordedAt: recorded.recordedAt,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Record the current declaration as the accepted baseline (`sungen audit --accept-viewpoint`). */
|
|
108
|
+
export function acceptViewpointBaseline(
|
|
109
|
+
projectRoot: string,
|
|
110
|
+
unitId: string,
|
|
111
|
+
current: { hash: string; ids: string[] },
|
|
112
|
+
): void {
|
|
113
|
+
const file = readBaselineFile(projectRoot);
|
|
114
|
+
file.units[unitId] = {
|
|
115
|
+
hash: current.hash,
|
|
116
|
+
ids: current.ids,
|
|
117
|
+
recordedAt: new Date().toISOString(),
|
|
118
|
+
};
|
|
119
|
+
writeBaselineFile(projectRoot, file);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Read the declaration without recording anything (used by `--accept-viewpoint`). */
|
|
123
|
+
export function readViewpointDeclaration(viewpointPath: string): { hash: string; ids: string[] } | null {
|
|
124
|
+
if (!fs.existsSync(viewpointPath)) return null;
|
|
125
|
+
const ids = parseViewpointOverview(viewpointPath).map((v) => v.id).sort();
|
|
126
|
+
if (ids.length === 0) return null;
|
|
127
|
+
return { hash: fingerprint(ids), ids };
|
|
128
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { getPackageVersion } from '../exporters/package-info';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Are this project's AI assets (commands + skills under `.claude/` etc.) the ones
|
|
7
|
+
* the running sungen would write?
|
|
8
|
+
*
|
|
9
|
+
* Upgrading the npm package does NOT refresh them — `sungen update` does. Nothing
|
|
10
|
+
* said so, so a project kept running prompts from an older build indefinitely: a
|
|
11
|
+
* QA upgraded, re-ran `/sungen:create-test`, and got the OLD command (141 lines vs
|
|
12
|
+
* the shipped 267) with none of the newer guidance. The failure is invisible
|
|
13
|
+
* because everything still "works", just to an older specification.
|
|
14
|
+
*
|
|
15
|
+
* The manifest already records the version + a hash per managed file; this only
|
|
16
|
+
* reads it and reports.
|
|
17
|
+
*/
|
|
18
|
+
export interface AssetsDrift {
|
|
19
|
+
recordedVersion: string;
|
|
20
|
+
runningVersion: string;
|
|
21
|
+
/** Managed files whose content no longer matches the manifest's hash. */
|
|
22
|
+
changed: number;
|
|
23
|
+
total: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function checkAssetsDrift(cwd: string): AssetsDrift | null {
|
|
27
|
+
const manifestPath = path.join(cwd, '.sungen', 'manifest.json');
|
|
28
|
+
if (!fs.existsSync(manifestPath)) return null; // not a sungen project (yet)
|
|
29
|
+
|
|
30
|
+
let manifest: { version?: string; managed?: Record<string, string> };
|
|
31
|
+
try {
|
|
32
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
33
|
+
} catch {
|
|
34
|
+
return null; // unreadable → say nothing
|
|
35
|
+
}
|
|
36
|
+
const recordedVersion = manifest.version ?? '';
|
|
37
|
+
const runningVersion = getPackageVersion();
|
|
38
|
+
const managed = manifest.managed ?? {};
|
|
39
|
+
const total = Object.keys(managed).length;
|
|
40
|
+
if (!recordedVersion || total === 0) return null;
|
|
41
|
+
|
|
42
|
+
// A version match is the common case — trust it and skip hashing entirely.
|
|
43
|
+
if (recordedVersion === runningVersion) return null;
|
|
44
|
+
|
|
45
|
+
let changed = 0;
|
|
46
|
+
for (const rel of Object.keys(managed)) {
|
|
47
|
+
if (!fs.existsSync(path.join(cwd, rel))) changed++;
|
|
48
|
+
}
|
|
49
|
+
return { recordedVersion, runningVersion, changed, total };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** One line, on stderr so `--json` output stays machine-readable. */
|
|
53
|
+
export function warnAssetsDrift(cwd: string): void {
|
|
54
|
+
const drift = checkAssetsDrift(cwd);
|
|
55
|
+
if (!drift) return;
|
|
56
|
+
const gray = '\x1b[90m';
|
|
57
|
+
const cyan = '\x1b[36m';
|
|
58
|
+
const reset = '\x1b[0m';
|
|
59
|
+
process.stderr.write(
|
|
60
|
+
`${gray}note: this project's AI commands/skills were written by sungen ${drift.recordedVersion}, ` +
|
|
61
|
+
`you are running ${drift.runningVersion} — run ${cyan}sungen update${reset}${gray} to refresh them ` +
|
|
62
|
+
`(upgrading the package alone does not).${reset}\n`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -68,15 +68,6 @@ 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
|
-
|
|
80
71
|
## Steps
|
|
81
72
|
|
|
82
73
|
{{#cap parallel-subagents}}
|
|
@@ -124,12 +115,16 @@ tag only, so an untagged requirement is later reported as an uncovered gap.
|
|
|
124
115
|
- **Fill test-viewpoint.md first** — I'll help you identify edge cases, known issues, and design decisions for this screen before generating tests
|
|
125
116
|
- **Continue without it** — generate tests from spec and other sources only
|
|
126
117
|
|
|
118
|
+
**A filled `test-viewpoint.md` is an INPUT — never an output.** Do NOT rewrite, re-order, or replace its viewpoint declarations as part of generating tests. It is the yardstick two scored axes measure the suite against (`traceability`, `atomicLedger`): rewrite it to match what you just generated and both read 100% by construction, while any viewpoint you quietly dropped stops being missing from anything. If the declared taxonomy genuinely does not fit the screen, **report the mismatch and ask** — propose the diff (ids added / removed and why) and let the QA decide. `sungen audit` reports `VIEWPOINT-BASELINE-CHANGED` and stops scoring both axes when this file moves, so an unannounced rewrite lowers the score rather than raising it.
|
|
119
|
+
|
|
127
120
|
**Context discovery (prefer an isolated agent).** Reading all sources here can flood this context. **Claude Code:** spawn the **`sungen-discovery`** sub-agent (Task tool, `subagent_type: sungen-discovery`) to read spec/figma/ui/live in isolation and return a **compact discovery report** (sources, completeness, conflicts, recommended route, key facts); use that report instead of pasting raw sources. **Copilot / no sub-agents:** do the reading inline as below.
|
|
128
121
|
{{/cap}}
|
|
129
122
|
{{^cap parallel-subagents}}
|
|
130
123
|
- If `test-viewpoint.md` exists → read it. If it only contains HTML comments (scaffold template), ask:
|
|
131
124
|
- **1) Fill test-viewpoint.md first** — identify edge cases, known issues, and design decisions before generating tests
|
|
132
125
|
- **2) Continue without it** — generate tests from spec and other sources only
|
|
126
|
+
|
|
127
|
+
**A filled `test-viewpoint.md` is an INPUT — never an output.** Do NOT rewrite, re-order, or replace its viewpoint declarations while generating tests. It is the yardstick two scored axes measure the suite against (`traceability`, `atomicLedger`): rewrite it to match what you just generated and both read 100% by construction, while any viewpoint you quietly dropped stops being missing from anything. If the declared taxonomy genuinely does not fit the screen, **report the mismatch and ask** — propose the diff (ids added / removed and why) and let the QA decide. `sungen audit` reports `VIEWPOINT-BASELINE-CHANGED` and stops scoring both axes when this file moves.
|
|
133
128
|
{{/cap}}
|
|
134
129
|
|
|
135
130
|
**Auto-detect visual source** — do NOT ask the user to pick a source. Instead, check what already exists and use it:
|
|
@@ -265,3 +260,23 @@ tag only, so an untagged requirement is later reported as an uncovered gap.
|
|
|
265
260
|
{{^cap parallel-subagents}}
|
|
266
261
|
**No selectors.yaml** — selectors are generated during `/sungen-run-test`.
|
|
267
262
|
{{/cap}}
|
|
263
|
+
|
|
264
|
+
## Finish — always hand the next step back
|
|
265
|
+
|
|
266
|
+
Do not stop after writing the files. Close every run with, in this order:
|
|
267
|
+
|
|
268
|
+
1. **The traceability check below** — an untagged requirement becomes an uncovered gap at delivery.
|
|
269
|
+
2. **The harness result in one line** — score, gate verdict, and the axis the `SCORE-CAPPED` finding
|
|
270
|
+
names (that finding is the single answer to "how do I raise the score").
|
|
271
|
+
3. **`AskUserQuestion` with the next actions** — never end with prose alone. Offer
|
|
272
|
+
`/sungen:run-test <name>` (recommended once the tiers are written), re-running `create-test` to
|
|
273
|
+
extend, and "Done for now". A run that ends without this leaves the operator guessing.
|
|
274
|
+
|
|
275
|
+
### Requirement traceability
|
|
276
|
+
|
|
277
|
+
Cross-check `requirements/spec.md` against the scenarios you wrote: every `FR-`/`TR-`/`NFR-` id
|
|
278
|
+
must either carry a `@spec:<id>` tag on the scenario that proves it, or be a conscious
|
|
279
|
+
out-of-scope decision you state in the summary. `sungen audit` reports `SPEC-TRACE-IMPLICIT` for
|
|
280
|
+
requirements it could only match by keyword — treat that list as a to-do: add the tag to the
|
|
281
|
+
proving scenario, do not leave the link to inference. Delivery's requirement table follows the
|
|
282
|
+
tag only, so an untagged requirement is later reported as an uncovered gap.
|
|
@@ -340,6 +340,7 @@ Security: [S1 – admin only]
|
|
|
340
340
|
`sungen audit` enforces these. Generate compliant output up front:
|
|
341
341
|
|
|
342
342
|
1. **Taxonomy-match** (`VP-TAXONOMY-MISMATCH`, gate-FAIL) — when `test-viewpoint.md` declares its own viewpoint IDs (e.g. `VP0`, `VP1`, … `VP12`, `MS-HP-001`, `MS-EH-001`), **reuse those IDs verbatim as the scenario codes**. Do NOT invent a generic `VP-UI / VP-LOGIC / VP-VAL` scheme — that breaks the coverage matrix. Only fall back to `VP-<CATEGORY>-<NNN>` when the viewpoint file declares no IDs.
|
|
343
|
+
- **Match the scenarios to the file — never the file to the scenarios.** A filled `test-viewpoint.md` is an input; do not rewrite its declarations to fit what you generated. That is not compliance, it is moving the yardstick: `traceability` + `atomicLedger` then read 100% by construction and a dropped viewpoint stops being missing from anything. Disagree with the taxonomy → propose the diff and ask. `sungen audit` reports `VIEWPOINT-BASELINE-CHANGED` and excludes both axes until a human accepts the change (`sungen audit --screen <name> --accept-viewpoint`).
|
|
343
344
|
2. **Spec-coverage triggers** (`TRIGGER-UNCOVERED`, gate-FAIL) — the Validation-Rules table lists a **trigger** per constraint (e.g. `blur, submit`). Generate one scenario **per (constraint × trigger)** — a `format` rule validating *on blur AND on submit* needs BOTH a blur scenario (`press Tab`) and a submit scenario (`click [Submit]` / `press Enter`). Never collapse the trigger × input matrix to one representative case.
|
|
344
345
|
3. **Claim-Proof** (`CLAIM-UNPROVEN`) — a title claiming `all`/`only`/`every`/`single`/`correct`/`same`/`changes`/`hidden`/`cleared`/`restored`/`independent`/`sanitized`/`announces` MUST have the matching assertion (`see all …`, count, `remember`+compare, `is hidden`, return-and-assert-empty, etc.). If the title promises it, the steps must prove it.
|
|
345
346
|
- **Negative / absence claims** (`does not` / `no` / `never` / `prevents` / `không` / `chưa` — any language; `no-side-effect/no-duplicate`, `negative-claim/absence`): the `Then` must **differ** between the claim holding and not holding. A terminal `see [X] page` that looks identical whether or not the bad thing happened proves nothing. For a side-effect that should NOT repeat (re-submit on back, re-charge, duplicate order, resend OTP), assert the **count is unchanged** (`User see [Records] table with {{one}}` / `row with {{count}}`); if it's not UI-observable, mark `@manual` with a request-count oracle (shape below). This is general — it covers any side-effect, not a fixed verb list.
|