@sun-asterisk/sungen 3.2.16-beta.1 → 3.2.16-beta.3
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 +166 -24
- 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 +40 -2
- 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 +49 -29
- package/dist/exporters/matrix/render-csv.js.map +1 -1
- package/dist/exporters/matrix/render-xlsx.d.ts +18 -8
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
- package/dist/exporters/matrix/render-xlsx.js +125 -52
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +29 -4
- package/dist/exporters/matrix/types.d.ts.map +1 -1
- package/dist/exporters/matrix/types.js +2 -2
- 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 +45 -9
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +36 -11
- package/package.json +3 -3
- package/src/cli/commands/delivery.ts +1 -0
- package/src/exporters/matrix/build.ts +168 -24
- package/src/exporters/matrix/export.ts +10 -0
- package/src/exporters/matrix/gates.ts +44 -2
- package/src/exporters/matrix/map-loader.ts +20 -1
- package/src/exporters/matrix/render-csv.ts +50 -30
- package/src/exporters/matrix/render-xlsx.ts +131 -55
- package/src/exporters/matrix/types.ts +33 -4
- package/src/exporters/matrix/wording.ts +157 -0
- package/src/orchestrator/templates/ai-src/commands/delivery.md +45 -9
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +36 -11
|
@@ -31,10 +31,18 @@ export interface MatrixTargetPaths {
|
|
|
31
31
|
featureFile: string;
|
|
32
32
|
testDataFile: string;
|
|
33
33
|
specFile: string;
|
|
34
|
+
/** requirements/spec.md — the requirement-id inventory for coverage (optional). */
|
|
35
|
+
specMdFile?: string;
|
|
34
36
|
resultsPath: string | null;
|
|
35
37
|
mapFile: string;
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
function readSpecText(paths: MatrixTargetPaths): string {
|
|
41
|
+
return paths.specMdFile && fs.existsSync(paths.specMdFile)
|
|
42
|
+
? fs.readFileSync(paths.specMdFile, 'utf-8')
|
|
43
|
+
: '';
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
export interface MatrixLoadResult {
|
|
39
47
|
model?: MatrixModel;
|
|
40
48
|
map?: DeliveryMap;
|
|
@@ -64,6 +72,7 @@ export function loadMatrixModel(paths: MatrixTargetPaths): MatrixLoadResult {
|
|
|
64
72
|
results,
|
|
65
73
|
map,
|
|
66
74
|
transformerVersion: getPackageVersion(),
|
|
75
|
+
specText: readSpecText(paths),
|
|
67
76
|
});
|
|
68
77
|
return { model, map, mapErrors: [] };
|
|
69
78
|
}
|
|
@@ -88,6 +97,7 @@ export function approveMatrix(paths: MatrixTargetPaths, groupIds?: string[]): {
|
|
|
88
97
|
const model = buildMatrix({
|
|
89
98
|
unit: paths.unit, feature, merged, testData, results: null, map,
|
|
90
99
|
transformerVersion: getPackageVersion(),
|
|
100
|
+
specText: readSpecText(paths),
|
|
91
101
|
});
|
|
92
102
|
const blocking = model.findings.filter((f) => f.severity === 'error');
|
|
93
103
|
if (blocking.length > 0) return { findings: blocking, approved: [] };
|
|
@@ -29,6 +29,7 @@ export function runGates(ctx: GateContext): MatrixFinding[] {
|
|
|
29
29
|
gateDExecutability(ctx, findings);
|
|
30
30
|
gateEDrift(ctx, findings);
|
|
31
31
|
gateGReviewState(ctx, findings);
|
|
32
|
+
gateWWording(ctx, findings);
|
|
32
33
|
return findings;
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -155,10 +156,11 @@ function gateCAggregation(ctx: GateContext, findings: MatrixFinding[]): void {
|
|
|
155
156
|
const heuristicsConfirmed = g.review === 'approved' && !stale;
|
|
156
157
|
|
|
157
158
|
// Hard signature parts — recomputed, never trusted from the map. ERROR on mismatch.
|
|
159
|
+
// Execution mode and priority are deliberately NOT here: they are coverage
|
|
160
|
+
// dimensions (account states may need seeded manual variants next to auto ones;
|
|
161
|
+
// an item takes the highest variant priority) — shown per variant, never split on.
|
|
158
162
|
const hard: Array<[string, (v: CoverageVariant) => string]> = [
|
|
159
|
-
['execution mode', (v) => v.mode],
|
|
160
163
|
['test layer', (v) => [...v.layers].sort().join('+')],
|
|
161
|
-
['priority', (v) => v.priority],
|
|
162
164
|
];
|
|
163
165
|
for (const [label, keyOf] of hard) {
|
|
164
166
|
const distinct = new Set(vs.map(keyOf));
|
|
@@ -241,6 +243,21 @@ function gateDExecutability(ctx: GateContext, findings: MatrixFinding[]): void {
|
|
|
241
243
|
}
|
|
242
244
|
}
|
|
243
245
|
}
|
|
246
|
+
|
|
247
|
+
// No template token may survive into a RENDERED cell (review B-04) — this also
|
|
248
|
+
// catches cross-referencing test-data values the one-level resolver couldn't close.
|
|
249
|
+
for (const [, vs] of variantsByVp) {
|
|
250
|
+
for (const v of vs) {
|
|
251
|
+
const rendered = [...v.precondition, ...v.trigger, ...v.oracle, ...v.verification, ...v.data];
|
|
252
|
+
const token = rendered.map((t) => t.match(/\{\{[^}]*\}\}/)).find(Boolean);
|
|
253
|
+
if (token) {
|
|
254
|
+
findings.push({
|
|
255
|
+
gate: 'D', severity: 'error', ref: v.ref,
|
|
256
|
+
message: `${v.ref}: unresolved template token ${token[0]} remains in the rendered output — the export is not deterministic for a tester`,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
244
261
|
}
|
|
245
262
|
|
|
246
263
|
// --- Gate E — drift (stale approval) ------------------------------------------
|
|
@@ -271,6 +288,31 @@ function gateEDrift(ctx: GateContext, findings: MatrixFinding[]): void {
|
|
|
271
288
|
}
|
|
272
289
|
}
|
|
273
290
|
|
|
291
|
+
// --- Gate W — controlled-language lint on the map's semantic fields --------------
|
|
292
|
+
|
|
293
|
+
/** Patterns that make customer-facing wording read machine-generated (review §10). */
|
|
294
|
+
const WORDING_SMELLS: Array<[RegExp, string]> = [
|
|
295
|
+
[/\{\{[^}]*\}\}/, 'an unresolved {{token}}'],
|
|
296
|
+
[/\b(?:Setup|Observable|Oracle):/, 'a generator label (Setup:/Observable:/Oracle:)'],
|
|
297
|
+
[/\bUser (?:fill|click|press|see|wait)\b/i, 'DSL phrasing ("User fill/click/see…") — write plain product language'],
|
|
298
|
+
[/\[[^\]]+\]/, 'a [selector-style] reference — use the visible UI label'],
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
function gateWWording(ctx: GateContext, findings: MatrixFinding[]): void {
|
|
302
|
+
for (const g of ctx.inputs.map.groups) {
|
|
303
|
+
for (const [field, text] of [['intent', g.intent], ['oracle', g.oracle]] as const) {
|
|
304
|
+
for (const [re, what] of WORDING_SMELLS) {
|
|
305
|
+
if (re.test(text)) {
|
|
306
|
+
findings.push({
|
|
307
|
+
gate: 'W', severity: 'warning', ref: g.id,
|
|
308
|
+
message: `group ${g.id} ${field} contains ${what}`,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
274
316
|
// --- Gate G — review state -----------------------------------------------------
|
|
275
317
|
|
|
276
318
|
function gateGReviewState(ctx: GateContext, findings: MatrixFinding[]): void {
|
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
12
|
-
import { DeliveryMap, MapCategory, MapDisposition, MapGroup, ReviewState } from './types';
|
|
12
|
+
import { DeliveryMap, MapCategory, MapDisposition, MapGroup, RequirementOverride, RequirementStatus, ReviewState } from './types';
|
|
13
13
|
|
|
14
14
|
const CATEGORIES: MapCategory[] = ['normal', 'abnormal', 'security', 'nfr'];
|
|
15
15
|
const REVIEW_STATES: ReviewState[] = ['proposed', 'approved'];
|
|
16
16
|
const DISPOSITIONS = ['excluded', 'blocked', 'covered_elsewhere', 'accepted_risk'];
|
|
17
|
+
const REQUIREMENT_STATUSES: RequirementStatus[] =
|
|
18
|
+
['covered', 'partially_covered', 'covered_elsewhere', 'planned', 'gap', 'not_applicable'];
|
|
17
19
|
|
|
18
20
|
export interface MapLoadResult {
|
|
19
21
|
map: DeliveryMap | null;
|
|
@@ -98,6 +100,21 @@ export function loadDeliveryMap(file: string): MapLoadResult {
|
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
const requirements: Record<string, RequirementOverride> = {};
|
|
104
|
+
if (doc.requirements !== undefined) {
|
|
105
|
+
if (!doc.requirements || typeof doc.requirements !== 'object' || Array.isArray(doc.requirements)) {
|
|
106
|
+
errors.push('`requirements` must be a mapping of requirement-id → { status, note }');
|
|
107
|
+
} else {
|
|
108
|
+
for (const [reqId, r] of Object.entries(doc.requirements as Record<string, unknown>)) {
|
|
109
|
+
const rr = (r && typeof r === 'object' ? r : {}) as Record<string, unknown>;
|
|
110
|
+
if (!REQUIREMENT_STATUSES.includes(String(rr.status) as RequirementStatus)) {
|
|
111
|
+
errors.push(`requirements.${reqId}: \`status\` must be one of ${REQUIREMENT_STATUSES.join(' | ')}`);
|
|
112
|
+
}
|
|
113
|
+
requirements[reqId] = { status: rr.status as RequirementStatus, note: rr.note ? String(rr.note) : undefined };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
101
118
|
const fingerprints: Record<string, string> = {};
|
|
102
119
|
if (doc.fingerprints && typeof doc.fingerprints === 'object' && !Array.isArray(doc.fingerprints)) {
|
|
103
120
|
for (const [k, v] of Object.entries(doc.fingerprints as Record<string, unknown>)) {
|
|
@@ -111,6 +128,7 @@ export function loadDeliveryMap(file: string): MapLoadResult {
|
|
|
111
128
|
formNo: doc.form_no !== undefined ? String(doc.form_no) : undefined,
|
|
112
129
|
groups,
|
|
113
130
|
dispositions,
|
|
131
|
+
requirements,
|
|
114
132
|
fingerprints,
|
|
115
133
|
};
|
|
116
134
|
return { map: errors.length > 0 ? null : map, errors };
|
|
@@ -135,6 +153,7 @@ export function writeDeliveryMap(file: string, map: DeliveryMap): void {
|
|
|
135
153
|
variants: g.variants,
|
|
136
154
|
})),
|
|
137
155
|
...(Object.keys(map.dispositions).length > 0 ? { dispositions: map.dispositions } : {}),
|
|
156
|
+
...(Object.keys(map.requirements).length > 0 ? { requirements: map.requirements } : {}),
|
|
138
157
|
...(Object.keys(map.fingerprints).length > 0 ? { fingerprints: map.fingerprints } : {}),
|
|
139
158
|
};
|
|
140
159
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -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 { itemModeLabel, 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
|
-
item
|
|
51
|
+
itemModeLabel(item),
|
|
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,25 @@ 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
|
-
|
|
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';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function itemResultLabel(item: DeliveryItem): string {
|
|
48
67
|
const { passed, failed, blocked, notRun } = item.resultCounts;
|
|
49
68
|
const total = item.variants.length;
|
|
50
69
|
const map: Record<ItemResult, string> = {
|
|
@@ -62,12 +81,27 @@ function variantResultLabel(v: CoverageVariant): string {
|
|
|
62
81
|
return statusToTestResult(v.result.status);
|
|
63
82
|
}
|
|
64
83
|
|
|
84
|
+
/** ISO date (2026-08-04) — unambiguous across locales (review §9). */
|
|
85
|
+
function isoDate(startTime: string | undefined): string {
|
|
86
|
+
if (!startTime) return '';
|
|
87
|
+
const d = new Date(startTime);
|
|
88
|
+
if (isNaN(d.getTime())) return '';
|
|
89
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
65
92
|
function numbered(lines: string[]): string {
|
|
66
93
|
if (lines.length === 0) return '';
|
|
67
94
|
if (lines.length === 1) return lines[0];
|
|
68
95
|
return lines.map((l, i) => `${i + 1}. ${l}`).join('\n');
|
|
69
96
|
}
|
|
70
97
|
|
|
98
|
+
/** Expected cell = observable outcomes; the HOW moves under a separate heading. */
|
|
99
|
+
function expectedWithVerification(oracle: string[], verification: string[]): string {
|
|
100
|
+
const expected = numbered(oracle);
|
|
101
|
+
if (verification.length === 0) return expected;
|
|
102
|
+
return `${expected}\nVerification method: ${verification.join(' ')}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
71
105
|
/** Red DRAFT banner on row 5 when the matrix is not approved for official delivery. */
|
|
72
106
|
function draftBanner(ws: ExcelJS.Worksheet, model: MatrixModel): void {
|
|
73
107
|
if (model.manifest.approvalState !== 'draft') return;
|
|
@@ -85,9 +119,10 @@ function draftBanner(ws: ExcelJS.Worksheet, model: MatrixModel): void {
|
|
|
85
119
|
|
|
86
120
|
const MATRIX_HEADERS = [
|
|
87
121
|
'ID', 'Target', 'Test Intent / Condition', 'Category', 'Priority', 'Precondition',
|
|
88
|
-
'Coverage / Test Data', 'Action / Trigger', 'Expected
|
|
122
|
+
'Coverage / Test Data', 'Action / Trigger', 'Expected Result', 'Mode', 'Trace',
|
|
89
123
|
'Result', 'Executed Date', 'Executor', 'Note\n(Evidence, DefectID)',
|
|
90
124
|
];
|
|
125
|
+
const RESULT_COL = 13; // column M
|
|
91
126
|
|
|
92
127
|
function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?: string): void {
|
|
93
128
|
const ws = wb.addWorksheet('Testcases');
|
|
@@ -95,7 +130,7 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
|
|
|
95
130
|
|
|
96
131
|
ws.columns = [
|
|
97
132
|
{ width: 3 }, // A margin
|
|
98
|
-
{ width:
|
|
133
|
+
{ width: 22 }, // B ID
|
|
99
134
|
{ width: 18 }, // C Target
|
|
100
135
|
{ width: 42 }, // D Intent / Condition
|
|
101
136
|
{ width: 11 }, // E Category
|
|
@@ -106,8 +141,8 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
|
|
|
106
141
|
{ width: 42 }, // J Expected
|
|
107
142
|
{ width: 11 }, // K Mode
|
|
108
143
|
{ width: 13 }, // L Trace
|
|
109
|
-
{ width:
|
|
110
|
-
{ width:
|
|
144
|
+
{ width: 22 }, // M Result
|
|
145
|
+
{ width: 12 }, // N Executed
|
|
111
146
|
{ width: 13 }, // O Executor
|
|
112
147
|
{ width: 24 }, // P Note
|
|
113
148
|
];
|
|
@@ -137,13 +172,55 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
|
|
|
137
172
|
|
|
138
173
|
let rowIdx = HEADER_ROW + 1;
|
|
139
174
|
for (const item of model.items) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
175
|
+
const parentRowIdx = rowIdx++;
|
|
176
|
+
|
|
177
|
+
// --- Variant sub-rows FIRST (to know their cell range for the parent formula).
|
|
178
|
+
// Every item gets sub-rows — the sub-row is where the source scenario id, the
|
|
179
|
+
// resolved data, and the manual result/evidence entry live (review B-05).
|
|
180
|
+
const firstChild = rowIdx;
|
|
181
|
+
for (const v of item.variants) {
|
|
182
|
+
const vr = ws.getRow(rowIdx++);
|
|
183
|
+
vr.outlineLevel = 1;
|
|
184
|
+
const vValues: ExcelJS.CellValue[] = [
|
|
185
|
+
v.ref,
|
|
186
|
+
'',
|
|
187
|
+
v.condition,
|
|
188
|
+
'', '', '',
|
|
189
|
+
numbered(v.data),
|
|
190
|
+
numbered(v.trigger),
|
|
191
|
+
expectedWithVerification(v.oracle, v.verification),
|
|
192
|
+
v.mode === 'manual' ? `Manual${v.manualReason ? ` (${v.manualReason})` : ''}` : 'Auto',
|
|
193
|
+
v.traces.join(', '),
|
|
194
|
+
variantResultLabel(v),
|
|
195
|
+
isoDate(v.result?.startTime),
|
|
196
|
+
'',
|
|
197
|
+
v.result?.error ? String(v.result.error).slice(0, 200) : '',
|
|
198
|
+
];
|
|
199
|
+
vValues.forEach((val, i) => dataCell(vr.getCell(2 + i), val, { center: i === 9 }));
|
|
200
|
+
// Manual-execution entry: constrain the Result cell to the known states.
|
|
201
|
+
vr.getCell(2 + 11).dataValidation = {
|
|
202
|
+
type: 'list', allowBlank: true, formulae: [`"${RESULT_STATES}"`],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const lastChild = rowIdx - 1;
|
|
206
|
+
|
|
207
|
+
// --- Parent row: only values that are genuinely COMMON to the whole item
|
|
208
|
+
// (review M-04); everything variant-specific stays on the sub-rows.
|
|
209
|
+
const pr = ws.getRow(parentRowIdx);
|
|
210
|
+
const triggerTexts = new Set(item.variants.map((v) => v.trigger.join('\n')));
|
|
211
|
+
const commonTrigger = triggerTexts.size === 1 ? item.variants[0].trigger : null;
|
|
212
|
+
const coverage = `${item.variants.length} variant(s): ${item.variants.map((v) => v.condition).join(' · ')}`;
|
|
213
|
+
|
|
214
|
+
// Live roll-up: derived from the child Result cells so hand-entered results
|
|
215
|
+
// recompute the parent — precedence failed → blocked → pending → partial → passed.
|
|
216
|
+
const rng = `M${firstChild}:M${lastChild}`;
|
|
217
|
+
const n = item.variants.length;
|
|
218
|
+
const rollUpFormula =
|
|
219
|
+
`IF(COUNTIF(${rng},"Failed")>0,COUNTIF(${rng},"Passed")&"/${n} Passed · "&COUNTIF(${rng},"Failed")&" Failed",` +
|
|
220
|
+
`IF(COUNTIF(${rng},"Blocked")>0,"Blocked",` +
|
|
221
|
+
`IF(COUNTIF(${rng},"Passed")=${n},"${n}/${n} Passed",` +
|
|
222
|
+
`IF(COUNTIF(${rng},"Passed")=0,"Pending",COUNTIF(${rng},"Passed")&"/${n} Passed · pending"))))`;
|
|
223
|
+
|
|
147
224
|
const values: ExcelJS.CellValue[] = [
|
|
148
225
|
item.id,
|
|
149
226
|
item.target,
|
|
@@ -152,12 +229,12 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
|
|
|
152
229
|
item.priority,
|
|
153
230
|
numbered(item.precondition),
|
|
154
231
|
coverage,
|
|
155
|
-
numbered(
|
|
232
|
+
commonTrigger ? numbered(commonTrigger) : '(differs per variant — see sub-rows)',
|
|
156
233
|
item.oracle,
|
|
157
|
-
item
|
|
234
|
+
itemModeLabel(item),
|
|
158
235
|
item.traces.join(', '),
|
|
159
|
-
itemResultLabel(item),
|
|
160
|
-
|
|
236
|
+
{ formula: rollUpFormula, result: itemResultLabel(item) } as ExcelJS.CellValue,
|
|
237
|
+
'',
|
|
161
238
|
'',
|
|
162
239
|
item.review === 'proposed' ? 'REVIEW REQUIRED' : '',
|
|
163
240
|
];
|
|
@@ -165,53 +242,52 @@ function addMatrixSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?
|
|
|
165
242
|
bold: true,
|
|
166
243
|
fill: SOFT_BLUE,
|
|
167
244
|
center: i === 3 || i === 4 || i === 9,
|
|
168
|
-
...(
|
|
245
|
+
...(i === 14 && item.review === 'proposed' ? { color: DRAFT_RED } : {}),
|
|
169
246
|
}));
|
|
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
247
|
}
|
|
195
248
|
|
|
196
249
|
ws.autoFilter = { from: { row: HEADER_ROW, column: 2 }, to: { row: rowIdx - 1, column: 16 } };
|
|
197
|
-
|
|
250
|
+
// Freeze the header band AND the ID + Target columns (review §9).
|
|
251
|
+
ws.views = [{ state: 'frozen', xSplit: 3, ySplit: HEADER_ROW }];
|
|
198
252
|
}
|
|
199
253
|
|
|
200
254
|
// ---------------------------------------------------------------------------
|
|
201
|
-
// Sheet 2 — Coverage (overview grid + dispositions + manifest)
|
|
255
|
+
// Sheet 2 — Coverage (requirements + overview grid + dispositions + manifest)
|
|
202
256
|
// ---------------------------------------------------------------------------
|
|
203
257
|
|
|
204
258
|
function addCoverageSheet(wb: ExcelJS.Workbook, model: MatrixModel, sungenVersion?: string): void {
|
|
205
259
|
const ws = wb.addWorksheet('Coverage');
|
|
206
260
|
ws.columns = [
|
|
207
|
-
{ width: 3 }, { width:
|
|
208
|
-
{ width: 16 }, { width:
|
|
261
|
+
{ width: 3 }, { width: 22 }, { width: 18 }, { width: 16 }, { width: 16 }, { width: 16 },
|
|
262
|
+
{ width: 16 }, { width: 44 },
|
|
209
263
|
];
|
|
210
264
|
renderReportHeaderBand(wb, ws, `${model.unit.toUpperCase()} COVERAGE OVERVIEW`, sungenVersion, model.formNo);
|
|
211
265
|
draftBanner(ws, model);
|
|
212
266
|
|
|
213
|
-
// --- Target × category grid ---
|
|
214
267
|
let rowIdx = 7;
|
|
268
|
+
|
|
269
|
+
// --- Requirement coverage (review §6): every requirement id gets ONE explicit status.
|
|
270
|
+
if (model.requirements.length > 0) {
|
|
271
|
+
dataCell(ws.getRow(rowIdx).getCell(2), 'Requirement coverage — every id has an explicit status', { bold: true });
|
|
272
|
+
rowIdx += 1;
|
|
273
|
+
const rh = ws.getRow(rowIdx++);
|
|
274
|
+
['Requirement', 'Status', 'Delivery items', 'Variants', 'Note'].forEach((l, i) => headerCell(rh.getCell(2 + i), l));
|
|
275
|
+
for (const r of model.requirements) {
|
|
276
|
+
const row = ws.getRow(rowIdx++);
|
|
277
|
+
dataCell(row.getCell(2), r.id, { bold: true });
|
|
278
|
+
dataCell(row.getCell(3), r.status, {
|
|
279
|
+
center: true,
|
|
280
|
+
...(r.status === 'gap' ? { fill: GAP_FILL, bold: true } : {}),
|
|
281
|
+
});
|
|
282
|
+
ws.mergeCells(rowIdx - 1, 4, rowIdx - 1, 6);
|
|
283
|
+
dataCell(row.getCell(4), r.items.join(', '));
|
|
284
|
+
dataCell(row.getCell(7), r.variantCount > 0 ? String(r.variantCount) : '', { center: true });
|
|
285
|
+
dataCell(row.getCell(8), r.note);
|
|
286
|
+
}
|
|
287
|
+
rowIdx += 1;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- Target × category grid ---
|
|
215
291
|
dataCell(ws.getRow(rowIdx).getCell(2), 'Coverage matrix — delivery items (coverage variants) per target × category', { bold: true });
|
|
216
292
|
rowIdx += 1;
|
|
217
293
|
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). */
|
|
@@ -112,8 +126,11 @@ export interface DeliveryItem {
|
|
|
112
126
|
oracle: string;
|
|
113
127
|
category: MapCategory;
|
|
114
128
|
review: ReviewState;
|
|
129
|
+
/** Highest priority among the variants (per-variant priorities stay on the sub-rows). */
|
|
115
130
|
priority: string;
|
|
116
|
-
mode
|
|
131
|
+
/** Execution mode is a coverage dimension, not a split — 'mixed' when auto and
|
|
132
|
+
* manual variants share one intent (e.g. account states needing seeded data). */
|
|
133
|
+
mode: 'auto' | 'manual' | 'mixed';
|
|
117
134
|
layers: MatrixLayer[];
|
|
118
135
|
/** Union of variant traces (exact per-variant traces stay on the variants). */
|
|
119
136
|
traces: string[];
|
|
@@ -133,11 +150,21 @@ export interface MatrixDisposition {
|
|
|
133
150
|
reason: string;
|
|
134
151
|
}
|
|
135
152
|
|
|
153
|
+
/** One row of the requirement-coverage table (Coverage sheet). */
|
|
154
|
+
export interface RequirementCoverage {
|
|
155
|
+
id: string; // FR-003 / TR-001 / NFR-…
|
|
156
|
+
status: RequirementStatus;
|
|
157
|
+
/** Delivery items whose variants trace to this requirement. */
|
|
158
|
+
items: string[];
|
|
159
|
+
variantCount: number;
|
|
160
|
+
note: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
136
163
|
// ---------------------------------------------------------------------------
|
|
137
164
|
// Gate findings
|
|
138
165
|
// ---------------------------------------------------------------------------
|
|
139
166
|
|
|
140
|
-
export type GateId = 'A' | 'B' | 'C' | 'D' | 'E' | 'G';
|
|
167
|
+
export type GateId = 'A' | 'B' | 'C' | 'D' | 'E' | 'G' | 'R' | 'W';
|
|
141
168
|
export type FindingSeverity = 'error' | 'review' | 'warning';
|
|
142
169
|
|
|
143
170
|
export interface MatrixFinding {
|
|
@@ -168,9 +195,11 @@ export interface MatrixModel {
|
|
|
168
195
|
formNo: string;
|
|
169
196
|
items: DeliveryItem[];
|
|
170
197
|
dispositions: MatrixDisposition[];
|
|
198
|
+
/** Requirement coverage — empty when the unit has no requirements/spec.md ids. */
|
|
199
|
+
requirements: RequirementCoverage[];
|
|
171
200
|
findings: MatrixFinding[];
|
|
172
201
|
manifest: MatrixManifest;
|
|
173
202
|
}
|
|
174
203
|
|
|
175
|
-
/** Complexity warning threshold (rules draft Gate I) — echoed in the manifest. */
|
|
176
|
-
export const MAX_VARIANTS_PER_ITEM =
|
|
204
|
+
/** Complexity warning threshold (rules draft Gate I; raised for compact grouping) — echoed in the manifest. */
|
|
205
|
+
export const MAX_VARIANTS_PER_ITEM = 20;
|