@sun-asterisk/sungen 3.2.16-beta.1 → 3.2.16-beta.2
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.
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +1 -0
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts +8 -1
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +158 -22
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/export.d.ts +2 -0
- package/dist/exporters/matrix/export.d.ts.map +1 -1
- package/dist/exporters/matrix/export.js +7 -0
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/gates.d.ts.map +1 -1
- package/dist/exporters/matrix/gates.js +37 -0
- package/dist/exporters/matrix/gates.js.map +1 -1
- package/dist/exporters/matrix/map-loader.d.ts.map +1 -1
- package/dist/exporters/matrix/map-loader.js +18 -0
- package/dist/exporters/matrix/map-loader.js.map +1 -1
- package/dist/exporters/matrix/render-csv.d.ts +3 -2
- package/dist/exporters/matrix/render-csv.d.ts.map +1 -1
- package/dist/exporters/matrix/render-csv.js +48 -28
- package/dist/exporters/matrix/render-csv.js.map +1 -1
- package/dist/exporters/matrix/render-xlsx.d.ts +16 -8
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
- package/dist/exporters/matrix/render-xlsx.js +116 -52
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +23 -1
- package/dist/exporters/matrix/types.d.ts.map +1 -1
- package/dist/exporters/matrix/types.js.map +1 -1
- package/dist/exporters/matrix/wording.d.ts +45 -0
- package/dist/exporters/matrix/wording.d.ts.map +1 -0
- package/dist/exporters/matrix/wording.js +150 -0
- package/dist/exporters/matrix/wording.js.map +1 -0
- package/dist/orchestrator/templates/ai-src/commands/delivery.md +27 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +31 -8
- package/package.json +3 -3
- package/src/cli/commands/delivery.ts +1 -0
- package/src/exporters/matrix/build.ts +160 -22
- package/src/exporters/matrix/export.ts +10 -0
- package/src/exporters/matrix/gates.ts +41 -0
- package/src/exporters/matrix/map-loader.ts +20 -1
- package/src/exporters/matrix/render-csv.ts +49 -29
- package/src/exporters/matrix/render-xlsx.ts +122 -55
- package/src/exporters/matrix/types.ts +27 -1
- package/src/exporters/matrix/wording.ts +157 -0
- package/src/orchestrator/templates/ai-src/commands/delivery.md +27 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +31 -8
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Flat CSV rendering of the matrix — same model as the workbook, one row per
|
|
3
|
-
* delivery item (`Level: item`) followed by its variant rows (`Level: variant
|
|
4
|
-
*
|
|
3
|
+
* delivery item (`Level: item`) followed by its variant rows (`Level: variant`;
|
|
4
|
+
* every item has them — the variant row carries the source scenario id, data,
|
|
5
|
+
* and result), so downstream pipelines can re-derive both views from one file.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
import { statusToTestResult
|
|
8
|
+
import { statusToTestResult } from '../playwright-report-parser';
|
|
9
|
+
import { itemResultLabel } from './render-xlsx';
|
|
8
10
|
import { MatrixModel } from './types';
|
|
9
11
|
|
|
10
12
|
const HEADERS = [
|
|
11
13
|
'Level', 'ID', 'Target', 'Test Intent / Condition', 'Category', 'Priority',
|
|
12
|
-
'Precondition', 'Coverage / Test Data', 'Action / Trigger', 'Expected
|
|
14
|
+
'Precondition', 'Coverage / Test Data', 'Action / Trigger', 'Expected Result',
|
|
13
15
|
'Mode', 'Trace', 'Result', 'Executed Date', 'Note',
|
|
14
16
|
];
|
|
15
17
|
|
|
@@ -18,6 +20,13 @@ function esc(value: string): string {
|
|
|
18
20
|
return value;
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
function isoDate(startTime: string | undefined): string {
|
|
24
|
+
if (!startTime) return '';
|
|
25
|
+
const d = new Date(startTime);
|
|
26
|
+
if (isNaN(d.getTime())) return '';
|
|
27
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
export function renderMatrixCsv(model: MatrixModel): string {
|
|
22
31
|
const BOM = '\ufeff'; // Excel compatibility (Vietnamese/Japanese content)
|
|
23
32
|
const lines: string[] = [];
|
|
@@ -26,8 +35,8 @@ export function renderMatrixCsv(model: MatrixModel): string {
|
|
|
26
35
|
lines.push(HEADERS.map(esc).join(','));
|
|
27
36
|
|
|
28
37
|
for (const item of model.items) {
|
|
29
|
-
const
|
|
30
|
-
const
|
|
38
|
+
const triggerTexts = new Set(item.variants.map((v) => v.trigger.join('\n')));
|
|
39
|
+
const commonTrigger = triggerTexts.size === 1 ? item.variants[0].trigger.join('\n') : '(differs per variant — see variant rows)';
|
|
31
40
|
lines.push([
|
|
32
41
|
'item',
|
|
33
42
|
item.id,
|
|
@@ -36,35 +45,46 @@ export function renderMatrixCsv(model: MatrixModel): string {
|
|
|
36
45
|
item.category,
|
|
37
46
|
item.priority,
|
|
38
47
|
item.precondition.join('\n'),
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
`${item.variants.length} variant(s): ${item.variants.map((v) => v.condition).join(' · ')}`,
|
|
49
|
+
commonTrigger,
|
|
41
50
|
item.oracle,
|
|
42
51
|
item.mode === 'manual' ? 'Manual' : 'Auto',
|
|
43
52
|
item.traces.join(' '),
|
|
44
|
-
item
|
|
45
|
-
|
|
53
|
+
itemResultLabel(item),
|
|
54
|
+
'',
|
|
46
55
|
item.review === 'proposed' ? 'REVIEW REQUIRED' : '',
|
|
47
56
|
].map(esc).join(','));
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
58
|
+
for (const v of item.variants) {
|
|
59
|
+
const expected = v.verification.length > 0
|
|
60
|
+
? `${v.oracle.join('\n')}\nVerification method: ${v.verification.join(' ')}`
|
|
61
|
+
: v.oracle.join('\n');
|
|
62
|
+
lines.push([
|
|
63
|
+
'variant',
|
|
64
|
+
v.ref,
|
|
65
|
+
'',
|
|
66
|
+
v.condition,
|
|
67
|
+
'', '',
|
|
68
|
+
'',
|
|
69
|
+
v.data.join('\n'),
|
|
70
|
+
v.trigger.join('\n'),
|
|
71
|
+
expected,
|
|
72
|
+
v.mode === 'manual' ? 'Manual' : 'Auto',
|
|
73
|
+
v.traces.join(' '),
|
|
74
|
+
v.result ? statusToTestResult(v.result.status) : 'Pending',
|
|
75
|
+
isoDate(v.result?.startTime),
|
|
76
|
+
v.result?.error ? String(v.result.error).slice(0, 200) : '',
|
|
77
|
+
].map(esc).join(','));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Requirement coverage appendix — keeps the CSV self-contained for pipelines.
|
|
82
|
+
if (model.requirements.length > 0) {
|
|
83
|
+
lines.push('');
|
|
84
|
+
lines.push('# Requirement coverage');
|
|
85
|
+
lines.push(['Requirement', 'Status', 'Delivery items', 'Variants', 'Note'].join(','));
|
|
86
|
+
for (const r of model.requirements) {
|
|
87
|
+
lines.push([r.id, r.status, r.items.join(' '), String(r.variantCount), r.note].map(esc).join(','));
|
|
68
88
|
}
|
|
69
89
|
}
|
|
70
90
|
return BOM + lines.join('\n') + '\n';
|
|
@@ -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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
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
|
|
30
|
+
import { statusToTestResult } from '../playwright-report-parser';
|
|
24
31
|
import { CoverageVariant, DeliveryItem, ItemResult, 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,14 +45,16 @@ 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
|
-
|
|
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 {
|
|
57
|
+
export function itemResultLabel(item: DeliveryItem): string {
|
|
48
58
|
const { passed, failed, blocked, notRun } = item.resultCounts;
|
|
49
59
|
const total = item.variants.length;
|
|
50
60
|
const map: Record<ItemResult, string> = {
|
|
@@ -62,12 +72,27 @@ function variantResultLabel(v: CoverageVariant): string {
|
|
|
62
72
|
return statusToTestResult(v.result.status);
|
|
63
73
|
}
|
|
64
74
|
|
|
75
|
+
/** ISO date (2026-08-04) — unambiguous across locales (review §9). */
|
|
76
|
+
function isoDate(startTime: string | undefined): string {
|
|
77
|
+
if (!startTime) return '';
|
|
78
|
+
const d = new Date(startTime);
|
|
79
|
+
if (isNaN(d.getTime())) return '';
|
|
80
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
65
83
|
function numbered(lines: string[]): string {
|
|
66
84
|
if (lines.length === 0) return '';
|
|
67
85
|
if (lines.length === 1) return lines[0];
|
|
68
86
|
return lines.map((l, i) => `${i + 1}. ${l}`).join('\n');
|
|
69
87
|
}
|
|
70
88
|
|
|
89
|
+
/** Expected cell = observable outcomes; the HOW moves under a separate heading. */
|
|
90
|
+
function expectedWithVerification(oracle: string[], verification: string[]): string {
|
|
91
|
+
const expected = numbered(oracle);
|
|
92
|
+
if (verification.length === 0) return expected;
|
|
93
|
+
return `${expected}\nVerification method: ${verification.join(' ')}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
71
96
|
/** Red DRAFT banner on row 5 when the matrix is not approved for official delivery. */
|
|
72
97
|
function draftBanner(ws: ExcelJS.Worksheet, model: MatrixModel): void {
|
|
73
98
|
if (model.manifest.approvalState !== 'draft') return;
|
|
@@ -85,9 +110,10 @@ function draftBanner(ws: ExcelJS.Worksheet, model: MatrixModel): void {
|
|
|
85
110
|
|
|
86
111
|
const MATRIX_HEADERS = [
|
|
87
112
|
'ID', 'Target', 'Test Intent / Condition', 'Category', 'Priority', 'Precondition',
|
|
88
|
-
'Coverage / Test Data', 'Action / Trigger', 'Expected
|
|
113
|
+
'Coverage / Test Data', 'Action / Trigger', 'Expected Result', 'Mode', 'Trace',
|
|
89
114
|
'Result', 'Executed Date', 'Executor', 'Note\n(Evidence, DefectID)',
|
|
90
115
|
];
|
|
116
|
+
const RESULT_COL = 13; // column M
|
|
91
117
|
|
|
92
118
|
function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?: string): void {
|
|
93
119
|
const ws = wb.addWorksheet('Testcases');
|
|
@@ -95,7 +121,7 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
|
|
|
95
121
|
|
|
96
122
|
ws.columns = [
|
|
97
123
|
{ width: 3 }, // A margin
|
|
98
|
-
{ width:
|
|
124
|
+
{ width: 22 }, // B ID
|
|
99
125
|
{ width: 18 }, // C Target
|
|
100
126
|
{ width: 42 }, // D Intent / Condition
|
|
101
127
|
{ width: 11 }, // E Category
|
|
@@ -106,8 +132,8 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
|
|
|
106
132
|
{ width: 42 }, // J Expected
|
|
107
133
|
{ width: 11 }, // K Mode
|
|
108
134
|
{ width: 13 }, // L Trace
|
|
109
|
-
{ width:
|
|
110
|
-
{ width:
|
|
135
|
+
{ width: 22 }, // M Result
|
|
136
|
+
{ width: 12 }, // N Executed
|
|
111
137
|
{ width: 13 }, // O Executor
|
|
112
138
|
{ width: 24 }, // P Note
|
|
113
139
|
];
|
|
@@ -137,13 +163,55 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
|
|
|
137
163
|
|
|
138
164
|
let rowIdx = HEADER_ROW + 1;
|
|
139
165
|
for (const item of model.items) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
166
|
+
const parentRowIdx = rowIdx++;
|
|
167
|
+
|
|
168
|
+
// --- Variant sub-rows FIRST (to know their cell range for the parent formula).
|
|
169
|
+
// Every item gets sub-rows — the sub-row is where the source scenario id, the
|
|
170
|
+
// resolved data, and the manual result/evidence entry live (review B-05).
|
|
171
|
+
const firstChild = rowIdx;
|
|
172
|
+
for (const v of item.variants) {
|
|
173
|
+
const vr = ws.getRow(rowIdx++);
|
|
174
|
+
vr.outlineLevel = 1;
|
|
175
|
+
const vValues: ExcelJS.CellValue[] = [
|
|
176
|
+
v.ref,
|
|
177
|
+
'',
|
|
178
|
+
v.condition,
|
|
179
|
+
'', '', '',
|
|
180
|
+
numbered(v.data),
|
|
181
|
+
numbered(v.trigger),
|
|
182
|
+
expectedWithVerification(v.oracle, v.verification),
|
|
183
|
+
v.mode === 'manual' ? `Manual${v.manualReason ? ` (${v.manualReason})` : ''}` : 'Auto',
|
|
184
|
+
v.traces.join(', '),
|
|
185
|
+
variantResultLabel(v),
|
|
186
|
+
isoDate(v.result?.startTime),
|
|
187
|
+
'',
|
|
188
|
+
v.result?.error ? String(v.result.error).slice(0, 200) : '',
|
|
189
|
+
];
|
|
190
|
+
vValues.forEach((val, i) => dataCell(vr.getCell(2 + i), val, { center: i === 9 }));
|
|
191
|
+
// Manual-execution entry: constrain the Result cell to the known states.
|
|
192
|
+
vr.getCell(2 + 11).dataValidation = {
|
|
193
|
+
type: 'list', allowBlank: true, formulae: [`"${RESULT_STATES}"`],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const lastChild = rowIdx - 1;
|
|
197
|
+
|
|
198
|
+
// --- Parent row: only values that are genuinely COMMON to the whole item
|
|
199
|
+
// (review M-04); everything variant-specific stays on the sub-rows.
|
|
200
|
+
const pr = ws.getRow(parentRowIdx);
|
|
201
|
+
const triggerTexts = new Set(item.variants.map((v) => v.trigger.join('\n')));
|
|
202
|
+
const commonTrigger = triggerTexts.size === 1 ? item.variants[0].trigger : null;
|
|
203
|
+
const coverage = `${item.variants.length} variant(s): ${item.variants.map((v) => v.condition).join(' · ')}`;
|
|
204
|
+
|
|
205
|
+
// Live roll-up: derived from the child Result cells so hand-entered results
|
|
206
|
+
// recompute the parent — precedence failed → blocked → pending → partial → passed.
|
|
207
|
+
const rng = `M${firstChild}:M${lastChild}`;
|
|
208
|
+
const n = item.variants.length;
|
|
209
|
+
const rollUpFormula =
|
|
210
|
+
`IF(COUNTIF(${rng},"Failed")>0,COUNTIF(${rng},"Passed")&"/${n} Passed · "&COUNTIF(${rng},"Failed")&" Failed",` +
|
|
211
|
+
`IF(COUNTIF(${rng},"Blocked")>0,"Blocked",` +
|
|
212
|
+
`IF(COUNTIF(${rng},"Passed")=${n},"${n}/${n} Passed",` +
|
|
213
|
+
`IF(COUNTIF(${rng},"Passed")=0,"Pending",COUNTIF(${rng},"Passed")&"/${n} Passed · pending"))))`;
|
|
214
|
+
|
|
147
215
|
const values: ExcelJS.CellValue[] = [
|
|
148
216
|
item.id,
|
|
149
217
|
item.target,
|
|
@@ -152,12 +220,12 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
|
|
|
152
220
|
item.priority,
|
|
153
221
|
numbered(item.precondition),
|
|
154
222
|
coverage,
|
|
155
|
-
numbered(
|
|
223
|
+
commonTrigger ? numbered(commonTrigger) : '(differs per variant — see sub-rows)',
|
|
156
224
|
item.oracle,
|
|
157
|
-
item.mode === 'manual' ?
|
|
225
|
+
item.mode === 'manual' ? 'Manual' : 'Auto',
|
|
158
226
|
item.traces.join(', '),
|
|
159
|
-
itemResultLabel(item),
|
|
160
|
-
|
|
227
|
+
{ formula: rollUpFormula, result: itemResultLabel(item) } as ExcelJS.CellValue,
|
|
228
|
+
'',
|
|
161
229
|
'',
|
|
162
230
|
item.review === 'proposed' ? 'REVIEW REQUIRED' : '',
|
|
163
231
|
];
|
|
@@ -165,53 +233,52 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
|
|
|
165
233
|
bold: true,
|
|
166
234
|
fill: SOFT_BLUE,
|
|
167
235
|
center: i === 3 || i === 4 || i === 9,
|
|
168
|
-
...(
|
|
236
|
+
...(i === 14 && item.review === 'proposed' ? { color: DRAFT_RED } : {}),
|
|
169
237
|
}));
|
|
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
238
|
}
|
|
195
239
|
|
|
196
240
|
ws.autoFilter = { from: { row: HEADER_ROW, column: 2 }, to: { row: rowIdx - 1, column: 16 } };
|
|
197
|
-
|
|
241
|
+
// Freeze the header band AND the ID + Target columns (review §9).
|
|
242
|
+
ws.views = [{ state: 'frozen', xSplit: 3, ySplit: HEADER_ROW }];
|
|
198
243
|
}
|
|
199
244
|
|
|
200
245
|
// ---------------------------------------------------------------------------
|
|
201
|
-
// Sheet 2 — Coverage (overview grid + dispositions + manifest)
|
|
246
|
+
// Sheet 2 — Coverage (requirements + overview grid + dispositions + manifest)
|
|
202
247
|
// ---------------------------------------------------------------------------
|
|
203
248
|
|
|
204
249
|
function addCoverageSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?: string): void {
|
|
205
250
|
const ws = wb.addWorksheet('Coverage');
|
|
206
251
|
ws.columns = [
|
|
207
|
-
{ width: 3 }, { width:
|
|
208
|
-
{ width: 16 }, { width:
|
|
252
|
+
{ width: 3 }, { width: 22 }, { width: 18 }, { width: 16 }, { width: 16 }, { width: 16 },
|
|
253
|
+
{ width: 16 }, { width: 44 },
|
|
209
254
|
];
|
|
210
255
|
renderReportHeaderBand(wb, ws, `${model.unit.toUpperCase()} COVERAGE OVERVIEW`, sungenVersion, model.formNo);
|
|
211
256
|
draftBanner(ws, model);
|
|
212
257
|
|
|
213
|
-
// --- Target × category grid ---
|
|
214
258
|
let rowIdx = 7;
|
|
259
|
+
|
|
260
|
+
// --- Requirement coverage (review §6): every requirement id gets ONE explicit status.
|
|
261
|
+
if (model.requirements.length > 0) {
|
|
262
|
+
dataCell(ws.getRow(rowIdx).getCell(2), 'Requirement coverage — every id has an explicit status', { bold: true });
|
|
263
|
+
rowIdx += 1;
|
|
264
|
+
const rh = ws.getRow(rowIdx++);
|
|
265
|
+
['Requirement', 'Status', 'Delivery items', 'Variants', 'Note'].forEach((l, i) => headerCell(rh.getCell(2 + i), l));
|
|
266
|
+
for (const r of model.requirements) {
|
|
267
|
+
const row = ws.getRow(rowIdx++);
|
|
268
|
+
dataCell(row.getCell(2), r.id, { bold: true });
|
|
269
|
+
dataCell(row.getCell(3), r.status, {
|
|
270
|
+
center: true,
|
|
271
|
+
...(r.status === 'gap' ? { fill: GAP_FILL, bold: true } : {}),
|
|
272
|
+
});
|
|
273
|
+
ws.mergeCells(rowIdx - 1, 4, rowIdx - 1, 6);
|
|
274
|
+
dataCell(row.getCell(4), r.items.join(', '));
|
|
275
|
+
dataCell(row.getCell(7), r.variantCount > 0 ? String(r.variantCount) : '', { center: true });
|
|
276
|
+
dataCell(row.getCell(8), r.note);
|
|
277
|
+
}
|
|
278
|
+
rowIdx += 1;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// --- Target × category grid ---
|
|
215
282
|
dataCell(ws.getRow(rowIdx).getCell(2), 'Coverage matrix — delivery items (coverage variants) per target × category', { bold: true });
|
|
216
283
|
rowIdx += 1;
|
|
217
284
|
const gridHeader = ws.getRow(rowIdx++);
|
|
@@ -38,6 +38,14 @@ export interface MapDisposition {
|
|
|
38
38
|
reason?: string;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export type RequirementStatus =
|
|
42
|
+
| 'covered' | 'partially_covered' | 'covered_elsewhere' | 'planned' | 'gap' | 'not_applicable';
|
|
43
|
+
|
|
44
|
+
export interface RequirementOverride {
|
|
45
|
+
status: RequirementStatus;
|
|
46
|
+
note?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
export interface DeliveryMap {
|
|
42
50
|
version: number;
|
|
43
51
|
unit: string;
|
|
@@ -46,6 +54,9 @@ export interface DeliveryMap {
|
|
|
46
54
|
groups: MapGroup[];
|
|
47
55
|
/** VP-id → intentionally-not-grouped disposition. */
|
|
48
56
|
dispositions: Record<string, MapDisposition>;
|
|
57
|
+
/** Requirement-id → reviewed status override (partially_covered / not_applicable / …).
|
|
58
|
+
* Without an override a requirement is `covered` when a variant traces to it, else `gap`. */
|
|
59
|
+
requirements: Record<string, RequirementOverride>;
|
|
49
60
|
/** VP-id → scenario fingerprint stamped at approval time (drift detector input). */
|
|
50
61
|
fingerprints: Record<string, string>;
|
|
51
62
|
}
|
|
@@ -90,6 +101,9 @@ export interface CoverageVariant {
|
|
|
90
101
|
oracleShape: string[];
|
|
91
102
|
/** Oracle for display. */
|
|
92
103
|
oracle: string[];
|
|
104
|
+
/** HOW to check (manual `Oracle:` lines — tools, panes, queries). Rendered as
|
|
105
|
+
* "Verification method" under the expected result, never inside it. */
|
|
106
|
+
verification: string[];
|
|
93
107
|
/** Precondition profile: auth role + scenario-level Given shapes — signature input. */
|
|
94
108
|
preconditionProfile: string;
|
|
95
109
|
/** Human preconditions (auth + own Given steps). */
|
|
@@ -133,11 +147,21 @@ export interface MatrixDisposition {
|
|
|
133
147
|
reason: string;
|
|
134
148
|
}
|
|
135
149
|
|
|
150
|
+
/** One row of the requirement-coverage table (Coverage sheet). */
|
|
151
|
+
export interface RequirementCoverage {
|
|
152
|
+
id: string; // FR-003 / TR-001 / NFR-…
|
|
153
|
+
status: RequirementStatus;
|
|
154
|
+
/** Delivery items whose variants trace to this requirement. */
|
|
155
|
+
items: string[];
|
|
156
|
+
variantCount: number;
|
|
157
|
+
note: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
136
160
|
// ---------------------------------------------------------------------------
|
|
137
161
|
// Gate findings
|
|
138
162
|
// ---------------------------------------------------------------------------
|
|
139
163
|
|
|
140
|
-
export type GateId = 'A' | 'B' | 'C' | 'D' | 'E' | 'G';
|
|
164
|
+
export type GateId = 'A' | 'B' | 'C' | 'D' | 'E' | 'G' | 'R' | 'W';
|
|
141
165
|
export type FindingSeverity = 'error' | 'review' | 'warning';
|
|
142
166
|
|
|
143
167
|
export interface MatrixFinding {
|
|
@@ -168,6 +192,8 @@ export interface MatrixModel {
|
|
|
168
192
|
formNo: string;
|
|
169
193
|
items: DeliveryItem[];
|
|
170
194
|
dispositions: MatrixDisposition[];
|
|
195
|
+
/** Requirement coverage — empty when the unit has no requirements/spec.md ids. */
|
|
196
|
+
requirements: RequirementCoverage[];
|
|
171
197
|
findings: MatrixFinding[];
|
|
172
198
|
manifest: MatrixManifest;
|
|
173
199
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wording normalization — deterministic rendering step AFTER semantic
|
|
3
|
+
* normalization (review feedback §10): turn sungen DSL steps into controlled
|
|
4
|
+
* manual-test English without changing the target, condition, trigger,
|
|
5
|
+
* precondition, oracle, or trace.
|
|
6
|
+
*
|
|
7
|
+
* - Actions render in the imperative: "User fill [Email] field with X"
|
|
8
|
+
* → "Enter X in the Email field."
|
|
9
|
+
* - Expected results render as observable assertions (never tester actions):
|
|
10
|
+
* "User see [Jobs] page" → "The Jobs page is displayed."
|
|
11
|
+
* - Manual `# Tester verifies:` labels (Setup:/Action:/Observable:/Oracle:)
|
|
12
|
+
* become structured fields instead of prose: Setup → precondition,
|
|
13
|
+
* Action → action, Observable → expected, Oracle → verification method.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// `[Email] field` → `Email field` (the visible label + its element type).
|
|
17
|
+
function deRef(text: string): string {
|
|
18
|
+
return text.replace(/\[([^\]]+)\]/g, '$1');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sentence(text: string): string {
|
|
22
|
+
let s = text.trim().replace(/\s+/g, ' ');
|
|
23
|
+
if (!s) return s;
|
|
24
|
+
s = s.charAt(0).toUpperCase() + s.slice(1);
|
|
25
|
+
if (!/[.!?…]$/.test(s)) s += '.';
|
|
26
|
+
return s;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Render one action step in the imperative. Pattern table covers the common
|
|
31
|
+
* sungen step verbs; anything unmatched falls back to actor-stripped text —
|
|
32
|
+
* still readable, never a raw `User fill`.
|
|
33
|
+
*/
|
|
34
|
+
export function renderAction(raw: string): string {
|
|
35
|
+
let s = raw.trim().replace(/^(User|The user)\s+/i, '');
|
|
36
|
+
|
|
37
|
+
const rules: Array<[RegExp, (m: RegExpMatchArray) => string]> = [
|
|
38
|
+
// fill [X] field with V
|
|
39
|
+
[/^fills? \[([^\]]+)\][a-z ]* with (.+)$/i, (m) => `Enter ${m[2]} in the ${m[1]} field`],
|
|
40
|
+
// clear [X] field
|
|
41
|
+
[/^clears? \[([^\]]+)\](.*)$/i, (m) => `Clear the ${m[1]}${m[2] || ' field'}`],
|
|
42
|
+
// click [X] <type>
|
|
43
|
+
[/^clicks? \[([^\]]+)\]\s*(\w+)?$/i, (m) => `Click the ${m[1]}${m[2] ? ` ${m[2]}` : ''}`],
|
|
44
|
+
// press <Key> at/in [X] field
|
|
45
|
+
[/^press(?:es)? (.+?) (?:at|in|inside) \[([^\]]+)\](?: field)?$/i, (m) => `Press ${m[1]} in the ${m[2]} field`],
|
|
46
|
+
[/^press(?:es)? (.+)$/i, (m) => `Press ${m[1]}`],
|
|
47
|
+
// select V in/from [X] dropdown
|
|
48
|
+
[/^selects? (.+?) (?:in|from) \[([^\]]+)\]\s*(\w+)?$/i, (m) => `Select ${m[1]} in the ${m[2]}${m[3] ? ` ${m[3]}` : ''}`],
|
|
49
|
+
// check/uncheck [X] checkbox
|
|
50
|
+
[/^(un)?checks? \[([^\]]+)\]\s*(\w+)?$/i, (m) => `${m[1] ? 'Uncheck' : 'Check'} the ${m[2]}${m[3] ? ` ${m[3]}` : ''}`],
|
|
51
|
+
// hover [X]
|
|
52
|
+
[/^hovers? (?:over )?\[([^\]]+)\]\s*(\w+)?$/i, (m) => `Hover over the ${m[1]}${m[2] ? ` ${m[2]}` : ''}`],
|
|
53
|
+
// upload V to [X]
|
|
54
|
+
[/^uploads? (.+?) (?:to|into) \[([^\]]+)\]\s*(\w+)?$/i, (m) => `Upload ${m[1]} to the ${m[2]}${m[3] ? ` ${m[3]}` : ''}`],
|
|
55
|
+
// is on [X] page (as an action = navigate)
|
|
56
|
+
[/^is on \[([^\]]+)\] page(.*)$/i, (m) => `Open the ${m[1]} page${m[2] ?? ''}`],
|
|
57
|
+
// wait for [X] <type> (is )?visible
|
|
58
|
+
[/^waits? for \[([^\]]+)\]\s*(\w+)?(?: is)?(?: visible)?$/i, (m) => `Wait until the ${m[1]}${m[2] ? ` ${m[2]}` : ''} is visible`],
|
|
59
|
+
// scroll to [X]
|
|
60
|
+
[/^scrolls? (?:to|into) \[([^\]]+)\]\s*(\w+)?$/i, (m) => `Scroll to the ${m[1]}${m[2] ? ` ${m[2]}` : ''}`],
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
for (const [re, out] of rules) {
|
|
64
|
+
const m = s.match(re);
|
|
65
|
+
if (m) return sentence(deRef(out(m)));
|
|
66
|
+
}
|
|
67
|
+
return sentence(deRef(s));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Render one expected step as an observable assertion (no tester action, no
|
|
72
|
+
* `should`, no DSL `User see`).
|
|
73
|
+
*/
|
|
74
|
+
export function renderExpected(raw: string): string {
|
|
75
|
+
let s = raw.trim().replace(/^(User|The user)\s+/i, '');
|
|
76
|
+
|
|
77
|
+
const rules: Array<[RegExp, (m: RegExpMatchArray) => string]> = [
|
|
78
|
+
// see [X] page
|
|
79
|
+
[/^sees? \[([^\]]+)\] page$/i, (m) => `The ${m[1]} page is displayed`],
|
|
80
|
+
// see [X] <type> with V
|
|
81
|
+
[/^sees? \[([^\]]+)\]\s*(\w+)? with (.+)$/i, (m) => `The ${m[1]}${m[2] ? ` ${m[2]}` : ''} shows ${m[3]}`],
|
|
82
|
+
// see [X] <type> contains V
|
|
83
|
+
[/^sees? \[([^\]]+)\]\s*(\w+)? contains (.+)$/i, (m) => `The ${m[1]}${m[2] ? ` ${m[2]}` : ''} contains ${m[3]}`],
|
|
84
|
+
// see [X] <type> has text V
|
|
85
|
+
[/^sees? \[([^\]]+)\]\s*(\w+)? has text (.+)$/i, (m) => `The ${m[1]}${m[2] ? ` ${m[2]}` : ''} shows ${m[3]}`],
|
|
86
|
+
// see [X] <type> is hidden / is disabled / is enabled / …
|
|
87
|
+
[/^sees? \[([^\]]+)\]\s*(\w+)? is (.+)$/i, (m) => `The ${m[1]}${m[2] ? ` ${m[2]}` : ''} is ${m[3]}`],
|
|
88
|
+
// not see [X] <type>
|
|
89
|
+
[/^(?:do(?:es)? )?not sees? \[([^\]]+)\]\s*(\w+)?$/i, (m) => `The ${m[1]}${m[2] ? ` ${m[2]}` : ''} is not displayed`],
|
|
90
|
+
// see [X] <type>
|
|
91
|
+
[/^sees? \[([^\]]+)\]\s*(\w+)?$/i, (m) => `The ${m[1]}${m[2] ? ` ${m[2]}` : ''} is visible`],
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
for (const [re, out] of rules) {
|
|
95
|
+
const m = s.match(re);
|
|
96
|
+
if (m) return sentence(deRef(out(m)));
|
|
97
|
+
}
|
|
98
|
+
return sentence(deRef(s));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Precondition wording: a state, not an action ("The user is signed out."). */
|
|
102
|
+
export function renderPrecondition(raw: string): string {
|
|
103
|
+
const s = raw.trim().replace(/^(User|The user)\s+/i, '');
|
|
104
|
+
const m = s.match(/^is on \[([^\]]+)\] page(.*)$/i);
|
|
105
|
+
if (m) return sentence(`The user is on the ${m[1]} page${m[2] ?? ''}`);
|
|
106
|
+
return sentence(deRef(`The user ${s.charAt(0).toLowerCase()}${s.slice(1)}`));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Manual `# Tester verifies:` comment classification (structured, label-free)
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
export interface ManualProcedure {
|
|
114
|
+
/** Setup/Precondition/Arrange lines — the state to establish first. */
|
|
115
|
+
preconditions: string[];
|
|
116
|
+
/** Action/unlabelled lines — the imperative procedure. */
|
|
117
|
+
actions: string[];
|
|
118
|
+
/** Observable/Expect/Result/Assert lines — the observable outcome. */
|
|
119
|
+
expected: string[];
|
|
120
|
+
/** Oracle/Verify lines — HOW to check (tools, panes, queries). */
|
|
121
|
+
verification: string[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Split a manual scenario's numbered comment lines into the four structured
|
|
126
|
+
* fields. Labels are consumed (structured), never left inside the prose —
|
|
127
|
+
* review feedback §10.2(3). Continuation lines append to the previous item;
|
|
128
|
+
* pre-amble (rationale/header/dividers) is skipped.
|
|
129
|
+
*/
|
|
130
|
+
export function classifyManualComments(comments: string[]): ManualProcedure {
|
|
131
|
+
const out: ManualProcedure = { preconditions: [], actions: [], expected: [], verification: [] };
|
|
132
|
+
let last: { list: string[]; idx: number } | null = null;
|
|
133
|
+
|
|
134
|
+
const bucketOf = (label: string): keyof ManualProcedure => {
|
|
135
|
+
if (/setup|precondition|arrange|given/i.test(label)) return 'preconditions';
|
|
136
|
+
if (/oracle|verify|verification|how to check/i.test(label)) return 'verification';
|
|
137
|
+
if (/observ|expect|result|then|assert/i.test(label)) return 'expected';
|
|
138
|
+
return 'actions';
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
for (const raw of comments) {
|
|
142
|
+
const line = raw.trim();
|
|
143
|
+
if (!line) continue;
|
|
144
|
+
if (/^[-=*_]{2,}/.test(line)) { last = null; continue; }
|
|
145
|
+
const m = line.match(/^\d+[.)]\s*(?:([A-Za-z][A-Za-z /]*?):\s*)?(.+)$/);
|
|
146
|
+
if (m) {
|
|
147
|
+
const label = (m[1] || '').trim();
|
|
148
|
+
const text = m[2].trim();
|
|
149
|
+
const list = out[bucketOf(label)];
|
|
150
|
+
list.push(text);
|
|
151
|
+
last = { list, idx: list.length - 1 };
|
|
152
|
+
} else if (last) {
|
|
153
|
+
last.list[last.idx] += ' ' + line;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|