@sun-asterisk/sungen 3.2.15 → 3.2.16-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/dist/cli/commands/delivery.d.ts.map +1 -1
  2. package/dist/cli/commands/delivery.js +197 -13
  3. package/dist/cli/commands/delivery.js.map +1 -1
  4. package/dist/dashboard/templates/index.html +1 -1
  5. package/dist/exporters/feature-parser.d.ts +16 -1
  6. package/dist/exporters/feature-parser.d.ts.map +1 -1
  7. package/dist/exporters/feature-parser.js +21 -2
  8. package/dist/exporters/feature-parser.js.map +1 -1
  9. package/dist/exporters/matrix/build.d.ts +61 -0
  10. package/dist/exporters/matrix/build.d.ts.map +1 -0
  11. package/dist/exporters/matrix/build.js +497 -0
  12. package/dist/exporters/matrix/build.js.map +1 -0
  13. package/dist/exporters/matrix/export.d.ts +50 -0
  14. package/dist/exporters/matrix/export.d.ts.map +1 -0
  15. package/dist/exporters/matrix/export.js +152 -0
  16. package/dist/exporters/matrix/export.js.map +1 -0
  17. package/dist/exporters/matrix/fingerprint.d.ts +20 -0
  18. package/dist/exporters/matrix/fingerprint.d.ts.map +1 -0
  19. package/dist/exporters/matrix/fingerprint.js +83 -0
  20. package/dist/exporters/matrix/fingerprint.js.map +1 -0
  21. package/dist/exporters/matrix/gates.d.ts +21 -0
  22. package/dist/exporters/matrix/gates.d.ts.map +1 -0
  23. package/dist/exporters/matrix/gates.js +367 -0
  24. package/dist/exporters/matrix/gates.js.map +1 -0
  25. package/dist/exporters/matrix/map-loader.d.ts +22 -0
  26. package/dist/exporters/matrix/map-loader.d.ts.map +1 -0
  27. package/dist/exporters/matrix/map-loader.js +193 -0
  28. package/dist/exporters/matrix/map-loader.js.map +1 -0
  29. package/dist/exporters/matrix/render-csv.d.ts +9 -0
  30. package/dist/exporters/matrix/render-csv.d.ts.map +1 -0
  31. package/dist/exporters/matrix/render-csv.js +92 -0
  32. package/dist/exporters/matrix/render-csv.js.map +1 -0
  33. package/dist/exporters/matrix/render-xlsx.d.ts +41 -0
  34. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -0
  35. package/dist/exporters/matrix/render-xlsx.js +430 -0
  36. package/dist/exporters/matrix/render-xlsx.js.map +1 -0
  37. package/dist/exporters/matrix/types.d.ts +199 -0
  38. package/dist/exporters/matrix/types.d.ts.map +1 -0
  39. package/dist/exporters/matrix/types.js +15 -0
  40. package/dist/exporters/matrix/types.js.map +1 -0
  41. package/dist/exporters/matrix/wording.d.ts +61 -0
  42. package/dist/exporters/matrix/wording.d.ts.map +1 -0
  43. package/dist/exporters/matrix/wording.js +221 -0
  44. package/dist/exporters/matrix/wording.js.map +1 -0
  45. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  46. package/dist/exporters/scenario-merger.js +2 -1
  47. package/dist/exporters/scenario-merger.js.map +1 -1
  48. package/dist/exporters/spec-parser.d.ts.map +1 -1
  49. package/dist/exporters/spec-parser.js +2 -1
  50. package/dist/exporters/spec-parser.js.map +1 -1
  51. package/dist/exporters/xlsx-report-builder.d.ts +1 -1
  52. package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
  53. package/dist/exporters/xlsx-report-builder.js +3 -3
  54. package/dist/exporters/xlsx-report-builder.js.map +1 -1
  55. package/dist/harness/audit.d.ts.map +1 -1
  56. package/dist/harness/audit.js +16 -2
  57. package/dist/harness/audit.js.map +1 -1
  58. package/dist/harness/blindspot.d.ts.map +1 -1
  59. package/dist/harness/blindspot.js +2 -1
  60. package/dist/harness/blindspot.js.map +1 -1
  61. package/dist/harness/capability-plan.d.ts.map +1 -1
  62. package/dist/harness/capability-plan.js +3 -2
  63. package/dist/harness/capability-plan.js.map +1 -1
  64. package/dist/harness/capability.d.ts +25 -0
  65. package/dist/harness/capability.d.ts.map +1 -1
  66. package/dist/harness/capability.js +44 -2
  67. package/dist/harness/capability.js.map +1 -1
  68. package/dist/harness/feedback.d.ts.map +1 -1
  69. package/dist/harness/feedback.js +3 -2
  70. package/dist/harness/feedback.js.map +1 -1
  71. package/dist/harness/flow-check.d.ts.map +1 -1
  72. package/dist/harness/flow-check.js +2 -1
  73. package/dist/harness/flow-check.js.map +1 -1
  74. package/dist/harness/flow-plan.d.ts.map +1 -1
  75. package/dist/harness/flow-plan.js +3 -2
  76. package/dist/harness/flow-plan.js.map +1 -1
  77. package/dist/harness/intent.d.ts.map +1 -1
  78. package/dist/harness/intent.js +2 -1
  79. package/dist/harness/intent.js.map +1 -1
  80. package/dist/harness/journey.d.ts.map +1 -1
  81. package/dist/harness/journey.js +3 -2
  82. package/dist/harness/journey.js.map +1 -1
  83. package/dist/harness/ledger.d.ts.map +1 -1
  84. package/dist/harness/ledger.js +3 -2
  85. package/dist/harness/ledger.js.map +1 -1
  86. package/dist/harness/manifest.d.ts.map +1 -1
  87. package/dist/harness/manifest.js +4 -3
  88. package/dist/harness/manifest.js.map +1 -1
  89. package/dist/harness/parse.d.ts.map +1 -1
  90. package/dist/harness/parse.js +16 -3
  91. package/dist/harness/parse.js.map +1 -1
  92. package/dist/harness/quality-gates.d.ts.map +1 -1
  93. package/dist/harness/quality-gates.js +2 -1
  94. package/dist/harness/quality-gates.js.map +1 -1
  95. package/dist/harness/read-text.d.ts +22 -0
  96. package/dist/harness/read-text.d.ts.map +1 -0
  97. package/dist/harness/read-text.js +64 -0
  98. package/dist/harness/read-text.js.map +1 -0
  99. package/dist/harness/script-check.d.ts.map +1 -1
  100. package/dist/harness/script-check.js +3 -2
  101. package/dist/harness/script-check.js.map +1 -1
  102. package/dist/harness/sensors.d.ts.map +1 -1
  103. package/dist/harness/sensors.js +2 -10
  104. package/dist/harness/sensors.js.map +1 -1
  105. package/dist/harness/spec-coverage.d.ts +5 -0
  106. package/dist/harness/spec-coverage.d.ts.map +1 -1
  107. package/dist/harness/spec-coverage.js +17 -7
  108. package/dist/harness/spec-coverage.js.map +1 -1
  109. package/dist/harness/trace.d.ts.map +1 -1
  110. package/dist/harness/trace.js +4 -3
  111. package/dist/harness/trace.js.map +1 -1
  112. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  113. package/dist/harness/viewpoint-ledger.js +2 -1
  114. package/dist/harness/viewpoint-ledger.js.map +1 -1
  115. package/dist/orchestrator/templates/ai-src/commands/create-test.md +10 -1
  116. package/dist/orchestrator/templates/ai-src/commands/delivery.md +203 -120
  117. package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
  118. package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
  119. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +102 -6
  120. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +1 -0
  121. package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -1
  122. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +23 -0
  123. package/package.json +4 -4
  124. package/src/cli/commands/delivery.ts +227 -15
  125. package/src/dashboard/templates/index.html +1 -1
  126. package/src/exporters/feature-parser.ts +21 -2
  127. package/src/exporters/matrix/build.ts +532 -0
  128. package/src/exporters/matrix/export.ts +149 -0
  129. package/src/exporters/matrix/fingerprint.ts +49 -0
  130. package/src/exporters/matrix/gates.ts +400 -0
  131. package/src/exporters/matrix/map-loader.ts +163 -0
  132. package/src/exporters/matrix/render-csv.ts +94 -0
  133. package/src/exporters/matrix/render-xlsx.ts +420 -0
  134. package/src/exporters/matrix/types.ts +226 -0
  135. package/src/exporters/matrix/wording.ts +221 -0
  136. package/src/exporters/scenario-merger.ts +2 -1
  137. package/src/exporters/spec-parser.ts +2 -1
  138. package/src/exporters/xlsx-report-builder.ts +3 -2
  139. package/src/harness/audit.ts +17 -3
  140. package/src/harness/blindspot.ts +2 -1
  141. package/src/harness/capability-plan.ts +3 -2
  142. package/src/harness/capability.ts +53 -3
  143. package/src/harness/feedback.ts +3 -2
  144. package/src/harness/flow-check.ts +2 -1
  145. package/src/harness/flow-plan.ts +3 -2
  146. package/src/harness/intent.ts +2 -1
  147. package/src/harness/journey.ts +3 -2
  148. package/src/harness/ledger.ts +3 -2
  149. package/src/harness/manifest.ts +4 -3
  150. package/src/harness/parse.ts +17 -3
  151. package/src/harness/quality-gates.ts +2 -1
  152. package/src/harness/read-text.ts +28 -0
  153. package/src/harness/script-check.ts +3 -2
  154. package/src/harness/sensors.ts +2 -1
  155. package/src/harness/spec-coverage.ts +22 -7
  156. package/src/harness/trace.ts +4 -3
  157. package/src/harness/viewpoint-ledger.ts +2 -1
  158. package/src/orchestrator/templates/ai-src/commands/create-test.md +10 -1
  159. package/src/orchestrator/templates/ai-src/commands/delivery.md +203 -120
  160. package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
  161. package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
  162. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +102 -6
  163. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +1 -0
  164. package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -1
  165. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +23 -0
