@sun-asterisk/sungen 3.2.14 → 3.2.16-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.
Files changed (127) hide show
  1. package/dist/cli/commands/delivery.d.ts.map +1 -1
  2. package/dist/cli/commands/delivery.js +167 -51
  3. package/dist/cli/commands/delivery.js.map +1 -1
  4. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  5. package/dist/dashboard/snapshot-builder.js +3 -46
  6. package/dist/dashboard/snapshot-builder.js.map +1 -1
  7. package/dist/dashboard/templates/index.html +1 -1
  8. package/dist/exporters/feature-parser.js +1 -1
  9. package/dist/exporters/feature-parser.js.map +1 -1
  10. package/dist/exporters/locale-variants.d.ts +38 -0
  11. package/dist/exporters/locale-variants.d.ts.map +1 -0
  12. package/dist/exporters/locale-variants.js +159 -0
  13. package/dist/exporters/locale-variants.js.map +1 -0
  14. package/dist/exporters/matrix/build.d.ts +36 -0
  15. package/dist/exporters/matrix/build.d.ts.map +1 -0
  16. package/dist/exporters/matrix/build.js +267 -0
  17. package/dist/exporters/matrix/build.js.map +1 -0
  18. package/dist/exporters/matrix/export.d.ts +47 -0
  19. package/dist/exporters/matrix/export.d.ts.map +1 -0
  20. package/dist/exporters/matrix/export.js +136 -0
  21. package/dist/exporters/matrix/export.js.map +1 -0
  22. package/dist/exporters/matrix/fingerprint.d.ts +20 -0
  23. package/dist/exporters/matrix/fingerprint.d.ts.map +1 -0
  24. package/dist/exporters/matrix/fingerprint.js +83 -0
  25. package/dist/exporters/matrix/fingerprint.js.map +1 -0
  26. package/dist/exporters/matrix/gates.d.ts +21 -0
  27. package/dist/exporters/matrix/gates.d.ts.map +1 -0
  28. package/dist/exporters/matrix/gates.js +258 -0
  29. package/dist/exporters/matrix/gates.js.map +1 -0
  30. package/dist/exporters/matrix/map-loader.d.ts +22 -0
  31. package/dist/exporters/matrix/map-loader.d.ts.map +1 -0
  32. package/dist/exporters/matrix/map-loader.js +173 -0
  33. package/dist/exporters/matrix/map-loader.js.map +1 -0
  34. package/dist/exporters/matrix/render-csv.d.ts +8 -0
  35. package/dist/exporters/matrix/render-csv.d.ts.map +1 -0
  36. package/dist/exporters/matrix/render-csv.js +69 -0
  37. package/dist/exporters/matrix/render-csv.js.map +1 -0
  38. package/dist/exporters/matrix/render-xlsx.d.ts +17 -0
  39. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -0
  40. package/dist/exporters/matrix/render-xlsx.js +298 -0
  41. package/dist/exporters/matrix/render-xlsx.js.map +1 -0
  42. package/dist/exporters/matrix/types.d.ts +153 -0
  43. package/dist/exporters/matrix/types.d.ts.map +1 -0
  44. package/dist/exporters/matrix/types.js +15 -0
  45. package/dist/exporters/matrix/types.js.map +1 -0
  46. package/dist/exporters/scenario-merger.js +1 -1
  47. package/dist/exporters/scenario-merger.js.map +1 -1
  48. package/dist/exporters/spec-parser.js +1 -1
  49. package/dist/exporters/spec-parser.js.map +1 -1
  50. package/dist/exporters/xlsx-report-builder.d.ts +1 -1
  51. package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
  52. package/dist/exporters/xlsx-report-builder.js +3 -3
  53. package/dist/exporters/xlsx-report-builder.js.map +1 -1
  54. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-match-data.hbs +3 -11
  55. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +11 -1
  56. package/dist/generators/test-generator/code-generator.js +1 -1
  57. package/dist/generators/test-generator/code-generator.js.map +1 -1
  58. package/dist/generators/test-generator/diagnostics.d.ts +4 -1
  59. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  60. package/dist/generators/test-generator/diagnostics.js +3 -0
  61. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  62. package/dist/generators/test-generator/step-mapper.d.ts +6 -0
  63. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  64. package/dist/generators/test-generator/step-mapper.js +41 -2
  65. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  66. package/dist/harness/annotation-overrides.d.ts +4 -1
  67. package/dist/harness/annotation-overrides.d.ts.map +1 -1
  68. package/dist/harness/annotation-overrides.js +5 -2
  69. package/dist/harness/annotation-overrides.js.map +1 -1
  70. package/dist/harness/audit.d.ts.map +1 -1
  71. package/dist/harness/audit.js +6 -1
  72. package/dist/harness/audit.js.map +1 -1
  73. package/dist/harness/capability.d.ts +25 -0
  74. package/dist/harness/capability.d.ts.map +1 -1
  75. package/dist/harness/capability.js +44 -2
  76. package/dist/harness/capability.js.map +1 -1
  77. package/dist/harness/quality-gates.d.ts.map +1 -1
  78. package/dist/harness/quality-gates.js +5 -0
  79. package/dist/harness/quality-gates.js.map +1 -1
  80. package/dist/orchestrator/templates/ai-src/commands/create-test.md +1 -1
  81. package/dist/orchestrator/templates/ai-src/commands/delivery.md +118 -120
  82. package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
  83. package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
  84. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +43 -7
  85. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +1 -0
  86. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +8 -0
  87. package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -1
  88. package/dist/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +8 -0
  89. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +1 -0
  90. package/dist/orchestrator/templates/specs-test-data.ts +13 -4
  91. package/package.json +4 -4
  92. package/src/cli/commands/delivery.ts +199 -71
  93. package/src/dashboard/snapshot-builder.ts +11 -61
  94. package/src/dashboard/templates/index.html +1 -1
  95. package/src/exporters/feature-parser.ts +1 -1
  96. package/src/exporters/locale-variants.ts +157 -0
  97. package/src/exporters/matrix/build.ts +294 -0
  98. package/src/exporters/matrix/export.ts +124 -0
  99. package/src/exporters/matrix/fingerprint.ts +49 -0
  100. package/src/exporters/matrix/gates.ts +284 -0
  101. package/src/exporters/matrix/map-loader.ts +142 -0
  102. package/src/exporters/matrix/render-csv.ts +71 -0
  103. package/src/exporters/matrix/render-xlsx.ts +289 -0
  104. package/src/exporters/matrix/types.ts +176 -0
  105. package/src/exporters/scenario-merger.ts +1 -1
  106. package/src/exporters/spec-parser.ts +1 -1
  107. package/src/exporters/xlsx-report-builder.ts +3 -2
  108. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-match-data.hbs +3 -11
  109. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +11 -1
  110. package/src/generators/test-generator/code-generator.ts +1 -1
  111. package/src/generators/test-generator/diagnostics.ts +4 -1
  112. package/src/generators/test-generator/step-mapper.ts +43 -2
  113. package/src/harness/annotation-overrides.ts +5 -2
  114. package/src/harness/audit.ts +7 -2
  115. package/src/harness/capability.ts +53 -3
  116. package/src/harness/quality-gates.ts +4 -0
  117. package/src/orchestrator/templates/ai-src/commands/create-test.md +1 -1
  118. package/src/orchestrator/templates/ai-src/commands/delivery.md +118 -120
  119. package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
  120. package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
  121. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +43 -7
  122. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +1 -0
  123. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +8 -0
  124. package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -1
  125. package/src/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +8 -0
  126. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +1 -0
  127. package/src/orchestrator/templates/specs-test-data.ts +13 -4
