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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/dist/cli/commands/delivery.d.ts.map +1 -1
  2. package/dist/cli/commands/delivery.js +67 -20
  3. package/dist/cli/commands/delivery.js.map +1 -1
  4. package/dist/exporters/feature-parser.d.ts +16 -1
  5. package/dist/exporters/feature-parser.d.ts.map +1 -1
  6. package/dist/exporters/feature-parser.js +21 -2
  7. package/dist/exporters/feature-parser.js.map +1 -1
  8. package/dist/exporters/matrix/build.d.ts +27 -2
  9. package/dist/exporters/matrix/build.d.ts.map +1 -1
  10. package/dist/exporters/matrix/build.js +277 -47
  11. package/dist/exporters/matrix/build.js.map +1 -1
  12. package/dist/exporters/matrix/export.d.ts +7 -4
  13. package/dist/exporters/matrix/export.d.ts.map +1 -1
  14. package/dist/exporters/matrix/export.js +22 -6
  15. package/dist/exporters/matrix/export.js.map +1 -1
  16. package/dist/exporters/matrix/gates.d.ts.map +1 -1
  17. package/dist/exporters/matrix/gates.js +112 -3
  18. package/dist/exporters/matrix/gates.js.map +1 -1
  19. package/dist/exporters/matrix/map-loader.d.ts.map +1 -1
  20. package/dist/exporters/matrix/map-loader.js +20 -0
  21. package/dist/exporters/matrix/map-loader.js.map +1 -1
  22. package/dist/exporters/matrix/render-csv.d.ts +3 -2
  23. package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
  24. package/dist/exporters/matrix/render-csv.js +53 -30
  25. package/dist/exporters/matrix/render-csv.js.map +1 -1
  26. package/dist/exporters/matrix/render-xlsx.d.ts +32 -8
  27. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
  28. package/dist/exporters/matrix/render-xlsx.js +215 -83
  29. package/dist/exporters/matrix/render-xlsx.js.map +1 -1
  30. package/dist/exporters/matrix/types.d.ts +53 -7
  31. package/dist/exporters/matrix/types.d.ts.map +1 -1
  32. package/dist/exporters/matrix/types.js +2 -2
  33. package/dist/exporters/matrix/types.js.map +1 -1
  34. package/dist/exporters/matrix/wording.d.ts +61 -0
  35. package/dist/exporters/matrix/wording.d.ts.map +1 -0
  36. package/dist/exporters/matrix/wording.js +221 -0
  37. package/dist/exporters/matrix/wording.js.map +1 -0
  38. package/dist/exporters/scenario-merger.d.ts.map +1 -1
  39. package/dist/exporters/scenario-merger.js +2 -1
  40. package/dist/exporters/scenario-merger.js.map +1 -1
  41. package/dist/exporters/spec-parser.d.ts.map +1 -1
  42. package/dist/exporters/spec-parser.js +2 -1
  43. package/dist/exporters/spec-parser.js.map +1 -1
  44. package/dist/harness/audit.d.ts.map +1 -1
  45. package/dist/harness/audit.js +11 -2
  46. package/dist/harness/audit.js.map +1 -1
  47. package/dist/harness/blindspot.d.ts.map +1 -1
  48. package/dist/harness/blindspot.js +2 -1
  49. package/dist/harness/blindspot.js.map +1 -1
  50. package/dist/harness/capability-plan.d.ts.map +1 -1
  51. package/dist/harness/capability-plan.js +3 -2
  52. package/dist/harness/capability-plan.js.map +1 -1
  53. package/dist/harness/feedback.d.ts.map +1 -1
  54. package/dist/harness/feedback.js +3 -2
  55. package/dist/harness/feedback.js.map +1 -1
  56. package/dist/harness/flow-check.d.ts.map +1 -1
  57. package/dist/harness/flow-check.js +2 -1
  58. package/dist/harness/flow-check.js.map +1 -1
  59. package/dist/harness/flow-plan.d.ts.map +1 -1
  60. package/dist/harness/flow-plan.js +3 -2
  61. package/dist/harness/flow-plan.js.map +1 -1
  62. package/dist/harness/intent.d.ts.map +1 -1
  63. package/dist/harness/intent.js +2 -1
  64. package/dist/harness/intent.js.map +1 -1
  65. package/dist/harness/journey.d.ts.map +1 -1
  66. package/dist/harness/journey.js +3 -2
  67. package/dist/harness/journey.js.map +1 -1
  68. package/dist/harness/ledger.d.ts.map +1 -1
  69. package/dist/harness/ledger.js +3 -2
  70. package/dist/harness/ledger.js.map +1 -1
  71. package/dist/harness/manifest.d.ts.map +1 -1
  72. package/dist/harness/manifest.js +4 -3
  73. package/dist/harness/manifest.js.map +1 -1
  74. package/dist/harness/parse.d.ts.map +1 -1
  75. package/dist/harness/parse.js +16 -3
  76. package/dist/harness/parse.js.map +1 -1
  77. package/dist/harness/quality-gates.d.ts.map +1 -1
  78. package/dist/harness/quality-gates.js +2 -1
  79. package/dist/harness/quality-gates.js.map +1 -1
  80. package/dist/harness/read-text.d.ts +22 -0
  81. package/dist/harness/read-text.d.ts.map +1 -0
  82. package/dist/harness/read-text.js +64 -0
  83. package/dist/harness/read-text.js.map +1 -0
  84. package/dist/harness/script-check.d.ts.map +1 -1
  85. package/dist/harness/script-check.js +3 -2
  86. package/dist/harness/script-check.js.map +1 -1
  87. package/dist/harness/sensors.d.ts.map +1 -1
  88. package/dist/harness/sensors.js +2 -10
  89. package/dist/harness/sensors.js.map +1 -1
  90. package/dist/harness/spec-coverage.d.ts +5 -0
  91. package/dist/harness/spec-coverage.d.ts.map +1 -1
  92. package/dist/harness/spec-coverage.js +17 -7
  93. package/dist/harness/spec-coverage.js.map +1 -1
  94. package/dist/harness/trace.d.ts.map +1 -1
  95. package/dist/harness/trace.js +4 -3
  96. package/dist/harness/trace.js.map +1 -1
  97. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  98. package/dist/harness/viewpoint-ledger.js +2 -1
  99. package/dist/harness/viewpoint-ledger.js.map +1 -1
  100. package/dist/orchestrator/templates/ai-src/commands/create-test.md +9 -0
  101. package/dist/orchestrator/templates/ai-src/commands/delivery.md +103 -18
  102. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +77 -11
  103. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +1 -0
  104. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +22 -0
  105. package/package.json +4 -4
  106. package/src/cli/commands/delivery.ts +68 -22
  107. package/src/exporters/feature-parser.ts +21 -2
  108. package/src/exporters/matrix/build.ts +281 -43
  109. package/src/exporters/matrix/export.ts +31 -6
  110. package/src/exporters/matrix/gates.ts +119 -3
  111. package/src/exporters/matrix/map-loader.ts +22 -1
  112. package/src/exporters/matrix/render-csv.ts +53 -30
  113. package/src/exporters/matrix/render-xlsx.ts +216 -85
  114. package/src/exporters/matrix/types.ts +58 -8
  115. package/src/exporters/matrix/wording.ts +221 -0
  116. package/src/exporters/scenario-merger.ts +2 -1
  117. package/src/exporters/spec-parser.ts +2 -1
  118. package/src/harness/audit.ts +11 -2
  119. package/src/harness/blindspot.ts +2 -1
  120. package/src/harness/capability-plan.ts +3 -2
  121. package/src/harness/feedback.ts +3 -2
  122. package/src/harness/flow-check.ts +2 -1
  123. package/src/harness/flow-plan.ts +3 -2
  124. package/src/harness/intent.ts +2 -1
  125. package/src/harness/journey.ts +3 -2
  126. package/src/harness/ledger.ts +3 -2
  127. package/src/harness/manifest.ts +4 -3
  128. package/src/harness/parse.ts +17 -3
  129. package/src/harness/quality-gates.ts +2 -1
  130. package/src/harness/read-text.ts +28 -0
  131. package/src/harness/script-check.ts +3 -2
  132. package/src/harness/sensors.ts +2 -1
  133. package/src/harness/spec-coverage.ts +22 -7
  134. package/src/harness/trace.ts +4 -3
  135. package/src/harness/viewpoint-ledger.ts +2 -1
  136. package/src/orchestrator/templates/ai-src/commands/create-test.md +9 -0
  137. package/src/orchestrator/templates/ai-src/commands/delivery.md +103 -18
  138. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +77 -11
  139. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +1 -0
  140. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +22 -0