@@ -1,21 +1,117 @@
1
1
  ---
2
2
  name: sungen-delivery
3
- description: 'Export Gherkin scenarios + Playwright results CSV test case deliverable. Auto-loaded by delivery command.'
3
+ description: 'Export the Test Case & Coverage Matrix (or the legacy per-scenario CSV) from Gherkin + Playwright results. Auto-loaded by delivery command.'
4
4
  user-invocable: false
5
5
  ---
6
6
 
7
7
  ## Purpose
8
8
 
9
- Export test cases from Sungen screens and flows to a standardized CSV file (format BM-2-901-13) for QA delivery.
9
+ Export test cases from Sungen units to the QA deliverable. Two modes:
10
10
 
11
- **This skill delegates all heavy work to the `sungen delivery` CLI.** The CLI is the single source of truth for parsing logic — do NOT re-parse files in AI. Your role is only to:
11
+ - **Matrix (default)** the **Test Case & Coverage Matrix**: one row per *delivery item* (one
12
+ target + one test intent + one oracle family); scenarios / `@cases` rows stay as collapsible
13
+ *coverage variant* sub-rows. Built for review, manual execution, customer hand-off, and
14
+ reporting — no numbered step dumps.
15
+ - **Legacy (`--legacy`/`--full`)** — the classic per-scenario BM-2-901-13 workbook (documented
16
+ in the second half of this skill; unchanged).
12
17
 
13
- 1. Invoke the CLI
14
- 2. Show its output verbatim
15
- 3. Help the user react to pre-flight failures
18
+ **This skill delegates all heavy work to the `sungen delivery` CLI.** The CLI is the single source
19
+ of truth for parsing + gating. The AI's only semantic contribution is **proposing the Delivery
20
+ Map** (grouping) — everything else is: invoke the CLI, show output verbatim, help on failures.
16
21
 
17
22
  ---
18
23
 