@@ -30,7 +30,7 @@ You generate 3 files for sungen — a Gherkin compiler that produces Playwright
30
30
  | `/sungen-create-data-test <name>` | Generate standardized test-data (valid/boundary/invalid + CHK trace) from the Data Factory catalog; no name = all units (asks to confirm) |
31
31
  | `/sungen-review <name>` | Score syntax, coverage, viewpoint quality (auto-detects screen or flow) |
32
32
  | `/sungen-run-test <name>` | Generate `selectors.yaml`, compile, run, auto-fix (auto-detects screen or flow) |
33
- | `/sungen-delivery [name...]` | Export test cases CSV for QA delivery (all screens if no arg) |
33
+ | `/sungen-delivery [name...]` | Export the Test Case & Coverage Matrix (review/manual/customer deliverable; `--legacy` = classic CSV; all screens if no arg) |
34
34
  | `/sungen-locale <name> <locale>` | Bootstrap i18n for a screen — audit selectors, detect locale switch, generate overlay (run before `/sungen-run-test --env <locale>`) |
35
35
 
36
36
  **Screen path:** add-screen → create-test → review → run-test → delivery.
@@ -1,21 +1,51 @@
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 (mode/layer/
33
+ priority recomputed and equal within a group — heuristic oracle-shape/precondition mismatches are
34
+ review-level, silenced once approved and unchanged) · D executability (precondition · condition+
35
+ data · trigger · oracle all renderable; every `{{var}}` resolves) · E drift (fingerprint mismatch
36
+ → back to review) · G review state (proposed groups block the official render; `--preview` renders
37
+ a DRAFT watermark).
38
+
39
+ **Workbook**: `Testcases` sheet (parent rows + outline-level-1 variant sub-rows — collapse for the
40
+ customer view, expand to execute; result roll-up failed→blocked→not_run→partial→passed, counts like
41
+ `7/8 Passed · 1 Failed`) + `Coverage` sheet (target × category grid with explicit `—` gaps,
42
+ dispositions, generation manifest). CSV is flat with a `Level` column (`item`/`variant`).
43
+ `delivery_item_count` ≠ progress — variants are the execution metric.
44
+
45
+ ---
46
+
47
+ ## Legacy mode (--legacy / --full)
48
+
19
49
  ## Architecture
