@sun-asterisk/sungen 3.2.17 → 3.2.18

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 (187) hide show
  1. package/dist/cli/commands/audit.d.ts.map +1 -1
  2. package/dist/cli/commands/audit.js +23 -0
  3. package/dist/cli/commands/audit.js.map +1 -1
  4. package/dist/cli/commands/delivery.d.ts.map +1 -1
  5. package/dist/cli/commands/delivery.js +209 -16
  6. package/dist/cli/commands/delivery.js.map +1 -1
  7. package/dist/cli/index.js +5 -0
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  10. package/dist/dashboard/snapshot-builder.js +8 -1
  11. package/dist/dashboard/snapshot-builder.js.map +1 -1
  12. package/dist/dashboard/templates/index.html +1 -1
  13. package/dist/exporters/api-environment.d.ts +11 -0
  14. package/dist/exporters/api-environment.d.ts.map +1 -0
  15. package/dist/exporters/api-environment.js +129 -0
  16. package/dist/exporters/api-environment.js.map +1 -0
  17. package/dist/exporters/feature-parser.d.ts +16 -1
  18. package/dist/exporters/feature-parser.d.ts.map +1 -1
  19. package/dist/exporters/feature-parser.js +21 -2
  20. package/dist/exporters/feature-parser.js.map +1 -1
  21. package/dist/exporters/matrix/build.d.ts +70 -0
  22. package/dist/exporters/matrix/build.d.ts.map +1 -0
  23. package/dist/exporters/matrix/build.js +516 -0
  24. package/dist/exporters/matrix/build.js.map +1 -0
  25. package/dist/exporters/matrix/export.d.ts +50 -0
  26. package/dist/exporters/matrix/export.d.ts.map +1 -0
  27. package/dist/exporters/matrix/export.js +152 -0
  28. package/dist/exporters/matrix/export.js.map +1 -0
  29. package/dist/exporters/matrix/fingerprint.d.ts +20 -0
  30. package/dist/exporters/matrix/fingerprint.d.ts.map +1 -0
  31. package/dist/exporters/matrix/fingerprint.js +83 -0
  32. package/dist/exporters/matrix/fingerprint.js.map +1 -0
  33. package/dist/exporters/matrix/gates.d.ts +21 -0
  34. package/dist/exporters/matrix/gates.d.ts.map +1 -0
  35. package/dist/exporters/matrix/gates.js +367 -0
  36. package/dist/exporters/matrix/gates.js.map +1 -0
  37. package/dist/exporters/matrix/map-loader.d.ts +22 -0
  38. package/dist/exporters/matrix/map-loader.d.ts.map +1 -0
  39. package/dist/exporters/matrix/map-loader.js +193 -0
  40. package/dist/exporters/matrix/map-loader.js.map +1 -0
  41. package/dist/exporters/matrix/render-csv.d.ts +9 -0
  42. package/dist/exporters/matrix/render-csv.d.ts.map +1 -0
  43. package/dist/exporters/matrix/render-csv.js +92 -0
  44. package/dist/exporters/matrix/render-csv.js.map +1 -0
  45. package/dist/exporters/matrix/render-xlsx.d.ts +41 -0
  46. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -0
  47. package/dist/exporters/matrix/render-xlsx.js +430 -0
  48. package/dist/exporters/matrix/render-xlsx.js.map +1 -0
  49. package/dist/exporters/matrix/types.d.ts +199 -0
  50. package/dist/exporters/matrix/types.d.ts.map +1 -0
  51. package/dist/exporters/matrix/types.js +15 -0
  52. package/dist/exporters/matrix/types.js.map +1 -0
  53. package/dist/exporters/matrix/wording.d.ts +61 -0
  54. package/dist/exporters/matrix/wording.d.ts.map +1 -0
  55. package/dist/exporters/matrix/wording.js +221 -0
  56. package/dist/exporters/matrix/wording.js.map +1 -0
  57. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  58. package/dist/exporters/scenario-merger.js +2 -1
  59. package/dist/exporters/scenario-merger.js.map +1 -1
  60. package/dist/exporters/spec-parser.d.ts.map +1 -1
  61. package/dist/exporters/spec-parser.js +2 -1
  62. package/dist/exporters/spec-parser.js.map +1 -1
  63. package/dist/exporters/xlsx-report-builder.d.ts +1 -1
  64. package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
  65. package/dist/exporters/xlsx-report-builder.js +3 -3
  66. package/dist/exporters/xlsx-report-builder.js.map +1 -1
  67. package/dist/harness/audit.d.ts +10 -0
  68. package/dist/harness/audit.d.ts.map +1 -1
  69. package/dist/harness/audit.js +98 -16
  70. package/dist/harness/audit.js.map +1 -1
  71. package/dist/harness/blindspot.d.ts.map +1 -1
  72. package/dist/harness/blindspot.js +2 -1
  73. package/dist/harness/blindspot.js.map +1 -1
  74. package/dist/harness/capability-plan.d.ts +2 -0
  75. package/dist/harness/capability-plan.d.ts.map +1 -1
  76. package/dist/harness/capability-plan.js +17 -4
  77. package/dist/harness/capability-plan.js.map +1 -1
  78. package/dist/harness/feedback.d.ts.map +1 -1
  79. package/dist/harness/feedback.js +3 -2
  80. package/dist/harness/feedback.js.map +1 -1
  81. package/dist/harness/flow-check.d.ts.map +1 -1
  82. package/dist/harness/flow-check.js +2 -1
  83. package/dist/harness/flow-check.js.map +1 -1
  84. package/dist/harness/flow-plan.d.ts.map +1 -1
  85. package/dist/harness/flow-plan.js +3 -2
  86. package/dist/harness/flow-plan.js.map +1 -1
  87. package/dist/harness/intent.d.ts.map +1 -1
  88. package/dist/harness/intent.js +2 -1
  89. package/dist/harness/intent.js.map +1 -1
  90. package/dist/harness/journey.d.ts.map +1 -1
  91. package/dist/harness/journey.js +3 -2
  92. package/dist/harness/journey.js.map +1 -1
  93. package/dist/harness/ledger.d.ts.map +1 -1
  94. package/dist/harness/ledger.js +3 -2
  95. package/dist/harness/ledger.js.map +1 -1
  96. package/dist/harness/manifest.d.ts.map +1 -1
  97. package/dist/harness/manifest.js +4 -3
  98. package/dist/harness/manifest.js.map +1 -1
  99. package/dist/harness/parse.d.ts.map +1 -1
  100. package/dist/harness/parse.js +16 -3
  101. package/dist/harness/parse.js.map +1 -1
  102. package/dist/harness/quality-gates.d.ts.map +1 -1
  103. package/dist/harness/quality-gates.js +2 -1
  104. package/dist/harness/quality-gates.js.map +1 -1
  105. package/dist/harness/read-text.d.ts +22 -0
  106. package/dist/harness/read-text.d.ts.map +1 -0
  107. package/dist/harness/read-text.js +64 -0
  108. package/dist/harness/read-text.js.map +1 -0
  109. package/dist/harness/script-check.d.ts.map +1 -1
  110. package/dist/harness/script-check.js +3 -2
  111. package/dist/harness/script-check.js.map +1 -1
  112. package/dist/harness/sensors.d.ts +13 -1
  113. package/dist/harness/sensors.d.ts.map +1 -1
  114. package/dist/harness/sensors.js +63 -20
  115. package/dist/harness/sensors.js.map +1 -1
  116. package/dist/harness/spec-coverage.d.ts +5 -0
  117. package/dist/harness/spec-coverage.d.ts.map +1 -1
  118. package/dist/harness/spec-coverage.js +17 -7
  119. package/dist/harness/spec-coverage.js.map +1 -1
  120. package/dist/harness/trace.d.ts.map +1 -1
  121. package/dist/harness/trace.js +4 -3
  122. package/dist/harness/trace.js.map +1 -1
  123. package/dist/harness/viewpoint-baseline.d.ts +49 -0
  124. package/dist/harness/viewpoint-baseline.d.ts.map +1 -0
  125. package/dist/harness/viewpoint-baseline.js +141 -0
  126. package/dist/harness/viewpoint-baseline.js.map +1 -0
  127. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  128. package/dist/harness/viewpoint-ledger.js +2 -1
  129. package/dist/harness/viewpoint-ledger.js.map +1 -1
  130. package/dist/orchestrator/assets-drift.d.ts +24 -0
  131. package/dist/orchestrator/assets-drift.d.ts.map +1 -0
  132. package/dist/orchestrator/assets-drift.js +80 -0
  133. package/dist/orchestrator/assets-drift.js.map +1 -0
  134. package/dist/orchestrator/templates/ai-src/commands/create-test.md +24 -0
  135. package/dist/orchestrator/templates/ai-src/commands/delivery.md +203 -120
  136. package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
  137. package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
  138. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +102 -6
  139. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +1 -0
  140. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +23 -0
  141. package/package.json +3 -3
  142. package/src/cli/commands/audit.ts +23 -1
  143. package/src/cli/commands/delivery.ts +240 -18
  144. package/src/cli/index.ts +6 -0
  145. package/src/dashboard/snapshot-builder.ts +8 -1
  146. package/src/dashboard/templates/index.html +1 -1
  147. package/src/exporters/api-environment.ts +97 -0
  148. package/src/exporters/feature-parser.ts +21 -2
  149. package/src/exporters/matrix/build.ts +551 -0
  150. package/src/exporters/matrix/export.ts +149 -0
  151. package/src/exporters/matrix/fingerprint.ts +49 -0
  152. package/src/exporters/matrix/gates.ts +400 -0
  153. package/src/exporters/matrix/map-loader.ts +163 -0
  154. package/src/exporters/matrix/render-csv.ts +94 -0
  155. package/src/exporters/matrix/render-xlsx.ts +420 -0
  156. package/src/exporters/matrix/types.ts +226 -0
  157. package/src/exporters/matrix/wording.ts +221 -0
  158. package/src/exporters/scenario-merger.ts +2 -1
  159. package/src/exporters/spec-parser.ts +2 -1
  160. package/src/exporters/xlsx-report-builder.ts +3 -2
  161. package/src/harness/audit.ts +104 -18
  162. package/src/harness/blindspot.ts +2 -1
  163. package/src/harness/capability-plan.ts +17 -4
  164. package/src/harness/feedback.ts +3 -2
  165. package/src/harness/flow-check.ts +2 -1
  166. package/src/harness/flow-plan.ts +3 -2
  167. package/src/harness/intent.ts +2 -1
  168. package/src/harness/journey.ts +3 -2
  169. package/src/harness/ledger.ts +3 -2
  170. package/src/harness/manifest.ts +4 -3
  171. package/src/harness/parse.ts +17 -3
  172. package/src/harness/quality-gates.ts +2 -1
  173. package/src/harness/read-text.ts +28 -0
  174. package/src/harness/script-check.ts +3 -2
  175. package/src/harness/sensors.ts +55 -8
  176. package/src/harness/spec-coverage.ts +22 -7
  177. package/src/harness/trace.ts +4 -3
  178. package/src/harness/viewpoint-baseline.ts +128 -0
  179. package/src/harness/viewpoint-ledger.ts +2 -1
  180. package/src/orchestrator/assets-drift.ts +64 -0
  181. package/src/orchestrator/templates/ai-src/commands/create-test.md +24 -0
  182. package/src/orchestrator/templates/ai-src/commands/delivery.md +203 -120
  183. package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
  184. package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
  185. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +102 -6
  186. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +1 -0
  187. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +23 -0