24
+ ## Matrix mode — the Delivery Map
25
+
26
+ `qa/<screens|flows|api>/<unit>/delivery/<unit>.map.yaml` — committed, AI-proposed
27
+ (`review: proposed`), gate-validated, QA-approved (`sungen delivery --approve` stamps scenario
28
+ fingerprints). Schema + grouping rules live in the delivery command instructions; the normative
29
+ spec is `docs/spec/delivery-coverage-matrix-spec.md`.
30
+
31
+ **Gates** (CLI `--check`): A source (VP-ids unique, oracle present, Background setup-only) ·
32
+ B mapping (every scenario in exactly one group XOR one disposition) · C aggregation (test layer
33
+ recomputed and equal within a group — **execution mode and priority are coverage dimensions, not
34
+ splits**: mixed items show `Auto n · Manual m` and take the highest variant priority; heuristic
35
+ oracle-shape/precondition mismatches are review-level, silenced once approved and unchanged) ·
36
+ D executability (precondition · condition+
37
+ data · trigger · oracle all renderable; every `{{var}}` resolves; **no template token may survive
38
+ into a rendered cell** — test-data cross-references are resolved for display) · E drift
39
+ (scenario fingerprint mismatch → back to review; the map's OWN reviewed
40
+ wording/grouping is fingerprinted as `__map__` too, so post-approval edits re-open review) · G review state (proposed groups block the official
41
+ render; `--preview` renders a DRAFT watermark) · K category class (a `VP-SEC-*` variant outside
42
+ `category: security`, or `VP-NFR-*` outside `nfr` → warning: the Coverage grid's column would render
43
+ empty and report a false gap) · R requirement coverage (spec FR/TR/NFR ids with no trace and no
44
+ `requirements:` status → warning; **and a `status: covered` override that no variant traces to** →
45
+ warning: tag the proving scenario `@spec:<id>` instead of asserting it in prose) · W wording lint (map intent/oracle containing
46
+ tokens, `[Selector]` refs, DSL phrasing, or generator labels → warning).
47
+
48
+ **Wording normalization (deterministic, after semantic normalization):** DSL steps render as
49
+ controlled manual-test English without changing meaning — actions in the imperative (`User fill
50
+ [Email] field with X` → `Enter X in the Email field.`), expected results as observable assertions
51
+ (`User see [Jobs] page` → `The Jobs page is displayed.`), preconditions as states (`The user is
52
+ signed out.`). Manual `# Tester verifies:` labels become structured fields: `Setup:` →
53
+ Precondition, `Action:` → Action, `Observable:` → Expected Result, `Oracle:` → a separate
54
+ `Verification method:` line. Sequence-sensitive flows keep event order: actions numbered with
55
+ mid-flow assertions inline as `Verify: …`; only the final Then block is the Expected Result.
56
+ Empty test values render as `(empty)`.
57
+
58
+ **Output format**: **XLSX only by default** (one artifact). `--format csv` writes the flat CSV
59
+ instead; `--format both` writes both.
60
+
61
+ **Scope decides strictness**: a **named** unit that is not ready aborts with exit 1 (pass
62
+ `--continue-on-missing` to tolerate it); a **no-argument sweep** exports every ready unit, lists
63
+ every unit it did not export with the reason, and exits 0 as long as it wrote something — a
64
+ scaffolded unit never blocks the ready ones.
65
+
66
+ **Workbook**: `Testcases` sheet — parent rows + outline-level-1 variant sub-rows for **every**
67
+ item (single-variant included: the sub-row carries the source VP-id, resolved data, and the
68
+ result/evidence entry). Sub-rows are **delta-only**: knowledge common to the whole item
69
+ (precondition, trigger) is written ONCE on the parent; a child repeats only what distinguishes it
70
+ (condition/data, precondition delta, trigger when it differs) plus its own precise oracle and
71
+ execution fields. The parent never carries placeholder text — when triggers differ the cell is
72
+ simply empty and the sub-rows carry them.
73
+
74
+ Parent preconditions are the **intersection** of the variants' and the parent's Action holds the
75
+ **shared leading steps**; each variant renders only its remaining steps, numbered to continue the
76
+ parent's list — so a shared prefix is written once, not repeated on every child. The parent's
77
+ Coverage cell is the map's short `dimensions:` digest (or the variant refs when none is declared),
78
+ never a concatenation of variant titles.
79
+
80
+ Variant Result cells hold exactly one of `Passed`/`Failed`/`Blocked`/`Pending`/`N/A` (dropdown) —
81
+ that vocabulary is the contract: the parent Result and the summary band are **live Excel formulas**
82
+ counting those words, so a parent label is always a composed `n/m Passed …` string (`All N/A` when
83
+ every variant is N/A) and never a bare state word. **`N/A` leaves the denominator.** Evidence and
84
+ Defect ID are separate columns; ID + Target frozen; ISO dates; landscape print with repeated
85
+ header rows.
86
+
87
+ `Coverage` sheet — requirement coverage table (every FR/TR/NFR id with an explicit status),
88
+ target × category grid with explicit `—` gaps, dispositions, manifest. CSV mirrors the same model
89
+ flat with a `Level` column (`item`/`variant`) + a requirement-coverage appendix.
90
+ `delivery_item_count` ≠ progress — variants are the execution metric.
91
+
92
+ **Id schemes**: the `VP-<CAT>-<NNN>` convention is a suggestion, not a requirement — a project's own
93
+ scheme (`SEC-123`, `PER-345`, `MS-HP-001`) is addressed the same way, and the category segment
94
+ (`SEC`, `PER`) still drives Gate K. Requirement ids come from the spec's own bolded declarations
95
+ (`- **REQ-12**: …`), so a project that does not use FR/TR/NFR still gets its coverage table.
96
+
97
+ **Runtime-captured values**: a scenario may produce its own values — `User remember [X] text as
98
+ {{v}}` (compared later) or a capability binding (`@query:`/`@api:`/`@dataFactory:`). These never
99
+ exist in test-data, so Gate D does not ask for them: the capture step renders as an instruction
100
+ (`Note the X text for later (as "v")`) and later references read `the captured v`.
101
+
102
+ **Data fidelity**: invisible test data is made visible, never normalized — `''` → `(empty)`,
103
+ whitespace-only → `(5 spaces)`, padded → `" value "` quoted verbatim. A trim/collapse here would
104
+ silently break the whitespace tests it describes.
105
+
106
+ **Authoring guidance the matrix rewards** (create-test side): payload/provider matrices (SQLi
107
+ payload lists, OAuth provider sets) belong in `@cases` datasets so each case is an atomic,
108
+ independently-reportable variant; keep dataset `case:` labels short and stable (`CHK-EMAIL-I1`),
109
+ with descriptions in other columns — the label is part of the variant's identity.
110
+
111
+ ---
112
+
113
+ ## Legacy mode (--legacy / --full)
114
+
19
115
  ## Architecture
20
116
 
21
117
  ```
@@ -288,6 +288,7 @@ Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `colu
288
288
 
289
289
  | Tag | Effect |
290
290
  |---|---|