@@ -2,14 +2,21 @@
2
2
  * Render the Test Case & Coverage Matrix workbook.
3
3
  *
4
4
  * Sheets:
5
- * 1. Testcases — one parent row per delivery item, variant sub-rows grouped with
6
- * Excel outline (expanded for execution; the level-1 outline button collapses
7
- * to the customer/review view — same sheet, one model, two views).
8
- * 2. Coverage — target × category grid (items + variants, `—` marks the gaps),
9
- * dispositions, and the generation manifest.
5
+ * 1. Testcases — one parent row per delivery item; EVERY item (including
6
+ * single-variant ones) carries variant sub-rows on Excel outline level 1 —
7
+ * the sub-row is where source traceability (VP-id), test data, and the
8
+ * manual result/evidence entry live (review B-05/B-06). Collapse outline
9
+ * level 1 for the customer/review view; expand to execute.
10
+ * 2. Coverage — requirement coverage (id ↔ items ↔ status), the target ×
11
+ * category grid (explicit `—` gaps), dispositions, and the manifest.
10
12
  *
11
- * Every sheet keeps the company ISO masthead (renderReportHeaderBand). A draft
12
- * matrix (unapproved groups / review findings) carries a red DRAFT banner.
13
+ * Manual-execution support: variant Result cells carry a dropdown
14
+ * (Passed/Failed/Blocked/Pending/N/A) and the parent Result is a live Excel
15
+ * formula derived from its children — a parent can never contradict its
16
+ * variants, even after QA edits results by hand (review B-06).
17
+ *
18
+ * Every sheet keeps the company ISO masthead. A draft matrix (unapproved
19
+ * groups / review findings) carries a red DRAFT banner.
13
20
  */
