@sun-asterisk/sungen 3.2.15 → 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 (84) hide show
  1. package/dist/cli/commands/delivery.d.ts.map +1 -1
  2. package/dist/cli/commands/delivery.js +138 -1
  3. package/dist/cli/commands/delivery.js.map +1 -1
  4. package/dist/dashboard/templates/index.html +1 -1
  5. package/dist/exporters/feature-parser.js +1 -1
  6. package/dist/exporters/feature-parser.js.map +1 -1
  7. package/dist/exporters/matrix/build.d.ts +36 -0
  8. package/dist/exporters/matrix/build.d.ts.map +1 -0
  9. package/dist/exporters/matrix/build.js +267 -0
  10. package/dist/exporters/matrix/build.js.map +1 -0
  11. package/dist/exporters/matrix/export.d.ts +47 -0
  12. package/dist/exporters/matrix/export.d.ts.map +1 -0
  13. package/dist/exporters/matrix/export.js +136 -0
  14. package/dist/exporters/matrix/export.js.map +1 -0
  15. package/dist/exporters/matrix/fingerprint.d.ts +20 -0
  16. package/dist/exporters/matrix/fingerprint.d.ts.map +1 -0
  17. package/dist/exporters/matrix/fingerprint.js +83 -0
  18. package/dist/exporters/matrix/fingerprint.js.map +1 -0
  19. package/dist/exporters/matrix/gates.d.ts +21 -0
  20. package/dist/exporters/matrix/gates.d.ts.map +1 -0
  21. package/dist/exporters/matrix/gates.js +258 -0
  22. package/dist/exporters/matrix/gates.js.map +1 -0
  23. package/dist/exporters/matrix/map-loader.d.ts +22 -0
  24. package/dist/exporters/matrix/map-loader.d.ts.map +1 -0
  25. package/dist/exporters/matrix/map-loader.js +173 -0
  26. package/dist/exporters/matrix/map-loader.js.map +1 -0
  27. package/dist/exporters/matrix/render-csv.d.ts +8 -0
  28. package/dist/exporters/matrix/render-csv.d.ts.map +1 -0
  29. package/dist/exporters/matrix/render-csv.js +69 -0
  30. package/dist/exporters/matrix/render-csv.js.map +1 -0
  31. package/dist/exporters/matrix/render-xlsx.d.ts +17 -0
  32. package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -0
  33. package/dist/exporters/matrix/render-xlsx.js +298 -0
  34. package/dist/exporters/matrix/render-xlsx.js.map +1 -0
  35. package/dist/exporters/matrix/types.d.ts +153 -0
  36. package/dist/exporters/matrix/types.d.ts.map +1 -0
  37. package/dist/exporters/matrix/types.js +15 -0
  38. package/dist/exporters/matrix/types.js.map +1 -0
  39. package/dist/exporters/scenario-merger.js +1 -1
  40. package/dist/exporters/scenario-merger.js.map +1 -1
  41. package/dist/exporters/spec-parser.js +1 -1
  42. package/dist/exporters/spec-parser.js.map +1 -1
  43. package/dist/exporters/xlsx-report-builder.d.ts +1 -1
  44. package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
  45. package/dist/exporters/xlsx-report-builder.js +3 -3
  46. package/dist/exporters/xlsx-report-builder.js.map +1 -1
  47. package/dist/harness/audit.d.ts.map +1 -1
  48. package/dist/harness/audit.js +6 -1
  49. package/dist/harness/audit.js.map +1 -1
  50. package/dist/harness/capability.d.ts +25 -0
  51. package/dist/harness/capability.d.ts.map +1 -1
  52. package/dist/harness/capability.js +44 -2
  53. package/dist/harness/capability.js.map +1 -1
  54. package/dist/orchestrator/templates/ai-src/commands/create-test.md +1 -1
  55. package/dist/orchestrator/templates/ai-src/commands/delivery.md +118 -120
  56. package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
  57. package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
  58. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +36 -6
  59. package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -1
  60. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +1 -0
  61. package/package.json +4 -4
  62. package/src/cli/commands/delivery.ts +168 -2
  63. package/src/dashboard/templates/index.html +1 -1
  64. package/src/exporters/feature-parser.ts +1 -1
  65. package/src/exporters/matrix/build.ts +294 -0
  66. package/src/exporters/matrix/export.ts +124 -0
  67. package/src/exporters/matrix/fingerprint.ts +49 -0
  68. package/src/exporters/matrix/gates.ts +284 -0
  69. package/src/exporters/matrix/map-loader.ts +142 -0
  70. package/src/exporters/matrix/render-csv.ts +71 -0
  71. package/src/exporters/matrix/render-xlsx.ts +289 -0
  72. package/src/exporters/matrix/types.ts +176 -0
  73. package/src/exporters/scenario-merger.ts +1 -1
  74. package/src/exporters/spec-parser.ts +1 -1
  75. package/src/exporters/xlsx-report-builder.ts +3 -2
  76. package/src/harness/audit.ts +7 -2
  77. package/src/harness/capability.ts +53 -3
  78. package/src/orchestrator/templates/ai-src/commands/create-test.md +1 -1
  79. package/src/orchestrator/templates/ai-src/commands/delivery.md +118 -120
  80. package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
  81. package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
  82. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +36 -6
  83. package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -1
  84. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +1 -0
