@codyswann/lisa 2.346.0 → 2.348.0
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/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +36 -1
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/copy-overwrite/scripts/bdd/baseline.mjs +224 -0
- package/expo/copy-overwrite/scripts/bdd/contract.mjs +121 -0
- package/expo/copy-overwrite/scripts/bdd/envelope.mjs +231 -0
- package/expo/copy-overwrite/scripts/bdd/parse.mjs +220 -0
- package/expo/copy-overwrite/scripts/bdd/render.mjs +170 -0
- package/expo/copy-overwrite/scripts/bdd/report.mjs +361 -0
- package/expo/copy-overwrite/scripts/bdd/validate.mjs +282 -0
- package/expo/copy-overwrite/scripts/bdd/waivers.mjs +210 -0
- package/expo/copy-overwrite/scripts/bdd-matrix.mjs +278 -0
- package/expo/copy-overwrite/scripts/check-bdd-coverage.mjs +657 -0
- package/expo/create-only/bdd/coverage-map.json +38 -0
- package/expo/create-only/bdd/features/.keep +5 -0
- package/expo/github-rulesets/bdd-coverage.json +33 -0
- package/expo/package-lisa/package.lisa.json +3 -0
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waiver validation.
|
|
3
|
+
*
|
|
4
|
+
* A waiver is a dated IOU, never coverage. It leaves the denominator, so the
|
|
5
|
+
* bookkeeping it owes is strict: who owns it, why, which ticket retires it,
|
|
6
|
+
* and when it expires. An IOU nobody owns and that never comes due is just a
|
|
7
|
+
* quieter coverage gap.
|
|
8
|
+
*
|
|
9
|
+
* @module scripts/bdd/waivers
|
|
10
|
+
*/
|
|
11
|
+
import { parseTrackerTag, runnersByPlatform } from "./contract.mjs";
|
|
12
|
+
|
|
13
|
+
const defect = (code, message) => ({ code, message });
|
|
14
|
+
|
|
15
|
+
/** Bookkeeping every waiver owes whoever has to re-litigate it later. */
|
|
16
|
+
const REQUIRED_FIELDS = [
|
|
17
|
+
"reason",
|
|
18
|
+
"owner",
|
|
19
|
+
"ticket",
|
|
20
|
+
"recordedAt",
|
|
21
|
+
"expiresAt",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/** ISO calendar date, the only accepted date shape. */
|
|
25
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validate every waiver in the coverage map.
|
|
29
|
+
* @param {object} input - Scenarios, contract, and the evaluation date.
|
|
30
|
+
* @returns {object[]} Defects found.
|
|
31
|
+
*/
|
|
32
|
+
export function validateWaivers({ scenarios, contract, today }) {
|
|
33
|
+
const byId = new Map(scenarios.map(scenario => [scenario.id, scenario]));
|
|
34
|
+
const platformRunners = runnersByPlatform(contract.runnerPlatforms);
|
|
35
|
+
const mapped = mappedKeys(contract);
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
const defects = [];
|
|
38
|
+
for (const [index, waiver] of (contract.platformWaivers ?? []).entries()) {
|
|
39
|
+
const at = `coverage-map.platformWaivers[${index}] ${waiver.scenario ?? "(no scenario)"}`;
|
|
40
|
+
const scenario = byId.get(waiver.scenario);
|
|
41
|
+
const blocking = blockingError(waiver, scenario, at);
|
|
42
|
+
if (blocking) {
|
|
43
|
+
defects.push(blocking);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
defects.push(...metadataDefects(waiver, at, today));
|
|
47
|
+
defects.push(...runnerDefects(waiver, platformRunners, at));
|
|
48
|
+
defects.push(...platformDefects(waiver, scenario, { mapped, seen }, at));
|
|
49
|
+
}
|
|
50
|
+
return defects;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Every `SCENARIO:platform` a mapping already claims.
|
|
55
|
+
* @param {object} contract - Parsed coverage map.
|
|
56
|
+
* @returns {Set<string>} Claimed keys.
|
|
57
|
+
*/
|
|
58
|
+
function mappedKeys(contract) {
|
|
59
|
+
return new Set(
|
|
60
|
+
(contract.mappings ?? []).flatMap(mapping =>
|
|
61
|
+
(mapping.platforms ?? []).map(
|
|
62
|
+
platform => `${mapping.scenario}:${platform}`
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The one problem that makes a waiver unusable, so its other checks are moot.
|
|
70
|
+
* @param {object} waiver - Raw waiver entry.
|
|
71
|
+
* @param {object|undefined} scenario - The scenario it names, if it exists.
|
|
72
|
+
* @param {string} at - Location label.
|
|
73
|
+
* @returns {object|undefined} The blocking defect, or undefined.
|
|
74
|
+
*/
|
|
75
|
+
function blockingError(waiver, scenario, at) {
|
|
76
|
+
if (!scenario) {
|
|
77
|
+
return defect(
|
|
78
|
+
"waiver-orphan",
|
|
79
|
+
`${at}: names a scenario that does not exist`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (!scenario.required) {
|
|
83
|
+
return defect(
|
|
84
|
+
"waiver-excluded",
|
|
85
|
+
`${at}: scenario is already out of the denominator (${scenario.lifecycle.join(", ")})`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (!Array.isArray(waiver.platforms) || waiver.platforms.length === 0) {
|
|
89
|
+
return defect("waiver-platform", `${at}: claims no platforms`);
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Require the full owner/reason/ticket/expiry record, and refuse an expired
|
|
96
|
+
* waiver — the time-box is what stops an IOU from becoming permanent.
|
|
97
|
+
* @param {object} waiver - Raw waiver entry.
|
|
98
|
+
* @param {string} at - Location label.
|
|
99
|
+
* @param {string} today - ISO date the run is evaluated against.
|
|
100
|
+
* @returns {object[]} Defects found.
|
|
101
|
+
*/
|
|
102
|
+
function metadataDefects(waiver, at, today) {
|
|
103
|
+
const defects = REQUIRED_FIELDS.filter(field => !waiver[field]).map(field =>
|
|
104
|
+
defect("waiver-metadata", `${at}: has no ${field}`)
|
|
105
|
+
);
|
|
106
|
+
for (const field of ["recordedAt", "expiresAt"]) {
|
|
107
|
+
if (waiver[field] && !ISO_DATE.test(waiver[field])) {
|
|
108
|
+
defects.push(
|
|
109
|
+
defect(
|
|
110
|
+
"waiver-metadata",
|
|
111
|
+
`${at}: ${field} must be an ISO date (YYYY-MM-DD)`
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (
|
|
117
|
+
waiver.ticket &&
|
|
118
|
+
!parseTrackerTag(String(waiver.ticket).replace(/^@/, ""))
|
|
119
|
+
) {
|
|
120
|
+
defects.push(
|
|
121
|
+
defect(
|
|
122
|
+
"waiver-metadata",
|
|
123
|
+
`${at}: ticket ${waiver.ticket} is not a valid tracker reference`
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (
|
|
128
|
+
waiver.expiresAt &&
|
|
129
|
+
ISO_DATE.test(waiver.expiresAt) &&
|
|
130
|
+
waiver.expiresAt < today
|
|
131
|
+
) {
|
|
132
|
+
defects.push(
|
|
133
|
+
defect(
|
|
134
|
+
"waiver-expired",
|
|
135
|
+
`${at}: expired on ${waiver.expiresAt}; retire it or re-authorize it`
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return defects;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A waiver says a RUNNER cannot decide a behavior, so when a platform has
|
|
144
|
+
* more than one configured runner the waiver must name which one.
|
|
145
|
+
* @param {object} waiver - Raw waiver entry.
|
|
146
|
+
* @param {Map<string, string[]>} platformRunners - Platform → configured runners.
|
|
147
|
+
* @param {string} at - Location label.
|
|
148
|
+
* @returns {object[]} Defects found.
|
|
149
|
+
*/
|
|
150
|
+
function runnerDefects(waiver, platformRunners, at) {
|
|
151
|
+
const ambiguous = waiver.platforms.filter(
|
|
152
|
+
platform => (platformRunners.get(platform) ?? []).length > 1
|
|
153
|
+
);
|
|
154
|
+
if (!waiver.runner) {
|
|
155
|
+
return ambiguous.length === 0
|
|
156
|
+
? []
|
|
157
|
+
: [
|
|
158
|
+
defect(
|
|
159
|
+
"waiver-runner",
|
|
160
|
+
`${at}: ${ambiguous.join(", ")} has multiple configured runners, so the waiver must name one`
|
|
161
|
+
),
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
return waiver.platforms
|
|
165
|
+
.filter(
|
|
166
|
+
platform => !(platformRunners.get(platform) ?? []).includes(waiver.runner)
|
|
167
|
+
)
|
|
168
|
+
.map(platform =>
|
|
169
|
+
defect(
|
|
170
|
+
"waiver-runner",
|
|
171
|
+
`${at}: runner ${waiver.runner} does not cover ${platform}`
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Per-platform problems on one usable waiver, recording accepted keys so the
|
|
178
|
+
* duplicate check works across waivers.
|
|
179
|
+
* @param {object} waiver - Raw waiver entry.
|
|
180
|
+
* @param {object} scenario - The scenario it names.
|
|
181
|
+
* @param {{mapped: Set<string>, seen: Set<string>}} keys - Claimed and already-seen keys.
|
|
182
|
+
* @param {string} at - Location label.
|
|
183
|
+
* @returns {object[]} Defects found.
|
|
184
|
+
*/
|
|
185
|
+
function platformDefects(waiver, scenario, { mapped, seen }, at) {
|
|
186
|
+
const defects = [];
|
|
187
|
+
for (const platform of waiver.platforms) {
|
|
188
|
+
const key = `${waiver.scenario}:${platform}`;
|
|
189
|
+
if (!scenario.platforms.includes(platform)) {
|
|
190
|
+
defects.push(
|
|
191
|
+
defect(
|
|
192
|
+
"waiver-platform",
|
|
193
|
+
`${at}: claims undeclared platform ${platform}`
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (mapped.has(key)) {
|
|
199
|
+
defects.push(
|
|
200
|
+
defect("waiver-masks-mapping", `${at}: ${key} already has a mapping`)
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (seen.has(key))
|
|
204
|
+
defects.push(
|
|
205
|
+
defect("waiver-duplicate", `${at}: duplicate waiver for ${key}`)
|
|
206
|
+
);
|
|
207
|
+
seen.add(key);
|
|
208
|
+
}
|
|
209
|
+
return defects;
|
|
210
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bdd-matrix — the per-scenario traceability matrix.
|
|
4
|
+
*
|
|
5
|
+
* The human-readable companion to `check-bdd-coverage.mjs`. It reuses that
|
|
6
|
+
* gate's parser and the same `bdd/coverage-map.json`, so the two can never
|
|
7
|
+
* disagree about what is covered.
|
|
8
|
+
*
|
|
9
|
+
* Every row separates the five facts the gate keeps apart: the behavior
|
|
10
|
+
* DECLARED, where it came from, whether automation is MAPPED per runner
|
|
11
|
+
* (traceability), whether that automation RAN, and what it returned. A `✓` in
|
|
12
|
+
* the mapped column asserts an aligned test exists and still contains its
|
|
13
|
+
* evidence string — never that it passed.
|
|
14
|
+
*
|
|
15
|
+
* Usage: node scripts/bdd-matrix.mjs [--write] [--results <file>]
|
|
16
|
+
*
|
|
17
|
+
* @module scripts/bdd-matrix
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from "node:fs";
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
22
|
+
|
|
23
|
+
import { declaredPlatforms, trackerUrl } from "./bdd/contract.mjs";
|
|
24
|
+
import { loadScenarios } from "./bdd/parse.mjs";
|
|
25
|
+
import { loadExecutionResults } from "./check-bdd-coverage.mjs";
|
|
26
|
+
|
|
27
|
+
const PACKAGE_ROOT = path.resolve(
|
|
28
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
29
|
+
".."
|
|
30
|
+
);
|
|
31
|
+
const OUT_REL = path.join("docs", "bdd-scenario-matrix.md");
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Index mappings by scenario and runner.
|
|
35
|
+
* @param {object} contract - Parsed coverage map.
|
|
36
|
+
* @returns {Map<string, object[]>} Scenario ID → its mappings.
|
|
37
|
+
*/
|
|
38
|
+
function mappingsByScenario(contract) {
|
|
39
|
+
const index = new Map();
|
|
40
|
+
for (const mapping of contract.mappings ?? []) {
|
|
41
|
+
index.set(mapping.scenario, [
|
|
42
|
+
...(index.get(mapping.scenario) ?? []),
|
|
43
|
+
mapping,
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
return index;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Index execution results by runner, file, and evidence.
|
|
51
|
+
* @param {readonly object[]} runs - Parsed execution-result documents.
|
|
52
|
+
* @returns {Map<string, object>} Lookup keyed by `runner|file|evidence`.
|
|
53
|
+
*/
|
|
54
|
+
function resultsIndex(runs) {
|
|
55
|
+
const index = new Map();
|
|
56
|
+
for (const run of runs) {
|
|
57
|
+
for (const result of run.results ?? []) {
|
|
58
|
+
index.set(`${run.runner}|${result.file}|${result.evidence}`, result);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return index;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Every `SCENARIO:platform` a waiver removes from the denominator.
|
|
66
|
+
*
|
|
67
|
+
* The matrix must know about waivers or it contradicts the burndown: a
|
|
68
|
+
* waived obligation is not an uncovered one, it is a dated IOU.
|
|
69
|
+
* @param {object} contract - Parsed coverage map.
|
|
70
|
+
* @returns {Set<string>} Waived keys.
|
|
71
|
+
*/
|
|
72
|
+
function waivedKeys(contract) {
|
|
73
|
+
return new Set(
|
|
74
|
+
(contract.platformWaivers ?? []).flatMap(waiver =>
|
|
75
|
+
(waiver.platforms ?? []).map(platform => `${waiver.scenario}:${platform}`)
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Render one scenario's mapped-automation cell for one runner.
|
|
82
|
+
* @param {readonly string[]} required - Platforms the scenario demands this runner cover.
|
|
83
|
+
* @param {readonly object[]} mappings - Mappings for this scenario and runner.
|
|
84
|
+
* @param {(platform: string) => boolean} isWaived - Whether that platform is waived.
|
|
85
|
+
* @returns {string} Cell text.
|
|
86
|
+
*/
|
|
87
|
+
function mappedCell(required, mappings, isWaived) {
|
|
88
|
+
if (required.length === 0) return "n/a";
|
|
89
|
+
const covered = new Set(mappings.flatMap(mapping => mapping.platforms ?? []));
|
|
90
|
+
const hit = required.filter(platform => covered.has(platform));
|
|
91
|
+
const waived = required.filter(
|
|
92
|
+
platform => !covered.has(platform) && isWaived(platform)
|
|
93
|
+
);
|
|
94
|
+
const parts = [];
|
|
95
|
+
if (hit.length > 0) parts.push(`✓ ${hit.join("+")}`);
|
|
96
|
+
if (waived.length > 0) parts.push(`waived ${waived.join("+")}`);
|
|
97
|
+
const accounted = hit.length + waived.length;
|
|
98
|
+
if (accounted === 0) return "✗ none";
|
|
99
|
+
if (accounted < required.length) parts.push("✗ rest");
|
|
100
|
+
return parts.join(", ");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Render one scenario's execution cell.
|
|
105
|
+
* @param {readonly object[]} mappings - Mappings for this scenario.
|
|
106
|
+
* @param {Map<string, object>} results - Execution results index.
|
|
107
|
+
* @param {boolean} supplied - Whether any run evidence was supplied.
|
|
108
|
+
* @returns {string} Cell text.
|
|
109
|
+
*/
|
|
110
|
+
function executionCell(mappings, results, supplied) {
|
|
111
|
+
if (!supplied) return "not supplied";
|
|
112
|
+
if (mappings.length === 0) return "—";
|
|
113
|
+
const statuses = mappings.map(
|
|
114
|
+
mapping =>
|
|
115
|
+
results.get(`${mapping.runner}|${mapping.file}|${mapping.evidence}`)
|
|
116
|
+
?.status ?? "not run"
|
|
117
|
+
);
|
|
118
|
+
const tally = counts =>
|
|
119
|
+
Object.entries(counts)
|
|
120
|
+
.map(([key, value]) => `${value} ${key}`)
|
|
121
|
+
.join(", ");
|
|
122
|
+
const counted = {};
|
|
123
|
+
for (const status of statuses) counted[status] = (counted[status] ?? 0) + 1;
|
|
124
|
+
return tally(counted);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Render a scenario's provenance and tracker references.
|
|
129
|
+
* @param {object} scenario - Parsed scenario.
|
|
130
|
+
* @param {object} trackers - The `trackers` block of the coverage map.
|
|
131
|
+
* @returns {string} Cell text.
|
|
132
|
+
*/
|
|
133
|
+
function sourceCell(scenario, trackers) {
|
|
134
|
+
const provenance = scenario.provenance.map(tag => `\`${tag}\``);
|
|
135
|
+
const tickets = scenario.trackers.map(reference => {
|
|
136
|
+
const url = trackerUrl(reference, trackers);
|
|
137
|
+
return url ? `[${reference.tag}](${url})` : reference.tag;
|
|
138
|
+
});
|
|
139
|
+
return [...provenance, ...tickets].join("<br>") || "—";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Render the full matrix document.
|
|
144
|
+
* @param {string} root - Repo root.
|
|
145
|
+
* @param {readonly string[]} resultFiles - Execution result documents to join.
|
|
146
|
+
* @returns {string} Markdown document.
|
|
147
|
+
*/
|
|
148
|
+
export function renderMatrix(root, resultFiles) {
|
|
149
|
+
const contract = JSON.parse(
|
|
150
|
+
fs.readFileSync(path.join(root, "bdd", "coverage-map.json"), "utf8")
|
|
151
|
+
);
|
|
152
|
+
const platforms = declaredPlatforms(contract.runnerPlatforms);
|
|
153
|
+
const scenarios = loadScenarios(root, platforms);
|
|
154
|
+
const execution = loadExecutionResults(root, resultFiles);
|
|
155
|
+
const results = resultsIndex(execution.runs);
|
|
156
|
+
const byScenario = mappingsByScenario(contract);
|
|
157
|
+
const waived = waivedKeys(contract);
|
|
158
|
+
const runners = Object.keys(contract.runnerPlatforms ?? {}).sort();
|
|
159
|
+
const byFeature = new Map();
|
|
160
|
+
for (const scenario of scenarios) {
|
|
161
|
+
byFeature.set(scenario.feature, [
|
|
162
|
+
...(byFeature.get(scenario.feature) ?? []),
|
|
163
|
+
scenario,
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
166
|
+
const header = [
|
|
167
|
+
"| ID | Behavior | Source | Lifecycle",
|
|
168
|
+
...runners.map(runner => ` | Mapped (${runner})`),
|
|
169
|
+
" | Executed |",
|
|
170
|
+
].join("");
|
|
171
|
+
const divider = `|---|---|---|---|${runners.map(() => "---|").join("")}---|`;
|
|
172
|
+
const sections = [...byFeature.keys()]
|
|
173
|
+
.sort((a, b) => a.localeCompare(b))
|
|
174
|
+
.map(feature =>
|
|
175
|
+
renderFeature({
|
|
176
|
+
feature,
|
|
177
|
+
scenarios: byFeature.get(feature),
|
|
178
|
+
byScenario,
|
|
179
|
+
contract,
|
|
180
|
+
runners,
|
|
181
|
+
results,
|
|
182
|
+
waived,
|
|
183
|
+
supplied: execution.runs.length > 0,
|
|
184
|
+
header,
|
|
185
|
+
divider,
|
|
186
|
+
})
|
|
187
|
+
)
|
|
188
|
+
.join("\n");
|
|
189
|
+
return preamble(scenarios, execution.runs.length > 0) + sections;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Render one feature's table.
|
|
194
|
+
* @param {object} input - Feature, its scenarios, and the shared indexes.
|
|
195
|
+
* @returns {string} Markdown section.
|
|
196
|
+
*/
|
|
197
|
+
function renderFeature({
|
|
198
|
+
feature,
|
|
199
|
+
scenarios,
|
|
200
|
+
byScenario,
|
|
201
|
+
contract,
|
|
202
|
+
runners,
|
|
203
|
+
results,
|
|
204
|
+
waived,
|
|
205
|
+
supplied,
|
|
206
|
+
header,
|
|
207
|
+
divider,
|
|
208
|
+
}) {
|
|
209
|
+
const rows = scenarios
|
|
210
|
+
.map(scenario => {
|
|
211
|
+
const mappings = byScenario.get(scenario.id) ?? [];
|
|
212
|
+
const cells = runners.map(runner => {
|
|
213
|
+
const covers = new Set(contract.runnerPlatforms[runner] ?? []);
|
|
214
|
+
const required = scenario.platforms.filter(platform =>
|
|
215
|
+
covers.has(platform)
|
|
216
|
+
);
|
|
217
|
+
return mappedCell(
|
|
218
|
+
required,
|
|
219
|
+
mappings.filter(mapping => mapping.runner === runner),
|
|
220
|
+
platform => waived.has(`${scenario.id}:${platform}`)
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
const lifecycle =
|
|
224
|
+
scenario.lifecycle.length > 0
|
|
225
|
+
? scenario.lifecycle.join(", ")
|
|
226
|
+
: "required";
|
|
227
|
+
return `| \`${scenario.id ?? "(no id)"}\` | ${scenario.name} | ${sourceCell(scenario, contract.trackers)} | ${lifecycle} | ${cells.join(" | ")} | ${executionCell(mappings, results, supplied)} |`;
|
|
228
|
+
})
|
|
229
|
+
.join("\n");
|
|
230
|
+
return `### ${feature}\n\n${header}\n${divider}\n${rows}\n`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The explanatory preamble.
|
|
235
|
+
* @param {readonly object[]} scenarios - Parsed scenarios.
|
|
236
|
+
* @param {boolean} supplied - Whether run evidence was supplied.
|
|
237
|
+
* @returns {string} Markdown preamble.
|
|
238
|
+
*/
|
|
239
|
+
function preamble(scenarios, supplied) {
|
|
240
|
+
const required = scenarios.filter(scenario => scenario.required).length;
|
|
241
|
+
return `# BDD scenario traceability matrix
|
|
242
|
+
|
|
243
|
+
Every declared behavior, where it came from, and what actually proves it.
|
|
244
|
+
Generated by \`node scripts/bdd-matrix.mjs --write\`; never hand-edited.
|
|
245
|
+
|
|
246
|
+
- **Mapped** — aligned automation exists in that runner for the platforms the scenario requires, and its evidence string still resolves. This is **traceability**, not a result: a mapped test that fails still shows \`✓\`.
|
|
247
|
+
- **Executed** — what the supplied run evidence returned for those mapped tests. ${supplied ? "Run evidence was supplied for this render." : "**No run evidence was supplied for this render**, so this column reads `not supplied` throughout — nothing here asserts any test ran."}
|
|
248
|
+
- **Lifecycle** — \`required\` counts toward coverage; \`blocked\`, \`reference-only\`, and \`superseded\` are excluded until that changes.
|
|
249
|
+
|
|
250
|
+
${scenarios.length} scenarios declared, ${required} required.
|
|
251
|
+
|
|
252
|
+
`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* CLI entry point.
|
|
257
|
+
* @returns {void}
|
|
258
|
+
*/
|
|
259
|
+
function main() {
|
|
260
|
+
const root = process.env.BDD_COVERAGE_ROOT || PACKAGE_ROOT;
|
|
261
|
+
const argv = process.argv.slice(2);
|
|
262
|
+
const resultFiles = [];
|
|
263
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
264
|
+
if (argv[index] === "--results" && argv[index + 1])
|
|
265
|
+
resultFiles.push(argv[index + 1]);
|
|
266
|
+
}
|
|
267
|
+
const body = renderMatrix(root, resultFiles);
|
|
268
|
+
if (!argv.includes("--write")) {
|
|
269
|
+
console.log(body);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
fs.mkdirSync(path.join(root, "docs"), { recursive: true });
|
|
273
|
+
fs.writeFileSync(path.join(root, OUT_REL), body);
|
|
274
|
+
console.log(`[bdd-matrix] wrote ${OUT_REL}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
278
|
+
main();
|