@@ -295,6 +295,7 @@ Options: `nth` `exact` `scope` `match` `variant` `frame` `contenteditable` `colu
295
295
 
296
296
  | Tag | Effect |
297
297
  |---|---|
298
+ | `@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. |
298
299
  | `@manual` | Skip in generation |
299
300
  | `@auth:role` | Use auth storage state for role |
300
301
  | `@no-auth` | Disable inherited auth |
@@ -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.
@@ -318,6 +340,7 @@ Security: [S1 – admin only]
318
340
  `sungen audit` enforces these. Generate compliant output up front:
319
341
 
320
342
  1. **Taxonomy-match** (`VP-TAXONOMY-MISMATCH`, gate-FAIL) — when `test-viewpoint.md` declares its own viewpoint IDs (e.g. `VP0`, `VP1`, … `VP12`, `MS-HP-001`, `MS-EH-001`), **reuse those IDs verbatim as the scenario codes**. Do NOT invent a generic `VP-UI / VP-LOGIC / VP-VAL` scheme — that breaks the coverage matrix. Only fall back to `VP-<CATEGORY>-<NNN>` when the viewpoint file declares no IDs.
343
+ - **Match the scenarios to the file — never the file to the scenarios.** A filled `test-viewpoint.md` is an input; do not rewrite its declarations to fit what you generated. That is not compliance, it is moving the yardstick: `traceability` + `atomicLedger` then read 100% by construction and a dropped viewpoint stops being missing from anything. Disagree with the taxonomy → propose the diff and ask. `sungen audit` reports `VIEWPOINT-BASELINE-CHANGED` and excludes both axes until a human accepts the change (`sungen audit --screen <name> --accept-viewpoint`).
321
344
  2. **Spec-coverage triggers** (`TRIGGER-UNCOVERED`, gate-FAIL) — the Validation-Rules table lists a **trigger** per constraint (e.g. `blur, submit`). Generate one scenario **per (constraint × trigger)** — a `format` rule validating *on blur AND on submit* needs BOTH a blur scenario (`press Tab`) and a submit scenario (`click [Submit]` / `press Enter`). Never collapse the trigger × input matrix to one representative case.
