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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/dist/cli/commands/delivery.d.ts.map +1 -1
  2. package/dist/cli/commands/delivery.js +167 -51
  3. package/dist/cli/commands/delivery.js.map +1 -1
  4. package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
  5. package/dist/dashboard/snapshot-builder.js +3 -46
  6. package/dist/dashboard/snapshot-builder.js.map +1 -1
  7. package/dist/dashboard/templates/index.html +1 -1
  8. package/dist/exporters/feature-parser.js +1 -1
  9. package/dist/exporters/feature-parser.js.map +1 -1
  10. package/dist/exporters/locale-variants.d.ts +38 -0
  11. package/dist/exporters/locale-variants.d.ts.map +1 -0
  12. package/dist/exporters/locale-variants.js +159 -0
  13. package/dist/exporters/locale-variants.js.map +1 -0
  14. package/dist/exporters/matrix/build.d.ts +36 -0
  15. package/dist/exporters/matrix/build.d.ts.map +1 -0
  16. package/dist/exporters/matrix/build.js +267 -0
  17. package/dist/exporters/matrix/build.js.map +1 -0
  18. package/dist/exporters/matrix/export.d.ts +47 -0
  19. package/dist/exporters/matrix/export.d.ts.map +1 -0
  20. package/dist/exporters/matrix/export.js +136 -0
  21. package/dist/exporters/matrix/export.js.map +1 -0
  22. package/dist/exporters/matrix/fingerprint.d.ts +20 -0
  23. package/dist/exporters/matrix/fingerprint.d.ts.map +1 -0
  24. package/dist/exporters/matrix/fingerprint.js +83 -0
  25. package/dist/exporters/matrix/fingerprint.js.map +1 -0
  26. package/dist/exporters/matrix/gates.d.ts +21 -0
  27. package/dist/exporters/matrix/gates.d.ts.map +1 -0
  28. package/dist/exporters/matrix/gates.js +258 -0
  29. package/dist/exporters/matrix/gates.js.map +1 -0
  30. package/dist/exporters/matrix/map-loader.d.ts +22 -0
  31. package/dist/exporters/matrix/map-loader.d.ts.map +1 -0
  32. package/dist/exporters/matrix/map-loader.js +173 -0
  33. package/dist/exporters/matrix/map-loader.js.map +1 -0
  34. package/dist/exporters/matrix/render-csv.d.ts +8 -0
  35. package/dist/exporters/matrix/render-csv.d.ts.map +1 -0
  36. package/dist/exporters/matrix/render-csv.js +69 -0
  37. package/dist/exporters/matrix/render-csv.js.map +1 -0
  38. package/dist/exporters/matrix/render-xlsx.d.ts +17 -0
  39. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -0
  40. package/dist/exporters/matrix/render-xlsx.js +298 -0
  41. package/dist/exporters/matrix/render-xlsx.js.map +1 -0
  42. package/dist/exporters/matrix/types.d.ts +153 -0
  43. package/dist/exporters/matrix/types.d.ts.map +1 -0
  44. package/dist/exporters/matrix/types.js +15 -0
  45. package/dist/exporters/matrix/types.js.map +1 -0
  46. package/dist/exporters/scenario-merger.js +1 -1
  47. package/dist/exporters/scenario-merger.js.map +1 -1
  48. package/dist/exporters/spec-parser.js +1 -1
  49. package/dist/exporters/spec-parser.js.map +1 -1
  50. package/dist/exporters/xlsx-report-builder.d.ts +1 -1
  51. package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
  52. package/dist/exporters/xlsx-report-builder.js +3 -3
  53. package/dist/exporters/xlsx-report-builder.js.map +1 -1
  54. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-match-data.hbs +3 -11
  55. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +11 -1
  56. package/dist/generators/test-generator/code-generator.js +1 -1
  57. package/dist/generators/test-generator/code-generator.js.map +1 -1
  58. package/dist/generators/test-generator/diagnostics.d.ts +4 -1
  59. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  60. package/dist/generators/test-generator/diagnostics.js +3 -0
  61. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  62. package/dist/generators/test-generator/step-mapper.d.ts +6 -0
  63. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  64. package/dist/generators/test-generator/step-mapper.js +41 -2
  65. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  66. package/dist/harness/annotation-overrides.d.ts +4 -1
  67. package/dist/harness/annotation-overrides.d.ts.map +1 -1
  68. package/dist/harness/annotation-overrides.js +5 -2
  69. package/dist/harness/annotation-overrides.js.map +1 -1
  70. package/dist/harness/audit.d.ts.map +1 -1
  71. package/dist/harness/audit.js +6 -1
  72. package/dist/harness/audit.js.map +1 -1
  73. package/dist/harness/capability.d.ts +25 -0
  74. package/dist/harness/capability.d.ts.map +1 -1
  75. package/dist/harness/capability.js +44 -2
  76. package/dist/harness/capability.js.map +1 -1
  77. package/dist/harness/quality-gates.d.ts.map +1 -1
  78. package/dist/harness/quality-gates.js +5 -0
  79. package/dist/harness/quality-gates.js.map +1 -1
  80. package/dist/orchestrator/templates/ai-src/commands/create-test.md +1 -1
  81. package/dist/orchestrator/templates/ai-src/commands/delivery.md +118 -120
  82. package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
  83. package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
  84. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +43 -7
  85. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +1 -0
  86. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +8 -0
  87. package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -1
  88. package/dist/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +8 -0
  89. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +1 -0
  90. package/dist/orchestrator/templates/specs-test-data.ts +13 -4
  91. package/package.json +4 -4
  92. package/src/cli/commands/delivery.ts +199 -71
  93. package/src/dashboard/snapshot-builder.ts +11 -61
  94. package/src/dashboard/templates/index.html +1 -1
  95. package/src/exporters/feature-parser.ts +1 -1
  96. package/src/exporters/locale-variants.ts +157 -0
  97. package/src/exporters/matrix/build.ts +294 -0
  98. package/src/exporters/matrix/export.ts +124 -0
  99. package/src/exporters/matrix/fingerprint.ts +49 -0
  100. package/src/exporters/matrix/gates.ts +284 -0
  101. package/src/exporters/matrix/map-loader.ts +142 -0
  102. package/src/exporters/matrix/render-csv.ts +71 -0
  103. package/src/exporters/matrix/render-xlsx.ts +289 -0
  104. package/src/exporters/matrix/types.ts +176 -0
  105. package/src/exporters/scenario-merger.ts +1 -1
  106. package/src/exporters/spec-parser.ts +1 -1
  107. package/src/exporters/xlsx-report-builder.ts +3 -2
  108. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-match-data.hbs +3 -11
  109. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +11 -1
  110. package/src/generators/test-generator/code-generator.ts +1 -1
  111. package/src/generators/test-generator/diagnostics.ts +4 -1
  112. package/src/generators/test-generator/step-mapper.ts +43 -2
  113. package/src/harness/annotation-overrides.ts +5 -2
  114. package/src/harness/audit.ts +7 -2
  115. package/src/harness/capability.ts +53 -3
  116. package/src/harness/quality-gates.ts +4 -0
  117. package/src/orchestrator/templates/ai-src/commands/create-test.md +1 -1
  118. package/src/orchestrator/templates/ai-src/commands/delivery.md +118 -120
  119. package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
  120. package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
  121. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +43 -7
  122. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +1 -0
  123. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +8 -0
  124. package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -1
  125. package/src/orchestrator/templates/ai-src/skills/sungen-locale/SKILL.md +8 -0
  126. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +1 -0
  127. package/src/orchestrator/templates/specs-test-data.ts +13 -4
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Render the Test Case & Coverage Matrix workbook.
3
+ *
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.
10
+ *
11
+ * Every sheet keeps the company ISO masthead (renderReportHeaderBand). A draft
12
+ * matrix (unapproved groups / review findings) carries a red DRAFT banner.
13
+ */
14
+
15
+ import * as ExcelJS from 'exceljs';
16
+ import {
17
+ renderReportHeaderBand,
18
+ allBordersBlack,
19
+ ARIAL_FONT,
20
+ BLACK,
21
+ LAVENDER,
22
+ } from '../xlsx-report-builder';
23
+ import { statusToTestResult, formatExecutedDate } from '../playwright-report-parser';
24
+ import { CoverageVariant, DeliveryItem, ItemResult, MatrixModel, MapCategory } from './types';
25
+
26
+ const CATEGORY_ORDER: MapCategory[] = ['normal', 'abnormal', 'security', 'nfr'];
27
+ const SOFT_BLUE = { argb: 'FFDCE6F1' };
28
+ const DRAFT_RED = { argb: 'FFC00000' };
29
+ const GAP_FILL = { argb: 'FFFCE4EC' };
30
+
31
+ function headerCell(c: ExcelJS.Cell, label: string): void {
32
+ c.value = label;
33
+ c.font = { bold: true, size: 10, color: BLACK, name: ARIAL_FONT };
34
+ c.fill = { type: 'pattern', pattern: 'solid', fgColor: LAVENDER };
35
+ c.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
36
+ c.border = allBordersBlack;
37
+ }
38
+
39
+ function dataCell(c: ExcelJS.Cell, value: ExcelJS.CellValue, opts?: { bold?: boolean; fill?: { argb: string }; center?: boolean; color?: { argb: string } }): void {
40
+ c.value = value;
41
+ c.font = { bold: opts?.bold ?? false, size: 10, name: ARIAL_FONT, ...(opts?.color ? { color: opts.color } : {}) };
42
+ c.alignment = { horizontal: opts?.center ? 'center' : 'left', vertical: 'top', wrapText: true };
43
+ c.border = allBordersBlack;
44
+ if (opts?.fill) c.fill = { type: 'pattern', pattern: 'solid', fgColor: opts.fill };
45
+ }
46
+
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];
58
+ }
59
+
60
+ function variantResultLabel(v: CoverageVariant): string {
61
+ if (!v.result) return 'Pending';
62
+ return statusToTestResult(v.result.status);
63
+ }
64
+
65
+ function numbered(lines: string[]): string {
66
+ if (lines.length === 0) return '';
67
+ if (lines.length === 1) return lines[0];
68
+ return lines.map((l, i) => `${i + 1}. ${l}`).join('\n');
69
+ }
70
+
71
+ /** Red DRAFT banner on row 5 when the matrix is not approved for official delivery. */
72
+ function draftBanner(ws: ExcelJS.Worksheet, model: MatrixModel): void {
73
+ if (model.manifest.approvalState !== 'draft') return;
74
+ ws.mergeCells('B5:H5');
75
+ const c = ws.getCell('B5');
76
+ c.value = 'DRAFT — NOT APPROVED FOR OFFICIAL DELIVERY (review-required findings or proposed groups remain)';
77
+ c.font = { bold: true, size: 11, name: ARIAL_FONT, color: { argb: 'FFFFFFFF' } };
78
+ c.fill = { type: 'pattern', pattern: 'solid', fgColor: DRAFT_RED };
79
+ c.alignment = { horizontal: 'center', vertical: 'middle' };
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Sheet 1 — Testcases (the matrix)
84
+ // ---------------------------------------------------------------------------
85
+
86
+ const MATRIX_HEADERS = [
87
+ '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)',
90
+ ];
91
+
92
+ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?: string): void {
93
+ const ws = wb.addWorksheet('Testcases');
94
+ ws.properties.outlineLevelRow = 1;
95
+
96
+ ws.columns = [
97
+ { width: 3 }, // A margin
98
+ { width: 20 }, // B ID
99
+ { width: 18 }, // C Target
100
+ { width: 42 }, // D Intent / Condition
101
+ { width: 11 }, // E Category
102
+ { width: 9 }, // F Priority
103
+ { width: 26 }, // G Precondition
104
+ { width: 34 }, // H Coverage / Data
105
+ { width: 34 }, // I Trigger
106
+ { width: 42 }, // J Expected
107
+ { width: 11 }, // K Mode
108
+ { width: 13 }, // L Trace
109
+ { width: 17 }, // M Result
110
+ { width: 13 }, // N Executed
111
+ { width: 13 }, // O Executor
112
+ { width: 24 }, // P Note
113
+ ];
114
+
115
+ renderReportHeaderBand(wb, ws, `${model.unit.toUpperCase()} TEST CASE & COVERAGE MATRIX`, sungenVersion, model.formNo);
116
+ draftBanner(ws, model);
117
+
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);
123
+ 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) => {
129
+ const c = band.getCell(2 + i);
130
+ dataCell(c, label, { bold: true, fill: SOFT_BLUE, center: true });
131
+ });
132
+
133
+ const HEADER_ROW = 8;
134
+ const hr = ws.getRow(HEADER_ROW);
135
+ hr.height = 28;
136
+ MATRIX_HEADERS.forEach((label, i) => headerCell(hr.getCell(2 + i), label));
137
+
138
+ let rowIdx = HEADER_ROW + 1;
139
+ 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(' · ')}`;
147
+ const values: ExcelJS.CellValue[] = [
148
+ item.id,
149
+ item.target,
150
+ item.intent,
151
+ item.category,
152
+ item.priority,
153
+ numbered(item.precondition),
154
+ coverage,
155
+ numbered(item.trigger),
156
+ item.oracle,
157
+ item.mode === 'manual' ? `Manual${only?.manualReason ? ` (${only.manualReason})` : ''}` : 'Auto',
158
+ item.traces.join(', '),
159
+ itemResultLabel(item),
160
+ single ? formatExecutedDate(only!.result?.startTime) : '',
161
+ '',
162
+ item.review === 'proposed' ? 'REVIEW REQUIRED' : '',
163
+ ];
164
+ values.forEach((v, i) => dataCell(pr.getCell(2 + i), v, {
165
+ bold: true,
166
+ fill: SOFT_BLUE,
167
+ center: i === 3 || i === 4 || i === 9,
168
+ ...(String(values[14]).startsWith('REVIEW') && i === 14 ? { color: DRAFT_RED } : {}),
169
+ }));
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
+ }
195
+
196
+ ws.autoFilter = { from: { row: HEADER_ROW, column: 2 }, to: { row: rowIdx - 1, column: 16 } };
197
+ ws.views = [{ state: 'frozen', ySplit: HEADER_ROW }];
198
+ }
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Sheet 2 — Coverage (overview grid + dispositions + manifest)
202
+ // ---------------------------------------------------------------------------
203
+
204
+ function addCoverageSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?: string): void {
205
+ const ws = wb.addWorksheet('Coverage');
206
+ ws.columns = [
207
+ { width: 3 }, { width: 30 }, { width: 16 }, { width: 16 }, { width: 16 }, { width: 16 },
208
+ { width: 16 }, { width: 40 },
209
+ ];
210
+ renderReportHeaderBand(wb, ws, `${model.unit.toUpperCase()} COVERAGE OVERVIEW`, sungenVersion, model.formNo);
211
+ draftBanner(ws, model);
212
+
213
+ // --- Target × category grid ---
214
+ let rowIdx = 7;
215
+ dataCell(ws.getRow(rowIdx).getCell(2), 'Coverage matrix — delivery items (coverage variants) per target × category', { bold: true });
216
+ rowIdx += 1;
217
+ const gridHeader = ws.getRow(rowIdx++);
218
+ headerCell(gridHeader.getCell(2), 'Target');
219
+ CATEGORY_ORDER.forEach((cat, i) => headerCell(gridHeader.getCell(3 + i), cat));
220
+ headerCell(gridHeader.getCell(3 + CATEGORY_ORDER.length), 'Total');
221
+
222
+ const targets = Array.from(new Set(model.items.map((i) => i.target)));
223
+ for (const target of targets) {
224
+ const row = ws.getRow(rowIdx++);
225
+ dataCell(row.getCell(2), target, { bold: true });
226
+ let totalItems = 0;
227
+ let totalVariants = 0;
228
+ CATEGORY_ORDER.forEach((cat, i) => {
229
+ const cell = row.getCell(3 + i);
230
+ const items = model.items.filter((it) => it.target === target && it.category === cat);
231
+ const variants = items.reduce((a, it) => a + it.variants.length, 0);
232
+ totalItems += items.length;
233
+ totalVariants += variants;
234
+ // `—` is an explicit statement: this target × category cell has NO coverage.
235
+ dataCell(cell, items.length === 0 ? '—' : `${items.length} (${variants})`, {
236
+ center: true,
237
+ ...(items.length === 0 ? { fill: GAP_FILL } : {}),
238
+ });
239
+ });
240
+ dataCell(row.getCell(3 + CATEGORY_ORDER.length), `${totalItems} (${totalVariants})`, { center: true, bold: true });
241
+ }
242
+
243
+ // --- Dispositions ---
244
+ if (model.dispositions.length > 0) {
245
+ rowIdx += 1;
246
+ dataCell(ws.getRow(rowIdx).getCell(2), 'Not in the matrix (explicit dispositions)', { bold: true });
247
+ rowIdx += 1;
248
+ const dh = ws.getRow(rowIdx++);
249
+ ['Source', 'Disposition', 'Reason', 'Title'].forEach((l, i) => headerCell(dh.getCell(2 + i), l));
250
+ for (const d of model.dispositions) {
251
+ const row = ws.getRow(rowIdx++);
252
+ dataCell(row.getCell(2), d.vpId);
253
+ dataCell(row.getCell(3), d.as, { center: true });
254
+ dataCell(row.getCell(4), d.reason);
255
+ dataCell(row.getCell(5), d.title);
256
+ }
257
+ }
258
+
259
+ // --- Manifest ---
260
+ rowIdx += 1;
261
+ dataCell(ws.getRow(rowIdx).getCell(2), 'Generation manifest', { bold: true });
262
+ rowIdx += 1;
263
+ const m = model.manifest;
264
+ const manifestRows: Array<[string, string]> = [
265
+ ['Unit', m.unit],
266
+ ['Approval state', m.approvalState],
267
+ ['Delivery items', String(m.itemCount)],
268
+ ['Coverage variants', String(m.variantCount)],
269
+ ['Source fingerprint', m.sourceFingerprint],
270
+ ['Map fingerprint', m.mapFingerprint],
271
+ ['Transformer version', m.transformerVersion],
272
+ ['Max variants / item (warn)', String(m.thresholds.maxVariantsPerItem)],
273
+ ];
274
+ for (const [k, v] of manifestRows) {
275
+ const row = ws.getRow(rowIdx++);
276
+ dataCell(row.getCell(2), k, { bold: true, fill: SOFT_BLUE });
277
+ ws.mergeCells(rowIdx - 1, 3, rowIdx - 1, 7);
278
+ dataCell(row.getCell(3), v);
279
+ }
280
+ }
281
+
282
+ // ---------------------------------------------------------------------------
283
+
284
+ export function renderMatrixXlsx(model: MatrixModel, sungenVersion?: string): ExcelJS.Workbook {
285
+ const wb = new ExcelJS.Workbook();
286
+ addMatrixSheet(wb, model, sungenVersion);
287
+ addCoverageSheet(wb, model, sungenVersion);
288
+ return wb;
289
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Delivery v2 — Test Case & Coverage Matrix model (docs/spec/delivery-coverage-matrix-spec.md).
3
+ *
4
+ * Gherkin stays the source of test meaning; the matrix is a derived presentation:
5
+ * one delivery item = one target + one intent + one oracle family, containing one or
6
+ * more atomic coverage variants (scenario / @cases row). Grouping semantics live in the
7
+ * committed Delivery Map (`qa/<kind>/<unit>/delivery/<unit>.map.yaml`) — AI-proposed,
8
+ * gate-validated, QA-approved.
9
+ */
10
+
11
+ import { PlaywrightResult } from '../types';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Delivery Map (the committed grouping artifact)
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export type MapCategory = 'normal' | 'abnormal' | 'security' | 'nfr';
18
+ export type ReviewState = 'proposed' | 'approved';
19
+ export type DispositionKind = 'excluded' | 'blocked' | 'covered_elsewhere' | 'accepted_risk';
20
+
21
+ export interface MapGroup {
22
+ /** Stable delivery id, unique per unit, order-independent (e.g. DI-EMAIL-FORMAT). */
23
+ id: string;
24
+ /** ONE primary target: field/component dot-path, function/flow phrase, or `METHOD /path`. */
25
+ target: string;
26
+ /** One coherent test intent (the primary behavior/rule verified). */
27
+ intent: string;
28
+ /** Oracle family in words — the shared Pass/Fail observation. */
29
+ oracle: string;
30
+ category: MapCategory;
31
+ review: ReviewState;
32
+ /** Variant refs: `VP-ID` (all @cases rows when the scenario has a dataset) or `VP-ID#label`. */
33
+ variants: string[];
34
+ }
35
+
36
+ export interface MapDisposition {
37
+ as: DispositionKind;
38
+ reason?: string;
39
+ }
40
+
41
+ export interface DeliveryMap {
42
+ version: number;
43
+ unit: string;
44
+ /** ISO form number rendered in the workbook masthead (default BM-2-901-13). */
45
+ formNo?: string;
46
+ groups: MapGroup[];
47
+ /** VP-id → intentionally-not-grouped disposition. */
48
+ dispositions: Record<string, MapDisposition>;
49
+ /** VP-id → scenario fingerprint stamped at approval time (drift detector input). */
50
+ fingerprints: Record<string, string>;
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Derived model (deterministic, computed from the .feature at build time)
55
+ // ---------------------------------------------------------------------------
56
+
57
+ /** Execution layers a variant verifies on — cross-checked with the verification scope. */
58
+ export type MatrixLayer = 'ui' | 'api' | 'db';
59
+
60
+ /**
61
+ * One atomic coverage variant — the unit of execution, result, and traceability.
62
+ * Backed by one scenario, or one `@cases` dataset row of a scenario.
63
+ */
64
+ export interface CoverageVariant {
65
+ /** `VP-ID` or `VP-ID#label` for a dataset row. */
66
+ ref: string;
67
+ vpId: string;
68
+ /** Dataset-row label when this variant is one `@cases` row. */
69
+ caseLabel?: string;
70
+ /** Human title (scenario name without the VP prefix; row label appended for @cases). */
71
+ title: string;
72
+ /** Viewpoint category from the VP prefix (LOGIC/VAL/UI/SEC/…). */
73
+ vpCategory: string;
74
+ priority: string; // High | Normal | Low
75
+ /** 'auto' | 'manual'; manual keeps its @manual:Mx reason. */
76
+ mode: 'auto' | 'manual';
77
+ manualReason?: string;
78
+ layers: MatrixLayer[];
79
+ /** @spec:* requirement traces. */
80
+ traces: string[];
81
+ /** Condition summary: differentiating dimension shown on the sub-row. */
82
+ condition: string;
83
+ /** Resolved data pairs (test-data values / dataset-row columns), `key: value` lines. */
84
+ data: string[];
85
+ /** Normalized trigger shape (When steps, `{{var}}` collapsed) — signature input. */
86
+ triggerShape: string[];
87
+ /** Trigger for display (When steps, vars resolved). */
88
+ trigger: string[];
89
+ /** Normalized oracle shape (Then steps / manual Observable+Oracle) — signature input. */
90
+ oracleShape: string[];
91
+ /** Oracle for display. */
92
+ oracle: string[];
93
+ /** Precondition profile: auth role + scenario-level Given shapes — signature input. */
94
+ preconditionProfile: string;
95
+ /** Human preconditions (auth + own Given steps). */
96
+ precondition: string[];
97
+ /** 'simple' (G*W*T*) | 'sequence' (mid-flow Given / multi round-trip) | 'manual'. */
98
+ procedureProfile: 'simple' | 'sequence' | 'manual';
99
+ /** Canonical scenario fingerprint (drift detection). */
100
+ fingerprint: string;
101
+ /** Execution result, when the run report has one for this variant. */
102
+ result?: PlaywrightResult;
103
+ }
104
+
105
+ export type ItemResult = 'passed' | 'failed' | 'blocked' | 'partial' | 'not_run';
106
+
107
+ /** One summarized row in the matrix — a group of variants sharing the signature. */
108
+ export interface DeliveryItem {
109
+ id: string;
110
+ target: string;
111
+ intent: string;
112
+ oracle: string;
113
+ category: MapCategory;
114
+ review: ReviewState;
115
+ priority: string;
116
+ mode: 'auto' | 'manual';
117
+ layers: MatrixLayer[];
118
+ /** Union of variant traces (exact per-variant traces stay on the variants). */
119
+ traces: string[];
120
+ precondition: string[];
121
+ /** Common trigger when all variants share one shape, else '(per variant)'. */
122
+ trigger: string[];
123
+ variants: CoverageVariant[];
124
+ /** Derived roll-up — never entered independently (Gate F). */
125
+ result: ItemResult;
126
+ resultCounts: { passed: number; failed: number; blocked: number; notRun: number };
127
+ }
128
+
129
+ export interface MatrixDisposition {
130
+ vpId: string;
131
+ title: string;
132
+ as: DispositionKind;
133
+ reason: string;
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Gate findings
138
+ // ---------------------------------------------------------------------------
139
+
140
+ export type GateId = 'A' | 'B' | 'C' | 'D' | 'E' | 'G';
141
+ export type FindingSeverity = 'error' | 'review' | 'warning';
142
+
143
+ export interface MatrixFinding {
144
+ gate: GateId;
145
+ severity: FindingSeverity;
146
+ /** Delivery item / VP-id the finding anchors to, when applicable. */
147
+ ref?: string;
148
+ message: string;
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // The assembled matrix
153
+ // ---------------------------------------------------------------------------
154
+
155
+ export interface MatrixManifest {
156
+ unit: string;
157
+ transformerVersion: string;
158
+ sourceFingerprint: string; // canonical hash over all scenario fingerprints
159
+ mapFingerprint: string; // canonical hash of the map content
160
+ approvalState: 'approved' | 'draft';
161
+ itemCount: number;
162
+ variantCount: number;
163
+ thresholds: { maxVariantsPerItem: number };
164
+ }
165
+
166
+ export interface MatrixModel {
167
+ unit: string;
168
+ formNo: string;
169
+ items: DeliveryItem[];
170
+ dispositions: MatrixDisposition[];
171
+ findings: MatrixFinding[];
172
+ manifest: MatrixManifest;
173
+ }
174
+
175
+ /** Complexity warning threshold (rules draft Gate I) — echoed in the manifest. */
176
+ export const MAX_VARIANTS_PER_ITEM = 12;
@@ -162,7 +162,7 @@ function findMatchingSpecTest(scenario: ScenarioMetadata, tests: SpecTest[]): Sp
162
162
  if (exact) return exact;
163
163
 
164
164
  // Fallback: match by VP-ID prefix (ignore trailing text differences)
165
- const vpMatch = scenario.name.match(/^(VP-[A-Z][A-Z0-9]*-\d+[a-zA-Z]?)/);
165
+ const vpMatch = scenario.name.match(/^(VP-[A-Z][A-Z0-9]*-\d+(?:[a-zA-Z]|-[A-Z0-9]+)?)/);
166
166
  if (vpMatch) {
167
167
  const vpId = vpMatch[1];
168
168
  const byVp = tests.find((t) => t.vpId === vpId);
@@ -165,7 +165,7 @@ function classifyBlock(block: CodeBlock): 'precondition' | 'step' | 'expectation
165
165
  * Try to extract VP ID (e.g., VP-UI-001) from the start of a scenario title.
166
166
  */
167
167
  function extractVpId(title: string): string | undefined {
168
- const match = title.match(/^(VP-[A-Z][A-Z0-9]*-\d+[a-zA-Z]?)\b/);
168
+ const match = title.match(/^(VP-[A-Z][A-Z0-9]*-\d+(?:[a-zA-Z]|-[A-Z0-9]+)?)\b/);
169
169
  return match ? match[1] : undefined;
170
170
  }
171
171
 
@@ -143,6 +143,7 @@ export function renderReportHeaderBand(
143
143
  ws: ExcelJS.Worksheet,
144
144
  titleLabel: string,
145
145
  sungenVersion?: string,
146
+ formNo: string = 'BM-2-901-13',
146
147
  ): void {
147
148
  const issueDate = (() => {
148
149
  const d = new Date();
@@ -187,9 +188,9 @@ export function renderReportHeaderBand(
187
188
  d1.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true };
188
189
  d1.border = allBordersBlack;
189
190
 
190
- // G1 — No: BM-2-901-13
191
+ // G1 — No: <form number> (the company ISO form id; configurable via the delivery map)
191
192
  const g1 = ws.getCell('G1');
192
- g1.value = 'No: BM-2-901-13';
193
+ g1.value = `No: ${formNo}`;
193
194
  g1.font = { size: 12, name: TIMES };
194
195
  g1.alignment = { vertical: 'middle' };
195
196
  g1.border = { top: thinBlack, left: thinBlack, right: thinBlack };
@@ -1,15 +1,7 @@
1
- {{~#if isGiven~}}
2
1
  {
3
- const rows = {{> locator}}.locator('tbody').getByRole('row', { includeHidden: true }).filter({ visible: true });
2
+ const table = {{> locator}};
3
+ const rows = table.locator('tbody').getByRole('row', { includeHidden: true }).filter({ visible: true });
4
4
  {{#each assertions}}
5
- {{this}}
5
+ {{{this}}}
6
6
  {{/each~}}
7
7
  }
8
- {{~else~}}
9
- {
10
- const rows = {{> locator}}.locator('tbody').getByRole('row', { includeHidden: true }).filter({ visible: true });
11
- {{#each assertions}}
12
- {{this}}
13
- {{/each~}}
14
- }
15
- {{~/if}}
@@ -1,7 +1,17 @@
1
+ {{~#if rowSelector}}
2
+ {{~#if isGiven}}
3
+ const tableRow = {{> locator}}.locator('{{rowSelector}}');
4
+ await tableRow.filter({ hasText: '{{escapeQuotes filterValue}}' }).waitFor();
5
+ {{~else}}
6
+ const tableRow = {{> locator}}.locator('{{rowSelector}}');
7
+ await expect(tableRow).toContainText('{{escapeQuotes filterValue}}');
8
+ {{~/if}}
9
+ {{~else}}
1
10
  {{~#if isGiven}}
2
11
  const tableRow = {{> locator}}.getByRole('row', { includeHidden: true }).filter({ hasText: '{{escapeQuotes filterValue}}' }).filter({ visible: true });
3
12
  await tableRow.waitFor();
4
13
  {{~else}}
5
14
  const tableRow = {{> locator}}.getByRole('row', { includeHidden: true }).filter({ hasText: '{{escapeQuotes filterValue}}' }).filter({ visible: true });
6
15
  await expect(tableRow).toBeVisible();
7
- {{~/if}}
16
+ {{~/if}}
17
+ {{~/if}}
@@ -908,7 +908,7 @@ export class CodeGenerator {
908
908
  // them. Rewrite those to the per-row `rowData` view — each row then fires its own `@api` call
909
909
  // with that row's input and asserts that row's expected status/body (the success/failure matrix).
910
910
  // `testData.cases()/withRow()` (the global loader, no `.get/.bind/.set`) is intentionally left alone.
911
- return transformToRuntimeData(rendered, 'rowData').replace(/\btestData\.(get|bind|set)\(/g, 'rowData.$1(');
911
+ return transformToRuntimeData(rendered, 'rowData').replace(/\btestData\.(get|bind|set|raw)\(/g, 'rowData.$1(');
912
912
  }
913
913
  return rendered;
914
914
  }
@@ -17,12 +17,15 @@
17
17
  * action line + ≥1 labeled Observable:/Oracle:/Expected: line
18
18
  * SG-W009 — test-data references a `${QA_*}` secret that is declared in neither .env.qa
19
19
  * nor .env.qa.example — the test will throw "not set" the moment the key is used
20
+ * SG-W010 — a row step's [Ref] looks POSITIONAL (First/Last/Top/…) but has no selectors.yaml
21
+ * entry: the label is display-only, so the step checks existence anywhere in the
22
+ * table — an ordering rule would pass even when the row moved
20
23
  *
21
24
  * Diagnostics never block generation by themselves; `sungen generate --strict`
22
25
  * turns any collected diagnostic into a non-zero exit.
23
26
  */
24
27
 
25
- export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W005' | 'SG-W006' | 'SG-W007' | 'SG-W008' | 'SG-W009';
28
+ export type DiagnosticCode = 'SG-W001' | 'SG-W002' | 'SG-W003' | 'SG-W005' | 'SG-W006' | 'SG-W007' | 'SG-W008' | 'SG-W009' | 'SG-W010';
26
29
 
27
30
  export interface Diagnostic {
28
31
  code: DiagnosticCode;
@@ -37,6 +37,10 @@ export class StepMapper {
37
37
  /** Row scope: when active, column assertions are scoped to this row */
38
38
  private inRowScope: boolean = false;
39
39
  private rowScopeTable: string = '';
40
+ /** Count of row-scope-entering steps in the current scenario + whether the shared
41
+ * `tableRow` binding has been emitted (let-once + reassign when there are ≥2). */
42
+ private tableRowScopeSteps: number = 0;
43
+ private tableRowDeclared: boolean = false;
40
44
  /** SG-W007: an action step already ran in this scenario (alert handlers registered after it are too late) */
41
45
  private sawActionStep: boolean = false;
42
46
  /** Compile-time warning channel (SG-W001/W002); optional — absent means silent legacy behavior */
@@ -109,6 +113,12 @@ export class StepMapper {
109
113
  /**
110
114
  * Set scenario context for path variable resolution
111
115
  */
116
+ /** A `see [Ref] row in [Table] table with {{v}}` step — declares `tableRow` and enters row scope. */
117
+ private isRowScopeEntryStep(step: ParsedStep): boolean {
118
+ return step.elementType === 'row' && /\bin\b.*\btable\b/i.test(step.text) &&
119
+ /\bwith\b/i.test(step.text) && !/\bis\s+hidden\b/i.test(step.text);
120
+ }
121
+
112
122
  setScenarioContext(steps: ParsedStep[]): void {
113
123
  this.currentScenarioSteps = steps;
114
124
  // Reset dialog and frame scope at the start of each new scenario
@@ -116,6 +126,16 @@ export class StepMapper {
116
126
  this.inFrameScope = false;
117
127
  this.inRowScope = false;
118
128
  this.rowScopeTable = '';
129
+ // Row-scope steps declare `const tableRow` at FUNCTION scope on purpose — the
130
+ // following `[Col] column with {{v}}` steps reference it, so the declaration
131
+ // can't be block-wrapped like the self-contained row ACTIONS are. But two or
132
+ // more row steps in one scenario then redeclare the same const → the whole
133
+ // spec fails to parse ("Identifier 'tableRow' has already been declared").
134
+ // Count them up front: with ≥2 the first becomes `let` and the rest plain
135
+ // reassignments (which also correctly REBINDS the row scope); a single
136
+ // occurrence keeps `const` so existing single-row specs stay byte-identical.
137
+ this.tableRowScopeSteps = steps.filter((s) => this.isRowScopeEntryStep(s)).length;
138
+ this.tableRowDeclared = false;
119
139
  this.sawActionStep = false;
120
140
  this.lastPrimaryKeyword = 'Given';
121
141
  this.dataResolver.clearCaptured(); // captured vars are scenario-scoped
@@ -245,13 +265,34 @@ export class StepMapper {
245
265
 
246
266
  if (mappedStep) {
247
267
  // Enter row scope when a row-in-table step is matched
248
- if (step.elementType === 'row' && /\bin\b.*\btable\b/i.test(step.text) &&
249
- /\bwith\b/i.test(step.text) && !/\bis\s+hidden\b/i.test(step.text)) {
268
+ if (this.isRowScopeEntryStep(step)) {
250
269
  const brackets = step.text.match(/\[([^\]]+)\]/g) || [];
251
270
  this.inRowScope = true;
252
271
  this.rowScopeTable = brackets.length >= 2
253
272
  ? brackets[1].replace(/[\[\]]/g, '')
254
273
  : '';
274
+ // ≥2 row steps in this scenario: turn the duplicate `const tableRow`
275
+ // declarations into one `let` + reassignments (see setScenarioContext).
276
+ if (this.tableRowScopeSteps > 1 && mappedStep.code && mappedStep.code.includes('const tableRow')) {
277
+ if (this.tableRowDeclared) {
278
+ mappedStep.code = mappedStep.code.replace('const tableRow', 'tableRow');
279
+ } else {
280
+ mappedStep.code = mappedStep.code.replace('const tableRow', 'let tableRow');
281
+ this.tableRowDeclared = true;
282
+ }
283
+ }
284
+ // SG-W010 — a POSITIONAL-looking row label ([First X Row], [Last …]) without a
285
+ // selectors.yaml entry is display-only: the step degrades to an anywhere-in-the-
286
+ // table existence check and an ordering rule silently never fails. A resolved
287
+ // entry compiles to a positioned toContainText — its absence is detectable here.
288
+ const rowLabel = (brackets[0] || '').replace(/[\[\]]/g, '');
289
+ if (this.diagnostics && rowLabel &&
290
+ /\b(first|last|top|bottom|nth|\d+(st|nd|rd|th)?)\b/i.test(rowLabel) &&
291
+ mappedStep.code && !mappedStep.code.includes('toContainText')) {
292
+ this.diagnostics.add('SG-W010',
293
+ `row label "[${rowLabel}]" looks positional but has no selectors.yaml entry — it is a display label only, so this step checks existence ANYWHERE in the table (an ordering rule would pass even when the row moved). Declare it to position the row, e.g. "${rowLabel.toLowerCase()}:\\n type: locator\\n value: 'tbody tr:first-child'"`,
294
+ { feature: this.featureName, step: step.text });
295
+ }
255
296
  }
256
297
 
257
298
  if (this.verbose) {