@sun-asterisk/sungen 3.2.16 → 3.2.18-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 (206) 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/commands/ingest.d.ts.map +1 -1
  8. package/dist/cli/commands/ingest.js +56 -4
  9. package/dist/cli/commands/ingest.js.map +1 -1
  10. package/dist/cli/index.js +5 -0
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  13. package/dist/dashboard/snapshot-builder.js +8 -1
  14. package/dist/dashboard/snapshot-builder.js.map +1 -1
  15. package/dist/dashboard/templates/index.html +1 -1
  16. package/dist/exporters/api-environment.d.ts +11 -0
  17. package/dist/exporters/api-environment.d.ts.map +1 -0
  18. package/dist/exporters/api-environment.js +129 -0
  19. package/dist/exporters/api-environment.js.map +1 -0
  20. package/dist/exporters/feature-parser.d.ts +16 -1
  21. package/dist/exporters/feature-parser.d.ts.map +1 -1
  22. package/dist/exporters/feature-parser.js +21 -2
  23. package/dist/exporters/feature-parser.js.map +1 -1
  24. package/dist/exporters/matrix/build.d.ts +70 -0
  25. package/dist/exporters/matrix/build.d.ts.map +1 -0
  26. package/dist/exporters/matrix/build.js +516 -0
  27. package/dist/exporters/matrix/build.js.map +1 -0
  28. package/dist/exporters/matrix/export.d.ts +50 -0
  29. package/dist/exporters/matrix/export.d.ts.map +1 -0
  30. package/dist/exporters/matrix/export.js +152 -0
  31. package/dist/exporters/matrix/export.js.map +1 -0
  32. package/dist/exporters/matrix/fingerprint.d.ts +20 -0
  33. package/dist/exporters/matrix/fingerprint.d.ts.map +1 -0
  34. package/dist/exporters/matrix/fingerprint.js +83 -0
  35. package/dist/exporters/matrix/fingerprint.js.map +1 -0
  36. package/dist/exporters/matrix/gates.d.ts +21 -0
  37. package/dist/exporters/matrix/gates.d.ts.map +1 -0
  38. package/dist/exporters/matrix/gates.js +367 -0
  39. package/dist/exporters/matrix/gates.js.map +1 -0
  40. package/dist/exporters/matrix/map-loader.d.ts +22 -0
  41. package/dist/exporters/matrix/map-loader.d.ts.map +1 -0
  42. package/dist/exporters/matrix/map-loader.js +193 -0
  43. package/dist/exporters/matrix/map-loader.js.map +1 -0
  44. package/dist/exporters/matrix/render-csv.d.ts +9 -0
  45. package/dist/exporters/matrix/render-csv.d.ts.map +1 -0
  46. package/dist/exporters/matrix/render-csv.js +92 -0
  47. package/dist/exporters/matrix/render-csv.js.map +1 -0
  48. package/dist/exporters/matrix/render-xlsx.d.ts +41 -0
  49. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -0
  50. package/dist/exporters/matrix/render-xlsx.js +430 -0
  51. package/dist/exporters/matrix/render-xlsx.js.map +1 -0
  52. package/dist/exporters/matrix/types.d.ts +199 -0
  53. package/dist/exporters/matrix/types.d.ts.map +1 -0
  54. package/dist/exporters/matrix/types.js +15 -0
  55. package/dist/exporters/matrix/types.js.map +1 -0
  56. package/dist/exporters/matrix/wording.d.ts +61 -0
  57. package/dist/exporters/matrix/wording.d.ts.map +1 -0
  58. package/dist/exporters/matrix/wording.js +221 -0
  59. package/dist/exporters/matrix/wording.js.map +1 -0
  60. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  61. package/dist/exporters/scenario-merger.js +2 -1
  62. package/dist/exporters/scenario-merger.js.map +1 -1
  63. package/dist/exporters/spec-parser.d.ts.map +1 -1
  64. package/dist/exporters/spec-parser.js +2 -1
  65. package/dist/exporters/spec-parser.js.map +1 -1
  66. package/dist/exporters/xlsx-report-builder.d.ts +1 -1
  67. package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
  68. package/dist/exporters/xlsx-report-builder.js +3 -3
  69. package/dist/exporters/xlsx-report-builder.js.map +1 -1
  70. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-action.hbs +1 -1
  71. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-element-with-text.hbs +1 -1
  72. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-timeout.hbs +1 -1
  73. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
  74. package/dist/generators/test-generator/utils/runtime-data-transformer.js +28 -17
  75. package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
  76. package/dist/harness/audit.d.ts +10 -0
  77. package/dist/harness/audit.d.ts.map +1 -1
  78. package/dist/harness/audit.js +98 -16
  79. package/dist/harness/audit.js.map +1 -1
  80. package/dist/harness/blindspot.d.ts.map +1 -1
  81. package/dist/harness/blindspot.js +2 -1
  82. package/dist/harness/blindspot.js.map +1 -1
  83. package/dist/harness/capability-plan.d.ts +2 -0
  84. package/dist/harness/capability-plan.d.ts.map +1 -1
  85. package/dist/harness/capability-plan.js +17 -4
  86. package/dist/harness/capability-plan.js.map +1 -1
  87. package/dist/harness/feedback.d.ts.map +1 -1
  88. package/dist/harness/feedback.js +3 -2
  89. package/dist/harness/feedback.js.map +1 -1
  90. package/dist/harness/flow-check.d.ts.map +1 -1
  91. package/dist/harness/flow-check.js +2 -1
  92. package/dist/harness/flow-check.js.map +1 -1
  93. package/dist/harness/flow-plan.d.ts.map +1 -1
  94. package/dist/harness/flow-plan.js +3 -2
  95. package/dist/harness/flow-plan.js.map +1 -1
  96. package/dist/harness/intent.d.ts.map +1 -1
  97. package/dist/harness/intent.js +2 -1
  98. package/dist/harness/intent.js.map +1 -1
  99. package/dist/harness/journey.d.ts.map +1 -1
  100. package/dist/harness/journey.js +3 -2
  101. package/dist/harness/journey.js.map +1 -1
  102. package/dist/harness/ledger.d.ts.map +1 -1
  103. package/dist/harness/ledger.js +3 -2
  104. package/dist/harness/ledger.js.map +1 -1
  105. package/dist/harness/manifest.d.ts.map +1 -1
  106. package/dist/harness/manifest.js +4 -3
  107. package/dist/harness/manifest.js.map +1 -1
  108. package/dist/harness/parse.d.ts.map +1 -1
  109. package/dist/harness/parse.js +16 -3
  110. package/dist/harness/parse.js.map +1 -1
  111. package/dist/harness/quality-gates.d.ts.map +1 -1
  112. package/dist/harness/quality-gates.js +2 -1
  113. package/dist/harness/quality-gates.js.map +1 -1
  114. package/dist/harness/read-text.d.ts +22 -0
  115. package/dist/harness/read-text.d.ts.map +1 -0
  116. package/dist/harness/read-text.js +64 -0
  117. package/dist/harness/read-text.js.map +1 -0
  118. package/dist/harness/script-check.d.ts.map +1 -1
  119. package/dist/harness/script-check.js +3 -2
  120. package/dist/harness/script-check.js.map +1 -1
  121. package/dist/harness/sensors.d.ts +13 -1
  122. package/dist/harness/sensors.d.ts.map +1 -1
  123. package/dist/harness/sensors.js +63 -20
  124. package/dist/harness/sensors.js.map +1 -1
  125. package/dist/harness/spec-coverage.d.ts +5 -0
  126. package/dist/harness/spec-coverage.d.ts.map +1 -1
  127. package/dist/harness/spec-coverage.js +17 -7
  128. package/dist/harness/spec-coverage.js.map +1 -1
  129. package/dist/harness/trace.d.ts.map +1 -1
  130. package/dist/harness/trace.js +4 -3
  131. package/dist/harness/trace.js.map +1 -1
  132. package/dist/harness/viewpoint-baseline.d.ts +49 -0
  133. package/dist/harness/viewpoint-baseline.d.ts.map +1 -0
  134. package/dist/harness/viewpoint-baseline.js +141 -0
  135. package/dist/harness/viewpoint-baseline.js.map +1 -0
  136. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  137. package/dist/harness/viewpoint-ledger.js +2 -1
  138. package/dist/harness/viewpoint-ledger.js.map +1 -1
  139. package/dist/ingest/legacy-parser.d.ts +3 -0
  140. package/dist/ingest/legacy-parser.d.ts.map +1 -1
  141. package/dist/ingest/legacy-parser.js +52 -3
  142. package/dist/ingest/legacy-parser.js.map +1 -1
  143. package/dist/orchestrator/assets-drift.d.ts +24 -0
  144. package/dist/orchestrator/assets-drift.d.ts.map +1 -0
  145. package/dist/orchestrator/assets-drift.js +80 -0
  146. package/dist/orchestrator/assets-drift.js.map +1 -0
  147. package/dist/orchestrator/templates/ai-src/commands/create-test.md +24 -0
  148. package/dist/orchestrator/templates/ai-src/commands/delivery.md +203 -120
  149. package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
  150. package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
  151. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +102 -6
  152. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +8 -0
  153. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +23 -0
  154. package/package.json +4 -4
  155. package/src/cli/commands/audit.ts +23 -1
  156. package/src/cli/commands/delivery.ts +240 -18
  157. package/src/cli/commands/ingest.ts +57 -5
  158. package/src/cli/index.ts +6 -0
  159. package/src/dashboard/snapshot-builder.ts +8 -1
  160. package/src/dashboard/templates/index.html +1 -1
  161. package/src/exporters/api-environment.ts +97 -0
  162. package/src/exporters/feature-parser.ts +21 -2
  163. package/src/exporters/matrix/build.ts +551 -0
  164. package/src/exporters/matrix/export.ts +149 -0
  165. package/src/exporters/matrix/fingerprint.ts +49 -0
  166. package/src/exporters/matrix/gates.ts +400 -0
  167. package/src/exporters/matrix/map-loader.ts +163 -0
  168. package/src/exporters/matrix/render-csv.ts +94 -0
  169. package/src/exporters/matrix/render-xlsx.ts +420 -0
  170. package/src/exporters/matrix/types.ts +226 -0
  171. package/src/exporters/matrix/wording.ts +221 -0
  172. package/src/exporters/scenario-merger.ts +2 -1
  173. package/src/exporters/spec-parser.ts +2 -1
  174. package/src/exporters/xlsx-report-builder.ts +3 -2
  175. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-action.hbs +1 -1
  176. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-element-with-text.hbs +1 -1
  177. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-timeout.hbs +1 -1
  178. package/src/generators/test-generator/utils/runtime-data-transformer.ts +29 -18
  179. package/src/harness/audit.ts +104 -18
  180. package/src/harness/blindspot.ts +2 -1
  181. package/src/harness/capability-plan.ts +17 -4
  182. package/src/harness/feedback.ts +3 -2
  183. package/src/harness/flow-check.ts +2 -1
  184. package/src/harness/flow-plan.ts +3 -2
  185. package/src/harness/intent.ts +2 -1
  186. package/src/harness/journey.ts +3 -2
  187. package/src/harness/ledger.ts +3 -2
  188. package/src/harness/manifest.ts +4 -3
  189. package/src/harness/parse.ts +17 -3
  190. package/src/harness/quality-gates.ts +2 -1
  191. package/src/harness/read-text.ts +28 -0
  192. package/src/harness/script-check.ts +3 -2
  193. package/src/harness/sensors.ts +55 -8
  194. package/src/harness/spec-coverage.ts +22 -7
  195. package/src/harness/trace.ts +4 -3
  196. package/src/harness/viewpoint-baseline.ts +128 -0
  197. package/src/harness/viewpoint-ledger.ts +2 -1
  198. package/src/ingest/legacy-parser.ts +48 -3
  199. package/src/orchestrator/assets-drift.ts +64 -0
  200. package/src/orchestrator/templates/ai-src/commands/create-test.md +24 -0
  201. package/src/orchestrator/templates/ai-src/commands/delivery.md +203 -120
  202. package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
  203. package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
  204. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +102 -6
  205. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +8 -0
  206. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +23 -0