322
345
  3. **Claim-Proof** (`CLAIM-UNPROVEN`) — a title claiming `all`/`only`/`every`/`single`/`correct`/`same`/`changes`/`hidden`/`cleared`/`restored`/`independent`/`sanitized`/`announces` MUST have the matching assertion (`see all …`, count, `remember`+compare, `is hidden`, return-and-assert-empty, etc.). If the title promises it, the steps must prove it.
323
346
  - **Negative / absence claims** (`does not` / `no` / `never` / `prevents` / `không` / `chưa` — any language; `no-side-effect/no-duplicate`, `negative-claim/absence`): the `Then` must **differ** between the claim holding and not holding. A terminal `see [X] page` that looks identical whether or not the bad thing happened proves nothing. For a side-effect that should NOT repeat (re-submit on back, re-charge, duplicate order, resend OTP), assert the **count is unchanged** (`User see [Records] table with {{one}}` / `row with {{count}}`); if it's not UI-observable, mark `@manual` with a request-count oracle (shape below). This is general — it covers any side-effect, not a fixed verb list.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sungen",
3
- "version": "3.2.17",
3
+ "version": "3.2.18",
4
4
  "description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -39,8 +39,8 @@
39
39
  "@babel/types": "^7.28.5",
40
40
  "@cucumber/gherkin": "^37.0.0",
41
41
  "@cucumber/messages": "^31.0.0",
42
- "@sungen/driver-data-factory": "3.2.17",
43
- "@sungen/driver-ui": "3.2.17",
42
+ "@sungen/driver-data-factory": "3.2.18",
43
+ "@sungen/driver-ui": "3.2.18",
44
44
  "chalk": "^5.6.2",
45
45
  "commander": "^14.0.2",
46
46
  "dotenv": "^17.2.3",
@@ -1,8 +1,9 @@
1
1
  import { Command } from 'commander';
2
2
  import * as path from 'path';
3
3
  import * as fs from 'fs';