20
50
 
21
51
  ```
@@ -85,7 +115,13 @@ The `.xlsx` leads with an **`Overview`** roll-up sheet, then splits the test cas
85
115
  - **`Auto`** — automatable test cases (`Auto` + `Not compiled`).
86
116
  - **`Manual`** — `@manual` test cases (always present, header-only when there are none).
87
117
 
88
- Multi-locale (no `SUNGEN_ENV`): `Overview` + one **`<LOCALE> Auto`** sheet per locale + a single shared **`Manual`** sheet (manual TCs are locale-invariant). The **CSV stays one file with every row** — the `Testcase type` column distinguishes Auto vs Manual. The Overview lists every detail sheet, so each locale's counts appear on its own row.
118
+ Multi-locale: `Overview` + one **`<LOCALE> Auto`** sheet per locale + a single shared **`Manual`** sheet (manual TCs are locale-invariant). The **CSV stays one file with every row** — the `Testcase type` column distinguishes Auto vs Manual. The Overview lists every detail sheet, so each locale's counts appear on its own row.
119
+
120
+ **Which result files become locale sheets — locales are DECLARED, never guessed.** A result-file suffix (`<name>-test-result.<X>.json`) is a locale ONLY when `X` is listed in `qa/capabilities.yaml` `locales:` (written by `/sungen:locale`), or — when delivering with `--env <E>` / `SUNGEN_ENV=<E>` — when `X = <E>-<locale>`. Every delivery mixes results of ONE environment:
121
+ - `sungen delivery login` → base results + bare declared locales (`ja`); `dev`/`stg` suffixes are IGNORED with a hint (`→ sungen delivery login --env dev`)
122
+ - `sungen delivery login --env dev` → `dev` results as base + `dev-ja` sheets; everything else ignored
123
+ - `--env ja` (SUNGEN_ENV=<locale> convention) → that locale's run IS the base, labeled JA
124
+ The base sheet label comes from `base_locale:` (default **EN**). Undeclared env suffixes NEVER fabricate locale sheets — a project without `locales:` always gets one clean `Auto` sheet. Mobile per-OS suffixes (`android`, `ios.ja`, …) are exempt and keep the per-OS files; their Test Data column merges the after-OS tail overlay (`ios.ja` → `<name>.ja.yaml`), matching what the runtime merged.
89
125
 
90
126
  ---
91
127
 
@@ -94,6 +94,7 @@ needs any of these, it is a **finding for QA** — surface it in the run summary
94
94
  | `SG-W007` | Browser-alert handler step written AFTER an action — listener registers too late, the dialog auto-dismisses silently | Use the compound form `click [X] button and accept [OK] alert` (preferred), or move the alert step before the trigger |
95
95
  | `SG-W008` | `@manual` scenario incomplete in either direction: (a) HAS Gherkin steps but NO `Then` — a dangling `When` is invalid Gherkin, comments can NOT substitute; (b) documentation-style (NO steps) but its comments lack numbered action lines and/or a labeled oracle line — prose paragraphs / rationale-only comments export EMPTY Steps + Expected cells | Two valid shapes only: complete the Gherkin with a `Then`, or write the FULL procedure in the `# Tester verifies:` block as NUMBERED lines — action steps first (`1. <action>`), then labeled `Observable:`/`Oracle:`/`Expected:` lines (the number is what fills Steps, the label is what fills Expected results) |
96
96
  | `SG-W009` | test-data references `${QA_<NAME>}` (a SECRET env ref) but the name is declared in neither `.env.qa` nor `.env.qa.example` — the test throws "environment variable not set" the moment the key is used. (Only the `${QA_*}` shape is checked; any other `${...}` is literal data and never flagged.) | Add `QA_<NAME>=<value>` to `.env.qa` (gitignored) and list the NAME in `.env.qa.example` (the committed checklist). See `qa/TEST-DATA-GUIDE.md` |
97
+ | `SG-W010` | A row step's `[Ref]` looks POSITIONAL (`First…`/`Last…`/`Top…`/`nth`) but has no selectors.yaml entry — the label is display-only, so the step checks existence ANYWHERE in the table; an ordering/sort-stability rule would pass even when the row moved | Declare the row in selectors.yaml (a `type: locator` value RELATIVE to the table, e.g. `first contact row:` → `value: 'tbody tr:first-child'`) — the step then asserts `toContainText` on that exact row. This is a selectors.yaml-only fix — allowed inside the run-test auto-fix loop |
97
98
 