@@ -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);
@@ -1,14 +1,24 @@
1
1
  import { Command } from 'commander';
2
2
  import * as path from 'path';
3
3
  import * as fs from 'fs';
4
- import { parseLegacyFile, listSheets, baselineAudit, BaselineReport, LegacyInventory, inventoryToGherkin, reconcileViewpoints, renderViewpointOverview, fetchGoogleSheet } from '../../ingest';
4
+ import { parseLegacyFile, listSheets, baselineAudit, BaselineReport, LegacyInventory, inventoryToGherkin, reconcileViewpoints, renderViewpointOverview, fetchGoogleSheet, parseCSV } from '../../ingest';
5
+
6
+ // A tab name that contains commas must round-trip through the quote-aware
7
+ // --sheets parser — wrap it in doubled quotes inside the single-quoted shell arg.
8
+ const quoteTab = (name: string) => (name.includes(',') ? `""${name}""` : name);
5
9
 
6
10
  function renderInventoryMd(inv: LegacyInventory, r: BaselineReport): string {
7
11
  const lines: string[] = [];
8
12
  lines.push(`# Legacy Testcase Inventory — ${inv.source.file}`, '');
9
13
  lines.push(`Total testcases: **${r.total}**`, '');
10
14
  lines.push('## Sheets', '', '| Sheet | Type | Rows |', '|---|---|---|');
11
- for (const s of r.sheets) lines.push(`| ${s.name} | ${s.type} | ${s.rows} |`);
15
+ // Non-testcase tabs (Overview/SQL/review notes…) are classified but NEVER read
16
+ // say so, or the listing reads as if their content was ingested.
17
+ for (const s of r.sheets) lines.push(s.type === 'testcase'
18
+ ? `| ${s.name} | ${s.type} | ${s.rows} |`
19
+ : `| ${s.name} | ${s.type} | — (skipped) |`);
20
+ const skipped = r.sheets.filter((s) => s.type !== 'testcase').length;
21
+ if (skipped) lines.push('', `_${skipped} non-testcase tab(s) skipped — no content read._`);
12
22
  lines.push('', '## By category', '');
13
23
  for (const [k, v] of Object.entries(r.byCategory).sort((a, b) => b[1] - a[1])) lines.push(`- ${k}: ${v}`);
14
24
  lines.push('', `Depth: **${(r.depthRatio * 100).toFixed(0)}%** (${r.deepCount}/${r.total} assert a concrete expected value)`, '');
@@ -22,7 +32,10 @@ function render(inv: LegacyInventory, r: BaselineReport): void {
22
32
  L(`━━━ Legacy Ingest — ${inv.source.file} ━━━`);
23
33
  L('');
24
34
  L(` Sheets:`);
25
- for (const s of r.sheets) L(` • ${s.name} — ${s.type} (${s.rows})`);
35
+ for (const s of r.sheets)
36
+ L(` • ${s.name} — ${s.type}${s.type === 'testcase' ? ` (${s.rows})` : ' → skipped (no content read)'}`);
37
+ const skipped = r.sheets.filter((s) => s.type !== 'testcase').length;
38
+ if (skipped) L(` (${skipped} non-testcase tab(s) skipped)`);
26
39
  L('');
27
40
  L(` TOTAL testcases: ${r.total}`);
28
41
  L('');
@@ -88,13 +101,52 @@ export function registerIngestCommand(program: Command): void {
88
101
  console.log(' Tabs found:');
89
102
  for (const s of sheets) console.log(` • ${s.name} — ${s.type} (${s.rows} rows)`);
90
103
  console.log('');
91
- console.log(` Ingest the testcase tab(s) with: --sheets "${sheets.filter((s) => s.type === 'testcase').map((s) => s.name).join(',')}"`);
104
+ const quoted = sheets.filter((s) => s.type === 'testcase').map((s) => quoteTab(s.name)).join(',');
105
+ console.log(` Ingest the testcase tab(s) with: --sheets '${quoted}'`);
92
106
  console.log('');
93
107
  return;
94
108
  }
95
109
 
96
- const onlySheets: string[] | undefined = options.sheets ? String(options.sheets).split(',') : undefined;
110
+ // --sheets: legacy tab names may CONTAIN commas (field workbook:
111
+ // "EX-0403,0404,0405,0407-SearchRe"), so a bare split(',') shredded them and
112
+ // silently ingested nothing. Resolution order:
113
+ // 1. the WHOLE string equals one real tab name → that single tab;
114
+ // 2. else parse as a quote-aware CSV line ("Tab, with commas",Other);
115
+ // 3. any part matching NO real tab → exit non-zero listing the tabs —
116
+ // an empty inventory must never be produced silently.
117
+ let onlySheets: string[] | undefined;
118
+ if (options.sheets) {
119
+ const raw = String(options.sheets);
120
+ const available = (await listSheets(files)).map((sh) => sh.name);
121
+ const byLower = new Map(available.map((n) => [n.trim().toLowerCase(), n]));
122
+ const whole = byLower.get(raw.trim().toLowerCase());
123
+ if (whole) {
124
+ onlySheets = [whole];
125
+ } else {
126
+ const parts = (parseCSV(raw)[0] || []).map((p) => p.trim()).filter(Boolean);
127
+ const misses = parts.filter((pt) => !byLower.has(pt.toLowerCase()));
128
+ if (misses.length) {
129
+ throw new Error(
130
+ `--sheets: no tab named ${misses.map((m) => `"${m}"`).join(', ')}.\n` +
131
+ ` Available tabs:\n${available.map((n) => ` • ${n}`).join('\n')}\n` +
132
+ ` Tip: quote a tab name that contains commas: --sheets '"${available[0] || 'Tab, name'}"'`
133
+ );
134
+ }
135
+ onlySheets = parts.map((pt) => byLower.get(pt.toLowerCase())!);
136
+ }
137
+ }
97
138
  const inv = await parseLegacyFile(files, onlySheets);
139
+ for (const w of inv.warnings ?? []) console.warn(` \u26a0 ${w}`);
140
+ // Merge guard: without --sheets, EVERY tab that classifies as testcase is merged
141
+ // into one inventory. Field workbooks carry tabs for several screens (plus
142
+ // deprecated "Old\u2026" copies) \u2014 merging them silently poisons a per-screen
143
+ // inventory, so say it loudly and hand over the ready-made --sheets line.
144
+ if (!options.sheets) {
145
+ const tcTabs = inv.sheets.filter((sh) => sh.type === 'testcase');
146
+ if (tcTabs.length > 1) {
147
+ console.warn(` \u26a0 ${tcTabs.length} testcase tabs merged into ONE inventory \u2014 if they belong to different screens, scope with: --sheets '${tcTabs.map((sh) => quoteTab(sh.name)).join(',')}'`);
148
+ }
149
+ }
98
150
  const report = baselineAudit(inv);
99
151
  fs.writeFileSync(path.join(outDir, 'inventory.json'), JSON.stringify({ inventory: inv, baseline: report }, null, 2));
100
152
  fs.writeFileSync(path.join(outDir, 'inventory.md'), renderInventoryMd(inv, report));
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