@@ -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 };
@@ -17,7 +17,7 @@ import {
17
17
  } from './sensors';
18
18
  import { loadFlowScenarios } from './flow-check';
19
19
  import { manualReasonMismatches, MANUAL_REASONS, buildPlan } from './capability-plan';
20
- import { readCapabilities } from './capability';
20
+ import { readCapabilities, verificationScopeFindings } from './capability';
21
21
  import { readIntent, projectRootFromScreenDir, IntentProfile } from './intent';
22
22
  import { getProvenance, Provenance } from './provenance';
23
23
  import { specCoverage, SpecCoverageResult, parseSpecClauses } from './spec-coverage';
@@ -129,7 +129,8 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
129
129
  discoverAndRegisterCapabilities();
130
130
  // The active platform (web | mobile | …) activates its own capability for scoring + sensor routing.
131
131
  // `web` has no capability of its own → scoringCap stays the default `ui` (byte-identical).
132
- const platform = readCapabilities(projectRootFromScreenDir(screenDir)).platform;
132
+ const capProfile = readCapabilities(projectRootFromScreenDir(screenDir));
133
+ const platform = capProfile.platform;
133
134
  const defaultCap = capabilityRegistry.defaultCapabilityId();
134
135
  const scoringCapId = scoringCapabilityFor(catalogScreenName, defaultCap, platform);
135
136
  const scoringCap = scoringCapId ? capabilityRegistry.get(scoringCapId) : undefined;
@@ -223,6 +224,10 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
223
224
  for (const c of flowCredits) {
224
225
  findings.push(`COVERED-VIA-FLOW: critical theme "${c.theme}" is not covered on this screen but is deeply covered by flow "${c.flow}" — cross-screen depth correctly owned by the flow, so it is credited (not a screen gap). Verify with \`sungen flow-check\`.`);
225
226
  }