291
+ | `@spec:<id>` | **Traceability**: this scenario proves requirement `<id>` from `requirements/spec.md` (`@spec:FR-003`). Repeatable — a scenario may prove several. Read by `sungen audit` (requirement coverage) and by delivery (the Coverage sheet's requirement table). Without it the link is only inferred from wording and cannot survive an edit. |
291
292
  | `@manual` | Skip in generation |
292
293
  | `@auth:role` | Use auth storage state for role |
293
294
  | `@no-auth` | Disable inherited auth |
@@ -66,7 +66,7 @@ Use these when repairing GATE/DEPTH findings for the hard viewpoints (cart/detai
66
66
  2. **Stop when** `gateStatus == PASS` AND `findings` empty — or budget exhausted.
67
67
  3. **Never fake a pass.** A shallow `see [Cart] page` does not satisfy `cart-correctness`. If a gap is genuinely cross-screen or needs capabilities the DSL lacks (e.g. capture an element value to compare elsewhere), **report it as a residual gap / flow item** instead of forcing a green gate.
68
68
  4. **EP/data families are OK.** A `duplicates` cluster with `sameDataLikely=false` is an intentional equivalence-partition family (e.g. many invalid-email cases) — keep it; only collapse `sameDataLikely=true` exact duplicates.
69
- 5. **Advisory findings — surface, don't gate.** `MANUAL-REASON-MISMATCH` → fix the scenario's `@manual:Mx` code (so the planner recommends the right driver) during repair. `CAPABILITY-SUGGESTION` → **present it to the user as a next-step option** (e.g. "N @manual could be automated — `sungen capability add api db`?"), **recommend-only — never auto-install**. Neither fails the gate.
69
+ 5. **Advisory findings — surface, don't gate.** `MANUAL-REASON-MISMATCH` → fix the scenario's `@manual:Mx` code (so the planner recommends the right driver) during repair. `CAPABILITY-SUGGESTION` → **present it to the user as a next-step option** (e.g. "N @manual could be automated — `sungen capability add api db`?"), **recommend-only — never auto-install**. `VERIFICATION-OUT-OF-SCOPE (api|db)` → the unit uses `@api`/`@query`/`@requires:api|db` verification the project's recorded **test type** never opted into (E2E/UI-only per `capabilities.yaml`, and the viewpoint doesn't ask for it). During repair: **rewrite the oracle to be UI-observable** (or downgrade the deep check to an `(optional deep check: …)` note); only keep the API/DB verification if the project genuinely tests that layer — then record it (`verification: [ui, api]` / `sungen capability add`). Keep API/DB-in-E2E to the ≤20% band. Neither of these fails the gate.
70
70
 
71
71
  ## Discovery / fallback tree (when input is limited)
72
72
 
@@ -4,6 +4,28 @@ description: 'Use when create-test needs to translate spec.md/Figma/UI into .fea
4
4
  user-invocable: false
5
5
  ---
6
6
 
7
+
8
+ ## Traceability — tag the requirement each scenario proves
9
+
10
+ Every requirement id in `requirements/spec.md` (`FR-`/`TR-`/`NFR-`) must be reachable from a
11
+ scenario, and the link must be **written down, not inferred**:
12
+
13
+ ```gherkin
14
+ @high @spec:FR-003
15
+ Scenario: VP-VAL-002 An email in an invalid format shows the format error
16
+ ```
17
+
18
+ - Put `@spec:<id>` on the scenario that actually proves the requirement; repeat the tag when one
19
+ scenario proves several (`@spec:FR-003 @spec:FR-004`).
20
+ - One requirement may be proved by several scenarios — tag each of them.
21
+ - A requirement you deliberately do not cover here (performance budget, i18n suite, server-side
22
+ concern) needs no tag; record it later in the delivery map's `requirements:` section with a
23
+ status and reason.
24
+ - Why it matters: `sungen audit` also counts a requirement as covered when its keywords merely
25
+ resemble a scenario title, and reports `SPEC-TRACE-IMPLICIT` for those. Delivery's requirement
26
+ table follows **only** the explicit tag — an untagged requirement reads as an uncovered gap in
27
+ the customer-facing coverage sheet even when a scenario does prove it.
28
+
7
29
  ## ⚠️ Gotchas — read before generating
8
30
 
9
31
  - **Field-level test-data follows the Data Factory standard — don't hand-invent values.** When the unit has input fields, the field values (valid / boundary / invalid + error codes) come from the **`sungen-data-factory`** catalog via a field-map + `sungen data gen` (create-test step 5.3), not free-form guessing. Weave the standardized boundary/invalid sets into `@cases` with `CHK-*` trace. This skill still owns scenario structure, viewpoints, and non-field oracles.
@@ -328,6 +350,7 @@ Security: [S1 – admin only]
328
350
  Gate (`SG-W008`, both directions): a `@manual` with Gherkin steps but no `Then` fires — comments (even labeled `Oracle:` lines) can NOT substitute for the missing `Then`. A `@manual` with NO steps ALSO fires when its comments lack ≥1 numbered action line + ≥1 labeled `Observable:`/`Oracle:`/`Expected:` line — prose paragraphs and rationale-only comments export EMPTY Steps/Expected cells.
329
351
  In shape A the observable/oracle lines MUST carry their **label** (`Observable:` / `Oracle:` / `Expected:`) — the delivery parser extracts Expected results **by label**; unlabeled prose all lands in the Steps column and the Expected cell exports EMPTY. And the numbered action steps MUST precede them — without the actions the tester has an oracle but no procedure.
330
352
  **Oracle layer must match the unit** — a screen-unit manual defaults to a **UI oracle**: phrase the expected as what the tester SEES (on-screen message, list/table state after reload). If the business effect IS UI-observable, do NOT cite API responses or DB state as the oracle — "server rejects the request" is verified by *the error message on screen + no new row after reload*, not by reading the API response. Only when the effect has NO UI observation (mail content, stored hash, request count, response-body leak) may the oracle use another layer — then: (a) put the **layer label inside the line**: `Oracle: UI — …` / `Oracle: API — …` / `Oracle: DB — …` so QA knows the tooling needed from the deliverable alone; (b) tag `@manual:M2` so the capability planner recommends the api/db driver; (c) prefer the automation-ready `@requires:api`/`@query` form when the steps are expressible. A deep engineering check may ride along as `(optional deep check: …)` appended to the UI oracle — it must never REPLACE a UI-observable oracle.
353
+ **Respect the project's VERIFICATION SCOPE (the recorded test type).** Read `qa/capabilities.yaml` — the effective scope is `verification:` if set, else derived from `enabled` (`ui` always; `api`/`db` only if that driver is on). If the project is **E2E/UI-only** (no `api`/`db`) **and the test-viewpoint does not ask for API/DB verification**, do **NOT** author `@api`/`@query`/`@requires:api|db` verification — keep the oracle **UI-observable**, or leave the deep check as an `(optional deep check: …)` note. Author API/DB verification only when the scope (or an explicit viewpoint) calls for it, and even then keep the API/DB-in-E2E combination to a **≤20% band** of scenarios (the effects with no UI observation) — never the default. `sungen audit` reports `VERIFICATION-OUT-OF-SCOPE` when a unit uses API/DB verify the project never opted into.
331
354
  Use this comment shape:
332
355
  ```gherkin
333
356
  @high @manual
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sungen",
3
- "version": "3.2.15",
3
+ "version": "3.2.16-beta.10",
4
4
  "description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -12,7 +12,7 @@
12
12
  "copy-templates": "mkdir -p dist/generators/test-generator/adapters/playwright/templates/steps && mkdir -p dist/generators/test-generator/templates && mkdir -p dist/orchestrator/templates && mkdir -p dist/dashboard/templates && cp -r src/generators/test-generator/adapters/playwright/templates/*.hbs dist/generators/test-generator/adapters/playwright/templates/ 2>/dev/null || true && cp -r src/generators/test-generator/adapters/playwright/templates/steps dist/generators/test-generator/adapters/playwright/templates/ && mkdir -p dist/generators/test-generator/adapters/appium/templates/steps && cp -r src/generators/test-generator/adapters/appium/templates/*.hbs dist/generators/test-generator/adapters/appium/templates/ 2>/dev/null || true && cp -r src/generators/test-generator/adapters/appium/templates/steps dist/generators/test-generator/adapters/appium/templates/ && cp src/generators/test-generator/templates/*.hbs dist/generators/test-generator/templates/ 2>/dev/null || true && cp -r src/orchestrator/templates/* dist/orchestrator/templates/ && cp src/dashboard/templates/index.html dist/dashboard/templates/index.html && mkdir -p dist/harness/catalog && cp src/harness/catalog/*.yaml dist/harness/catalog/",
13
13
  "build:dashboard": "cd ../../dashboard && npm install --silent && npm run build && cd - && cp ../../dashboard/dist/index.html src/dashboard/templates/index.html",
14
14
  "dev": "tsx src/cli/index.ts",
15
- "test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/run.ts && tsx tests/eval/run.ts && tsx tests/exporter/run.ts && tsx tests/exporter/feature-parser-category.run.ts && tsx tests/exporter/api-detail-sheet.run.ts && tsx tests/exporter/overview-sheet.run.ts && tsx tests/exporter/delivery-divider-tcid.run.ts && tsx tests/exporter/note-expected-actual.run.ts && tsx tests/exporter/test-data-vars.run.ts && tsx tests/exporter/mobile-app-id.run.ts && tsx tests/exporter/api-testcase-cells.run.ts && tsx tests/exporter/delivery-cases-result.run.ts && tsx tests/exporter/precondition-auth-role.run.ts && tsx tests/exporter/report-builder-parity.run.ts && tsx tests/exporter/delivery-preflight-testdata.run.ts && tsx tests/exporter/delivery-preflight-selectors.run.ts && tsx tests/exporter/delivery-prerun-overview.run.ts && tsx tests/exporter/delivery-locale-env-discovery.run.ts && tsx tests/exporter/manual-testcase-testdata.run.ts && tsx tests/dashboard/api-flows-discovery.run.ts && tsx tests/dashboard/status-results-fallback.run.ts && tsx tests/codegen/verb-ref-masking.run.ts && tsx tests/codegen/table-aria-hidden-locators.run.ts && tsx tests/codegen/steps-base-not-compiled.run.ts && tsx tests/codegen/manual-oracle-gate.run.ts && tsx tests/codegen/locator-value-qualified-assertions.run.ts && tsx tests/codegen/table-match-data-column-mapping.run.ts && tsx tests/codegen/param-binding-native-type.run.ts && tsx tests/codegen/table-row-scope-redeclaration.run.ts && tsx tests/codegen/table-row-label-resolution.run.ts && tsx tests/api-runtime/base-path-url-join.run.ts && tsx tests/api-runtime/file-upload-multipart.run.ts && tsx tests/api-runtime/schema-assertion.run.ts && tsx tests/runtime/test-data-cross-ref.run.ts && tsx tests/runtime/test-data-array-projection.run.ts && tsx tests/runtime/env-secret-interpolation.run.ts && tsx tests/capabilities/run.ts && tsx tests/openapi/run.ts && tsx tests/api-field-coverage/run.ts && tsx tests/packaging/run.ts && tsx tests/generate-hint/run.ts && tsx tests/template-assertion/run.ts && tsx tests/journey/run.ts && tsx tests/harness/serial-cascade.run.ts && tsx tests/ai-skills/cross-assistant-orphan.run.ts && tsx tests/init/codex-mcp-config.run.ts && tsx src/orchestrator/ai-skills/golden-skills.test.ts && tsx tests/db-runtime/sql-placeholder-rewrite.run.ts && tsx tests/db-runtime/fallback-flow-codified.run.ts && tsx tests/db-runtime/cosmos-engine.run.ts && tsx tests/db-runtime/mongodb-engine.run.ts && tsx tests/db-runtime/dynamodb-engine.run.ts && tsx tests/db-runtime/mysql-integration.opt-in.run.ts && tsx tests/db-runtime/mongodb-integration.opt-in.run.ts && tsx tests/db-runtime/dynamodb-integration.opt-in.run.ts",
15
+ "test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/run.ts && tsx tests/eval/run.ts && tsx tests/exporter/run.ts && tsx tests/exporter/delivery-matrix.run.ts && tsx tests/exporter/delivery-scope-strictness.run.ts && tsx tests/exporter/feature-parser-category.run.ts && tsx tests/exporter/api-detail-sheet.run.ts && tsx tests/exporter/overview-sheet.run.ts && tsx tests/exporter/delivery-divider-tcid.run.ts && tsx tests/exporter/note-expected-actual.run.ts && tsx tests/exporter/test-data-vars.run.ts && tsx tests/exporter/mobile-app-id.run.ts && tsx tests/exporter/api-testcase-cells.run.ts && tsx tests/exporter/delivery-cases-result.run.ts && tsx tests/exporter/precondition-auth-role.run.ts && tsx tests/exporter/report-builder-parity.run.ts && tsx tests/exporter/delivery-preflight-testdata.run.ts && tsx tests/exporter/delivery-preflight-selectors.run.ts && tsx tests/exporter/delivery-prerun-overview.run.ts && tsx tests/exporter/delivery-locale-env-discovery.run.ts && tsx tests/exporter/manual-testcase-testdata.run.ts && tsx tests/dashboard/api-flows-discovery.run.ts && tsx tests/dashboard/status-results-fallback.run.ts && tsx tests/codegen/verb-ref-masking.run.ts && tsx tests/codegen/table-aria-hidden-locators.run.ts && tsx tests/codegen/steps-base-not-compiled.run.ts && tsx tests/codegen/manual-oracle-gate.run.ts && tsx tests/codegen/locator-value-qualified-assertions.run.ts && tsx tests/codegen/table-match-data-column-mapping.run.ts && tsx tests/codegen/param-binding-native-type.run.ts && tsx tests/codegen/table-row-scope-redeclaration.run.ts && tsx tests/codegen/table-row-label-resolution.run.ts && tsx tests/api-runtime/base-path-url-join.run.ts && tsx tests/api-runtime/file-upload-multipart.run.ts && tsx tests/api-runtime/schema-assertion.run.ts && tsx tests/runtime/test-data-cross-ref.run.ts && tsx tests/runtime/test-data-array-projection.run.ts && tsx tests/runtime/env-secret-interpolation.run.ts && tsx tests/capabilities/run.ts && tsx tests/openapi/run.ts && tsx tests/api-field-coverage/run.ts && tsx tests/packaging/run.ts && tsx tests/generate-hint/run.ts && tsx tests/template-assertion/run.ts && tsx tests/journey/run.ts && tsx tests/harness/serial-cascade.run.ts && tsx tests/harness/spec-trace-crlf.run.ts && tsx tests/ai-skills/cross-assistant-orphan.run.ts && tsx tests/init/codex-mcp-config.run.ts && tsx src/orchestrator/ai-skills/golden-skills.test.ts && tsx tests/db-runtime/sql-placeholder-rewrite.run.ts && tsx tests/db-runtime/fallback-flow-codified.run.ts && tsx tests/db-runtime/cosmos-engine.run.ts && tsx tests/db-runtime/mongodb-engine.run.ts && tsx tests/db-runtime/dynamodb-engine.run.ts && tsx tests/db-runtime/mysql-integration.opt-in.run.ts && tsx tests/db-runtime/mongodb-integration.opt-in.run.ts && tsx tests/db-runtime/dynamodb-integration.opt-in.run.ts",
16
16
  "test:update": "tsx tests/golden/run.ts --update && tsx tests/audit/run.ts --update && tsx tests/ingest/run.ts --update",
17
17
  "prepublishOnly": "npm run build:dashboard && npm run build"
18
18
  },
@@ -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.15",
43
- "@sungen/driver-ui": "3.2.15",
42
+ "@sungen/driver-data-factory": "3.2.16-beta.10",
43
+ "@sungen/driver-ui": "3.2.16-beta.10",
44
44
  "chalk": "^5.6.2",
45
45
  "commander": "^14.0.2",
46
46
  "dotenv": "^17.2.3",
@@ -25,6 +25,17 @@ import { renderXlsxMultiSheet, writeXlsx, buildApiDetailRows, addApiDetailSheet
25
25
  import { loadApiCatalog } from '../../exporters/api-catalog-loader';
26
26
  import { resolvePlatformAppId } from '../../exporters/mobile-app-id';
27
27
  import { EnvironmentInfo, PreflightCheck, ScreenSummary, TestCaseRow, ApiCatalogEntry } from '../../exporters/types';
28
+ import { mapFilePath } from '../../exporters/matrix/map-loader';
29
+ import {
30
+ MatrixTargetPaths,
31
+ loadMatrixModel,
32
+ approveMatrix,
33
+ writeMatrixDeliverables,
34
+ MatrixFormat,
35
+ hasErrors,
36
+ hasReview,
37
+ } from '../../exporters/matrix/export';
38
+ import { MatrixFinding } from '../../exporters/matrix/types';
28
39
 
29
40
  const COLOR = {
30
41
  reset: '\x1b[0m',
@@ -597,6 +608,129 @@ async function exportTarget(
597
608
  }
598
609
  }
599
610
 
611
+ // ----------------------------------------------------------------------------
612
+ // Matrix export (Delivery v2 — the default; --legacy keeps the classic workbook)
613
+ // ----------------------------------------------------------------------------
614
+
615
+ function matrixPathsFor(cwd: string, target: DeliveryTarget): MatrixTargetPaths {
616
+ const base = qaDir(cwd, target);
617
+ const genBase = generatedDir(cwd, target);
618
+ return {
619
+ cwd,
620
+ unit: target.featureBaseName,
621
+ label: target.isFlow ? `flow/${target.featureBaseName}` : target.featureBaseName,
622
+ featureFile: path.join(base, 'features', `${target.featureBaseName}.feature`),
623
+ testDataFile: resolveTestDataPathForTarget(cwd, target),
624
+ specFile: path.join(genBase, `${target.featureBaseName}.spec.ts`),
625
+ specMdFile: path.join(base, 'requirements', 'spec.md'),
626
+ resultsPath: resolveResultsPath(cwd, target),
627
+ mapFile: mapFilePath(base, target.featureBaseName),
628
+ };
629
+ }
630
+
631
+ function printFindings(findings: MatrixFinding[]): void {
632
+ const icon = { error: `${COLOR.red}✗`, review: `${COLOR.yellow}⚠`, warning: `${COLOR.gray}·` } as const;
633
+ for (const f of findings) {
634
+ log(` ${icon[f.severity]} [Gate ${f.gate}]${COLOR.reset} ${f.ref ? `${f.ref}: ` : ''}${f.message}`);
635
+ }
636
+ }
637
+
638
+ interface MatrixRunSummary {
639
+ label: string;
640
+ items: number;
641
+ variants: number;
642
+ passed: number;
643
+ failed: number;
644
+ file: string;
645
+ }
646
+
647
+ /**
648
+ * Matrix-mode export of one target. Returns null when blocked (missing map,
649
+ * gate errors, or unapproved groups without --preview) — the caller aggregates
650
+ * a non-zero exit.
651
+ */
652
+ async function exportMatrixTarget(
653
+ cwd: string,
654
+ target: DeliveryTarget,
655
+ opts: { check?: boolean; preview?: boolean; format?: MatrixFormat },
656
+ ): Promise<MatrixRunSummary | null> {
657
+ const paths = matrixPathsFor(cwd, target);
658
+ const { model, mapMissing, mapErrors } = loadMatrixModel(paths);
659
+
660
+ if (mapMissing) {
661
+ log(`${COLOR.yellow}${paths.label}: no delivery map${COLOR.reset} (${path.relative(cwd, paths.mapFile)})`);
662
+ log(` The matrix needs the reviewed grouping (target / intent / oracle per delivery item).`);
663
+ log(` ${COLOR.cyan}→ run /sungen:delivery ${target.screen} so the map is proposed for your review${COLOR.reset}`);
664
+ log(` ${COLOR.cyan}→ or export the classic per-scenario workbook: sungen delivery ${target.screen} --legacy${COLOR.reset}`);
665
+ return null;
666
+ }
667
+ if (mapErrors.length > 0 || !model) {
668
+ log(`${COLOR.red}${paths.label}: delivery map is invalid${COLOR.reset}`);
669
+ for (const e of mapErrors) log(` - ${e}`);
670
+ return null;
671
+ }
672
+
673
+ log(`${COLOR.bold}${paths.label}${COLOR.reset} — ${model.manifest.itemCount} delivery item(s), ${model.manifest.variantCount} coverage variant(s), state: ${model.manifest.approvalState}`);
674
+ printFindings(model.findings);
675
+
676
+ if (opts.check) {
677
+ // Gates-only run: the summary IS the output; exit code reflects errors.
678
+ return hasErrors(model.findings)
679
+ ? null
680
+ : { label: paths.label, items: model.manifest.itemCount, variants: model.manifest.variantCount, passed: 0, failed: 0, file: '(check only)' };
681
+ }
682
+ if (hasErrors(model.findings)) {
683
+ log(` ${COLOR.red}blocked:${COLOR.reset} fix the Gate errors above, then re-run.`);
684
+ return null;
685
+ }
686
+ if (hasReview(model.findings) && !opts.preview) {
687
+ log(` ${COLOR.yellow}not approved for official delivery.${COLOR.reset}`);
688
+ log(` ${COLOR.cyan}→ approve the reviewed grouping: sungen delivery ${target.screen} --approve${COLOR.reset}`);
689
+ log(` ${COLOR.cyan}→ or render a watermarked draft: sungen delivery ${target.screen} --preview${COLOR.reset}`);
690
+ return null;
691
+ }
692
+
693
+ const { csvPath, xlsxPath } = await writeMatrixDeliverables(paths, model, opts.format ?? 'xlsx');
694
+ const passed = model.items.reduce((a, i) => a + i.resultCounts.passed, 0);
695
+ const failed = model.items.reduce((a, i) => a + i.resultCounts.failed, 0);
696
+ const written = [xlsxPath, csvPath].filter((p): p is string => !!p).map((p) => path.relative(cwd, p));
697
+ for (const rel of written) log(` ${COLOR.green}→ ${rel}${COLOR.reset}`);
698
+ return {
699
+ label: paths.label,
700
+ items: model.manifest.itemCount,
701
+ variants: model.manifest.variantCount,
702
+ passed,
703
+ failed,
704
+ file: written.join(' + '),
705
+ };
706
+ }
707
+
708
+ /**
709
+ * One place that accounts for every target the run did NOT write, so a sweep is
710
+ * never silently partial: scaffolds skipped by preflight and units whose gates /
711
+ * missing map stopped the export.
712
+ */
713
+ function printSkipSummary(skipped: string[], notExported: string[]): void {
714
+ if (skipped.length === 0 && notExported.length === 0) return;
715
+ log(`${COLOR.gray}Not exported (${skipped.length + notExported.length}):${COLOR.reset}`);
716
+ for (const s of skipped) log(` ${COLOR.gray}· ${s}${COLOR.reset}`);
717
+ for (const n of notExported) log(` ${COLOR.gray}· ${n} (see the message above)${COLOR.reset}`);
718
+ log('');
719
+ }
720
+
721
+ function printMatrixSummaryTable(summaries: MatrixRunSummary[]): void {
722
+ log(`\n${COLOR.bold}Delivery matrix export complete${COLOR.reset}\n`);
723
+ const colWidth = Math.max(20, ...summaries.map((s) => s.label.length)) + 1;
724
+ log(` ${'Feature'.padEnd(colWidth)}Items Variants Passed Failed File(s)`);
725
+ log(' ' + '-'.repeat(colWidth + 50));
726
+ for (const s of summaries) {
727
+ log(
728
+ ` ${s.label.padEnd(colWidth)}${String(s.items).padStart(5)} ${String(s.variants).padStart(8)} ${String(s.passed).padStart(6)} ${String(s.failed).padStart(6)} ${s.file}`
729
+ );
730
+ }
731
+ log(`\n${COLOR.gray}Items = review rows (one target + intent + oracle family). Variants = execution units — use variant counts for progress.${COLOR.reset}\n`);
732
+ }
733
+
600
734
  // ----------------------------------------------------------------------------
601
735
  // Reporter
602
736
  // ----------------------------------------------------------------------------
@@ -667,23 +801,41 @@ function printSummaryTable(summaries: ScreenSummary[]): void {
667
801
  export function registerDeliveryCommand(program: Command): void {
668
802
  program
669
803
  .command('delivery')
670
- .description('Export Gherkin + Playwright results CSV test case deliverable')
804
+ .description('Export the Test Case & Coverage Matrix (Gherkin + results). --legacy keeps the classic per-scenario workbook.')
671
805
  .argument('[names...]', 'Specific screen or flow names. Omit to process all.')
672
806
  .option('--skip-preflight', 'Skip pre-flight checks (not recommended)')
673
807
  .option('--continue-on-missing', 'Skip targets with blocking misses instead of aborting')
674
808
  .option('--env <env>', 'Environment/locale of the run to deliver (sets SUNGEN_ENV; e.g. dev, staging, ja, dev-ja)')
675
- .action(async (names: string[], options: { skipPreflight?: boolean; continueOnMissing?: boolean; env?: string }) => {
809
+ .option('--legacy', 'Classic per-scenario-steps workbook (BM-2-901-13 layout)')
810
+ .option('--full', 'Alias of --legacy')
811
+ .option('--check', 'Matrix gates only — validate the delivery map, write nothing')
812
+ .option('--approve [ids]', 'Approve proposed groups (all, or a comma-separated id list) + stamp fingerprints')
813
+ .option('--preview', 'Render the matrix despite review-required findings (DRAFT watermark)')
814
+ .option('--format <format>', 'Matrix output format: xlsx (default) | csv | both', 'xlsx')
815
+ .action(async (names: string[], options: {
816
+ skipPreflight?: boolean; continueOnMissing?: boolean; env?: string;
817
+ legacy?: boolean; full?: boolean; check?: boolean; approve?: boolean | string; preview?: boolean;
818
+ format?: string;
819
+ }) => {
676
820
  try {
677
821
  // Same effect as running with SUNGEN_ENV=<env> — result-file resolution,
678
822
  // locale pairing, and test-data overlays all key off this one string.
679
823
  if (options.env) process.env.SUNGEN_ENV = options.env;
680
824
  const cwd = process.cwd();
681
825
 
826
+ // Matrix output format — one artifact by default (xlsx); csv/both on request.
827
+ const format = (options.format ?? 'xlsx').toLowerCase();
828
+ if (!['xlsx', 'csv', 'both'].includes(format)) {
829
+ console.error(`${COLOR.red}Invalid --format "${options.format}"${COLOR.reset} — use xlsx | csv | both`);
830
+ process.exit(1);
831
+ }
832
+
682
833
  // 1. Scope detection — each positional name expands into one target
683
834
  // per `.feature` file inside that screen/flow. Passing a specific
684
835
  // feature basename (e.g. `home-modal`) narrows to that one file.
685
836
  let targets: DeliveryTarget[];
686
- if (names && names.length > 0) {
837
+ const explicitScope = !!(names && names.length > 0);
838
+ if (explicitScope) {
687
839
  targets = names.flatMap((n) => resolveTargetsFromArg(cwd, n));
688
840
  } else {
689
841
  targets = listAllTargets(cwd);
@@ -696,8 +848,13 @@ export function registerDeliveryCommand(program: Command): void {
696
848
  const labels = targets.map((t) => t.isFlow ? `flow/${t.featureBaseName}` : t.featureBaseName);
697
849
  log(`${COLOR.bold}sungen delivery${COLOR.reset} — exporting ${targets.length} feature(s): ${labels.join(', ')}\n`);
698
850
 
699
- // 2. Pre-flight
851
+ // 2. Pre-flight — strictness follows the SCOPE. Naming a unit means "I want
852
+ // this one" (fail loudly); a no-argument sweep means "export everything
853
+ // that is ready", so scaffolded units (added but not yet authored) are
854
+ // skipped with a note instead of aborting the whole run — otherwise one
855
+ // untouched scaffold in a work-in-progress project produces zero files.
700
856
  let toExport: DeliveryTarget[];
857
+ const skipped: string[] = [];
701
858
  if (options.skipPreflight) {
702
859
  toExport = targets;
703
860
  } else {
@@ -706,28 +863,79 @@ export function registerDeliveryCommand(program: Command): void {
706
863
 
707
864
  const blockers = checks.filter(hasBlockingMissing);
708
865
  if (blockers.length > 0) {
709
- if (options.continueOnMissing) {
710
- const passedScreens = new Set(
711
- checks.filter((c) => !hasBlockingMissing(c)).map((c) => c.screen)
712
- );
713
- toExport = targets.filter((t) => {
714
- const label = t.isFlow ? `flow/${t.featureBaseName}` : t.featureBaseName;
715
- return passedScreens.has(label);
716
- });
717
- log(`${COLOR.yellow}Continuing with ${toExport.length} ready target(s).${COLOR.reset}\n`);
718
- } else {
866
+ if (explicitScope && !options.continueOnMissing) {
719
867
  console.error(
720
- `${COLOR.red}Aborted:${COLOR.reset} ${blockers.length} target(s) have blocking issues.\n` +
868
+ `${COLOR.red}Aborted:${COLOR.reset} ${blockers.length} named target(s) have blocking issues.\n` +
721
869
  `Run the suggested commands above, or use ${COLOR.cyan}--continue-on-missing${COLOR.reset} to skip them.`
722
870
  );
723
871
  process.exit(1);
724
872
  }
873
+ const readyLabels = new Set(
874
+ checks.filter((c) => !hasBlockingMissing(c)).map((c) => c.screen)
875
+ );
876
+ toExport = targets.filter((t) => {
877
+ const label = t.isFlow ? `flow/${t.featureBaseName}` : t.featureBaseName;
878
+ return readyLabels.has(label);
879
+ });
880
+ for (const b of blockers) skipped.push(`${b.screen} (not authored yet — run /sungen:create-test)`);
881
+ log(`${COLOR.yellow}Skipping ${blockers.length} target(s) that have no test cases yet; continuing with ${toExport.length}.${COLOR.reset}\n`);
725
882
  } else {
726
883
  toExport = targets;
727
884
  }
728
885
  }
729
886
 
730
887
  // 3. Export
888
+ const legacy = options.legacy || options.full;
889
+ if (!legacy) {
890
+ // Matrix mode (the default). --approve is its own sub-action.
891
+ if (options.approve !== undefined) {
892
+ const ids = typeof options.approve === 'string'
893
+ ? options.approve.split(',').map((s) => s.trim()).filter(Boolean)
894
+ : undefined;
895
+ let blocked = false;
896
+ for (const target of toExport) {
897
+ const paths = matrixPathsFor(cwd, target);
898
+ try {
899
+ const { findings, approved } = approveMatrix(paths, ids);
900
+ if (approved.length === 0) {
901
+ log(`${COLOR.red}${paths.label}: approval blocked by Gate errors${COLOR.reset}`);
902
+ printFindings(findings);
903
+ blocked = true;
904
+ } else {
905
+ log(`${COLOR.green}${paths.label}: approved ${approved.length} group(s)${COLOR.reset} — fingerprints stamped in ${path.relative(cwd, paths.mapFile)}`);
906
+ }
907
+ } catch (e) {
908
+ log(`${COLOR.red}${paths.label}:${COLOR.reset} ${e instanceof Error ? e.message : e}`);
909
+ blocked = true;
910
+ }
911
+ }
912
+ if (blocked) process.exit(1);
913
+ return;
914
+ }
915
+
916
+ const matrixSummaries: MatrixRunSummary[] = [];
917
+ const notExported: string[] = [];
918
+ for (const target of toExport) {
919
+ const s = await exportMatrixTarget(cwd, target, {
920
+ check: options.check, preview: options.preview, format: format as MatrixFormat,
921
+ });
922
+ if (s) matrixSummaries.push(s);
923
+ else notExported.push(target.isFlow ? `flow/${target.featureBaseName}` : target.featureBaseName);
924
+ }
925
+ if (matrixSummaries.length > 0 && !options.check) printMatrixSummaryTable(matrixSummaries);
926
+ printSkipSummary(skipped, notExported);
927
+ // A named target that produced nothing is a failure. A sweep is a failure
928
+ // only when it produced nothing at all — units still waiting for a map or
929
+ // an approval are the normal state of a live project. Preflight skips are
930
+ // not counted here: reaching this point in an explicit scope means
931
+ // --continue-on-missing was passed, which IS the opt-in to tolerate them.
932
+ const failed = explicitScope
933
+ ? notExported.length > 0
934
+ : matrixSummaries.length === 0;
935
+ if (failed) process.exit(1);
936
+ return;
937
+ }
938
+
731
939
  const env = getEnvironment(cwd);
732
940
  const summaries: ScreenSummary[] = [];
733
941
  for (const target of toExport) {
@@ -737,6 +945,10 @@ export function registerDeliveryCommand(program: Command): void {
737
945
 
738
946
  // 4. Summary
739
947
  printSummaryTable(summaries);
948
+ printSkipSummary(skipped, []);
949
+ // Same rule as the matrix path: every attempted target in an explicit scope
950
+ // must have produced a file; a sweep only fails when nothing was written.
951
+ if (explicitScope ? summaries.length < toExport.length : summaries.length === 0) process.exit(1);
740
952
  } catch (err) {
741
953
  console.error(`${COLOR.red}Fatal:${COLOR.reset} ${err instanceof Error ? err.message : err}`);
742
954
  process.exit(1);