14
21
 
15
22
  import * as ExcelJS from 'exceljs';
@@ -20,13 +27,14 @@ import {
20
27
  BLACK,
21
28
  LAVENDER,
22
29
  } from '../xlsx-report-builder';
23
- import { statusToTestResult, formatExecutedDate } from '../playwright-report-parser';
24
- import { CoverageVariant, DeliveryItem, ItemResult, MatrixModel, MapCategory } from './types';
30
+ import { variantState } from './build';
31
+ import { DeliveryItem, MatrixModel, MapCategory } from './types';
25
32
 
26
33
  const CATEGORY_ORDER: MapCategory[] = ['normal', 'abnormal', 'security', 'nfr'];
27
34
  const SOFT_BLUE = { argb: 'FFDCE6F1' };
28
35
  const DRAFT_RED = { argb: 'FFC00000' };
29
36
  const GAP_FILL = { argb: 'FFFCE4EC' };
37
+ const RESULT_STATES = 'Passed,Failed,Blocked,Pending,N/A';
30
38
 
31
39
  function headerCell(c: ExcelJS.Cell, label: string): void {
32
40
  c.value = label;
@@ -37,35 +45,74 @@ function headerCell(c: ExcelJS.Cell, label: string): void {
37
45
  }
38
46
 
39
47
  function dataCell(c: ExcelJS.Cell, value: ExcelJS.CellValue, opts?: { bold?: boolean; fill?: { argb: string }; center?: boolean; color?: { argb: string } }): void {
40
- c.value = value;
48
+ // An empty value stays a genuinely blank cell — never an empty shared string,
49
+ // which some renderers display as its string-table index (review M-03).
50
+ if (value !== '' && value !== undefined && value !== null) c.value = value;
41
51
  c.font = { bold: opts?.bold ?? false, size: 10, name: ARIAL_FONT, ...(opts?.color ? { color: opts.color } : {}) };
42
52
  c.alignment = { horizontal: opts?.center ? 'center' : 'left', vertical: 'top', wrapText: true };
43
53
  c.border = allBordersBlack;
44
54
  if (opts?.fill) c.fill = { type: 'pattern', pattern: 'solid', fgColor: opts.fill };
45
55
  }
46
56
 
47
- function itemResultLabel(item: DeliveryItem): string {
48
- const { passed, failed, blocked, notRun } = item.resultCounts;
49
- const total = item.variants.length;
50
- const map: Record<ItemResult, string> = {
51
- passed: `${passed}/${total} Passed`,
52
- failed: `${passed}/${total} Passed · ${failed} Failed`,
53
- blocked: `${blocked} Blocked`,
54
- partial: `${passed}/${total} Passed · ${notRun} not run`,
55
- not_run: 'Pending',
56
- };
57
- return map[item.result];
57
+ /** Mode is a coverage dimension: mixed items show both counts. */
58
+ export function itemModeLabel(item: DeliveryItem): string {
59
+ if (item.mode === 'mixed') {
60
+ const auto = item.variants.filter((v) => v.mode === 'auto').length;
61
+ return `Auto ${auto} · Manual ${item.variants.length - auto}`;
62
+ }
63
+ return item.mode === 'manual' ? 'Manual' : 'Auto';
58
64
  }
59
65
 
60
- function variantResultLabel(v: CoverageVariant): string {
61
- if (!v.result) return 'Pending';
62
- return statusToTestResult(v.result.status);
66
+ /**
67
+ * Parent roll-up label. It must NEVER be exactly one of the variant state words
68
+ * (`Passed`/`Failed`/`Blocked`/`Pending`/`N/A`) — the summary band counts the
69
+ * Result column with exact-match COUNTIF over parent AND child rows, so a parent
70
+ * reading plain "Pending" was counted as an extra pending variant (109 instead
71
+ * of 77 on st-login).
72
+ */
73
+ export function itemResultLabel(item: DeliveryItem): string {
74
+ const { passed, failed, blocked, na } = item.resultCounts;
75
+ const eff = item.variants.length - na;
76
+ const suffix = na > 0 ? ` · ${na} N/A` : '';
77
+ if (item.result === 'na') return 'All N/A';
78
+ if (item.result === 'failed') return `${passed}/${eff} Passed · ${failed} Failed${suffix}`;
79
+ if (item.result === 'blocked') return `${passed}/${eff} Passed · ${blocked} Blocked${suffix}`;
80
+ return `${passed}/${eff} Passed${suffix}`;
63
81
  }
64
82
 
65
- function numbered(lines: string[]): string {
83
+ /** ISO date (2026-08-04) — unambiguous across locales (review §9). */
84
+ function isoDate(startTime: string | undefined): string {
85
+ if (!startTime) return '';
86
+ const d = new Date(startTime);
87
+ if (isNaN(d.getTime())) return '';
88
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
89
+ }
90
+
91
+ function numbered(lines: string[], start = 1): string {
66
92
  if (lines.length === 0) return '';
67
- if (lines.length === 1) return lines[0];
68
- return lines.map((l, i) => `${i + 1}. ${l}`).join('\n');
93
+ if (lines.length === 1 && start === 1) return lines[0];
94
+ return lines.map((l, i) => `${start + i}. ${l}`).join('\n');
95
+ }
96
+
97
+ /**
98
+ * Compact coverage summary for the parent row. A declared `dimensions:` digest
99
+ * wins ("15 variants — required ×3 · format ×9 …"); otherwise a single variant
100
+ * shows its condition and a group falls back to its variant REFS. Concatenating
101
+ * every variant's full title turned this cell into an 800-character paragraph.
102
+ */
103
+ export function coverageSummary(item: DeliveryItem): string {
104
+ const n = item.variants.length;
105
+ const head = `${n} variant${n === 1 ? '' : 's'}:`;
106
+ if (item.dimensions) return `${head} ${item.dimensions}`;
107
+ if (n === 1) return `${head} ${item.variants[0].condition}`;
108
+ return `${head} ${item.variants.map((v) => v.ref).join(' · ')}`;
109
+ }
110
+
111
+ /** Expected cell = observable outcomes; the HOW moves under a separate heading. */
112
+ function expectedWithVerification(oracle: string[], verification: string[]): string {
113
+ const expected = numbered(oracle);
114
+ if (verification.length === 0) return expected;
115
+ return `${expected}\nVerification method: ${verification.join(' ')}`;
69
116
  }
70
117
 
71
118
  /** Red DRAFT banner on row 5 when the matrix is not approved for official delivery. */
@@ -85,9 +132,10 @@ function draftBanner(ws: ExcelJS.Worksheet, model: MatrixModel): void {
85
132
 
86
133
  const MATRIX_HEADERS = [
87
134
  'ID', 'Target', 'Test Intent / Condition', 'Category', 'Priority', 'Precondition',
88
- 'Coverage / Test Data', 'Action / Trigger', 'Expected (Oracle)', 'Mode', 'Trace',
89
- 'Result', 'Executed Date', 'Executor', 'Note\n(Evidence, DefectID)',
135
+ 'Coverage / Test Data', 'Action / Trigger', 'Expected Result', 'Mode', 'Trace',
136
+ 'Result', 'Executed Date', 'Executor', 'Note / Evidence', 'Defect ID',
90
137
  ];
138
+ const RESULT_COL = 13; // column M
91
139
 
92
140
  function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?: string): void {
93
141
  const ws = wb.addWorksheet('Testcases');
@@ -95,7 +143,7 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
95
143
 
96
144
  ws.columns = [
97
145
  { width: 3 }, // A margin
98
- { width: 20 }, // B ID
146
+ { width: 22 }, // B ID
99
147
  { width: 18 }, // C Target
100
148
  { width: 42 }, // D Intent / Condition
101
149
  { width: 11 }, // E Category
@@ -106,28 +154,43 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
106
154
  { width: 42 }, // J Expected
107
155
  { width: 11 }, // K Mode
108
156
  { width: 13 }, // L Trace
109
- { width: 17 }, // M Result
110
- { width: 13 }, // N Executed
157
+ { width: 22 }, // M Result
158
+ { width: 12 }, // N Executed
111
159
  { width: 13 }, // O Executor
112
- { width: 24 }, // P Note
160
+ { width: 22 }, // P Note / Evidence
161
+ { width: 12 }, // Q Defect ID
113
162
  ];
114
163
 
115
164
  renderReportHeaderBand(wb, ws, `${model.unit.toUpperCase()} TEST CASE & COVERAGE MATRIX`, sungenVersion, model.formNo);
116
165
  draftBanner(ws, model);
117
166
 
118
- // Summary band (row 6) — items vs variants stay separate metrics by design (I8):
119
- // the ITEM count is reviewer workload, the VARIANT count is execution workload.
120
- const passed = model.items.reduce((a, i) => a + i.resultCounts.passed, 0);
121
- const failed = model.items.reduce((a, i) => a + i.resultCounts.failed, 0);
122
- const notRun = model.items.reduce((a, i) => a + i.resultCounts.notRun + i.resultCounts.blocked, 0);
167
+ // Summary band (row 6) — items vs variants stay separate metrics by design (I8).
168
+ // The execution counters are LIVE formulas over the variant Result cells, so
169
+ // hand-entered results update the totals (review GAP-06). Variant cells hold the
170
+ // exact state words and parent cells NEVER do (itemResultLabel + the roll-up
171
+ // formula always compose a "n/m Passed …" label), so exact-match COUNTIF over
172
+ // the whole column counts variants only.
173
+ const sum = (k: 'passed' | 'failed' | 'blocked' | 'notRun' | 'na'): number =>
174
+ model.items.reduce((a, i) => a + i.resultCounts[k], 0);
175
+ const passed = sum('passed');
176
+ const failed = sum('failed');
177
+ const blocked = sum('blocked');
178
+ const notRun = sum('notRun');
179
+ const na = sum('na');
180
+ const RANGE = 'M9:M10000';
123
181
  const band = ws.getRow(6);
124
- [
125
- `Delivery items: ${model.manifest.itemCount}`,
126
- `Coverage variants: ${model.manifest.variantCount}`,
127
- `Passed: ${passed}`, `Failed: ${failed}`, `Pending: ${notRun}`,
128
- ].forEach((label, i) => {
182
+ const bandCells: Array<[string, ExcelJS.CellValue]> = [
183
+ ['items', `Delivery items: ${model.manifest.itemCount}`],
184
+ ['variants', `Coverage variants: ${model.manifest.variantCount}`],
185
+ ['passed', { formula: `"Passed: "&COUNTIF(${RANGE},"Passed")`, result: `Passed: ${passed}` } as ExcelJS.CellValue],
186
+ ['failed', { formula: `"Failed: "&COUNTIF(${RANGE},"Failed")`, result: `Failed: ${failed}` } as ExcelJS.CellValue],
187
+ ['blocked', { formula: `"Blocked: "&COUNTIF(${RANGE},"Blocked")`, result: `Blocked: ${blocked}` } as ExcelJS.CellValue],
188
+ ['pending', { formula: `"Pending: "&COUNTIF(${RANGE},"Pending")`, result: `Pending: ${notRun}` } as ExcelJS.CellValue],
189
+ ['na', { formula: `"N/A: "&COUNTIF(${RANGE},"N/A")`, result: `N/A: ${na}` } as ExcelJS.CellValue],
190
+ ];
191
+ bandCells.forEach(([, v], i) => {
129
192
  const c = band.getCell(2 + i);
130
- dataCell(c, label, { bold: true, fill: SOFT_BLUE, center: true });
193
+ dataCell(c, v, { bold: true, fill: SOFT_BLUE, center: true });
131
194
  });
132
195
 
133
196
  const HEADER_ROW = 8;
@@ -137,13 +200,65 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
137
200
 
138
201
  let rowIdx = HEADER_ROW + 1;
139
202
  for (const item of model.items) {
140
- // Parent row — the delivery item (the review unit).
141
- const pr = ws.getRow(rowIdx++);
142
- const single = item.variants.length === 1;
143
- const only = single ? item.variants[0] : undefined;
144
- const coverage = single
145
- ? numbered(only!.data)
146
- : `${item.variants.length} variants: ${item.variants.map((v) => v.condition).join(' · ')}`;
203
+ const parentRowIdx = rowIdx++;
204
+
205
+ // --- Variant sub-rows FIRST (to know their cell range for the parent formula).
206
+ // Every item gets sub-rows — the sub-row is where the source scenario id, the
207
+ // resolved data, and the manual result/evidence entry live (review B-05).
208
+ const firstChild = rowIdx;
209
+ // Delta-only sub-rows (review GAP-01): content identical across the whole
210
+ // item lives ONCE on the parent — a child repeats only what distinguishes it.
211
+ // The child's Expected Result is always kept: it is the precise source
212
+ // oracle (the semantic anchor), not a repetition of the parent's summary.
213
+ for (const v of item.variants) {
214
+ const vr = ws.getRow(rowIdx++);
215
+ vr.outlineLevel = 1;
216
+ const vValues: ExcelJS.CellValue[] = [
217
+ v.ref,
218
+ '',
219
+ v.condition,
220
+ '', '',
221
+ numbered(item.preconditionDeltas[v.ref] ?? []),
222
+ numbered(v.data),
223
+ // Only the steps after the parent's shared prefix, numbered so the
224
+ // parent's steps + these read as one continuous procedure.
225
+ numbered(item.triggerDeltas[v.ref] ?? [], item.trigger.length + 1),
226
+ expectedWithVerification(v.oracle, v.verification),
227
+ v.mode === 'manual' ? `Manual${v.manualReason ? ` (${v.manualReason})` : ''}` : 'Auto',
228
+ v.traces.join(', '),
229
+ variantState(v),
230
+ isoDate(v.result?.startTime),
231
+ '',
232
+ v.result?.error ? String(v.result.error).slice(0, 200) : '',
233
+ '',
234
+ ];
235
+ vValues.forEach((val, i) => dataCell(vr.getCell(2 + i), val, { center: i === 9 }));
236
+ // Manual-execution entry: constrain the Result cell to the known states.
237
+ vr.getCell(2 + 11).dataValidation = {
238
+ type: 'list', allowBlank: true, formulae: [`"${RESULT_STATES}"`],
239
+ };
240
+ }
241
+ const lastChild = rowIdx - 1;
242
+
243
+ // --- Parent row: only values that are genuinely COMMON to the whole item
244
+ // (review M-04); everything variant-specific stays on the sub-rows.
245
+ const pr = ws.getRow(parentRowIdx);
246
+ const coverage = coverageSummary(item);
247
+
248
+ // Live roll-up derived from the child Result cells — precedence
249
+ // failed → blocked → pending → partial → passed. N/A variants leave the
250
+ // denominator (review GAP-06): eff = n − COUNTIF(N/A); all-N/A → "N/A".
251
+ const rng = `M${firstChild}:M${lastChild}`;
252
+ const n = item.variants.length;
253
+ const EFF = `(${n}-COUNTIF(${rng},"N/A"))`;
254
+ const NA = `IF(COUNTIF(${rng},"N/A")>0," · "&COUNTIF(${rng},"N/A")&" N/A","")`;
255
+ // Mirrors itemResultLabel exactly — and never yields a bare state word.
256
+ const rollUpFormula =
257
+ `IF(COUNTIF(${rng},"Failed")>0,COUNTIF(${rng},"Passed")&"/"&${EFF}&" Passed · "&COUNTIF(${rng},"Failed")&" Failed"&${NA},` +
258
+ `IF(COUNTIF(${rng},"Blocked")>0,COUNTIF(${rng},"Passed")&"/"&${EFF}&" Passed · "&COUNTIF(${rng},"Blocked")&" Blocked"&${NA},` +
259
+ `IF(${EFF}=0,"All N/A",` +
260
+ `COUNTIF(${rng},"Passed")&"/"&${EFF}&" Passed"&${NA})))`;
261
+
147
262
  const values: ExcelJS.CellValue[] = [
148
263
  item.id,
149
264
  item.target,
@@ -152,66 +267,82 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
152
267
  item.priority,
153
268
  numbered(item.precondition),
154
269
  coverage,
270
+ // The shared leading steps; each variant's remaining steps sit on its sub-row.
155
271
  numbered(item.trigger),
156
272
  item.oracle,
157
- item.mode === 'manual' ? `Manual${only?.manualReason ? ` (${only.manualReason})` : ''}` : 'Auto',
273
+ itemModeLabel(item),
158
274
  item.traces.join(', '),
159
- itemResultLabel(item),
160
- single ? formatExecutedDate(only!.result?.startTime) : '',
275
+ { formula: rollUpFormula, result: itemResultLabel(item) } as ExcelJS.CellValue,
276
+ '',
161
277
  '',
162
278
  item.review === 'proposed' ? 'REVIEW REQUIRED' : '',
279
+ '',
163
280
  ];
164
281
  values.forEach((v, i) => dataCell(pr.getCell(2 + i), v, {
165
282
  bold: true,
166
283
  fill: SOFT_BLUE,
167
284
  center: i === 3 || i === 4 || i === 9,
168
- ...(String(values[14]).startsWith('REVIEW') && i === 14 ? { color: DRAFT_RED } : {}),
285
+ ...(i === 14 && item.review === 'proposed' ? { color: DRAFT_RED } : {}),
169
286
  }));
170
-
171
- // Variant sub-rows — the execution units (outline level 1; collapse for review).
172
- if (!single) {
173
- for (const v of item.variants) {
174
- const vr = ws.getRow(rowIdx++);
175
- vr.outlineLevel = 1;
176
- const vValues: ExcelJS.CellValue[] = [
177
- v.ref,
178
- '',
179
- v.condition,
180
- '', '', '',
181
- numbered(v.data),
182
- numbered(v.trigger),
183
- numbered(v.oracle),
184
- v.mode === 'manual' ? `Manual${v.manualReason ? ` (${v.manualReason})` : ''}` : 'Auto',
185
- v.traces.join(', '),
186
- variantResultLabel(v),
187
- formatExecutedDate(v.result?.startTime),
188
- '',
189
- v.result?.error ? String(v.result.error).slice(0, 200) : '',
190
- ];
191
- vValues.forEach((val, i) => dataCell(vr.getCell(2 + i), val, { center: i === 9 }));
192
- }
193
- }
194
287
  }
195
288
 
196
- ws.autoFilter = { from: { row: HEADER_ROW, column: 2 }, to: { row: rowIdx - 1, column: 16 } };
197
- ws.views = [{ state: 'frozen', ySplit: HEADER_ROW }];
289
+ ws.autoFilter = { from: { row: HEADER_ROW, column: 2 }, to: { row: rowIdx - 1, column: 17 } };
290
+ // Freeze the header band AND the ID + Target columns (review §9).
291
+ ws.views = [{ state: 'frozen', xSplit: 3, ySplit: HEADER_ROW }];
292
+ // Print: landscape, fit width only (never squash all rows into one page),
293
+ // repeat the masthead + header rows on every page (review GAP-08).
294
+ ws.pageSetup = {
295
+ orientation: 'landscape',
296
+ fitToPage: true,
297
+ fitToWidth: 1,
298
+ fitToHeight: 0,
299
+ printTitlesRow: '1:8',
300
+ };
198
301
  }
199
302
 
200
303
  // ---------------------------------------------------------------------------
201
- // Sheet 2 — Coverage (overview grid + dispositions + manifest)
304
+ // Sheet 2 — Coverage (requirements + overview grid + dispositions + manifest)
202
305
  // ---------------------------------------------------------------------------
203
306
 
204
307
  function addCoverageSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?: string): void {
205
308
  const ws = wb.addWorksheet('Coverage');
206
309
  ws.columns = [
207
- { width: 3 }, { width: 30 }, { width: 16 }, { width: 16 }, { width: 16 }, { width: 16 },
208
- { width: 16 }, { width: 40 },
310
+ { width: 3 }, { width: 22 }, { width: 18 }, { width: 16 }, { width: 16 }, { width: 16 },
311
+ { width: 16 }, { width: 44 },
209
312
  ];
210
313
  renderReportHeaderBand(wb, ws, `${model.unit.toUpperCase()} COVERAGE OVERVIEW`, sungenVersion, model.formNo);
211
314
  draftBanner(ws, model);
212
315
 
213
- // --- Target × category grid ---
214
316
  let rowIdx = 7;
317
+
318
+ // --- Requirement coverage (review §6): every requirement id gets ONE explicit status.
319
+ if (model.requirements.length > 0) {
320
+ dataCell(ws.getRow(rowIdx).getCell(2), 'Requirement coverage — every id has an explicit status', { bold: true });
321
+ rowIdx += 1;
322
+ const rh = ws.getRow(rowIdx);
323
+ ws.mergeCells(rowIdx, 4, rowIdx, 6);
324
+ headerCell(rh.getCell(2), 'Requirement');
325
+ headerCell(rh.getCell(3), 'Status');
326
+ headerCell(rh.getCell(4), 'Delivery items');
327
+ headerCell(rh.getCell(7), 'Variants');
328
+ headerCell(rh.getCell(8), 'Note');
329
+ rowIdx += 1;
330
+ for (const r of model.requirements) {
331
+ const row = ws.getRow(rowIdx++);
332
+ dataCell(row.getCell(2), r.id, { bold: true });
333
+ dataCell(row.getCell(3), r.status, {
334
+ center: true,
335
+ ...(r.status === 'gap' ? { fill: GAP_FILL, bold: true } : {}),
336
+ });
337
+ ws.mergeCells(rowIdx - 1, 4, rowIdx - 1, 6);
338
+ dataCell(row.getCell(4), r.items.join(', '));
339
+ dataCell(row.getCell(7), r.variantCount > 0 ? String(r.variantCount) : '', { center: true });
340
+ dataCell(row.getCell(8), r.note);
341
+ }
342
+ rowIdx += 1;
343
+ }
344
+
345
+ // --- Target × category grid ---
215
346
  dataCell(ws.getRow(rowIdx).getCell(2), 'Coverage matrix — delivery items (coverage variants) per target × category', { bold: true });
216
347
  rowIdx += 1;
217
348
  const gridHeader = ws.getRow(rowIdx++);
@@ -29,6 +29,10 @@ export interface MapGroup {
29
29
  oracle: string;
30
30
  category: MapCategory;
31
31
  review: ReviewState;
32
+ /** Optional short digest of the coverage dimensions this item varies over
33
+ * (e.g. "required ×3 · format ×9 · full-width ×2"). Rendered on the parent row
34
+ * instead of concatenating every variant's title — the compact review view. */
35
+ dimensions?: string;
32
36
  /** Variant refs: `VP-ID` (all @cases rows when the scenario has a dataset) or `VP-ID#label`. */
33
37
  variants: string[];
34
38
  }
@@ -38,6 +42,14 @@ export interface MapDisposition {
38
42
  reason?: string;
39
43
  }
40
44
 
45
+ export type RequirementStatus =
46
+ | 'covered' | 'partially_covered' | 'covered_elsewhere' | 'planned' | 'gap' | 'not_applicable';
47
+
48
+ export interface RequirementOverride {
49
+ status: RequirementStatus;
50
+ note?: string;
51
+ }
52
+
41
53
  export interface DeliveryMap {
42
54
  version: number;
43
55
  unit: string;
@@ -46,7 +58,12 @@ export interface DeliveryMap {
46
58
  groups: MapGroup[];
47
59
  /** VP-id → intentionally-not-grouped disposition. */
48
60
  dispositions: Record<string, MapDisposition>;
49
- /** VP-id → scenario fingerprint stamped at approval time (drift detector input). */
61
+ /** Requirement-id → reviewed status override (partially_covered / not_applicable / …).
62
+ * Without an override a requirement is `covered` when a variant traces to it, else `gap`. */
63
+ requirements: Record<string, RequirementOverride>;
64
+ /** VP-id → scenario fingerprint stamped at approval time (drift detector input).
65
+ * The reserved key `__map__` holds the fingerprint of the map's own semantic
66
+ * content (groups + dispositions) so edited wording re-opens review (GAP-09). */
50
67
  fingerprints: Record<string, string>;
51
68
  }
52
69
 
@@ -78,6 +95,9 @@ export interface CoverageVariant {
78
95
  layers: MatrixLayer[];
79
96
  /** @spec:* requirement traces. */
80
97
  traces: string[];
98
+ /** Variables this scenario produces at RUN TIME (`remember … as {{v}}`, capability
99
+ * bindings) — they never exist in test-data, so Gate D must not demand them. */
100
+ runtimeVars: string[];
81
101
  /** Condition summary: differentiating dimension shown on the sub-row. */
82
102
  condition: string;
83
103
  /** Resolved data pairs (test-data values / dataset-row columns), `key: value` lines. */
@@ -90,6 +110,9 @@ export interface CoverageVariant {
90
110
  oracleShape: string[];
91
111
  /** Oracle for display. */
92
112
  oracle: string[];
113
+ /** HOW to check (manual `Oracle:` lines — tools, panes, queries). Rendered as
114
+ * "Verification method" under the expected result, never inside it. */
115
+ verification: string[];
93
116
  /** Precondition profile: auth role + scenario-level Given shapes — signature input. */
94
117
  preconditionProfile: string;
95
118
  /** Human preconditions (auth + own Given steps). */
@@ -102,7 +125,11 @@ export interface CoverageVariant {
102
125
  result?: PlaywrightResult;
103
126
  }
104
127
 
105
- export type ItemResult = 'passed' | 'failed' | 'blocked' | 'partial' | 'not_run';
128
+ export type ItemResult = 'passed' | 'failed' | 'blocked' | 'partial' | 'not_run' | 'na';
129
+
130
+ /** The exact word rendered in a variant's Result cell — the single vocabulary the
131
+ * parent's roll-up formula counts (COUNTIF is exact-match). */
132
+ export type VariantState = 'Passed' | 'Failed' | 'Blocked' | 'Pending' | 'N/A';
106
133
 
107
134
  /** One summarized row in the matrix — a group of variants sharing the signature. */
108
135
  export interface DeliveryItem {
@@ -110,20 +137,31 @@ export interface DeliveryItem {
110
137
  target: string;
111
138
  intent: string;
112
139
  oracle: string;
140
+ /** Short coverage-dimension digest from the map (compact parent view). */
141
+ dimensions?: string;
113
142
  category: MapCategory;
114
143
  review: ReviewState;
144
+ /** Highest priority among the variants (per-variant priorities stay on the sub-rows). */
115
145
  priority: string;
116
- mode: 'auto' | 'manual';
146
+ /** Execution mode is a coverage dimension, not a split — 'mixed' when auto and
147
+ * manual variants share one intent (e.g. account states needing seeded data). */
148
+ mode: 'auto' | 'manual' | 'mixed';
117
149
  layers: MatrixLayer[];
118
150
  /** Union of variant traces (exact per-variant traces stay on the variants). */
119
151
  traces: string[];
152
+ /** Preconditions shared by EVERY variant (the intersection) — written once here. */
120
153
  precondition: string[];
121
- /** Common trigger when all variants share one shape, else '(per variant)'. */
154
+ /** Leading trigger steps shared by every variant — written once here; each
155
+ * variant renders only its remaining steps (`triggerDeltas`). */
122
156
  trigger: string[];
123
157
  variants: CoverageVariant[];
158
+ /** variant ref → its precondition lines that are not on the parent. */
159
+ preconditionDeltas: Record<string, string[]>;
160
+ /** variant ref → its trigger steps after the shared prefix. */
161
+ triggerDeltas: Record<string, string[]>;
124
162
  /** Derived roll-up — never entered independently (Gate F). */
125
163
  result: ItemResult;
126
- resultCounts: { passed: number; failed: number; blocked: number; notRun: number };
164
+ resultCounts: { passed: number; failed: number; blocked: number; notRun: number; na: number };
127
165
  }
128
166
 
129
167
  export interface MatrixDisposition {
@@ -133,11 +171,21 @@ export interface MatrixDisposition {
133
171
  reason: string;
134
172
  }
135
173
 
174
+ /** One row of the requirement-coverage table (Coverage sheet). */
175
+ export interface RequirementCoverage {
176
+ id: string; // FR-003 / TR-001 / NFR-…
177
+ status: RequirementStatus;
178
+ /** Delivery items whose variants trace to this requirement. */
179
+ items: string[];
180
+ variantCount: number;
181
+ note: string;
182
+ }
183
+
136
184
  // ---------------------------------------------------------------------------
137
185
  // Gate findings
138
186
  // ---------------------------------------------------------------------------
139
187
 
140
- export type GateId = 'A' | 'B' | 'C' | 'D' | 'E' | 'G';
188
+ export type GateId = 'A' | 'B' | 'C' | 'D' | 'E' | 'G' | 'K' | 'R' | 'W';
141
189
  export type FindingSeverity = 'error' | 'review' | 'warning';
142
190
 
143
191
  export interface MatrixFinding {
@@ -168,9 +216,11 @@ export interface MatrixModel {
168
216
  formNo: string;
169
217
  items: DeliveryItem[];
170
218
  dispositions: MatrixDisposition[];
219
+ /** Requirement coverage — empty when the unit has no requirements/spec.md ids. */
220
+ requirements: RequirementCoverage[];
171
221
  findings: MatrixFinding[];
172
222
  manifest: MatrixManifest;
173
223
  }
174
224
 
175
- /** Complexity warning threshold (rules draft Gate I) — echoed in the manifest. */
176
- export const MAX_VARIANTS_PER_ITEM = 12;
225
+ /** Complexity warning threshold (rules draft Gate I; raised for compact grouping) — echoed in the manifest. */
226
+ export const MAX_VARIANTS_PER_ITEM = 20;