227
+ // Verification-scope (advisory): API/DB verification the project's recorded test type never asked for.
228
+ for (const v of verificationScopeFindings(featureText, fs.existsSync(viewpointPath) ? fs.readFileSync(viewpointPath, 'utf-8') : '', capProfile)) {
229
+ findings.push(`VERIFICATION-OUT-OF-SCOPE (${v.layer}): this unit ${v.message}`);
230
+ }
226
231
  for (const g of gate.gaps) {
227
232
  if (g.status === 'shallow') {
228
233
  findings.push(`GATE: critical theme "${g.theme}" is covered only by SHALLOW scenarios (no data assertion) → deepen with \`... with {{value}}\` / \`table ... with {{value}}\` (count @manual cross-screen too).`);
@@ -15,9 +15,55 @@ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
15
15
  export interface CapabilityProfile {
16
16
  platform?: string; // web | mobile | … (the runtime adapter)
17
17
  enabled: string[]; // driver ids enabled for this project
18
+ /**
19
+ * Which VERIFICATION layers this project's test scope uses — the recorded "test type"
20
+ * (`ui` = E2E-only; add `api` / `db` when the project also verifies via API/DB). Explicit here wins;
21
+ * when absent it is DERIVED from `enabled` (ui always; api if the api driver is on; db if db is on).
22
+ * Generation and the audit use it so an E2E-only project does not get @api/@query verification it
23
+ * never asked for.
24
+ */
25
+ verification?: string[];
18
26
  source: 'capabilities.yaml' | 'absent';
19
27
  }
20
28
 
29
+ export type VerifyLayer = 'ui' | 'api' | 'db';
30
+
31
+ /**
32
+ * Effective verification scope = the recorded "test type". Explicit `verification` wins; otherwise
33
+ * derived from the enabled drivers. `ui` is always in scope (every project observes the UI/E2E result).
34
+ */
35
+ export function verificationScope(profile: CapabilityProfile): Set<VerifyLayer> {
36
+ if (profile.verification?.length) {
37
+ const s = new Set<VerifyLayer>(['ui']);
38
+ for (const v of profile.verification) if (v === 'api' || v === 'db' || v === 'ui') s.add(v);
39
+ return s;
40
+ }
41
+ const s = new Set<VerifyLayer>(['ui']);
42
+ if (profile.enabled.includes('api')) s.add('api');
43
+ if (profile.enabled.includes('db')) s.add('db');
44
+ return s;
45
+ }
46
+
47
+ export interface VerifyScopeFinding { layer: 'api' | 'db'; message: string; }
48
+
49
+ /**
50
+ * Flag API/DB verification that is OUT OF the project's recorded scope: a unit whose scenarios use
51
+ * `@api`/`@requires:api` (or `@query`/`@requires:db`) while the project is not API/DB-testing AND the
52
+ * viewpoint never asked for that layer. Deterministic; advisory only (a nudge, never a hard gate).
53
+ */
54
+ export function verificationScopeFindings(featureText: string, viewpointText: string, profile: CapabilityProfile): VerifyScopeFinding[] {
55
+ const scope = verificationScope(profile);
56
+ const vp = (viewpointText || '').toLowerCase();
57
+ const out: VerifyScopeFinding[] = [];
58
+ if (/@api\b|@requires:\s*api\b/.test(featureText) && !scope.has('api') && !/\bapi\b/.test(vp)) {
59
+ out.push({ layer: 'api', message: 'uses @api / @requires:api verification, but this project is not API-testing (capabilities `enabled` has no `api`) and the viewpoint does not request API verification. Keep the oracle UI-observable, OR record API in scope (capabilities `verification: [ui, api]` or `sungen capability add api`). Reserve API/DB verify for the ≤20% of effects with no UI observation.' });
60
+ }
61
+ if (/@query\b|@requires:\s*db\b/.test(featureText) && !scope.has('db') && !/\b(database|db)\b/.test(vp)) {
62
+ out.push({ layer: 'db', message: 'uses @query / @requires:db verification, but this project is not DB-testing (capabilities `enabled` has no `db`) and the viewpoint does not request DB verification. Keep the oracle UI-observable, OR record DB in scope (capabilities `verification: [ui, db]` or `sungen capability add db`). Reserve API/DB verify for the ≤20% of effects with no UI observation.' });
63
+ }
64
+ return out;
65
+ }
66
+
21
67
  export interface DriverMeta {
22
68
  id: string;
23
69
  kind: 'platform' | 'capability';
@@ -41,6 +87,7 @@ export function readCapabilities(cwd: string): CapabilityProfile {
41
87
  return {
42
88
  platform: typeof y.platform === 'string' ? y.platform : undefined,
43
89
  enabled: Array.isArray(y.enabled) ? y.enabled.map(String) : [],
90
+ verification: Array.isArray(y.verification) ? y.verification.map(String) : undefined,
44
91
  source: 'capabilities.yaml',
45
92
  };
46
93
  } catch {
@@ -48,14 +95,17 @@ export function readCapabilities(cwd: string): CapabilityProfile {
48
95
  }
49
96
  }
50
97
 
51
- export function writeCapabilities(cwd: string, profile: { platform?: string; enabled: string[] }): string {
98
+ export function writeCapabilities(cwd: string, profile: { platform?: string; enabled: string[]; verification?: string[] }): string {
52
99
  const p = capabilitiesPath(cwd);
53
100
  fs.mkdirSync(path.dirname(p), { recursive: true });
54
101
  const body =
55
102
  '# Sungen capability profile — which runtime/drivers this project uses.\n' +
56
103
  '# platform: the runtime adapter (web → Playwright, mobile → Appium). No default.\n' +
57
- '# enabled: drivers turned on (added via `sungen capability add <driver>`).\n\n' +
58
- stringifyYaml({ platform: profile.platform, enabled: profile.enabled });
104
+ '# enabled: drivers turned on (added via `sungen capability add <driver>`).\n' +
105
+ '# verification: the TEST TYPE — which layers this project verifies (`ui` = E2E only; add `api`/`db`\n' +
106
+ '# only if the project actually verifies via API/Database). Absent → derived from `enabled`.\n' +
107
+ '# This keeps an E2E-only project from getting @api/@query verification it never asked for.\n\n' +
108
+ stringifyYaml({ platform: profile.platform, enabled: profile.enabled, ...(profile.verification ? { verification: profile.verification } : {}) });
59
109
  fs.writeFileSync(p, body, 'utf-8');
60
110
  return p;
61
111
  }
@@ -22,7 +22,7 @@ If `spec_figma.md` exists OR the user provides a Figma URL for the PAT flow:
22
22
 
23
23
  ## Platform detection (do this FIRST)
24
24
 
25
- Read `qa/capabilities.yaml` and check the `platform` field.
25
+ Read `qa/capabilities.yaml` and check the `platform` field — **and the verification scope** (`verification:` if set, else derived from `enabled`: `ui` always; `api`/`db` only if that driver is on). This is the project's recorded **test type**: an **E2E/UI-only** project (no `api`/`db`) must NOT get `@api`/`@query`/`@requires:api|db` verification unless the test-viewpoint explicitly asks for it — keep oracles UI-observable, and cap any in-scope API/DB-in-E2E verification at the **≤20% band** (see `sungen-tc-generation` → "Respect the project's verification scope"). `sungen audit` flags `VERIFICATION-OUT-OF-SCOPE`.
26
26
 
27
27
  - If `platform: mobile` → the visual-capture step (Step 3, "Auto-detect visual source") uses the **`sungen-capture-mobile`** skill (Appium MCP live capture) **instead of** the web `sungen-capture` (figma/live). Scenarios use mobile gesture vocabulary from `sungen-mobile-gestures` (tap, swipe, long-press — not click/hover) and `accessibility-id`-first selectors; mobile-specific steps (permission dialogs, clipboard, geolocation) are valid Gherkin, not errors. The `Background:` is a **navigation recipe** (see `add-screen --reach`), not a URL path. Everything else — viewpoints, `sungen-tc-generation`, the harness/audit loop — is platform-agnostic and unchanged.
28
28
  - If `platform: web` or field absent → follow the **web path** (all steps below, unchanged) — Figma/live capture via `sungen-capture` as documented in Step 3.