@sun-asterisk/sungen 3.2.21 → 3.2.22-beta.1
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 +8 -0
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts +3 -0
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +15 -0
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/inspect.d.ts +41 -0
- package/dist/cli/commands/inspect.d.ts.map +1 -0
- package/dist/cli/commands/inspect.js +134 -0
- package/dist/cli/commands/inspect.js.map +1 -0
- package/dist/cli/commands/trace.d.ts.map +1 -1
- package/dist/cli/commands/trace.js +9 -0
- package/dist/cli/commands/trace.js.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts +10 -0
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +38 -0
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/export.d.ts.map +1 -1
- package/dist/exporters/matrix/export.js +11 -0
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
- package/dist/exporters/matrix/render-xlsx.js +44 -1
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +10 -0
- package/dist/exporters/matrix/types.d.ts.map +1 -1
- package/dist/exporters/matrix/types.js.map +1 -1
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +1 -0
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/types.d.ts +2 -0
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +6 -1
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +5 -0
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +27 -19
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +22 -0
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/harness/audit.d.ts +2 -0
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +101 -10
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/flow-contract.d.ts +87 -0
- package/dist/harness/flow-contract.d.ts.map +1 -0
- package/dist/harness/flow-contract.js +259 -0
- package/dist/harness/flow-contract.js.map +1 -0
- package/dist/harness/flow-plan.d.ts +3 -0
- package/dist/harness/flow-plan.d.ts.map +1 -1
- package/dist/harness/flow-plan.js +6 -2
- package/dist/harness/flow-plan.js.map +1 -1
- package/dist/harness/parse.d.ts +5 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +29 -1
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/perf.d.ts +40 -0
- package/dist/harness/perf.d.ts.map +1 -0
- package/dist/harness/perf.js +136 -0
- package/dist/harness/perf.js.map +1 -0
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +13 -1
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/spec-coverage.d.ts +8 -0
- package/dist/harness/spec-coverage.d.ts.map +1 -1
- package/dist/harness/spec-coverage.js +60 -6
- package/dist/harness/spec-coverage.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/add-flow.md +51 -3
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +10 -0
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +23 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +60 -16
- package/dist/orchestrator/templates/qa-context.md +14 -1
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +8 -0
- package/src/cli/commands/delivery.ts +14 -2
- package/src/cli/commands/inspect.ts +128 -0
- package/src/cli/commands/trace.ts +9 -0
- package/src/cli/index.ts +2 -0
- package/src/exporters/matrix/build.ts +40 -0
- package/src/exporters/matrix/export.ts +11 -0
- package/src/exporters/matrix/render-xlsx.ts +45 -1
- package/src/exporters/matrix/types.ts +10 -0
- package/src/exporters/playwright-report-parser.ts +2 -0
- package/src/exporters/types.ts +2 -0
- package/src/generators/test-generator/diagnostics.ts +6 -1
- package/src/generators/test-generator/patterns/index.ts +30 -24
- package/src/generators/test-generator/step-mapper.ts +22 -0
- package/src/harness/audit.ts +104 -11
- package/src/harness/flow-contract.ts +261 -0
- package/src/harness/flow-plan.ts +10 -3
- package/src/harness/parse.ts +31 -1
- package/src/harness/perf.ts +112 -0
- package/src/harness/sensors.ts +13 -1
- package/src/harness/spec-coverage.ts +55 -5
- package/src/orchestrator/templates/ai-src/commands/add-flow.md +51 -3
- package/src/orchestrator/templates/ai-src/commands/create-test.md +10 -0
- package/src/orchestrator/templates/ai-src/commands/run-test.md +23 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +60 -16
- package/src/orchestrator/templates/qa-context.md +14 -1
|
@@ -603,24 +603,67 @@ error:
|
|
|
603
603
|
|
|
604
604
|
## Flow Test Generation
|
|
605
605
|
|
|
606
|
+
> **Terminology**: many QA teams call this level **System Test** (ISTQB: verify the fully
|
|
607
|
+
> integrated system against its specified requirements, designed around business
|
|
608
|
+
> processes/use cases). In sungen a *flow* is that system-test suite run through the real UI;
|
|
609
|
+
> its automation then serves as the regression suite on later runs. The names are
|
|
610
|
+
> interchangeable in conversation; the artifacts stay `qa/flows/<name>/`.
|
|
611
|
+
|
|
612
|
+
**Representative, not exhaustive (ISTQB system-test design rule).** A flow case takes ONE
|
|
613
|
+
representative input per branch — one valid value for the main flow, one error value that
|
|
614
|
+
triggers the exception branch and proves the journey recovers. The full equivalence/boundary
|
|
615
|
+
matrix belongs to the OWNING SCREEN (as `@cases` there), never re-run inside the flow: if the
|
|
616
|
+
screen already proved "the field rejects negatives", the flow only proves "the journey survives
|
|
617
|
+
one rejected input". `sungen audit` reports `FLOW-EXHAUSTIVE-INPUTS` when ≥3 same-shape flow
|
|
618
|
+
scenarios differ only in data. **Mocked dependencies**: a flow scenario using `@mock` is
|
|
619
|
+
delivered as *Pass (Mocked)* — verified handling logic, not the real dependency; the delivery's
|
|
620
|
+
External Dependency Tracking table lists it until re-verified real.
|
|
621
|
+
|
|
622
|
+
**Release selection & external legs**: when the contract declares `golden: true`, tag the
|
|
623
|
+
happy-path scenario(s) that prove the outcome with **`@golden`** — that is the set
|
|
624
|
+
`sungen inspect` (Final Inspection, Go/No-Go) runs before a release; `GOLDEN-MISSING` fires
|
|
625
|
+
until it exists, and a @golden scenario that passes only against mocks BLOCKS the release.
|
|
626
|
+
When the contract declares `external:` legs (another team/vendor's system), scenarios touching
|
|
627
|
+
those namespaces are the **System INTEGRATION** group — keep their oracles about the boundary
|
|
628
|
+
(contract/format/handoff), and expect `SIT-MOCK-ONLY` until they are re-verified real.
|
|
629
|
+
|
|
630
|
+
|
|
606
631
|
> **Auto-detect**: if path is `qa/flows/<name>/` → use this section. Skip Steps 1–4 above.
|
|
607
632
|
|
|
633
|
+
**Read `requirements/flow-contract.yaml` FIRST — it is the flow's boundary and the yardstick
|
|
634
|
+
`sungen audit` scores the flow against** (`flowCoverage` axis = journey phases HP/ER/EH automated;
|
|
635
|
+
`FLOW-OUTCOME-UNPROVEN`; `FLOW-SCOPE-CREEP`). No contract yet → author it with the user via the
|
|
636
|
+
boundary checklist in `add-flow` (one business goal · clear trigger · ONE observable outcome
|
|
637
|
+
valuable to the actor · name = "Verb + outcome"), THEN generate. **A filled contract is an INPUT —
|
|
638
|
+
never rewrite it to match your output** (same rule as `test-viewpoint.md`).
|
|
639
|
+
|
|
608
640
|
| Aspect | Screen | Flow |
|
|
609
641
|
|---|---|---|
|
|
610
|
-
| Section focus | UI patterns per section | Journey phases
|
|
642
|
+
| Section focus | UI patterns per section | Journey phases toward ONE declared outcome |
|
|
611
643
|
| Selector format | `[Element]` | `[Screen:Element]` (namespaced) |
|
|
612
644
|
| Test data keys | `{{variable}}` | `{{phase.variable}}` |
|
|
613
645
|
| Feature tag | `@auto` / `@smoke` etc. | `@flow` (required) |
|
|
614
|
-
|
|
|
646
|
+
| Scenario ids | `VP-<CATEGORY>-NNN` | `FL-<PHASE>-NNN` — phases: `HP` (happy path), `ER` (error recovery), `EH` (guards), `UI` (journey UI states, optional) |
|
|
615
647
|
|
|
616
|
-
**Scenarios to generate:**
|
|
648
|
+
**Scenarios to generate — every phase demanded by the contract, automated:**
|
|
617
649
|
|
|
618
|
-
|
|
|
619
|
-
|
|
620
|
-
|
|
|
621
|
-
|
|
|
622
|
-
|
|
|
623
|
-
| Cross-screen
|
|
650
|
+
| Phase | What to test | Scoring |
|
|
651
|
+
|---|---|---|
|
|
652
|
+
| `FL-HP` happy path | The complete journey ending in the contract's `outcome.assertion` — an AUTOMATED **data** assertion on `outcome.screen` (an order number, a summed total — not just "page visible"). This scenario is WHY the flow exists: it is the regression proof after a lib/framework upgrade. | uncovered → `flowCoverage` drops + `FLOW-OUTCOME-UNPROVEN` |
|
|
653
|
+
| `FL-ER` error recovery | Invalid input mid-flow → error shown → fix → the journey still completes. Validation must not trap the journey. | uncovered → `flowCoverage` drops |
|
|
654
|
+
| `FL-EH` guards | Direct URL access without the precondition · browser back · refresh · expired context — each ends in a safe observable state. | uncovered → `flowCoverage` drops |
|
|
655
|
+
| Cross-screen handoff | After every screen transition, assert the CARRIED state on the new screen (the added product's name in the cart, the email echoed on the sent screen). | blind tails cap `businessDepth` (`FLOW-HANDOFF-SHALLOW`) |
|
|
656
|
+
| Stateful regression (when `stateful:` declared) | Count/quantity proof · teardown (remove → empty) · multi-source add. | missing dims cap `businessDepth` (`FLOW-DEPTH`) |
|
|
657
|
+
|
|
658
|
+
**Boundary discipline while generating:** every scenario must serve the contract's goal. A scenario
|
|
659
|
+
that never touches `outcome.screen` and is not a guard (`EH`) or error-recovery (`ER`) belongs in a
|
|
660
|
+
DIFFERENT flow — propose the split instead of writing it here (`FLOW-SCOPE-CREEP` will flag it).
|
|
661
|
+
Auth persistence across transitions is part of `EH` unless the project declares it its own phase.
|
|
662
|
+
|
|
663
|
+
**Manual in flows**: always `@manual:Mx` with the reason code — bare `@manual` is flagged
|
|
664
|
+
(`MANUAL-CODE-MISSING`) because the capability planner cannot route it. Typical flow deferrals:
|
|
665
|
+
inbox/mail oracle → `M5`, network-request count → `M3`, context expiry control → `M7`. A
|
|
666
|
+
cross-screen scenario inside the flow's own goal is NOT manual — automate it here.
|
|
624
667
|
|
|
625
668
|
```gherkin
|
|
626
669
|
@flow @auth:user
|
|
@@ -630,19 +673,20 @@ Feature: Award Submission Flow
|
|
|
630
673
|
Given User is on [Login] page
|
|
631
674
|
|
|
632
675
|
@high
|
|
633
|
-
Scenario:
|
|
676
|
+
Scenario: FL-HP-001 A signed-in user's nomination is submitted and confirmed
|
|
634
677
|
When User fill [Login:Email] field with {{login.email}}
|
|
635
678
|
And User fill [Login:Password] field with {{login.password}}
|
|
636
679
|
And User click [Login:Submit] button
|
|
637
680
|
Then User see [Dashboard] page
|
|
638
|
-
|
|
639
|
-
@high
|
|
640
|
-
Scenario: User submits nomination
|
|
641
681
|
When User click [Dashboard:Awards] link
|
|
642
|
-
|
|
643
|
-
When User fill [Awards:Nominee] field with {{submission.nominee}}
|
|
682
|
+
And User fill [Awards:Nominee] field with {{submission.nominee}}
|
|
644
683
|
And User click [Awards:Submit] button
|
|
645
|
-
Then User see {{success_message}}
|
|
684
|
+
Then User see [Awards:Success Message] text with {{success_message}}
|
|
685
|
+
|
|
686
|
+
@high
|
|
687
|
+
Scenario: FL-EH-001 Direct access to the award form without login redirects to login
|
|
688
|
+
When User go to [Awards] page
|
|
689
|
+
Then User see [Login] page
|
|
646
690
|
```
|
|
647
691
|
|
|
648
692
|
```yaml
|
|
@@ -45,8 +45,21 @@ Example:
|
|
|
45
45
|
|
|
46
46
|
## Testing Strategy
|
|
47
47
|
|
|
48
|
+
Machine-readable intent — `sungen audit` reads these keys (Intent Profile). Values here are
|
|
49
|
+
live even when the surrounding text changes; an invalid value silently falls back to the default.
|
|
50
|
+
|
|
51
|
+
focus: functional
|
|
52
|
+
<!-- focus: functional | e-commerce | security | smoke — drives the audit's depth threshold -->
|
|
53
|
+
|
|
54
|
+
risk_tier: normal
|
|
55
|
+
<!-- risk_tier: high | normal | low -->
|
|
56
|
+
|
|
57
|
+
To silence driver suggestions in audit findings, add a line: capability_suggestions with value off.
|
|
58
|
+
|
|
48
59
|
**Focus areas** — what to cover thoroughly:
|
|
49
|
-
<!--
|
|
60
|
+
<!-- Prose for humans; the parseable value is the `focus:` key above.
|
|
61
|
+
Response-time budgets are NOT a focus value — declare them in qa/perf.yaml
|
|
62
|
+
(percentile + scenario_ms budgets; reported by `sungen delivery`). -->
|
|
50
63
|
<!-- Example: functional, security -->
|
|
51
64
|
|
|
52
65
|
**Mandatory coverage:**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sun-asterisk/sungen",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.22-beta.1",
|
|
4
4
|
"description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"@babel/types": "^7.28.5",
|
|
40
40
|
"@cucumber/gherkin": "^37.0.0",
|
|
41
41
|
"@cucumber/messages": "^31.0.0",
|
|
42
|
-
"@sungen/driver-data-factory": "3.2.
|
|
43
|
-
"@sungen/driver-ui": "3.2.
|
|
42
|
+
"@sungen/driver-data-factory": "3.2.22-beta.1",
|
|
43
|
+
"@sungen/driver-ui": "3.2.22-beta.1",
|
|
44
44
|
"chalk": "^5.6.2",
|
|
45
45
|
"commander": "^14.0.2",
|
|
46
46
|
"dotenv": "^17.2.3",
|
|
@@ -40,6 +40,14 @@ function render(r: AuditReport): void {
|
|
|
40
40
|
if (r.gate.gaps.length) for (const g of r.gate.gaps) L(` ✗ ${g.status === 'shallow' ? 'SHALLOW' : 'MISSING'}: ${g.theme}`);
|
|
41
41
|
else L(' ✓ all critical themes covered (with data assertions)');
|
|
42
42
|
L('');
|
|
43
|
+
if (r.flowQuality?.hasContract) {
|
|
44
|
+
const fq = r.flowQuality;
|
|
45
|
+
L(` ①F Flow contract — goal: ${fq.contract!.goal}`);
|
|
46
|
+
L(` outcome [${fq.contract!.outcome.screen}]: ${fq.outcomeProven ? '✓ proven (automated data assertion)' : fq.outcomeManualOnly ? '⚠ manual-only' : '✗ UNPROVEN'}`);
|
|
47
|
+
L(` phases: ${fq.phases.map((p) => `${p.phase}=${p.covered ? (p.automated ? '✓' : 'manual') : '✗'}`).join(' ')} → flowCoverage ${(fq.phaseRatio * 100).toFixed(0)}%`);
|
|
48
|
+
L(` handoffs asserted: ${fq.handoffs.asserted}/${fq.handoffs.total}${fq.offGoal.length ? ` ⚠ off-goal scenarios: ${fq.offGoal.length} (${fq.offGoalCategories.join(', ')})` : ''}`);
|
|
49
|
+
L('');
|
|
50
|
+
}
|
|
43
51
|
L(` ② Assertion depth — ${r.depth.businessCriticalShallow}/${r.depth.businessCriticalTotal} business-critical scenarios are shallow (visibility/nav only)`);
|
|
44
52
|
for (const s of r.depth.shallowBusinessCritical.slice(0, 8)) L(` ⚠ ${s.category}: ${s.name}`);
|
|
45
53
|
if (r.depth.shallowBusinessCritical.length > 8) L(` … +${r.depth.shallowBusinessCritical.length - 8} more`);
|
|
@@ -100,7 +100,7 @@ function listFeatureTargets(cwd: string, screen: string, kind: UnitKind): Delive
|
|
|
100
100
|
.sort((a, b) => a.featureBaseName.localeCompare(b.featureBaseName));
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function listAllTargets(cwd: string): DeliveryTarget[] {
|
|
103
|
+
export function listAllTargets(cwd: string): DeliveryTarget[] {
|
|
104
104
|
const targets: DeliveryTarget[] = [];
|
|
105
105
|
const scan = (kind: UnitKind, skip: (n: string) => boolean = () => false) => {
|
|
106
106
|
const root = path.join(cwd, 'qa', qaParent(kind));
|
|
@@ -629,7 +629,7 @@ async function exportTarget(
|
|
|
629
629
|
// Matrix export (Delivery v2 — the default; --legacy keeps the classic workbook)
|
|
630
630
|
// ----------------------------------------------------------------------------
|
|
631
631
|
|
|
632
|
-
function matrixPathsFor(cwd: string, target: DeliveryTarget): MatrixTargetPaths {
|
|
632
|
+
export function matrixPathsFor(cwd: string, target: DeliveryTarget): MatrixTargetPaths {
|
|
633
633
|
const base = qaDir(cwd, target);
|
|
634
634
|
const genBase = generatedDir(cwd, target);
|
|
635
635
|
return {
|
|
@@ -690,6 +690,18 @@ async function exportMatrixTarget(
|
|
|
690
690
|
}
|
|
691
691
|
|
|
692
692
|
log(`${COLOR.bold}${paths.label}${COLOR.reset} — ${model.manifest.itemCount} delivery item(s), ${model.manifest.variantCount} coverage variant(s), state: ${model.manifest.approvalState}`);
|
|
693
|
+
{
|
|
694
|
+
const mocked = model.items.flatMap((it) => it.variants.filter((v) => v.mocked));
|
|
695
|
+
if (mocked.length > 0) {
|
|
696
|
+
log(` ${COLOR.yellow}MOCKED${COLOR.reset}: ${mocked.length} variant(s) verified against mocks (${mocked.slice(0, 4).map((v) => v.ref).join(', ')}${mocked.length > 4 ? ', …' : ''}) — Pass (Mocked) ≠ Pass (Real); see the External Dependency Tracking block on the Coverage sheet.`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (model.perf) {
|
|
700
|
+
const p = model.perf;
|
|
701
|
+
const tone = p.pass ? COLOR.green : COLOR.yellow;
|
|
702
|
+
log(` ${tone}PERF ${p.pass ? 'PASS' : 'OVER BUDGET'}${COLOR.reset}: p${p.percentile} ${p.metric.replace('_ms', '')} ${p.measuredMs}ms ${p.pass ? '≤' : '>'} budget ${p.budgetMs}ms (${p.samples} run${p.samples === 1 ? '' : 's'})`);
|
|
703
|
+
for (const s of p.slowest) log(` ${COLOR.yellow}slowest:${COLOR.reset} ${s.ms}ms — ${s.title.slice(0, 90)}`);
|
|
704
|
+
}
|
|
693
705
|
printFindings(model.findings);
|
|
694
706
|
|
|
695
707
|
if (opts.check) {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sungen inspect` — Final Inspection: the deterministic Go/No-Go view. (#580 P9)
|
|
3
|
+
*
|
|
4
|
+
* The QA practice this implements: ONCE per release, run only the release-critical
|
|
5
|
+
* (@golden) journeys on the real environment with real (masked) data, on FROZEN code,
|
|
6
|
+
* and answer one question — is this build safe to release? Not a place to find new
|
|
7
|
+
* bugs; a place to refuse a release that is not proven.
|
|
8
|
+
*
|
|
9
|
+
* Every ingredient already exists, this command only reads them:
|
|
10
|
+
* - selection : @golden scenarios (tags pass through to Playwright — `--grep @golden`)
|
|
11
|
+
* - results : the units' Playwright JSON runs (same source delivery reads)
|
|
12
|
+
* - freeze : the approved delivery map's fingerprints — Gate E drift = not frozen
|
|
13
|
+
* - honesty : a @golden variant verified against MOCKS blocks GO — Pass (Mocked)
|
|
14
|
+
* says nothing about the real dependency (External Dependency Tracking)
|
|
15
|
+
* - perf : the qa/perf.yaml verdict, when configured
|
|
16
|
+
*
|
|
17
|
+
* Exit code 0 = GO, 2 = NO-GO — CI-usable as a release gate.
|
|
18
|
+
*/
|
|
19
|
+
import { Command } from 'commander';
|
|
20
|
+
import * as fs from 'fs';
|
|
21
|
+
import { listAllTargets, matrixPathsFor } from './delivery';
|
|
22
|
+
import { loadMatrixModel } from '../../exporters/matrix/export';
|
|
23
|
+
|
|
24
|
+
export interface UnitInspection {
|
|
25
|
+
unit: string;
|
|
26
|
+
approved: boolean;
|
|
27
|
+
frozen: boolean; // no Gate E drift against the approved fingerprints
|
|
28
|
+
goldenTotal: number;
|
|
29
|
+
goldenPassed: number;
|
|
30
|
+
goldenFailed: number;
|
|
31
|
+
goldenPending: number; // golden variants with no run result yet
|
|
32
|
+
goldenMocked: number; // golden variants verified only against mocks
|
|
33
|
+
perfPass: boolean | null; // null = no budget configured / no samples
|
|
34
|
+
reasons: string[]; // what blocks GO for this unit (empty = clean)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ProjectInspection {
|
|
38
|
+
units: UnitInspection[];
|
|
39
|
+
goldenTotal: number;
|
|
40
|
+
verdict: 'GO' | 'NO-GO';
|
|
41
|
+
reasons: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Pure aggregation over the delivery models — testable without the CLI. */
|
|
45
|
+
export function inspectProject(cwd: string): ProjectInspection {
|
|
46
|
+
const units: UnitInspection[] = [];
|
|
47
|
+
for (const target of listAllTargets(cwd)) {
|
|
48
|
+
const paths = matrixPathsFor(cwd, target);
|
|
49
|
+
if (!fs.existsSync(paths.mapFile)) continue; // no delivery map → not a delivered unit
|
|
50
|
+
const { model } = loadMatrixModel(paths);
|
|
51
|
+
if (!model) continue; // structural map errors surface in delivery
|
|
52
|
+
|
|
53
|
+
const variants = model.items.flatMap((it) => it.variants);
|
|
54
|
+
const golden = variants.filter((v) => v.golden);
|
|
55
|
+
if (golden.length === 0) continue; // Final Inspection reads only the golden set
|
|
56
|
+
|
|
57
|
+
const passed = golden.filter((v) => v.result?.status === 'passed');
|
|
58
|
+
const failed = golden.filter((v) => v.result && ['failed', 'timedOut', 'interrupted'].includes(v.result.status));
|
|
59
|
+
const pending = golden.filter((v) => !v.result);
|
|
60
|
+
const mocked = golden.filter((v) => v.mocked);
|
|
61
|
+
const approved = model.manifest.approvalState === 'approved';
|
|
62
|
+
const frozen = !model.findings.some((f) => f.gate === 'E');
|
|
63
|
+
const perfPass = model.perf ? model.perf.pass : null;
|
|
64
|
+
|
|
65
|
+
const reasons: string[] = [];
|
|
66
|
+
if (!approved) reasons.push('delivery map not approved');
|
|
67
|
+
if (!frozen) reasons.push('sources drifted since approval (not frozen — Gate E)');
|
|
68
|
+
if (failed.length) reasons.push(`${failed.length} golden variant(s) FAILED`);
|
|
69
|
+
if (pending.length) reasons.push(`${pending.length} golden variant(s) have no run result`);
|
|
70
|
+
if (mocked.length) reasons.push(`${mocked.length} golden variant(s) are mock-verified — Pass (Mocked) cannot release`);
|
|
71
|
+
if (perfPass === false) reasons.push('performance budget OVER (qa/perf.yaml)');
|
|
72
|
+
|
|
73
|
+
units.push({
|
|
74
|
+
unit: paths.label ?? target.name,
|
|
75
|
+
approved, frozen,
|
|
76
|
+
goldenTotal: golden.length,
|
|
77
|
+
goldenPassed: passed.length,
|
|
78
|
+
goldenFailed: failed.length,
|
|
79
|
+
goldenPending: pending.length,
|
|
80
|
+
goldenMocked: mocked.length,
|
|
81
|
+
perfPass,
|
|
82
|
+
reasons,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const goldenTotal = units.reduce((n, u) => n + u.goldenTotal, 0);
|
|
87
|
+
const reasons = units.flatMap((u) => u.reasons.map((r) => `${u.unit}: ${r}`));
|
|
88
|
+
if (goldenTotal === 0) reasons.push('no @golden scenario anywhere — nothing proves the release (tag the release-critical journeys)');
|
|
89
|
+
return { units, goldenTotal, verdict: reasons.length === 0 ? 'GO' : 'NO-GO', reasons };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function registerInspectCommand(program: Command): void {
|
|
93
|
+
program
|
|
94
|
+
.command('inspect')
|
|
95
|
+
.description('Final Inspection: Go/No-Go over the @golden set — results, freeze, mocks, perf (exit 2 = NO-GO)')
|
|
96
|
+
.option('--json', 'Output the raw JSON report')
|
|
97
|
+
.action((options) => {
|
|
98
|
+
try {
|
|
99
|
+
const report = inspectProject(process.cwd());
|
|
100
|
+
if (options.json) {
|
|
101
|
+
console.log(JSON.stringify(report, null, 2));
|
|
102
|
+
} else {
|
|
103
|
+
const L = console.log;
|
|
104
|
+
L('');
|
|
105
|
+
L(`━━━ Final Inspection — ${report.goldenTotal} @golden variant(s) across ${report.units.length} unit(s) ━━━`);
|
|
106
|
+
L('');
|
|
107
|
+
for (const u of report.units) {
|
|
108
|
+
const ok = u.reasons.length === 0;
|
|
109
|
+
L(` ${ok ? '✓' : '✗'} ${u.unit} — golden ${u.goldenPassed}/${u.goldenTotal} passed` +
|
|
110
|
+
`${u.goldenMocked ? ` · ${u.goldenMocked} mocked` : ''}` +
|
|
111
|
+
`${u.perfPass === null ? '' : u.perfPass ? ' · perf PASS' : ' · perf OVER'}` +
|
|
112
|
+
` · ${u.approved ? 'approved' : 'NOT approved'} · ${u.frozen ? 'frozen' : 'DRIFTED'}`);
|
|
113
|
+
for (const r of u.reasons) L(` • ${r}`);
|
|
114
|
+
}
|
|
115
|
+
L('');
|
|
116
|
+
L(` Verdict: ${report.verdict === 'GO' ? '✅ GO' : '⛔ NO-GO'}`);
|
|
117
|
+
for (const r of report.units.length === 0 ? report.reasons : (report.verdict === 'NO-GO' ? report.reasons.slice(0, 12) : [])) {
|
|
118
|
+
L(` • ${r}`);
|
|
119
|
+
}
|
|
120
|
+
L('');
|
|
121
|
+
}
|
|
122
|
+
if (report.verdict !== 'GO') process.exitCode = 2;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -55,6 +55,15 @@ export function registerTraceCommand(program: Command): void {
|
|
|
55
55
|
for (const line of r.mermaid.split('\n')) L(' ' + line);
|
|
56
56
|
L(' ```');
|
|
57
57
|
L('');
|
|
58
|
+
// ⑥ Deterministic next-step hand-back. The AI commands are INSTRUCTED to close
|
|
59
|
+
// every run by offering next actions, but a long generation run can end without
|
|
60
|
+
// it — leaving the operator guessing. Printing the choices here makes the
|
|
61
|
+
// hand-back survive the AI forgetting: trace is the converge step's last call.
|
|
62
|
+
L(' ⑥ NEXT STEPS');
|
|
63
|
+
L(` → /sungen:run-test ${name} (generate selectors, compile, run, auto-fix)`);
|
|
64
|
+
L(` → /sungen:create-test ${name} (extend coverage / next tier)`);
|
|
65
|
+
L(` → /sungen:delivery ${name} (export the Test Case & Coverage Matrix)`);
|
|
66
|
+
L('');
|
|
58
67
|
} catch (error) {
|
|
59
68
|
console.error('Error:', error instanceof Error ? error.message : error);
|
|
60
69
|
process.exit(1);
|
package/src/cli/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { registerFigmaCommand } from './commands/figma';
|
|
|
15
15
|
import { registerAddFlowCommand } from './commands/add-flow';
|
|
16
16
|
import { registerDashboardCommand } from './commands/dashboard';
|
|
17
17
|
import { registerAuditCommand } from './commands/audit';
|
|
18
|
+
import { registerInspectCommand } from './commands/inspect';
|
|
18
19
|
import { registerDepthLintCommand } from './commands/depth-lint';
|
|
19
20
|
import { registerJourneyCommand } from './commands/journey';
|
|
20
21
|
import { registerGateCommand } from './commands/gate';
|
|
@@ -63,6 +64,7 @@ async function main() {
|
|
|
63
64
|
registerAddFlowCommand(program);
|
|
64
65
|
registerDashboardCommand(program);
|
|
65
66
|
registerAuditCommand(program);
|
|
67
|
+
registerInspectCommand(program);
|
|
66
68
|
registerDepthLintCommand(program);
|
|
67
69
|
registerJourneyCommand(program);
|
|
68
70
|
registerGateCommand(program);
|
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
MAX_VARIANTS_PER_ITEM,
|
|
33
33
|
} from './types';
|
|
34
34
|
import { runGates } from './gates';
|
|
35
|
+
import { capabilityRegistry } from '../../capabilities/registry';
|
|
36
|
+
import { discoverAndRegisterCapabilities } from '../../capabilities/discover';
|
|
35
37
|
|
|
36
38
|
// ---------------------------------------------------------------------------
|
|
37
39
|
// Variant derivation
|
|
@@ -42,6 +44,27 @@ function normalizeShape(text: string): string {
|
|
|
42
44
|
return text.replace(/\{\{[^}]*\}\}/g, '{{*}}').replace(/\s+/g, ' ').trim();
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Is this scenario verified against MOCKED responses, and against which named deps? (#580)
|
|
49
|
+
* The mock capability owns its grammar: prefer the registry's `annotations` + `detectsStep`
|
|
50
|
+
* (driver-mock exports isMockStep — install AND call-count forms); fall back to literal
|
|
51
|
+
* twins of that grammar when the driver is not installed on this machine.
|
|
52
|
+
*/
|
|
53
|
+
export function detectMocked(tags: string[], stepTexts: string[]): { mocked: boolean; mockDeps: string[] } {
|
|
54
|
+
discoverAndRegisterCapabilities();
|
|
55
|
+
const cap = capabilityRegistry.get('mock');
|
|
56
|
+
const tagForms = cap?.annotations ?? ['@mock', '@network'];
|
|
57
|
+
const tagged = tags.some((t) => tagForms.some((a) => t === a || t.startsWith(`${a}:`)));
|
|
58
|
+
const isMockStep = cap?.detectsStep ?? ((t: string) =>
|
|
59
|
+
/\[[^\]]+\]\s+mock\s+is\s+active\b/i.test(t) ||
|
|
60
|
+
/\[[^\]]+\]\s+mock\s+called\s+(?:\{\{[^}]+\}\}|\d+)\s+times?\b/i.test(t));
|
|
61
|
+
const mockDeps = Array.from(new Set(stepTexts
|
|
62
|
+
.filter((t) => isMockStep(t))
|
|
63
|
+
.map((t) => (t.match(/\[([^\]]+)\]\s+mock\b/i) || [])[1])
|
|
64
|
+
.filter((x): x is string => Boolean(x))));
|
|
65
|
+
return { mocked: tagged || mockDeps.length > 0, mockDeps };
|
|
66
|
+
}
|
|
67
|
+
|
|
45
68
|
function deriveLayers(tags: string[]): MatrixLayer[] {
|
|
46
69
|
const layers = new Set<MatrixLayer>(['ui']);
|
|
47
70
|
if (tags.some((t) => t === '@api' || t.startsWith('@api:') || t === '@requires:api')) layers.add('api');
|
|
@@ -178,12 +201,29 @@ export function deriveVariants(inputs: Pick<BuildInputs, 'feature' | 'merged' |
|
|
|
178
201
|
...(apiCallsAreTrigger ? [] : apiRequestLines),
|
|
179
202
|
]));
|
|
180
203
|
|
|
204
|
+
// Mocked-run detection (#580): a pass against a scripted response must never read as a
|
|
205
|
+
// pass against the real dependency. The GRAMMAR authority is the mock capability itself
|
|
206
|
+
// (driver-mock declares `annotations` and exports `detectsStep`) — consult it instead of
|
|
207
|
+
// re-writing its regexes here, so new tag/step forms are picked up automatically. The
|
|
208
|
+
// literal fallback keeps delivery honest on machines WITHOUT the driver installed
|
|
209
|
+
// (delivery reads only feature text + results, it never compiles). Background steps are
|
|
210
|
+
// scanned too — a Background install mocks every scenario in the feature, and the
|
|
211
|
+
// driver's own spec warns that per-scenario tracking misses them.
|
|
212
|
+
const scenarioStepTexts = [
|
|
213
|
+
...inputs.feature.backgroundOrderedSteps.map((st) => st.text),
|
|
214
|
+
...m.feature.orderedSteps.map((st) => st.text),
|
|
215
|
+
];
|
|
216
|
+
const { mocked, mockDeps } = detectMocked(tags, scenarioStepTexts);
|
|
217
|
+
|
|
181
218
|
const base = {
|
|
182
219
|
vpId,
|
|
183
220
|
vpCategory,
|
|
184
221
|
priority: extractPriority(tags),
|
|
185
222
|
mode: mode as 'auto' | 'manual',
|
|
186
223
|
manualReason: manualReason(tags),
|
|
224
|
+
mocked,
|
|
225
|
+
...(mockDeps.length ? { mockDeps } : {}),
|
|
226
|
+
golden: tags.includes('@golden'),
|
|
187
227
|
layers: deriveLayers(tags),
|
|
188
228
|
traces: tags.filter((t) => t.startsWith('@spec:')).map((t) => t.slice('@spec:'.length)),
|
|
189
229
|
runtimeVars: [...runtimeVars],
|
|
@@ -17,6 +17,7 @@ import { mergeFeatureAndSpec } from '../scenario-merger';
|
|
|
17
17
|
import { getPackageVersion } from '../package-info';
|
|
18
18
|
import { writeCsv } from '../csv-exporter';
|
|
19
19
|
import { writeXlsx } from '../xlsx-exporter';
|
|
20
|
+
import { loadPerfConfig, perfVerdict } from '../../harness/perf';
|
|
20
21
|
import { loadDeliveryMap, writeDeliveryMap } from './map-loader';
|
|
21
22
|
import { mapContentFingerprint } from './fingerprint';
|
|
22
23
|
import { buildMatrix, deriveVariants } from './build';
|
|
@@ -89,6 +90,16 @@ export function loadMatrixModel(paths: MatrixTargetPaths): MatrixLoadResult {
|
|
|
89
90
|
specText: readSpecText(paths),
|
|
90
91
|
apiCatalog: catalogFor(paths),
|
|
91
92
|
});
|
|
93
|
+
// Perf verdict (#569): budgets are config (qa/perf.yaml), samples are the run's
|
|
94
|
+
// per-test durations. Advisory — reported here because delivery is where runs
|
|
95
|
+
// are read; the audit's score stays a pure function of the design artifacts.
|
|
96
|
+
const perfConfig = loadPerfConfig(paths.cwd);
|
|
97
|
+
if (perfConfig && results) {
|
|
98
|
+
const samples = [...results.values()]
|
|
99
|
+
.filter((r) => (r.status === 'passed' || r.status === 'failed' || r.status === 'timedOut') && typeof r.duration === 'number')
|
|
100
|
+
.map((r) => ({ title: r.testTitle, ms: r.duration as number }));
|
|
101
|
+
model.perf = perfVerdict(perfConfig, paths.unit, samples);
|
|
102
|
+
}
|
|
92
103
|
return { model, map, mapErrors: [] };
|
|
93
104
|
}
|
|
94
105
|
|
|
@@ -224,7 +224,7 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
|
|
|
224
224
|
// parent's steps + these read as one continuous procedure.
|
|
225
225
|
numbered(item.triggerDeltas[v.ref] ?? [], item.trigger.length + 1),
|
|
226
226
|
expectedWithVerification(v.oracle, v.verification),
|
|
227
|
-
v.mode === 'manual' ? `Manual${v.manualReason ? ` (${v.manualReason})` : ''}` : 'Auto',
|
|
227
|
+
v.mode === 'manual' ? `Manual${v.manualReason ? ` (${v.manualReason})` : ''}` : (v.mocked ? 'Auto · Mocked' : 'Auto'),
|
|
228
228
|
v.traces.join(', '),
|
|
229
229
|
variantState(v),
|
|
230
230
|
isoDate(v.result?.startTime),
|
|
@@ -315,6 +315,50 @@ function addCoverageSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersio
|
|
|
315
315
|
|
|
316
316
|
let rowIdx = 7;
|
|
317
317
|
|
|
318
|
+
// --- Performance budget (#569) — advisory: config (qa/perf.yaml) vs measured runs.
|
|
319
|
+
if (model.perf) {
|
|
320
|
+
const p = model.perf;
|
|
321
|
+
dataCell(ws.getRow(rowIdx).getCell(2), 'Performance budget', { bold: true });
|
|
322
|
+
rowIdx++;
|
|
323
|
+
const r = ws.getRow(rowIdx);
|
|
324
|
+
dataCell(r.getCell(2), `p${p.percentile} ${p.metric.replace('_ms', ' time')}`);
|
|
325
|
+
dataCell(r.getCell(3), p.pass ? 'PASS' : 'OVER BUDGET', { bold: true });
|
|
326
|
+
dataCell(r.getCell(4), `measured ${p.measuredMs}ms`);
|
|
327
|
+
dataCell(r.getCell(5), `budget ${p.budgetMs}ms`);
|
|
328
|
+
dataCell(r.getCell(6), `${p.samples} runs`);
|
|
329
|
+
if (!p.pass && p.slowest.length) dataCell(r.getCell(8), `slowest: ${p.slowest.map((x) => `${x.ms}ms ${x.title.slice(0, 40)}`).join(' · ')}`);
|
|
330
|
+
rowIdx += 2;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// --- External Dependency Tracking (#580, ISTQB system-test practice): every case that
|
|
334
|
+
// ran against a MOCK is listed centrally. Its Result stays in the canonical vocabulary
|
|
335
|
+
// (the roll-up formulas depend on it); THIS table is where "Pass (Mocked) ≠ Pass (Real)"
|
|
336
|
+
// is visible — a case verified only against mocks is not Done until re-verified real.
|
|
337
|
+
{
|
|
338
|
+
const mockedVars = model.items.flatMap((it) => it.variants.filter((v) => v.mocked).map((v) => ({ it, v })));
|
|
339
|
+
if (mockedVars.length > 0) {
|
|
340
|
+
dataCell(ws.getRow(rowIdx).getCell(2), 'External Dependency Tracking — mock-verified cases (Pass here = Pass (Mocked), not Pass (Real))', { bold: true });
|
|
341
|
+
rowIdx++;
|
|
342
|
+
const hd = ws.getRow(rowIdx);
|
|
343
|
+
dataCell(hd.getCell(2), 'Variant');
|
|
344
|
+
dataCell(hd.getCell(3), 'Mocked dependency');
|
|
345
|
+
dataCell(hd.getCell(4), 'Delivery item');
|
|
346
|
+
dataCell(hd.getCell(5), 'Result');
|
|
347
|
+
dataCell(hd.getCell(8), 'Real-environment re-verify');
|
|
348
|
+
rowIdx++;
|
|
349
|
+
for (const { it, v } of mockedVars) {
|
|
350
|
+
const r = ws.getRow(rowIdx);
|
|
351
|
+
dataCell(r.getCell(2), v.ref);
|
|
352
|
+
dataCell(r.getCell(3), (v.mockDeps ?? []).join(', ') || '(scenario-level @mock)');
|
|
353
|
+
dataCell(r.getCell(4), it.id);
|
|
354
|
+
dataCell(r.getCell(5), variantState(v));
|
|
355
|
+
dataCell(r.getCell(8), '');
|
|
356
|
+
rowIdx++;
|
|
357
|
+
}
|
|
358
|
+
rowIdx++;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
318
362
|
// --- Requirement coverage (review §6): every requirement id gets ONE explicit status.
|
|
319
363
|
if (model.requirements.length > 0) {
|
|
320
364
|
dataCell(ws.getRow(rowIdx).getCell(2), 'Requirement coverage — every id has an explicit status', { bold: true });
|
|
@@ -92,6 +92,14 @@ export interface CoverageVariant {
|
|
|
92
92
|
/** 'auto' | 'manual'; manual keeps its @manual:Mx reason. */
|
|
93
93
|
mode: 'auto' | 'manual';
|
|
94
94
|
manualReason?: string;
|
|
95
|
+
/** The scenario runs against MOCKED network responses (@mock / `[x] mock is active`) —
|
|
96
|
+
* a pass verifies the handling logic, not the real dependency (Pass (Mocked) ≠ Pass (Real),
|
|
97
|
+
* #580: ISTQB external-dependency tracking). */
|
|
98
|
+
mocked?: boolean;
|
|
99
|
+
/** Named mocked dependencies (`[name] mock is active` refs), for the tracking table. */
|
|
100
|
+
mockDeps?: string[];
|
|
101
|
+
/** Release-critical: tagged @golden — the Final Inspection set (`sungen inspect`, #580 P9). */
|
|
102
|
+
golden?: boolean;
|
|
95
103
|
layers: MatrixLayer[];
|
|
96
104
|
/** @spec:* requirement traces. */
|
|
97
105
|
traces: string[];
|
|
@@ -225,6 +233,8 @@ export interface MatrixModel {
|
|
|
225
233
|
requirements: RequirementCoverage[];
|
|
226
234
|
findings: MatrixFinding[];
|
|
227
235
|
manifest: MatrixManifest;
|
|
236
|
+
/** Perf verdict (#569) — present when qa/perf.yaml sets a budget AND the run produced durations. */
|
|
237
|
+
perf?: import('../../harness/perf').PerfVerdict | null;
|
|
228
238
|
}
|
|
229
239
|
|
|
230
240
|
/** Complexity warning threshold (rules draft Gate I; raised for compact grouping) — echoed in the manifest. */
|
|
@@ -54,6 +54,7 @@ interface PlaywrightSpec {
|
|
|
54
54
|
projectId?: string;
|
|
55
55
|
results?: Array<{
|
|
56
56
|
status?: string; // 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interrupted'
|
|
57
|
+
duration?: number; // ms — Playwright emits this on every result (#569 perf budgets)
|
|
57
58
|
startTime?: string;
|
|
58
59
|
error?: { message?: string; stack?: string };
|
|
59
60
|
attachments?: Array<{ name?: string; path?: string; contentType?: string }>;
|
|
@@ -120,6 +121,7 @@ export function loadPlaywrightReport(reportPath: string): Map<string, Playwright
|
|
|
120
121
|
result.set(stripped, {
|
|
121
122
|
testTitle: stripped,
|
|
122
123
|
status,
|
|
124
|
+
duration: typeof res?.duration === 'number' ? res.duration : undefined,
|
|
123
125
|
startTime: res?.startTime,
|
|
124
126
|
error: errorMsg,
|
|
125
127
|
tracePath: trace,
|
package/src/exporters/types.ts
CHANGED
|
@@ -121,6 +121,8 @@ export interface SpecFileData {
|
|
|
121
121
|
export interface PlaywrightResult {
|
|
122
122
|
testTitle: string;
|
|
123
123
|
status: 'passed' | 'failed' | 'skipped' | 'timedOut' | 'interrupted' | 'unknown';
|
|
124
|
+
/** Wall-clock run time in ms (from Playwright's results[].duration) — perf budgets (#569). */
|
|
125
|
+
duration?: number;
|
|
124
126
|
startTime?: string;
|
|
125
127
|
error?: string;
|
|
126
128
|
tracePath?: string;
|
|
@@ -35,12 +35,17 @@
|
|
|
35
35
|
* nothing for the query half to assert, so the step checks the PATH only. Not an
|
|
36
36
|
* error (the value may legitimately vary per environment), but the step asserts
|
|
37
37
|
* strictly less than it appears to
|
|
38
|
+
* SG-W015 — `see [X] card/item with {{n}}` where the value is a bare number: this compiles
|
|
39
|
+
* to a TEXT assertion (toHaveText('3')), NOT a count — an author reaching for
|
|
40
|
+
* "3 cards are shown" gets an assertion that can only pass if the card's text
|
|
41
|
+
* IS the digit. Count semantics exist on row/table ("see [T] table with {{n}}",
|
|
42
|
+
* "see [Ref] row with {{n}}") and lists ("should have count") only
|
|
38
43
|
*
|
|
39
44
|
* Diagnostics never block generation by themselves; `sungen generate --strict`
|
|
40
45
|
* turns any collected diagnostic into a non-zero exit.
|
|
41
46
|
*/
|
|
42
47
|
|
|
43
|
-
export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W005' | 'SG-W006' | 'SG-W007' | 'SG-W008' | 'SG-W009' | 'SG-W010' | 'SG-W011' | 'SG-W012' | 'SG-W013' | 'SG-W014';
|
|
48
|
+
export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W005' | 'SG-W006' | 'SG-W007' | 'SG-W008' | 'SG-W009' | 'SG-W010' | 'SG-W011' | 'SG-W012' | 'SG-W013' | 'SG-W014' | 'SG-W015';
|
|
44
49
|
|
|
45
50
|
export interface Diagnostic {
|
|
46
51
|
code: DiagnosticCode;
|