4
- import { runAudit, AuditReport } from '../../harness/audit';
4
+ import { runAudit, AuditReport, catalogIdFromScreenDir } from '../../harness/audit';
5
5
  import { reportSlug } from '../../harness/unit-paths';
6
+ import { acceptViewpointBaseline } from '../../harness/viewpoint-baseline';
6
7
 
7
8
  function findScreenDir(name: string): string | null {
8
9
  // `name` may be a bare screen/flow/area, or an api unit id (`api/<area>`, `api/flows/<flow>`).
@@ -78,7 +79,12 @@ function render(r: AuditReport): void {
78
79
  if (r.calibration) {
79
80
  const ax = Object.entries(r.calibration.axes).map(([k, v]) => `${k}=${(v * 100).toFixed(0)}%`).join(' · ');
80
81
  L(` ⑨ Calibration — ${ax}`);
82
+ const na = r.calibration.notApplicable ?? [];
83
+ if (na.length) L(` not scored (no evidence in this unit): ${na.join(', ')}`);
81
84
  L(` weakest: ${r.calibration.weakest.axis} ${(r.calibration.weakest.value * 100).toFixed(0)}%${r.calibration.inflated ? ' ⚠ SCORE-INFLATED-BY-BREADTH' : ''}`);
85
+ if (r.calibration.cappedAt !== undefined) {
86
+ L(` ⚠ score held at ${r.calibration.cappedAt.toFixed(1)} by the weakest axis — fix it to lift the number`);
87
+ }
82
88
  L('');
83
89
  }
84
90
  L(' ── Findings (Repair targets) ──');
@@ -95,6 +101,7 @@ export function registerAuditCommand(program: Command): void {
95
101
  .option('--api <name>', 'API-first area or api flow to audit (e.g. orders, flows/signup)')
96
102
  .option('--area <name>', 'Alias of --api — an API-first area (qa/api/<name>)')
97
103
  .option('--json', 'Output the raw JSON report')
104
+ .option('--accept-viewpoint', 'Accept the current test-viewpoint.md as the baseline the suite is measured against')
98
105
  .action((options) => {
99
106
  try {
100
107
  const name = options.screen || options.api || options.area;
@@ -104,6 +111,21 @@ export function registerAuditCommand(program: Command): void {
104
111
 
105
112
  const report = runAudit(dir, name);
106
113
 
114
+ // The baseline is only ever written HERE, never by the measurement itself:
115
+ // a first sighting has nothing to compare against, and any later change is
116
+ // a human decision (`--accept-viewpoint`) — see harness/viewpoint-baseline.ts.
117
+ const unitId = catalogIdFromScreenDir(dir);
118
+ const vb = report.viewpointBaseline;
119
+ if (vb.status === 'new' || (options.acceptViewpoint && vb.status === 'changed')) {
120
+ acceptViewpointBaseline(process.cwd(), unitId, { hash: vb.hash, ids: vb.ids });
121
+ if (options.acceptViewpoint && !options.json) {
122
+ console.log(`\n ✓ test-viewpoint.md accepted as the baseline for ${name} (${vb.ids.length} viewpoint id(s)).`);
123
+ console.log(' Re-run the audit to score atomicLedger + traceability against it.\n');
124
+ }
125
+ } else if (options.acceptViewpoint && !options.json) {
126
+ console.log(`\n ○ nothing to accept for ${name} — the viewpoint baseline is ${vb.status}.\n`);
127
+ }
128
+
107
129
  // Persist report under .sungen/reports/ (flat slug: api flow `flows/x` → `flows-x-audit.json`).
108
130
  const outDir = path.join(process.cwd(), '.sungen', 'reports');
109
131
  fs.mkdirSync(outDir, { recursive: true });
@@ -23,8 +23,20 @@ import {
23
23
  } from '../../exporters/csv-exporter';
24
24
  import { renderXlsxMultiSheet, writeXlsx, buildApiDetailRows, addApiDetailSheet } from '../../exporters/xlsx-exporter';
25
25
  import { loadApiCatalog } from '../../exporters/api-catalog-loader';
26
+ import { resolveApiEnvironment } from '../../exporters/api-environment';
26
27
  import { resolvePlatformAppId } from '../../exporters/mobile-app-id';
27
28
  import { EnvironmentInfo, PreflightCheck, ScreenSummary, TestCaseRow, ApiCatalogEntry } from '../../exporters/types';
29
+ import { mapFilePath } from '../../exporters/matrix/map-loader';
30
+ import {
31
+ MatrixTargetPaths,
32
+ loadMatrixModel,
33
+ approveMatrix,
34
+ writeMatrixDeliverables,
35
+ MatrixFormat,
36
+ hasErrors,
37
+ hasReview,
38
+ } from '../../exporters/matrix/export';
39
+ import { MatrixFinding } from '../../exporters/matrix/types';
28
40
 
29
41
  const COLOR = {
30
42
  reset: '\x1b[0m',
@@ -421,6 +433,15 @@ async function exportTarget(
421
433
  const apiCatalog = loadApiCatalog(cwd, target.kind, target.screen);
422
434
  const hasApiCatalog = target.kind === 'api' && Object.keys(apiCatalog).length > 0;
423
435
 
436
+ // API units call the datasource base_url (datasources.yaml + .env.qa), NOT the web
437
+ // baseURL from playwright.config.ts — the two hosts routinely differ, and an
438
+ // API-only project has no web baseURL at all. Resolve the real environment for the
439
+ // Test Environment cell; on any gap (no datasources.yaml / no base_url) keep the
440
+ // generic env unchanged. Shared with the dashboard so both surfaces agree.
441
+ const unitEnv = target.kind === 'api'
442
+ ? { ...env, ...(resolveApiEnvironment(cwd, apiCatalog) ?? {}) }
443
+ : env;
444
+
424
445
  // Decide between single-locale and aggregated multi-locale flows.
425
446
  // • SUNGEN_ENV set → single locale (existing behaviour, no change)
426
447
  // • SUNGEN_ENV unset → discover every *-test-result*.json variant.
@@ -440,7 +461,7 @@ async function exportTarget(
440
461
  merged,
441
462
  testData: loadTestData(testDataFile),
442
463
  results: resolveResultsPath(cwd, target) ? loadPlaywrightReport(resolveResultsPath(cwd, target)!) : null,
443
- env,
464
+ env: unitEnv,
444
465
  selectorKeyMap,
445
466
  kind: target.kind,
446
467
  apiCatalog,
@@ -500,7 +521,7 @@ async function exportTarget(
500
521
  // showed base-language values in the Test Data column.
501
522
  testData: loadTestData(testDataFile, variant.overlay ?? null),
502
523
  results: variant.resultsPath ? loadPlaywrightReport(variant.resultsPath) : null,
503
- env,
524
+ env: unitEnv,
504
525
  selectorKeyMap,
505
526
  kind: target.kind,
506
527
  apiCatalog,
@@ -551,7 +572,7 @@ async function exportTarget(
551
572
  merged,
552
573
  testData: variantTestData,
553
574
  results: variantResults,
554
- env,
575
+ env: unitEnv,
555
576
  selectorKeyMap,
556
577
  kind: target.kind,
557
578
  apiCatalog,
@@ -597,6 +618,129 @@ async function exportTarget(
597
618
  }
598
619
  }
599
620
 
621
+ // ----------------------------------------------------------------------------
622
+ // Matrix export (Delivery v2 — the default; --legacy keeps the classic workbook)
623
+ // ----------------------------------------------------------------------------
624
+
625
+ function matrixPathsFor(cwd: string, target: DeliveryTarget): MatrixTargetPaths {
626
+ const base = qaDir(cwd, target);
627
+ const genBase = generatedDir(cwd, target);
628
+ return {
629
+ cwd,
630
+ unit: target.featureBaseName,
631
+ label: target.isFlow ? `flow/${target.featureBaseName}` : target.featureBaseName,
632
+ featureFile: path.join(base, 'features', `${target.featureBaseName}.feature`),
633
+ testDataFile: resolveTestDataPathForTarget(cwd, target),
634
+ specFile: path.join(genBase, `${target.featureBaseName}.spec.ts`),
635
+ specMdFile: path.join(base, 'requirements', 'spec.md'),
636
+ resultsPath: resolveResultsPath(cwd, target),
637
+ mapFile: mapFilePath(base, target.featureBaseName),
638
+ };
639
+ }
640
+
641
+ function printFindings(findings: MatrixFinding[]): void {
642
+ const icon = { error: `${COLOR.red}✗`, review: `${COLOR.yellow}⚠`, warning: `${COLOR.gray}·` } as const;
643
+ for (const f of findings) {
644
+ log(` ${icon[f.severity]} [Gate ${f.gate}]${COLOR.reset} ${f.ref ? `${f.ref}: ` : ''}${f.message}`);
645
+ }
646
+ }
647
+
648
+ interface MatrixRunSummary {
649
+ label: string;
650
+ items: number;
651
+ variants: number;
652
+ passed: number;
653
+ failed: number;
654
+ file: string;
655
+ }
656
+
657
+ /**
658
+ * Matrix-mode export of one target. Returns null when blocked (missing map,
659
+ * gate errors, or unapproved groups without --preview) — the caller aggregates
660
+ * a non-zero exit.
661
+ */
662
+ async function exportMatrixTarget(
663
+ cwd: string,
664
+ target: DeliveryTarget,
665
+ opts: { check?: boolean; preview?: boolean; format?: MatrixFormat },
666
+ ): Promise<MatrixRunSummary | null> {
667
+ const paths = matrixPathsFor(cwd, target);
668
+ const { model, mapMissing, mapErrors } = loadMatrixModel(paths);
669
+
670
+ if (mapMissing) {
671
+ log(`${COLOR.yellow}${paths.label}: no delivery map${COLOR.reset} (${path.relative(cwd, paths.mapFile)})`);
672
+ log(` The matrix needs the reviewed grouping (target / intent / oracle per delivery item).`);
673
+ log(` ${COLOR.cyan}→ run /sungen:delivery ${target.screen} so the map is proposed for your review${COLOR.reset}`);
674
+ log(` ${COLOR.cyan}→ or export the classic per-scenario workbook: sungen delivery ${target.screen} --legacy${COLOR.reset}`);
675
+ return null;
676
+ }
677
+ if (mapErrors.length > 0 || !model) {
678
+ log(`${COLOR.red}${paths.label}: delivery map is invalid${COLOR.reset}`);
679
+ for (const e of mapErrors) log(` - ${e}`);
680
+ return null;
681
+ }
682
+
683
+ log(`${COLOR.bold}${paths.label}${COLOR.reset} — ${model.manifest.itemCount} delivery item(s), ${model.manifest.variantCount} coverage variant(s), state: ${model.manifest.approvalState}`);
684
+ printFindings(model.findings);
685
+
686
+ if (opts.check) {
687
+ // Gates-only run: the summary IS the output; exit code reflects errors.
688
+ return hasErrors(model.findings)
689
+ ? null
690
+ : { label: paths.label, items: model.manifest.itemCount, variants: model.manifest.variantCount, passed: 0, failed: 0, file: '(check only)' };
691
+ }
692
+ if (hasErrors(model.findings)) {
693
+ log(` ${COLOR.red}blocked:${COLOR.reset} fix the Gate errors above, then re-run.`);
694
+ return null;
695
+ }
696
+ if (hasReview(model.findings) && !opts.preview) {
697
+ log(` ${COLOR.yellow}not approved for official delivery.${COLOR.reset}`);
698
+ log(` ${COLOR.cyan}→ approve the reviewed grouping: sungen delivery ${target.screen} --approve${COLOR.reset}`);
699
+ log(` ${COLOR.cyan}→ or render a watermarked draft: sungen delivery ${target.screen} --preview${COLOR.reset}`);
700
+ return null;
701
+ }
702
+
703
+ const { csvPath, xlsxPath } = await writeMatrixDeliverables(paths, model, opts.format ?? 'xlsx');
704
+ const passed = model.items.reduce((a, i) => a + i.resultCounts.passed, 0);
705
+ const failed = model.items.reduce((a, i) => a + i.resultCounts.failed, 0);
706
+ const written = [xlsxPath, csvPath].filter((p): p is string => !!p).map((p) => path.relative(cwd, p));
707
+ for (const rel of written) log(` ${COLOR.green}→ ${rel}${COLOR.reset}`);
708
+ return {
709
+ label: paths.label,
710
+ items: model.manifest.itemCount,
711
+ variants: model.manifest.variantCount,
712
+ passed,
713
+ failed,
714
+ file: written.join(' + '),
715
+ };
716
+ }
717
+
718
+ /**
719
+ * One place that accounts for every target the run did NOT write, so a sweep is
720
+ * never silently partial: scaffolds skipped by preflight and units whose gates /
721
+ * missing map stopped the export.
722
+ */
723
+ function printSkipSummary(skipped: string[], notExported: string[]): void {
724
+ if (skipped.length === 0 && notExported.length === 0) return;
725
+ log(`${COLOR.gray}Not exported (${skipped.length + notExported.length}):${COLOR.reset}`);
726
+ for (const s of skipped) log(` ${COLOR.gray}· ${s}${COLOR.reset}`);
727
+ for (const n of notExported) log(` ${COLOR.gray}· ${n} (see the message above)${COLOR.reset}`);
728
+ log('');
729
+ }
730
+
731
+ function printMatrixSummaryTable(summaries: MatrixRunSummary[]): void {
732
+ log(`\n${COLOR.bold}Delivery matrix export complete${COLOR.reset}\n`);
733
+ const colWidth = Math.max(20, ...summaries.map((s) => s.label.length)) + 1;
734
+ log(` ${'Feature'.padEnd(colWidth)}Items Variants Passed Failed File(s)`);
735
+ log(' ' + '-'.repeat(colWidth + 50));
736
+ for (const s of summaries) {
737
+ log(
738
+ ` ${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}`
739
+ );
740
+ }
741
+ log(`\n${COLOR.gray}Items = review rows (one target + intent + oracle family). Variants = execution units — use variant counts for progress.${COLOR.reset}\n`);
742
+ }
743
+
600
744
  // ----------------------------------------------------------------------------
601
745
  // Reporter
602
746
  // ----------------------------------------------------------------------------
@@ -667,23 +811,41 @@ function printSummaryTable(summaries: ScreenSummary[]): void {
667
811
  export function registerDeliveryCommand(program: Command): void {
668
812
  program
669
813
  .command('delivery')
670
- .description('Export Gherkin + Playwright results CSV test case deliverable')
814
+ .description('Export the Test Case & Coverage Matrix (Gherkin + results). --legacy keeps the classic per-scenario workbook.')
671
815
  .argument('[names...]', 'Specific screen or flow names. Omit to process all.')
672
816
  .option('--skip-preflight', 'Skip pre-flight checks (not recommended)')
673
817
  .option('--continue-on-missing', 'Skip targets with blocking misses instead of aborting')
674
818
  .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 }) => {
819
+ .option('--legacy', 'Classic per-scenario-steps workbook (BM-2-901-13 layout)')
820
+ .option('--full', 'Alias of --legacy')
821
+ .option('--check', 'Matrix gates only — validate the delivery map, write nothing')
822
+ .option('--approve [ids]', 'Approve proposed groups (all, or a comma-separated id list) + stamp fingerprints')
823
+ .option('--preview', 'Render the matrix despite review-required findings (DRAFT watermark)')
824
+ .option('--format <format>', 'Matrix output format: xlsx (default) | csv | both', 'xlsx')
825
+ .action(async (names: string[], options: {
826
+ skipPreflight?: boolean; continueOnMissing?: boolean; env?: string;
827
+ legacy?: boolean; full?: boolean; check?: boolean; approve?: boolean | string; preview?: boolean;
828
+ format?: string;
829
+ }) => {
676
830
  try {
677
831
  // Same effect as running with SUNGEN_ENV=<env> — result-file resolution,
678
832
  // locale pairing, and test-data overlays all key off this one string.
679
833
  if (options.env) process.env.SUNGEN_ENV = options.env;
680
834
  const cwd = process.cwd();
681
835
 
836
+ // Matrix output format — one artifact by default (xlsx); csv/both on request.
837
+ const format = (options.format ?? 'xlsx').toLowerCase();
838
+ if (!['xlsx', 'csv', 'both'].includes(format)) {
839
+ console.error(`${COLOR.red}Invalid --format "${options.format}"${COLOR.reset} — use xlsx | csv | both`);
840
+ process.exit(1);
841
+ }
842
+
682
843
  // 1. Scope detection — each positional name expands into one target
683
844
  // per `.feature` file inside that screen/flow. Passing a specific
684
845
  // feature basename (e.g. `home-modal`) narrows to that one file.
685
846
  let targets: DeliveryTarget[];
686
- if (names && names.length > 0) {
847
+ const explicitScope = !!(names && names.length > 0);
848
+ if (explicitScope) {
687
849
  targets = names.flatMap((n) => resolveTargetsFromArg(cwd, n));
688
850
  } else {
689
851
  targets = listAllTargets(cwd);
@@ -696,8 +858,13 @@ export function registerDeliveryCommand(program: Command): void {
696
858
  const labels = targets.map((t) => t.isFlow ? `flow/${t.featureBaseName}` : t.featureBaseName);
697
859
  log(`${COLOR.bold}sungen delivery${COLOR.reset} — exporting ${targets.length} feature(s): ${labels.join(', ')}\n`);
698
860
 
699
- // 2. Pre-flight
861
+ // 2. Pre-flight — strictness follows the SCOPE. Naming a unit means "I want
862
+ // this one" (fail loudly); a no-argument sweep means "export everything
863
+ // that is ready", so scaffolded units (added but not yet authored) are
864
+ // skipped with a note instead of aborting the whole run — otherwise one
865
+ // untouched scaffold in a work-in-progress project produces zero files.
700
866
  let toExport: DeliveryTarget[];
867
+ const skipped: string[] = [];
701
868
  if (options.skipPreflight) {
702
869
  toExport = targets;
703
870
  } else {
@@ -706,28 +873,79 @@ export function registerDeliveryCommand(program: Command): void {
706
873
 
707
874
  const blockers = checks.filter(hasBlockingMissing);
708
875
  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 {
876
+ if (explicitScope && !options.continueOnMissing) {
719
877
  console.error(
720
- `${COLOR.red}Aborted:${COLOR.reset} ${blockers.length} target(s) have blocking issues.\n` +
878
+ `${COLOR.red}Aborted:${COLOR.reset} ${blockers.length} named target(s) have blocking issues.\n` +
721
879
  `Run the suggested commands above, or use ${COLOR.cyan}--continue-on-missing${COLOR.reset} to skip them.`
722
880
  );
723
881
  process.exit(1);
724
882
  }
883
+ const readyLabels = new Set(
884
+ checks.filter((c) => !hasBlockingMissing(c)).map((c) => c.screen)
885
+ );
886
+ toExport = targets.filter((t) => {
887
+ const label = t.isFlow ? `flow/${t.featureBaseName}` : t.featureBaseName;
888
+ return readyLabels.has(label);
889
+ });
890
+ for (const b of blockers) skipped.push(`${b.screen} (not authored yet — run /sungen:create-test)`);
891
+ log(`${COLOR.yellow}Skipping ${blockers.length} target(s) that have no test cases yet; continuing with ${toExport.length}.${COLOR.reset}\n`);
725
892
  } else {
726
893
  toExport = targets;
727
894
  }
728
895
  }
729
896
 
730
897
  // 3. Export
898
+ const legacy = options.legacy || options.full;
899
+ if (!legacy) {
900
+ // Matrix mode (the default). --approve is its own sub-action.
901
+ if (options.approve !== undefined) {
902
+ const ids = typeof options.approve === 'string'
903
+ ? options.approve.split(',').map((s) => s.trim()).filter(Boolean)
904
+ : undefined;
905
+ let blocked = false;
906
+ for (const target of toExport) {
907
+ const paths = matrixPathsFor(cwd, target);
908
+ try {
909
+ const { findings, approved } = approveMatrix(paths, ids);
910
+ if (approved.length === 0) {
911
+ log(`${COLOR.red}${paths.label}: approval blocked by Gate errors${COLOR.reset}`);
912
+ printFindings(findings);
913
+ blocked = true;
914
+ } else {
915
+ log(`${COLOR.green}${paths.label}: approved ${approved.length} group(s)${COLOR.reset} — fingerprints stamped in ${path.relative(cwd, paths.mapFile)}`);
916
+ }
917
+ } catch (e) {
918
+ log(`${COLOR.red}${paths.label}:${COLOR.reset} ${e instanceof Error ? e.message : e}`);
919
+ blocked = true;
920
+ }
921
+ }
922
+ if (blocked) process.exit(1);
923
+ return;
924
+ }
925
+
926
+ const matrixSummaries: MatrixRunSummary[] = [];
927
+ const notExported: string[] = [];
928
+ for (const target of toExport) {
929
+ const s = await exportMatrixTarget(cwd, target, {
930
+ check: options.check, preview: options.preview, format: format as MatrixFormat,
931
+ });
932
+ if (s) matrixSummaries.push(s);
933
+ else notExported.push(target.isFlow ? `flow/${target.featureBaseName}` : target.featureBaseName);
934
+ }
935
+ if (matrixSummaries.length > 0 && !options.check) printMatrixSummaryTable(matrixSummaries);
936
+ printSkipSummary(skipped, notExported);
937
+ // A named target that produced nothing is a failure. A sweep is a failure
938
+ // only when it produced nothing at all — units still waiting for a map or
939
+ // an approval are the normal state of a live project. Preflight skips are
940
+ // not counted here: reaching this point in an explicit scope means
941
+ // --continue-on-missing was passed, which IS the opt-in to tolerate them.
942
+ const failed = explicitScope
943
+ ? notExported.length > 0
944
+ : matrixSummaries.length === 0;
945
+ if (failed) process.exit(1);
946
+ return;
947
+ }
948
+
731
949
  const env = getEnvironment(cwd);
732
950
  const summaries: ScreenSummary[] = [];
733
951
  for (const target of toExport) {
@@ -737,6 +955,10 @@ export function registerDeliveryCommand(program: Command): void {
737
955
 
738
956
  // 4. Summary
739
957
  printSummaryTable(summaries);
958
+ printSkipSummary(skipped, []);
959
+ // Same rule as the matrix path: every attempted target in an explicit scope
960
+ // must have produced a file; a sweep only fails when nothing was written.
961
+ if (explicitScope ? summaries.length < toExport.length : summaries.length === 0) process.exit(1);
740
962
  } catch (err) {
741
963
  console.error(`${COLOR.red}Fatal:${COLOR.reset} ${err instanceof Error ? err.message : err}`);
742
964
  process.exit(1);
package/src/cli/index.ts CHANGED
@@ -35,6 +35,7 @@ import { registerRepairCommand } from './commands/repair';
35
35
  import { registerLoginCommand } from './commands/login';
36
36
  import { capabilityRegistry } from '../capabilities/registry';
37
37
  import { discoverAndRegisterCapabilities } from '../capabilities/discover';
38
+ import { warnAssetsDrift } from '../orchestrator/assets-drift';
38
39
 
39
40
  // Read version from package.json so `--version` never drifts from the released version.
40
41
  const { version } = require('../../package.json') as { version: string };
@@ -88,6 +89,11 @@ async function main() {
88
89
  for (const registerCommand of cap.cliCommands ?? []) registerCommand(program);
89
90
  }
90
91
 
92
+ // Every command warns once when the project's AI assets came from a different
93
+ // build — `update` is the only thing that refreshes them, and nothing used to
94
+ // say so (stderr, so `--json` output stays parseable).
95
+ program.hook('preAction', () => { warnAssetsDrift(process.cwd()); });
96
+
91
97
  await program.parseAsync(process.argv);
92
98
  }
93
99
 
@@ -23,6 +23,7 @@ import { loadPlaywrightReport, resolveResultsPath } from '../exporters/playwrigh
23
23
  import { mergeFeatureAndSpec } from '../exporters/scenario-merger';
24
24
  import { buildScreenSnapshot } from '../exporters/json-exporter';
25
25
  import { loadApiCatalog } from '../exporters/api-catalog-loader';
26
+ import { resolveApiEnvironment } from '../exporters/api-environment';
26
27
  import { buildApiDetailRows } from '../exporters/xlsx-exporter';
27
28
  import { resolvePlatformAppId } from '../exporters/mobile-app-id';
28
29
  import { discoverLocaleVariants, LocaleVariant } from '../exporters/locale-variants';
@@ -132,9 +133,15 @@ function buildOneScreen(
132
133
  // shared+unit merge delivery.ts uses — so Steps/Expected cells and the API
133
134
  // detail sheet match the delivery CSV/XLSX exactly.
134
135
  const apiCatalog = loadApiCatalog(cwd, target.kind, target.name);
136
+ // API units call the datasource base_url, not the web baseURL from
137
+ // playwright.config.ts — same Test-Environment override as delivery.ts, so the
138
+ // dashboard's Suites/Export cells never disagree with the CSV/XLSX deliverable.
139
+ const unitEnv = target.kind === 'api'
140
+ ? { ...env, ...(resolveApiEnvironment(cwd, apiCatalog) ?? {}) }
141
+ : env;
135
142
  const features: FeatureSnapshot[] = [];
136
143
  for (const featureBaseName of featureBaseNames) {
137
- features.push(...buildOneFeature(cwd, target, featureBaseName, env, specLink, mobile, apiCatalog));
144
+ features.push(...buildOneFeature(cwd, target, featureBaseName, unitEnv, specLink, mobile, apiCatalog));
138
145
  }
139
146
  if (features.length === 0) return null;
140
147