98
99
  ### Runtime error → `Test data "<key>" references ${QA_*} but the environment variable is not set`
99
100
 
@@ -127,6 +127,14 @@ User see [Table] table match data:
127
127
 
128
128
  Row scope: `see [Ref] row in [Table] table with {{v}}` enters scope. Subsequent `see [Col] column with {{v}}` checks cell in that row. Use `table match data:` for multi-row verification.
129
129
 
130
+ **[Ref] of a row step is a display label UNLESS declared in selectors.yaml.** Without an entry the step checks the value exists in ANY row — a name like `[First Contact Row]` does NOT position the row, and an ordering rule would pass even when the row moved (SG-W010 warns on positional-looking undeclared labels). To assert a POSITION, declare the row (a `type: locator` value RELATIVE to the table):
131
+ ```yaml
132
+ first contact row:
133
+ type: locator
134
+ value: 'tbody tr:first-child'
135
+ ```
136
+ → compiles to `expect(table.locator('tbody tr:first-child')).toContainText(v)` — the exact row must hold the value — and still enters row scope for `[Col] column` checks.
137
+
130
138
  ### Database verification (optional Data Driver)
131
139
 
132
140
  Read-only DB-state checks. **Prefer named queries** — SQL lives in `qa/screens/<screen>/database/queries.yaml` (reviewed once, parameterized). Invoke with the `@query:<name>` annotation; it binds the result rows to `{{name}}`, then assert with `expect`:
@@ -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
 
@@ -34,6 +34,14 @@ Take a screen/flow whose `selectors/*.yaml` and `.feature` files were authored a
34
34
  1. `selectors/<feature>.yaml` — hardcoded `name`/`value` replaced with `{{var}}`
35
35
  2. `test-data/<feature>.yaml` — base locale, complete with all new keys
36
36
  3. `test-data/<feature>.<locale>.yaml` — overlay with only the keys that change
37
+ 4. **`qa/capabilities.yaml` — DECLARE the locale** (required since 3.2.15): append the code to `locales:` and set `base_locale:` (language of the base test-data; tool default is `en`). Delivery/dashboard treat ONLY declared codes as locales — undeclared result-file suffixes are assumed to be ENVIRONMENTS (dev/staging) and excluded from locale sheets:
38
+ ```yaml
39
+ platform: web
40
+ enabled: [web]
41
+ base_locale: en # or vi/ja — whatever the base test-data is written in
42
+ locales: [ja] # append every bootstrapped locale here
43
+ ```
44
+ Env × locale combos use the compound `SUNGEN_ENV=<env>-<locale>` (e.g. `staging-ja`, overlay `<feature>.staging-ja.yaml`) — the locale segment LAST; delivery recognizes it when the tail matches a declared locale.
37
45
  4. (Optional) `selectors/<feature>.yaml` Pages block updated when locale uses URL prefix or query param
38
46
 
39
47
  After this skill finishes, `sungen run-test <name> --env <locale>` Just Works.
@@ -328,6 +328,7 @@ Security: [S1 – admin only]
328
328
  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
329
  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
330
  **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.
331
+ **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
332
  Use this comment shape:
