@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.
Files changed (37) hide show
  1. package/dist/cli/commands/audit.d.ts.map +1 -1
  2. package/dist/cli/commands/audit.js +17 -0
  3. package/dist/cli/commands/audit.js.map +1 -1
  4. package/dist/cli/index.js +5 -0
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/exporters/matrix/build.d.ts +9 -0
  7. package/dist/exporters/matrix/build.d.ts.map +1 -1
  8. package/dist/exporters/matrix/build.js +20 -1
  9. package/dist/exporters/matrix/build.js.map +1 -1
  10. package/dist/harness/audit.d.ts +4 -0
  11. package/dist/harness/audit.d.ts.map +1 -1
  12. package/dist/harness/audit.js +20 -3
  13. package/dist/harness/audit.js.map +1 -1
  14. package/dist/harness/capability-plan.d.ts +2 -0
  15. package/dist/harness/capability-plan.d.ts.map +1 -1
  16. package/dist/harness/capability-plan.js +14 -2
  17. package/dist/harness/capability-plan.js.map +1 -1
  18. package/dist/harness/viewpoint-baseline.d.ts +49 -0
  19. package/dist/harness/viewpoint-baseline.d.ts.map +1 -0
  20. package/dist/harness/viewpoint-baseline.js +141 -0
  21. package/dist/harness/viewpoint-baseline.js.map +1 -0
  22. package/dist/orchestrator/assets-drift.d.ts +24 -0
  23. package/dist/orchestrator/assets-drift.d.ts.map +1 -0
  24. package/dist/orchestrator/assets-drift.js +80 -0
  25. package/dist/orchestrator/assets-drift.js.map +1 -0
  26. package/dist/orchestrator/templates/ai-src/commands/create-test.md +24 -9
  27. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +1 -0
  28. package/package.json +4 -4
  29. package/src/cli/commands/audit.ts +18 -1
  30. package/src/cli/index.ts +6 -0
  31. package/src/exporters/matrix/build.ts +20 -1
  32. package/src/harness/audit.ts +22 -4
  33. package/src/harness/capability-plan.ts +14 -2
  34. package/src/harness/viewpoint-baseline.ts +128 -0
  35. package/src/orchestrator/assets-drift.ts +64 -0
  36. package/src/orchestrator/templates/ai-src/commands/create-test.md +24 -9
  37. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +1 -0
@@ -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
- const inferred = inferFromText(parts.join(' '));
154
- if (inferred && inferred !== explicit) out.push({ scenario: m[1].trim(), explicit, inferred });
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.