@sun-asterisk/sungen 3.2.16-beta.1 → 3.2.16-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +67 -20
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/exporters/feature-parser.d.ts +16 -1
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +21 -2
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts +27 -2
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +277 -47
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/export.d.ts +7 -4
- package/dist/exporters/matrix/export.d.ts.map +1 -1
- package/dist/exporters/matrix/export.js +22 -6
- 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 +112 -3
- 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 +20 -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 +53 -30
- package/dist/exporters/matrix/render-csv.js.map +1 -1
- package/dist/exporters/matrix/render-xlsx.d.ts +32 -8
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
- package/dist/exporters/matrix/render-xlsx.js +215 -83
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +53 -7
- 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 +61 -0
- package/dist/exporters/matrix/wording.d.ts.map +1 -0
- package/dist/exporters/matrix/wording.js +221 -0
- package/dist/exporters/matrix/wording.js.map +1 -0
- package/dist/exporters/scenario-merger.d.ts.map +1 -1
- package/dist/exporters/scenario-merger.js +2 -1
- package/dist/exporters/scenario-merger.js.map +1 -1
- package/dist/exporters/spec-parser.d.ts.map +1 -1
- package/dist/exporters/spec-parser.js +2 -1
- package/dist/exporters/spec-parser.js.map +1 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +11 -2
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/blindspot.d.ts.map +1 -1
- package/dist/harness/blindspot.js +2 -1
- package/dist/harness/blindspot.js.map +1 -1
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +3 -2
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/feedback.d.ts.map +1 -1
- package/dist/harness/feedback.js +3 -2
- package/dist/harness/feedback.js.map +1 -1
- package/dist/harness/flow-check.d.ts.map +1 -1
- package/dist/harness/flow-check.js +2 -1
- package/dist/harness/flow-check.js.map +1 -1
- package/dist/harness/flow-plan.d.ts.map +1 -1
- package/dist/harness/flow-plan.js +3 -2
- package/dist/harness/flow-plan.js.map +1 -1
- package/dist/harness/intent.d.ts.map +1 -1
- package/dist/harness/intent.js +2 -1
- package/dist/harness/intent.js.map +1 -1
- package/dist/harness/journey.d.ts.map +1 -1
- package/dist/harness/journey.js +3 -2
- package/dist/harness/journey.js.map +1 -1
- package/dist/harness/ledger.d.ts.map +1 -1
- package/dist/harness/ledger.js +3 -2
- package/dist/harness/ledger.js.map +1 -1
- package/dist/harness/manifest.d.ts.map +1 -1
- package/dist/harness/manifest.js +4 -3
- package/dist/harness/manifest.js.map +1 -1
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +16 -3
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/quality-gates.d.ts.map +1 -1
- package/dist/harness/quality-gates.js +2 -1
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/harness/read-text.d.ts +22 -0
- package/dist/harness/read-text.d.ts.map +1 -0
- package/dist/harness/read-text.js +64 -0
- package/dist/harness/read-text.js.map +1 -0
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +3 -2
- package/dist/harness/script-check.js.map +1 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +2 -10
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/spec-coverage.d.ts +5 -0
- package/dist/harness/spec-coverage.d.ts.map +1 -1
- package/dist/harness/spec-coverage.js +17 -7
- package/dist/harness/spec-coverage.js.map +1 -1
- package/dist/harness/trace.d.ts.map +1 -1
- package/dist/harness/trace.js +4 -3
- package/dist/harness/trace.js.map +1 -1
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +2 -1
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +9 -0
- package/dist/orchestrator/templates/ai-src/commands/delivery.md +103 -18
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +77 -11
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +1 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +22 -0
- package/package.json +4 -4
- package/src/cli/commands/delivery.ts +68 -22
- package/src/exporters/feature-parser.ts +21 -2
- package/src/exporters/matrix/build.ts +281 -43
- package/src/exporters/matrix/export.ts +31 -6
- package/src/exporters/matrix/gates.ts +119 -3
- package/src/exporters/matrix/map-loader.ts +22 -1
- package/src/exporters/matrix/render-csv.ts +53 -30
- package/src/exporters/matrix/render-xlsx.ts +216 -85
- package/src/exporters/matrix/types.ts +58 -8
- package/src/exporters/matrix/wording.ts +221 -0
- package/src/exporters/scenario-merger.ts +2 -1
- package/src/exporters/spec-parser.ts +2 -1
- package/src/harness/audit.ts +11 -2
- package/src/harness/blindspot.ts +2 -1
- package/src/harness/capability-plan.ts +3 -2
- package/src/harness/feedback.ts +3 -2
- package/src/harness/flow-check.ts +2 -1
- package/src/harness/flow-plan.ts +3 -2
- package/src/harness/intent.ts +2 -1
- package/src/harness/journey.ts +3 -2
- package/src/harness/ledger.ts +3 -2
- package/src/harness/manifest.ts +4 -3
- package/src/harness/parse.ts +17 -3
- package/src/harness/quality-gates.ts +2 -1
- package/src/harness/read-text.ts +28 -0
- package/src/harness/script-check.ts +3 -2
- package/src/harness/sensors.ts +2 -1
- package/src/harness/spec-coverage.ts +22 -7
- package/src/harness/trace.ts +4 -3
- package/src/harness/viewpoint-ledger.ts +2 -1
- package/src/orchestrator/templates/ai-src/commands/create-test.md +9 -0
- package/src/orchestrator/templates/ai-src/commands/delivery.md +103 -18
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +77 -11
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +1 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +22 -0
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { splitVpAndName, extractTestcaseType } from '../feature-parser';
|
|
15
|
+
import { mapContentFingerprint } from './fingerprint';
|
|
15
16
|
import { CoverageVariant, MatrixFinding, MAX_VARIANTS_PER_ITEM } from './types';
|
|
16
17
|
import { expandMapRef, BuildInputs } from './build';
|
|
17
18
|
|
|
@@ -29,9 +30,44 @@ export function runGates(ctx: GateContext): MatrixFinding[] {
|
|
|
29
30
|
gateDExecutability(ctx, findings);
|
|
30
31
|
gateEDrift(ctx, findings);
|
|
31
32
|
gateGReviewState(ctx, findings);
|
|
33
|
+
gateWWording(ctx, findings);
|
|
34
|
+
gateKCategory(ctx, findings);
|
|
32
35
|
return findings;
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
// --- Gate K — category vs the source viewpoint class -------------------------
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Viewpoint classes whose delivery category is not a judgement call — the source
|
|
42
|
+
* decides it. Matched on the id's category segment, so a project scheme without
|
|
43
|
+
* the `VP-` prefix (`SEC-123`, `PER-345`) classifies the same as `VP-SEC-001`.
|
|
44
|
+
*/
|
|
45
|
+
const VP_CATEGORY_RULE: Array<[RegExp, string, string]> = [
|
|
46
|
+
[/^(SEC|SECURITY)$/i, 'security', 'SEC'],
|
|
47
|
+
[/^(NFR|PERF|PER|PERFORMANCE|A11Y|ACCESSIBILITY)$/i, 'nfr', 'NFR/PERF/A11Y'],
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A `VP-SEC-*` scenario filed under `normal`/`abnormal` empties the Coverage
|
|
52
|
+
* sheet's `security` column — the grid then reports a gap the unit does not
|
|
53
|
+
* have while hiding the security work it does have. Same for `VP-NFR-*`.
|
|
54
|
+
* (normal vs abnormal stays a judgement call and is never checked.)
|
|
55
|
+
*/
|
|
56
|
+
function gateKCategory(ctx: GateContext, findings: MatrixFinding[]): void {
|
|
57
|
+
for (const g of ctx.inputs.map.groups) {
|
|
58
|
+
const vs = g.variants.flatMap((ref) => expandMapRef(ref, ctx.variantsByVp));
|
|
59
|
+
for (const [re, expected, label] of VP_CATEGORY_RULE) {
|
|
60
|
+
const hits = vs.filter((v) => re.test(v.vpCategory));
|
|
61
|
+
if (hits.length > 0 && g.category !== expected) {
|
|
62
|
+
findings.push({
|
|
63
|
+
gate: 'K', severity: 'warning', ref: g.id,
|
|
64
|
+
message: `group ${g.id} is \`category: ${g.category}\` but ${hits.length}/${vs.length} of its variants are ${label} scenarios (${hits.slice(0, 3).map((v) => v.vpId).join(', ')}) — use \`category: ${expected}\` or the Coverage grid's ${expected} column stays empty`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
35
71
|
// --- Gate A — source quality ------------------------------------------------
|
|
36
72
|
|
|
37
73
|
function gateASource(ctx: GateContext, findings: MatrixFinding[]): void {
|
|
@@ -155,10 +191,11 @@ function gateCAggregation(ctx: GateContext, findings: MatrixFinding[]): void {
|
|
|
155
191
|
const heuristicsConfirmed = g.review === 'approved' && !stale;
|
|
156
192
|
|
|
157
193
|
// Hard signature parts — recomputed, never trusted from the map. ERROR on mismatch.
|
|
194
|
+
// Execution mode and priority are deliberately NOT here: they are coverage
|
|
195
|
+
// dimensions (account states may need seeded manual variants next to auto ones;
|
|
196
|
+
// an item takes the highest variant priority) — shown per variant, never split on.
|
|
158
197
|
const hard: Array<[string, (v: CoverageVariant) => string]> = [
|
|
159
|
-
['execution mode', (v) => v.mode],
|
|
160
198
|
['test layer', (v) => [...v.layers].sort().join('+')],
|
|
161
|
-
['priority', (v) => v.priority],
|
|
162
199
|
];
|
|
163
200
|
for (const [label, keyOf] of hard) {
|
|
164
201
|
const distinct = new Set(vs.map(keyOf));
|
|
@@ -231,9 +268,14 @@ function gateDExecutability(ctx: GateContext, findings: MatrixFinding[]): void {
|
|
|
231
268
|
for (const [vpId, vs] of variantsByVp) {
|
|
232
269
|
const scenario = ctx.inputs.merged.find((m) => splitVpAndName(m.feature.name).vpId === vpId);
|
|
233
270
|
if (!scenario) continue;
|
|
271
|
+
// Values produced during the run (`remember … as {{v}}`, capability bindings)
|
|
272
|
+
// are provided by the test itself — demanding a test-data entry for them
|
|
273
|
+
// blocked 14 scenarios of a real suite that compiles and runs fine.
|
|
274
|
+
const runtime = new Set(vs.flatMap((v) => v.runtimeVars));
|
|
234
275
|
for (const varName of scenario.feature.referencedVars) {
|
|
235
276
|
const inRows = vs.some((v) => v.caseLabel !== undefined); // dataset rows carry their own columns
|
|
236
|
-
|
|
277
|
+
const atRuntime = runtime.has(varName) || runtime.has(varName.split(/[.[]/)[0]);
|
|
278
|
+
if (!inRows && !atRuntime && (!testData || !(varName in testData))) {
|
|
237
279
|
findings.push({
|
|
238
280
|
gate: 'D', severity: 'error', ref: vpId,
|
|
239
281
|
message: `${vpId}: {{${varName}}} does not resolve in test-data — the variant is not reproducible`,
|
|
@@ -241,6 +283,21 @@ function gateDExecutability(ctx: GateContext, findings: MatrixFinding[]): void {
|
|
|
241
283
|
}
|
|
242
284
|
}
|
|
243
285
|
}
|
|
286
|
+
|
|
287
|
+
// No template token may survive into a RENDERED cell (review B-04) — this also
|
|
288
|
+
// catches cross-referencing test-data values the one-level resolver couldn't close.
|
|
289
|
+
for (const [, vs] of variantsByVp) {
|
|
290
|
+
for (const v of vs) {
|
|
291
|
+
const rendered = [...v.precondition, ...v.trigger, ...v.oracle, ...v.verification, ...v.data];
|
|
292
|
+
const token = rendered.map((t) => t.match(/\{\{[^}]*\}\}/)).find(Boolean);
|
|
293
|
+
if (token) {
|
|
294
|
+
findings.push({
|
|
295
|
+
gate: 'D', severity: 'error', ref: v.ref,
|
|
296
|
+
message: `${v.ref}: unresolved template token ${token[0]} remains in the rendered output — the export is not deterministic for a tester`,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
244
301
|
}
|
|
245
302
|
|
|
246
303
|
// --- Gate E — drift (stale approval) ------------------------------------------
|
|
@@ -249,6 +306,25 @@ function gateEDrift(ctx: GateContext, findings: MatrixFinding[]): void {
|
|
|
249
306
|
const { map } = ctx.inputs;
|
|
250
307
|
const { variantsByVp } = ctx;
|
|
251
308
|
|
|
309
|
+
// The map's own semantics (targets/intents/oracles/grouping) are part of what
|
|
310
|
+
// was approved — an edit after approval must be re-reviewed, not silently
|
|
311
|
+
// published (review GAP-09).
|
|
312
|
+
const stampedMap = map.fingerprints.__map__;
|
|
313
|
+
const liveMap = mapContentFingerprint(map.groups, map.dispositions);
|
|
314
|
+
if (map.groups.some((g) => g.review === 'approved')) {
|
|
315
|
+
if (!stampedMap) {
|
|
316
|
+
findings.push({
|
|
317
|
+
gate: 'E', severity: 'review',
|
|
318
|
+
message: 'the delivery map has no stamped content fingerprint — run `sungen delivery --approve` to record the reviewed wording',
|
|
319
|
+
});
|
|
320
|
+
} else if (stampedMap !== liveMap) {
|
|
321
|
+
findings.push({
|
|
322
|
+
gate: 'E', severity: 'review',
|
|
323
|
+
message: 'the delivery map wording/grouping changed since approval — re-review, then `sungen delivery --approve`',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
252
328
|
for (const g of map.groups) {
|
|
253
329
|
if (g.review !== 'approved') continue; // proposed groups are already under review
|
|
254
330
|
for (const ref of g.variants) {
|
|
@@ -271,6 +347,46 @@ function gateEDrift(ctx: GateContext, findings: MatrixFinding[]): void {
|
|
|
271
347
|
}
|
|
272
348
|
}
|
|
273
349
|
|
|
350
|
+
// --- Gate W — controlled-language lint on the map's semantic fields --------------
|
|
351
|
+
|
|
352
|
+
/** Patterns that make customer-facing wording read machine-generated (review §10). */
|
|
353
|
+
const WORDING_SMELLS: Array<[RegExp, string]> = [
|
|
354
|
+
[/\{\{[^}]*\}\}/, 'an unresolved {{token}}'],
|
|
355
|
+
[/\b(?:Setup|Observable|Oracle):/, 'a generator label (Setup:/Observable:/Oracle:)'],
|
|
356
|
+
[/\bUser (?:fill|click|press|see|wait)\b/i, 'DSL phrasing ("User fill/click/see…") — write plain product language'],
|
|
357
|
+
[/\[[^\]]+\]/, 'a [selector-style] reference — use the visible UI label'],
|
|
358
|
+
];
|
|
359
|
+
|
|
360
|
+
function gateWWording(ctx: GateContext, findings: MatrixFinding[]): void {
|
|
361
|
+
for (const g of ctx.inputs.map.groups) {
|
|
362
|
+
const variantCount = g.variants.flatMap((ref) => expandMapRef(ref, ctx.variantsByVp)).length;
|
|
363
|
+
// Without a digest the parent row can only list variant refs — a reviewer
|
|
364
|
+
// then has to expand the item to learn which dimensions it covers.
|
|
365
|
+
if (variantCount > 3 && !g.dimensions) {
|
|
366
|
+
findings.push({
|
|
367
|
+
gate: 'W', severity: 'warning', ref: g.id,
|
|
368
|
+
message: `group ${g.id} has ${variantCount} variants and no \`dimensions:\` digest — add a short one (e.g. "required ×3 · format ×9") so the collapsed view stays informative`,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
if (g.dimensions && g.dimensions.length > 120) {
|
|
372
|
+
findings.push({
|
|
373
|
+
gate: 'W', severity: 'warning', ref: g.id,
|
|
374
|
+
message: `group ${g.id} \`dimensions:\` is ${g.dimensions.length} chars — keep the digest short (≤120)`,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
for (const [field, text] of ([['intent', g.intent], ['oracle', g.oracle], ...(g.dimensions ? [['dimensions', g.dimensions] as const] : [])] as const)) {
|
|
378
|
+
for (const [re, what] of WORDING_SMELLS) {
|
|
379
|
+
if (re.test(text)) {
|
|
380
|
+
findings.push({
|
|
381
|
+
gate: 'W', severity: 'warning', ref: g.id,
|
|
382
|
+
message: `group ${g.id} ${field} contains ${what}`,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
274
390
|
// --- Gate G — review state -----------------------------------------------------
|
|
275
391
|
|
|
276
392
|
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;
|
|
@@ -76,6 +78,7 @@ export function loadDeliveryMap(file: string): MapLoadResult {
|
|
|
76
78
|
target: String(grp.target ?? ''),
|
|
77
79
|
intent: String(grp.intent ?? ''),
|
|
78
80
|
oracle: String(grp.oracle ?? ''),
|
|
81
|
+
...(grp.dimensions !== undefined ? { dimensions: String(grp.dimensions) } : {}),
|
|
79
82
|
category: grp.category as MapCategory,
|
|
80
83
|
review,
|
|
81
84
|
variants: Array.isArray(variants) ? variants.map(String) : [],
|
|
@@ -98,6 +101,21 @@ export function loadDeliveryMap(file: string): MapLoadResult {
|
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
|
|
104
|
+
const requirements: Record<string, RequirementOverride> = {};
|
|
105
|
+
if (doc.requirements !== undefined) {
|
|
106
|
+
if (!doc.requirements || typeof doc.requirements !== 'object' || Array.isArray(doc.requirements)) {
|
|
107
|
+
errors.push('`requirements` must be a mapping of requirement-id → { status, note }');
|
|
108
|
+
} else {
|
|
109
|
+
for (const [reqId, r] of Object.entries(doc.requirements as Record<string, unknown>)) {
|
|
110
|
+
const rr = (r && typeof r === 'object' ? r : {}) as Record<string, unknown>;
|
|
111
|
+
if (!REQUIREMENT_STATUSES.includes(String(rr.status) as RequirementStatus)) {
|
|
112
|
+
errors.push(`requirements.${reqId}: \`status\` must be one of ${REQUIREMENT_STATUSES.join(' | ')}`);
|
|
113
|
+
}
|
|
114
|
+
requirements[reqId] = { status: rr.status as RequirementStatus, note: rr.note ? String(rr.note) : undefined };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
101
119
|
const fingerprints: Record<string, string> = {};
|
|
102
120
|
if (doc.fingerprints && typeof doc.fingerprints === 'object' && !Array.isArray(doc.fingerprints)) {
|
|
103
121
|
for (const [k, v] of Object.entries(doc.fingerprints as Record<string, unknown>)) {
|
|
@@ -111,6 +129,7 @@ export function loadDeliveryMap(file: string): MapLoadResult {
|
|
|
111
129
|
formNo: doc.form_no !== undefined ? String(doc.form_no) : undefined,
|
|
112
130
|
groups,
|
|
113
131
|
dispositions,
|
|
132
|
+
requirements,
|
|
114
133
|
fingerprints,
|
|
115
134
|
};
|
|
116
135
|
return { map: errors.length > 0 ? null : map, errors };
|
|
@@ -130,11 +149,13 @@ export function writeDeliveryMap(file: string, map: DeliveryMap): void {
|
|
|
130
149
|
target: g.target,
|
|
131
150
|
intent: g.intent,
|
|
132
151
|
oracle: g.oracle,
|
|
152
|
+
...(g.dimensions !== undefined ? { dimensions: g.dimensions } : {}),
|
|
133
153
|
category: g.category,
|
|
134
154
|
review: g.review,
|
|
135
155
|
variants: g.variants,
|
|
136
156
|
})),
|
|
137
157
|
...(Object.keys(map.dispositions).length > 0 ? { dispositions: map.dispositions } : {}),
|
|
158
|
+
...(Object.keys(map.requirements).length > 0 ? { requirements: map.requirements } : {}),
|
|
138
159
|
...(Object.keys(map.fingerprints).length > 0 ? { fingerprints: map.fingerprints } : {}),
|
|
139
160
|
};
|
|
140
161
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -1,16 +1,18 @@
|
|
|
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 {
|
|
8
|
+
import { variantState } from './build';
|
|
9
|
+
import { coverageSummary, 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
|
|
13
|
-
'Mode', 'Trace', 'Result', 'Executed Date', 'Note',
|
|
14
|
+
'Precondition', 'Coverage / Test Data', 'Action / Trigger', 'Expected Result',
|
|
15
|
+
'Mode', 'Trace', 'Result', 'Executed Date', 'Note / Evidence', 'Defect ID',
|
|
14
16
|
];
|
|
15
17
|
|
|
16
18
|
function esc(value: string): string {
|
|
@@ -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,6 @@ 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 single = item.variants.length === 1;
|
|
30
|
-
const only = single ? item.variants[0] : undefined;
|
|
31
38
|
lines.push([
|
|
32
39
|
'item',
|
|
33
40
|
item.id,
|
|
@@ -36,35 +43,51 @@ export function renderMatrixCsv(model: MatrixModel): string {
|
|
|
36
43
|
item.category,
|
|
37
44
|
item.priority,
|
|
38
45
|
item.precondition.join('\n'),
|
|
39
|
-
|
|
46
|
+
coverageSummary(item),
|
|
47
|
+
// Shared leading steps only; each variant row carries its remaining steps.
|
|
40
48
|
item.trigger.join('\n'),
|
|
41
49
|
item.oracle,
|
|
42
|
-
item
|
|
50
|
+
itemModeLabel(item),
|
|
43
51
|
item.traces.join(' '),
|
|
44
|
-
item
|
|
45
|
-
|
|
52
|
+
itemResultLabel(item),
|
|
53
|
+
'',
|
|
46
54
|
item.review === 'proposed' ? 'REVIEW REQUIRED' : '',
|
|
55
|
+
'',
|
|
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
|
+
// Precondition DELTA — variant-specific setup only (review GAP-03).
|
|
69
|
+
(item.preconditionDeltas[v.ref] ?? []).join('\n'),
|
|
70
|
+
v.data.join('\n'),
|
|
71
|
+
// Steps after the parent's shared prefix only.
|
|
72
|
+
(item.triggerDeltas[v.ref] ?? []).join('\n'),
|
|
73
|
+
expected,
|
|
74
|
+
v.mode === 'manual' ? 'Manual' : 'Auto',
|
|
75
|
+
v.traces.join(' '),
|
|
76
|
+
variantState(v),
|
|
77
|
+
isoDate(v.result?.startTime),
|
|
78
|
+
v.result?.error ? String(v.result.error).slice(0, 200) : '',
|
|
79
|
+
'',
|
|
80
|
+
].map(esc).join(','));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Requirement coverage appendix — keeps the CSV self-contained for pipelines.
|
|
85
|
+
if (model.requirements.length > 0) {
|
|
86
|
+
lines.push('');
|
|
87
|
+
lines.push('# Requirement coverage');
|
|
88
|
+
lines.push(['Requirement', 'Status', 'Delivery items', 'Variants', 'Note'].join(','));
|
|
89
|
+
for (const r of model.requirements) {
|
|
90
|
+
lines.push([r.id, r.status, r.items.join(' '), String(r.variantCount), r.note].map(esc).join(','));
|
|
68
91
|
}
|
|
69
92
|
}
|
|
70
93
|
return BOM + lines.join('\n') + '\n';
|