332
333
  ```gherkin
333
334
  @high @manual
@@ -62,16 +62,25 @@ export class TestDataLoader {
62
62
 
63
63
  /**
64
64
  * Resolve a key to its RAW, uncoerced value (array/object/number kept as-is) — for binding
65
- * DB query parameters, where type-strict stores (MySQL numeric columns, MongoDB) require the
66
- * native type. get() stringifies for Gherkin text; raw() must not. Throws on missing, mirroring
67
- * get()'s guard.
65
+ * DB query / API request params, where type-strict sinks (MySQL numeric columns, MongoDB,
66
+ * a JSON request body) require the native type. get() stringifies for Gherkin text; raw()
67
+ * must not. A string value still has its embedded `{{ref}}` cross-references resolved (same
68
+ * as get()), so an override like `token={{login.body.token}}` or `id={{prefix}}-001` still
69
+ * interpolates — only the final scalar/array/object type is preserved uncoerced. Arrays and
70
+ * objects are returned as-is; refs embedded inside their elements are not interpolated (unlike
71
+ * get()'s JSON-text pass), since raw() callers bind structured values directly, not text.
72
+ * Throws on missing, mirroring get()'s guard.
68
73
  */
69
74
  raw(key: string): any {
70
75
  const value = this.resolve(key);
71
76
  if (value === undefined || value === null) {
72
77
  throw new Error(`Test data key not found: ${key}`);
73
78
  }
74
- if (typeof value === 'string') assertNoUnresolvedEnvRef(key, value);
79
+ if (typeof value === 'string') {
80
+ const result = this.interpolate(value);
81
+ assertNoUnresolvedEnvRef(key, result);
82
+ return result;
83
+ }
75
84
  return value;
76
85
  }
77
86
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sungen",
3
- "version": "3.2.14",
3
+ "version": "3.2.16-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",
@@ -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/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/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/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",
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.14",
43
- "@sungen/driver-ui": "3.2.14",
42
+ "@sungen/driver-data-factory": "3.2.16-beta.1",
43
+ "@sungen/driver-ui": "3.2.16-beta.1",
44
44
  "chalk": "^5.6.2",
45
45
  "commander": "^14.0.2",
46
46
  "dotenv": "^17.2.3",
@@ -13,6 +13,7 @@ import { parseSpecFile } from '../../exporters/spec-parser';
13
13
  import { loadTestData } from '../../exporters/test-data-resolver';
14
14
  import { loadSelectorKeyMap } from '../../exporters/selector-key-resolver';
15
15
  import { loadPlaywrightReport, resolveResultsPath as resolveResultsPathShared } from '../../exporters/playwright-report-parser';
16
+ import { discoverLocaleVariants } from '../../exporters/locale-variants';
16
17
  import { mergeFeatureAndSpec } from '../../exporters/scenario-merger';
17
18
  import {
18
19
  buildTestCaseRows,
@@ -24,6 +25,16 @@ import { renderXlsxMultiSheet, writeXlsx, buildApiDetailRows, addApiDetailSheet
24
25
  import { loadApiCatalog } from '../../exporters/api-catalog-loader';
25
26
  import { resolvePlatformAppId } from '../../exporters/mobile-app-id';
26
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
+ hasErrors,
35
+ hasReview,
36
+ } from '../../exporters/matrix/export';
37
+ import { MatrixFinding } from '../../exporters/matrix/types';
27
38
 
28
39
  const COLOR = {
29
40
  reset: '\x1b[0m',
@@ -167,70 +178,11 @@ function resolveResultsPath(cwd: string, target: DeliveryTarget): string | null
167
178
  return resolveResultsPathShared(cwd, generatedDir(cwd, target), target.featureBaseName);
168
179
  }
169
180
 
170
- /**
171
- * A single locale variant of a delivery target.
172
- *
173
- * `locale === ''` denotes the base locale (file `<name>-test-result.json`,
174
- * test-data without overlay). Non-empty values come from
175
- * `<name>-test-result.<locale>.json` files found in the generated directory.
176
- *
177
- * `displayCode` is what we show in sheet names — `BASE_LOCALE.toUpperCase()`
178
- * for base, the locale code uppercased otherwise (e.g. `EN`, `JA`, `EN-US`).
179
- */
180
- interface LocaleVariant {
181
- locale: string; // '' for base, 'en' / 'ja' / 'staging-ja' for variants
182
- displayCode: string; // 'VI' / 'EN' / 'JA' — used in sheet name
183
- resultsPath: string | null;
184
- }
185
-
186
- /**
187
- * Base locale assumed when no `SUNGEN_ENV` and no explicit override.
188
- * Configurable later via `qa/.sungen-config.yaml` or similar — currently
189
- * hardcoded since every existing sungen project uses Vietnamese as the
190
- * source-of-truth `test-data/<feature>.yaml`.
191
- */
192
- const DEFAULT_BASE_LOCALE = 'vi';
193
-
194
- /**
195
- * Discover every locale variant available for a feature by scanning the
196
- * generated directory for `<featureBaseName>-test-result*.json` files.
197
- *
198
- * Result always includes the base (locale = '') as the first entry — even if
199
- * `<name>-test-result.json` doesn't exist, callers still need somewhere to
200
- * fall back to (Pending status). Variants follow alphabetically by locale
201
- * code, so the sheet order is deterministic across runs.
202
- */
203
- function discoverLocaleVariants(cwd: string, target: DeliveryTarget): LocaleVariant[] {
204
- const genDir = generatedDir(cwd, target);
205
- const prefix = `${target.featureBaseName}-test-result`;
206
- const variants: LocaleVariant[] = [];
207
-
208
- // Base variant: per-target result file if present, else fall back to the global
209
- // test-results/results.json (what playwright.config writes by default) via resolveResultsPath.
210
- variants.push({
211
- locale: '',
212
- displayCode: DEFAULT_BASE_LOCALE.toUpperCase(),
213
- resultsPath: resolveResultsPath(cwd, target),
214
- });
215
-
216
- if (fs.existsSync(genDir)) {
217
- const localeFiles = fs.readdirSync(genDir)
218
- .filter((f) => f.startsWith(`${prefix}.`) && f.endsWith('.json') && f !== `${prefix}.json`)
219
- .sort();
220
- for (const f of localeFiles) {
221
- // Strip prefix + '.' on the left, '.json' on the right → locale code.
222
- const locale = f.slice(prefix.length + 1, -'.json'.length);
223
- if (!locale) continue;
224
- variants.push({
225
- locale,
226
- displayCode: locale.toUpperCase(),
227
- resultsPath: path.join(genDir, f),
228
- });
229
- }
230
- }
231
-
232
- return variants;
233
- }
181
+ // Locale/env variant discovery is SHARED with the dashboard snapshot builder —
182
+ // see exporters/locale-variants.ts for the classification rules (declared
183
+ // locales via qa/capabilities.yaml, env pairing, mobile OS exemption). The two
184
+ // surfaces previously hand-mirrored this logic and drifted into the
185
+ // fake-locale bug (any env-suffixed result file became a "locale" sheet).
234
186
 
235
187
  function resolveTestDataPathForTarget(cwd: string, target: DeliveryTarget): string {
236
188
  return path.join(qaDir(cwd, target), 'test-data', `${target.featureBaseName}.yaml`);
@@ -522,7 +474,17 @@ async function exportTarget(
522
474
  return buildSummary(label, rows, path.relative(cwd, csvPath));
523
475
  }
524
476
 
525
- const variants = discoverLocaleVariants(cwd, target);
477
+ const { variants, ignored } = discoverLocaleVariants(
478
+ cwd, generatedDir(cwd, target), target.featureBaseName,
479
+ () => resolveResultsPath(cwd, target),
480
+ );
481
+ if (ignored.length > 0) {
482
+ const env = process.env.SUNGEN_ENV;
483
+ log(` ${COLOR.gray}Ignored result file(s) from other environments: ${ignored.join(', ')}${COLOR.reset}`);
484
+ for (const suffix of ignored) {
485
+ log(` ${COLOR.cyan}→ sungen delivery ${target.screen} --env ${suffix}${COLOR.reset}${env ? '' : ` ${COLOR.gray}(this run uses the base results only)${COLOR.reset}`}`);
486
+ }
487
+ }
526
488
 
527
489
  // Mobile: emit ONE deliverable per OS (csv + xlsx), each from its own
528
490
  // `<feature>-test-result.<os>.json`. Android/iOS are SEPARATE platform runs,
@@ -542,7 +504,11 @@ async function exportTarget(
542
504
  featureName: feature.featureName,
543
505
  featurePath: resolvePlatformAppId(featurePath, variant.locale), // OS-precise app id
544
506
  merged,
545
- testData: loadTestData(testDataFile, variant.locale || null),
507
+ // Overlay = the after-OS tail (what SUNGEN_ENV was during that run):
508
+ // `android.ja` merged <name>.ja.yaml at runtime — the old code passed the
509
+ // full `android.ja` suffix, looked up a nonexistent overlay, and silently
510
+ // showed base-language values in the Test Data column.
511
+ testData: loadTestData(testDataFile, variant.overlay ?? null),
546
512
  results: variant.resultsPath ? loadPlaywrightReport(variant.resultsPath) : null,
547
513
  env,
548
514
  selectorKeyMap,
@@ -579,9 +545,11 @@ async function exportTarget(
579
545
  const multiLocale = variants.length > 1;
580
546
 
581
547
  for (const variant of variants) {
582
- // For the base variant the overlay merge is skipped (`locale: null`);
583
- // for locale variants the matching `<feature>.<locale>.yaml` is merged.
584
- const variantTestData = loadTestData(testDataFile, variant.locale || null);
548
+ // Overlay parity with the RUNTIME: merge exactly what TestDataLoader merged
549
+ // during that run (the run's SUNGEN_ENV) the base variant under --env dev
550
+ // merges <feature>.dev.yaml, a ja variant merges <feature>.ja.yaml, plain
551
+ // base merges nothing.
552
+ const variantTestData = loadTestData(testDataFile, variant.overlay ?? null);
585
553
  const variantResults = variant.resultsPath ? loadPlaywrightReport(variant.resultsPath) : null;
586
554
  const variantRows = buildTestCaseRows({
587
555
  screen: label,
@@ -639,6 +607,114 @@ async function exportTarget(
639
607
  }
640
608
  }
641
609
 
610
+ // ----------------------------------------------------------------------------
611
+ // Matrix export (Delivery v2 — the default; --legacy keeps the classic workbook)
612
+ // ----------------------------------------------------------------------------
613
+
614
+ function matrixPathsFor(cwd: string, target: DeliveryTarget): MatrixTargetPaths {
615
+ const base = qaDir(cwd, target);
616
+ const genBase = generatedDir(cwd, target);
617
+ return {
618
+ cwd,
619
+ unit: target.featureBaseName,
620
+ label: target.isFlow ? `flow/${target.featureBaseName}` : target.featureBaseName,
621
+ featureFile: path.join(base, 'features', `${target.featureBaseName}.feature`),
622
+ testDataFile: resolveTestDataPathForTarget(cwd, target),
623
+ specFile: path.join(genBase, `${target.featureBaseName}.spec.ts`),
624
+ resultsPath: resolveResultsPath(cwd, target),
625
+ mapFile: mapFilePath(base, target.featureBaseName),
626
+ };
627
+ }
628
+
629
+ function printFindings(findings: MatrixFinding[]): void {
630
+ const icon = { error: `${COLOR.red}✗`, review: `${COLOR.yellow}⚠`, warning: `${COLOR.gray}·` } as const;
631
+ for (const f of findings) {
632
+ log(` ${icon[f.severity]} [Gate ${f.gate}]${COLOR.reset} ${f.ref ? `${f.ref}: ` : ''}${f.message}`);
633
+ }
634
+ }
635
+
636
+ interface MatrixRunSummary {
637
+ label: string;
638
+ items: number;
639
+ variants: number;
640
+ passed: number;
641
+ failed: number;
642
+ file: string;
643
+ }
644
+
645
+ /**
646
+ * Matrix-mode export of one target. Returns null when blocked (missing map,
647
+ * gate errors, or unapproved groups without --preview) — the caller aggregates
648
+ * a non-zero exit.
649
+ */
650
+ async function exportMatrixTarget(
651
+ cwd: string,
652
+ target: DeliveryTarget,
653
+ opts: { check?: boolean; preview?: boolean },
654
+ ): Promise<MatrixRunSummary | null> {
655
+ const paths = matrixPathsFor(cwd, target);
656
+ const { model, mapMissing, mapErrors } = loadMatrixModel(paths);
657
+
658
+ if (mapMissing) {
659
+ log(`${COLOR.yellow}${paths.label}: no delivery map${COLOR.reset} (${path.relative(cwd, paths.mapFile)})`);
660
+ log(` The matrix needs the reviewed grouping (target / intent / oracle per delivery item).`);
661
+ log(` ${COLOR.cyan}→ run /sungen:delivery ${target.screen} so the map is proposed for your review${COLOR.reset}`);
662
+ log(` ${COLOR.cyan}→ or export the classic per-scenario workbook: sungen delivery ${target.screen} --legacy${COLOR.reset}`);
663
+ return null;
664
+ }
665
+ if (mapErrors.length > 0 || !model) {
666
+ log(`${COLOR.red}${paths.label}: delivery map is invalid${COLOR.reset}`);
667
+ for (const e of mapErrors) log(` - ${e}`);
668
+ return null;
669
+ }
670
+
671
+ log(`${COLOR.bold}${paths.label}${COLOR.reset} — ${model.manifest.itemCount} delivery item(s), ${model.manifest.variantCount} coverage variant(s), state: ${model.manifest.approvalState}`);
672
+ printFindings(model.findings);
673
+
674
+ if (opts.check) {
675
+ // Gates-only run: the summary IS the output; exit code reflects errors.
676
+ return hasErrors(model.findings)
677
+ ? null
678
+ : { label: paths.label, items: model.manifest.itemCount, variants: model.manifest.variantCount, passed: 0, failed: 0, file: '(check only)' };
679
+ }
680
+ if (hasErrors(model.findings)) {
681
+ log(` ${COLOR.red}blocked:${COLOR.reset} fix the Gate errors above, then re-run.`);
682
+ return null;
683
+ }
684
+ if (hasReview(model.findings) && !opts.preview) {
685
+ log(` ${COLOR.yellow}not approved for official delivery.${COLOR.reset}`);
686
+ log(` ${COLOR.cyan}→ approve the reviewed grouping: sungen delivery ${target.screen} --approve${COLOR.reset}`);
687
+ log(` ${COLOR.cyan}→ or render a watermarked draft: sungen delivery ${target.screen} --preview${COLOR.reset}`);
688
+ return null;
689
+ }
690
+
691
+ const { csvPath, xlsxPath } = await writeMatrixDeliverables(paths, model);
692
+ const passed = model.items.reduce((a, i) => a + i.resultCounts.passed, 0);
693
+ const failed = model.items.reduce((a, i) => a + i.resultCounts.failed, 0);
694
+ log(` ${COLOR.green}→ ${path.relative(cwd, xlsxPath)}${COLOR.reset} ${COLOR.gray}(+ ${path.relative(cwd, csvPath)})${COLOR.reset}`);
695
+ return {
696
+ label: paths.label,
697
+ items: model.manifest.itemCount,
698
+ variants: model.manifest.variantCount,
699
+ passed,
700
+ failed,
701
+ file: path.relative(cwd, xlsxPath),
702
+ };
703
+ }
704
+
705
+ function printMatrixSummaryTable(summaries: MatrixRunSummary[]): void {
706
+ log(`\n${COLOR.bold}Delivery matrix export complete${COLOR.reset}\n`);
707
+ const colWidth = Math.max(20, ...summaries.map((s) => s.label.length)) + 1;
708
+ log(` ${'Feature'.padEnd(colWidth)}Items Variants Passed Failed File`);
709
+ log(' ' + '-'.repeat(colWidth + 50));
710
+ for (const s of summaries) {
711
+ log(
712
+ ` ${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}`
713
+ );
714
+ }
715
+ log(`\n${COLOR.gray}Items = review rows (one target + intent + oracle family). Variants = execution units — use variant counts for progress.${COLOR.reset}\n`);
716
+ }
717
+
642
718
  // ----------------------------------------------------------------------------
643
719
  // Reporter
644
720
  // ----------------------------------------------------------------------------
@@ -709,12 +785,24 @@ function printSummaryTable(summaries: ScreenSummary[]): void {
709
785
  export function registerDeliveryCommand(program: Command): void {
710
786
  program
711
787
  .command('delivery')
712
- .description('Export Gherkin + Playwright results CSV test case deliverable')
788
+ .description('Export the Test Case & Coverage Matrix (Gherkin + results). --legacy keeps the classic per-scenario workbook.')
713
789
  .argument('[names...]', 'Specific screen or flow names. Omit to process all.')
714
790
  .option('--skip-preflight', 'Skip pre-flight checks (not recommended)')
715
791
  .option('--continue-on-missing', 'Skip targets with blocking misses instead of aborting')
716
- .action(async (names: string[], options: { skipPreflight?: boolean; continueOnMissing?: boolean }) => {
792
+ .option('--env <env>', 'Environment/locale of the run to deliver (sets SUNGEN_ENV; e.g. dev, staging, ja, dev-ja)')
793
+ .option('--legacy', 'Classic per-scenario-steps workbook (BM-2-901-13 layout)')
794
+ .option('--full', 'Alias of --legacy')
795
+ .option('--check', 'Matrix gates only — validate the delivery map, write nothing')
796
+ .option('--approve [ids]', 'Approve proposed groups (all, or a comma-separated id list) + stamp fingerprints')
797
+ .option('--preview', 'Render the matrix despite review-required findings (DRAFT watermark)')
798
+ .action(async (names: string[], options: {
799
+ skipPreflight?: boolean; continueOnMissing?: boolean; env?: string;
800
+ legacy?: boolean; full?: boolean; check?: boolean; approve?: boolean | string; preview?: boolean;
801
+ }) => {
717
802
  try {
803
+ // Same effect as running with SUNGEN_ENV=<env> — result-file resolution,
804
+ // locale pairing, and test-data overlays all key off this one string.
805
+ if (options.env) process.env.SUNGEN_ENV = options.env;
718
806
  const cwd = process.cwd();
719
807
 
720
808
  // 1. Scope detection — each positional name expands into one target
@@ -766,6 +854,46 @@ export function registerDeliveryCommand(program: Command): void {
766
854
  }
767
855
 
768
856
  // 3. Export
857
+ const legacy = options.legacy || options.full;
858
+ if (!legacy) {
859
+ // Matrix mode (the default). --approve is its own sub-action.
860
+ if (options.approve !== undefined) {
861
+ const ids = typeof options.approve === 'string'
862
+ ? options.approve.split(',').map((s) => s.trim()).filter(Boolean)
863
+ : undefined;
864
+ let blocked = false;
865
+ for (const target of toExport) {
866
+ const paths = matrixPathsFor(cwd, target);
867
+ try {
868
+ const { findings, approved } = approveMatrix(paths, ids);
869
+ if (approved.length === 0) {
870
+ log(`${COLOR.red}${paths.label}: approval blocked by Gate errors${COLOR.reset}`);
871
+ printFindings(findings);
872
+ blocked = true;
873
+ } else {
874
+ log(`${COLOR.green}${paths.label}: approved ${approved.length} group(s)${COLOR.reset} — fingerprints stamped in ${path.relative(cwd, paths.mapFile)}`);
875
+ }
876
+ } catch (e) {
877
+ log(`${COLOR.red}${paths.label}:${COLOR.reset} ${e instanceof Error ? e.message : e}`);
878
+ blocked = true;
879
+ }
880
+ }
881
+ if (blocked) process.exit(1);
882
+ return;
883
+ }
884
+
885
+ const matrixSummaries: MatrixRunSummary[] = [];
886
+ let failedTargets = 0;
887
+ for (const target of toExport) {
888
+ const s = await exportMatrixTarget(cwd, target, { check: options.check, preview: options.preview });
889
+ if (s) matrixSummaries.push(s);
890
+ else failedTargets++;
891
+ }
892
+ if (matrixSummaries.length > 0 && !options.check) printMatrixSummaryTable(matrixSummaries);
893
+ if (failedTargets > 0) process.exit(1);
894
+ return;
895
+ }
896
+
769
897
  const env = getEnvironment(cwd);
770
898
  const summaries: ScreenSummary[] = [];
771
899
  for (const target of toExport) {