@codyswann/lisa 4.74.0 → 4.74.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all/copy-overwrite/scripts/lib/npm-update-contract.mjs +2 -1
- package/all/copy-overwrite/scripts/lib/npm-update-gate-hooks.mjs +2 -1
- package/all/copy-overwrite/scripts/lib/npm-update-gh-grants.mjs +2 -1
- package/all/copy-overwrite/scripts/lib/npm-update-gh-requests.mjs +2 -1
- package/all/copy-overwrite/scripts/lib/npm-update-orchestrator.mjs +2 -1
- package/all/copy-overwrite/scripts/lib/npm-update-process-core.mjs +2 -1
- package/all/copy-overwrite/scripts/lib/npm-update-worker-inspection.mjs +2 -1
- package/all/copy-overwrite/scripts/lib/npm-update-worker-lifecycle.mjs +2 -1
- package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +18 -19
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +66 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +45 -13
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/copy-overwrite/scripts/bdd/ci-summary.mjs +37 -0
- package/expo/copy-overwrite/scripts/bdd/projection-model.mjs +157 -0
- package/expo/copy-overwrite/scripts/bdd/projection-ownership.mjs +300 -0
- package/expo/copy-overwrite/scripts/bdd/projection-paths.mjs +152 -0
- package/expo/copy-overwrite/scripts/bdd/projection-render.mjs +303 -0
- package/expo/copy-overwrite/scripts/bdd/projection-write.mjs +84 -0
- package/expo/copy-overwrite/scripts/bdd/projection.mjs +30 -0
- package/expo/copy-overwrite/scripts/bdd-matrix.mjs +54 -18
- package/expo/copy-overwrite/scripts/check-bdd-coverage.mjs +36 -27
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/rules/reference/bdd-e2e-coverage.md +9 -7
- 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-copilot/rules/reference/bdd-e2e-coverage.md +9 -7
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/rules/bdd-e2e-coverage-reference.mdc +9 -7
- 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
- package/plugins/src/base/rules/reference/bdd-e2e-coverage.md +9 -7
- package/scripts/two-channel-couplings.json +1 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
/** Static entrypoints and deterministic feature-local document rendering. */
|
|
5
|
+
import { byCodeUnit } from "./contract.mjs";
|
|
6
|
+
import { cell } from "./markdown-cell.mjs";
|
|
7
|
+
import {
|
|
8
|
+
featureArtifactPaths,
|
|
9
|
+
FEATURE_PROJECTION_SCHEMA,
|
|
10
|
+
} from "./projection-paths.mjs";
|
|
11
|
+
|
|
12
|
+
export const PROJECTION_INDEX_SCHEMA = "lisa-bdd-projection-index-v1";
|
|
13
|
+
export const MATRIX_COMMAND =
|
|
14
|
+
'BDD_BASE_SHA="$(git merge-base HEAD origin/HEAD)" bun run bdd:matrix';
|
|
15
|
+
export const COVERAGE_COMMAND =
|
|
16
|
+
'BDD_BASE_SHA="$(git merge-base HEAD origin/HEAD)" bun run bdd:coverage:write';
|
|
17
|
+
export const REPORT_COMMAND =
|
|
18
|
+
'BDD_BASE_SHA="$(git merge-base HEAD origin/HEAD)" node scripts/check-bdd-coverage.mjs --report';
|
|
19
|
+
export const AGGREGATE_MATRIX_COMMAND = "node scripts/bdd-matrix.mjs";
|
|
20
|
+
const COMMAND_CONTEXT =
|
|
21
|
+
"The node scripts/... examples use installed host paths. In the Lisa monorepo, use BDD_COVERAGE_ROOT=. and the expo/copy-overwrite/scripts/ prefix instead; the coverage report still requires BDD_BASE_SHA.";
|
|
22
|
+
|
|
23
|
+
/** Sort object properties recursively; retain meaningful ordered arrays. */
|
|
24
|
+
function stableValue(value) {
|
|
25
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
26
|
+
if (value === null || typeof value !== "object") return value;
|
|
27
|
+
return Object.fromEntries(
|
|
28
|
+
Object.keys(value)
|
|
29
|
+
.sort(byCodeUnit)
|
|
30
|
+
.map(key => [key, stableValue(value[key])])
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Complete records remain machine-readable without generation timestamps. */
|
|
35
|
+
export const projectionJson = value =>
|
|
36
|
+
`${JSON.stringify(stableValue(value), null, 2)}\n`;
|
|
37
|
+
|
|
38
|
+
/** Keep table data inert, including HTML, Markdown links and code delimiters. */
|
|
39
|
+
export const projectionCell = value =>
|
|
40
|
+
cell(
|
|
41
|
+
String(value ?? "")
|
|
42
|
+
.replace(/&/g, "&")
|
|
43
|
+
.replace(/</g, "<")
|
|
44
|
+
.replace(/>/g, ">")
|
|
45
|
+
.replace(/`/g, "`")
|
|
46
|
+
.replace(/\[/g, "[")
|
|
47
|
+
.replace(/\]/g, "]")
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
/** A bounded, encoded ownership marker cannot be terminated by source text. */
|
|
51
|
+
export const projectionMarker = (sourceFile, kind) =>
|
|
52
|
+
`<!-- ${FEATURE_PROJECTION_SCHEMA} ${kind} source=${encodeURIComponent(sourceFile)} -->`;
|
|
53
|
+
|
|
54
|
+
export const STATIC_ARTIFACTS = new Map([
|
|
55
|
+
[
|
|
56
|
+
"bdd/coverage-report.json",
|
|
57
|
+
projectionJson({
|
|
58
|
+
schemaVersion: PROJECTION_INDEX_SCHEMA,
|
|
59
|
+
generator: "lisa-bdd-feature-projection",
|
|
60
|
+
featureReports: "bdd/reports/v1/features/",
|
|
61
|
+
scenarioMatrices: "docs/bdd-scenario-matrix/",
|
|
62
|
+
coverageDocuments: "docs/e2e-bdd-coverage/",
|
|
63
|
+
sourceIdentity: "Full repository-relative bdd/features/*.feature path",
|
|
64
|
+
commandContext: COMMAND_CONTEXT,
|
|
65
|
+
commands: { matrix: MATRIX_COMMAND, coverage: COVERAGE_COMMAND },
|
|
66
|
+
aggregateCommands: {
|
|
67
|
+
matrix: AGGREGATE_MATRIX_COMMAND,
|
|
68
|
+
coverage: REPORT_COMMAND,
|
|
69
|
+
},
|
|
70
|
+
aggregate:
|
|
71
|
+
"The aggregate matrix displays declared mappings. The --report runtime report and feature leaves validate evidence resolution; CI publishes the global summary.",
|
|
72
|
+
}),
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
"docs/bdd-scenario-matrix.md",
|
|
76
|
+
`# BDD scenario traceability matrix
|
|
77
|
+
|
|
78
|
+
Generated by \`${MATRIX_COMMAND}\`; never hand-edited.
|
|
79
|
+
|
|
80
|
+
Feature documents live under [bdd-scenario-matrix/](bdd-scenario-matrix/), using the complete repository-relative source path as their suffix. Their matching JSON records live under [../bdd/reports/v1/features/](../bdd/reports/v1/features/).
|
|
81
|
+
|
|
82
|
+
Each document preserves its declarations, tracker references, mapped evidence and supplied execution outcomes. Traceability proves automation exists; it never proves that automation ran or passed. Waivers are never coverage.
|
|
83
|
+
|
|
84
|
+
${COMMAND_CONTEXT}
|
|
85
|
+
|
|
86
|
+
Run \`${AGGREGATE_MATRIX_COMMAND}\` for the current aggregate matrix. Its mapping cells display declared, configured mappings; they do not validate that each evidence title still exists. Evidence-resolution credit comes from the feature leaves and \`${REPORT_COMMAND}\`. CI publishes current global counts and the full runtime coverage summary. This entrypoint intentionally stores no feature list or global counts.
|
|
87
|
+
`,
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
"docs/e2e-bdd-coverage.md",
|
|
91
|
+
`# BDD behavior contract — coverage burndown
|
|
92
|
+
|
|
93
|
+
Generated from \`bdd/features/*.feature\` and \`bdd/coverage-map.json\`.
|
|
94
|
+
Regenerated by \`${COVERAGE_COMMAND}\`; never hand-edited.
|
|
95
|
+
|
|
96
|
+
## What each number means
|
|
97
|
+
|
|
98
|
+
Traceability coverage reports aligned mappings with evidence still present. **This is not execution coverage and not a pass rate.** Supplied execution outcomes remain separate. A waiver is an owned, dated IOU outside the obligation denominator; it is never coverage.
|
|
99
|
+
|
|
100
|
+
Feature documents live under [e2e-bdd-coverage/](e2e-bdd-coverage/) with the complete source path as their suffix. Complete local records live under [../bdd/reports/v1/features/](../bdd/reports/v1/features/).
|
|
101
|
+
|
|
102
|
+
${COMMAND_CONTEXT}
|
|
103
|
+
|
|
104
|
+
Run \`${REPORT_COMMAND}\` for the current full runtime report. \`node scripts/check-bdd-coverage.mjs --report\` also emits diagnostic JSON, but can exit nonzero when a required baseline is absent or validation finds defects. CI publishes global counts, discovered-test inventory, floor evaluations and defects. Unassociated retirement records remain in \`bdd/coverage-map.json\`. This entrypoint intentionally stores no feature list or global counts.
|
|
105
|
+
`,
|
|
106
|
+
],
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
/** Fixed headers keep author text exclusively inside escaped cells. */
|
|
110
|
+
function table(headers, rows) {
|
|
111
|
+
const body = rows.length
|
|
112
|
+
? rows.map(row => `| ${row.map(projectionCell).join(" | ")} |`).join("\n")
|
|
113
|
+
: `| ${headers.map(() => "—").join(" | ")} |`;
|
|
114
|
+
return `| ${headers.join(" | ")} |\n|${headers.map(() => "---").join("|")}|\n${body}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Preserve every mapped string and its genuine evidence verdict. */
|
|
118
|
+
function mappingTable(scenarios) {
|
|
119
|
+
return table(
|
|
120
|
+
[
|
|
121
|
+
"Scenario",
|
|
122
|
+
"Runner",
|
|
123
|
+
"Platforms",
|
|
124
|
+
"File",
|
|
125
|
+
"Evidence",
|
|
126
|
+
"Evidence resolution",
|
|
127
|
+
"Execution",
|
|
128
|
+
"Run",
|
|
129
|
+
],
|
|
130
|
+
scenarios.flatMap(row =>
|
|
131
|
+
row.mappings.map(mapping => [
|
|
132
|
+
row.id,
|
|
133
|
+
mapping.runner,
|
|
134
|
+
mapping.platforms.join(", "),
|
|
135
|
+
mapping.file,
|
|
136
|
+
mapping.evidence,
|
|
137
|
+
mapping.evidenceResolution.ok
|
|
138
|
+
? "resolved"
|
|
139
|
+
: `invalid: ${mapping.evidenceResolution.detail}`,
|
|
140
|
+
mapping.execution.status,
|
|
141
|
+
mapping.execution.runId,
|
|
142
|
+
])
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Declaration facts include ordered primary steps and complete tag scopes. */
|
|
148
|
+
function declarationTables(scenarios) {
|
|
149
|
+
return table(
|
|
150
|
+
[
|
|
151
|
+
"Scenario",
|
|
152
|
+
"Behavior",
|
|
153
|
+
"Feature",
|
|
154
|
+
"Source line",
|
|
155
|
+
"Platforms",
|
|
156
|
+
"Lifecycle",
|
|
157
|
+
"Tags",
|
|
158
|
+
"Steps",
|
|
159
|
+
],
|
|
160
|
+
scenarios.map(row => [
|
|
161
|
+
row.id,
|
|
162
|
+
row.name,
|
|
163
|
+
row.feature,
|
|
164
|
+
row.line,
|
|
165
|
+
row.platforms.join(", "),
|
|
166
|
+
row.required ? "required" : row.lifecycle.join(", "),
|
|
167
|
+
row.tags.join(", "),
|
|
168
|
+
row.primarySteps.join("; "),
|
|
169
|
+
])
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Human-readable references remain separate from executable evidence. */
|
|
174
|
+
function trackerTable(scenarios) {
|
|
175
|
+
return table(
|
|
176
|
+
["Scenario", "Reference", "URL"],
|
|
177
|
+
scenarios.flatMap(row =>
|
|
178
|
+
row.trackers.map(reference => [
|
|
179
|
+
row.id,
|
|
180
|
+
reference.tag,
|
|
181
|
+
reference.url ?? "No URL template declared",
|
|
182
|
+
])
|
|
183
|
+
)
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Waiver compatibility retains the original seven-column contract. */
|
|
188
|
+
function waiverSection(scenarios) {
|
|
189
|
+
const entries = scenarios.flatMap(row => row.waivers);
|
|
190
|
+
const ledger = table(
|
|
191
|
+
["Scenario", "Platforms", "Runner", "Owner", "Reason", "Ticket", "Expires"],
|
|
192
|
+
entries.map(entry => [
|
|
193
|
+
entry.scenario,
|
|
194
|
+
Array.isArray(entry.platforms)
|
|
195
|
+
? entry.platforms.join(", ")
|
|
196
|
+
: entry.platforms,
|
|
197
|
+
entry.runner,
|
|
198
|
+
entry.owner,
|
|
199
|
+
entry.reason,
|
|
200
|
+
entry.ticket,
|
|
201
|
+
entry.expiresAt,
|
|
202
|
+
])
|
|
203
|
+
);
|
|
204
|
+
const dates = table(
|
|
205
|
+
["Scenario", "Recorded at"],
|
|
206
|
+
entries.map(entry => [entry.scenario, entry.recordedAt])
|
|
207
|
+
);
|
|
208
|
+
return `${ledger}\n\nWaiver recording dates (complete records are retained in JSON):\n\n${dates}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Retirement fields stay readable; unrelated retirements remain input-owned. */
|
|
212
|
+
function retirementTable(scenarios) {
|
|
213
|
+
return table(
|
|
214
|
+
["Scenario", "Platforms", "Approved by", "Reason", "Ticket", "Recorded at"],
|
|
215
|
+
scenarios
|
|
216
|
+
.flatMap(row => row.retirements)
|
|
217
|
+
.map(entry => [
|
|
218
|
+
entry.scenario,
|
|
219
|
+
Array.isArray(entry.platforms)
|
|
220
|
+
? entry.platforms.join(", ")
|
|
221
|
+
: entry.platforms,
|
|
222
|
+
entry.approvedBy,
|
|
223
|
+
entry.reason,
|
|
224
|
+
entry.ticket,
|
|
225
|
+
entry.recordedAt,
|
|
226
|
+
])
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Local obligations never claim execution credit or turn waivers into proof. */
|
|
231
|
+
function obligationTable(scenarios) {
|
|
232
|
+
return table(
|
|
233
|
+
[
|
|
234
|
+
"Scenario",
|
|
235
|
+
"Platform",
|
|
236
|
+
"Configured runners",
|
|
237
|
+
"Traceability",
|
|
238
|
+
"Waived",
|
|
239
|
+
"Gap",
|
|
240
|
+
],
|
|
241
|
+
scenarios.flatMap(row =>
|
|
242
|
+
row.obligations.map(value => [
|
|
243
|
+
row.id,
|
|
244
|
+
value.platform,
|
|
245
|
+
value.runners.join(", "),
|
|
246
|
+
value.covered ? "covered" : "uncovered",
|
|
247
|
+
value.waived ? "yes" : "no",
|
|
248
|
+
value.gap ? "yes" : "no",
|
|
249
|
+
])
|
|
250
|
+
)
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Both leaf documents carry the same local facts without a global roster. */
|
|
255
|
+
function localDocument(model, kind) {
|
|
256
|
+
const title =
|
|
257
|
+
kind === "matrix"
|
|
258
|
+
? "BDD scenario traceability matrix"
|
|
259
|
+
: "BDD behavior contract — feature coverage burndown";
|
|
260
|
+
return `${projectionMarker(model.sourceFile, kind)}
|
|
261
|
+
# ${title}
|
|
262
|
+
|
|
263
|
+
Source: ${projectionCell(model.sourceFile)}
|
|
264
|
+
|
|
265
|
+
Traceability proves aligned automation exists, never that it ran or passed. A waiver is never coverage. Execution marked “not supplied” is unknown; “not run” means no supplied result matched this mapping. Retry outcomes retain the worst supplied result.
|
|
266
|
+
|
|
267
|
+
## Declarations
|
|
268
|
+
|
|
269
|
+
${declarationTables(model.scenarios)}
|
|
270
|
+
|
|
271
|
+
## Tracker references
|
|
272
|
+
|
|
273
|
+
${trackerTable(model.scenarios)}
|
|
274
|
+
|
|
275
|
+
## Mapped evidence and execution
|
|
276
|
+
|
|
277
|
+
${mappingTable(model.scenarios)}
|
|
278
|
+
|
|
279
|
+
## Local obligations
|
|
280
|
+
|
|
281
|
+
${obligationTable(model.scenarios)}
|
|
282
|
+
|
|
283
|
+
## Waived obligations
|
|
284
|
+
|
|
285
|
+
${waiverSection(model.scenarios)}
|
|
286
|
+
|
|
287
|
+
## Associated retirements
|
|
288
|
+
|
|
289
|
+
${retirementTable(model.scenarios)}
|
|
290
|
+
|
|
291
|
+
The matching JSON leaf retains complete local declarations, mappings, waiver and retirement records. Current global inventory, counts, floor evaluations and defects are published by the runtime CI summary.
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Render all three canonical leaves for one source identity. */
|
|
296
|
+
export function renderFeatureArtifacts(model) {
|
|
297
|
+
const [json, matrix, coverage] = featureArtifactPaths(model.sourceFile);
|
|
298
|
+
return new Map([
|
|
299
|
+
[json, projectionJson(model)],
|
|
300
|
+
[matrix, localDocument(model, "matrix")],
|
|
301
|
+
[coverage, localDocument(model, "coverage")],
|
|
302
|
+
]);
|
|
303
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
/** Apply a preflighted ownership plan with no recursive deletions. */
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
8
|
+
import {
|
|
9
|
+
inspectProjectionPath,
|
|
10
|
+
refuseProjection,
|
|
11
|
+
} from "./projection-paths.mjs";
|
|
12
|
+
import {
|
|
13
|
+
ownedFeatureLeaf,
|
|
14
|
+
prepareProjectionWrite,
|
|
15
|
+
validateProjectionTarget,
|
|
16
|
+
} from "./projection-ownership.mjs";
|
|
17
|
+
|
|
18
|
+
/** Create each missing parent only after the complete plan has passed. */
|
|
19
|
+
function ensureParents(root, relative) {
|
|
20
|
+
const parts = relative.split("/").slice(0, -1);
|
|
21
|
+
for (let count = 1; count <= parts.length; count += 1) {
|
|
22
|
+
const directory = parts.slice(0, count).join("/");
|
|
23
|
+
if (!inspectProjectionPath(root, directory, "directory")) {
|
|
24
|
+
fs.mkdirSync(path.join(root, directory));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Refuse changed ownership before replacing an individually prepared target. */
|
|
30
|
+
function checkUnchanged(root, entry) {
|
|
31
|
+
const previous = validateProjectionTarget(root, entry.relative, entry.body);
|
|
32
|
+
if (previous !== entry.previous)
|
|
33
|
+
refuseProjection(`target changed after preflight: ${entry.relative}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Exclusive same-directory temporaries avoid following or replacing foreign files. */
|
|
37
|
+
function replaceLeaf(root, entry) {
|
|
38
|
+
ensureParents(root, entry.relative);
|
|
39
|
+
checkUnchanged(root, entry);
|
|
40
|
+
const absolute = path.join(root, entry.relative);
|
|
41
|
+
const temporary = `${absolute}.lisa-bdd-${randomBytes(12).toString("hex")}.tmp`;
|
|
42
|
+
let created = false;
|
|
43
|
+
try {
|
|
44
|
+
fs.writeFileSync(temporary, entry.body, {
|
|
45
|
+
encoding: "utf8",
|
|
46
|
+
flag: "wx",
|
|
47
|
+
mode: 0o644,
|
|
48
|
+
});
|
|
49
|
+
created = true;
|
|
50
|
+
checkUnchanged(root, entry);
|
|
51
|
+
fs.renameSync(temporary, absolute);
|
|
52
|
+
created = false;
|
|
53
|
+
} finally {
|
|
54
|
+
if (created) fs.unlinkSync(temporary);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Recheck the exact owned bytes before removing one obsolete regular leaf. */
|
|
59
|
+
function removeObsolete(root, entry) {
|
|
60
|
+
if (!inspectProjectionPath(root, entry.relative)) {
|
|
61
|
+
refuseProjection(
|
|
62
|
+
`obsolete leaf changed after preflight: ${entry.relative}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const absolute = path.join(root, entry.relative);
|
|
66
|
+
const body = fs.readFileSync(absolute, "utf8");
|
|
67
|
+
if (body !== entry.body || !ownedFeatureLeaf(entry.relative, body)) {
|
|
68
|
+
refuseProjection(
|
|
69
|
+
`obsolete ownership changed after preflight: ${entry.relative}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
fs.unlinkSync(absolute);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Return actual writes; a byte-identical repeat returns zero and touches nothing. */
|
|
76
|
+
export function writeProjectionArtifacts(root, artifacts) {
|
|
77
|
+
const canonicalRoot = fs.realpathSync(root);
|
|
78
|
+
if (!fs.statSync(canonicalRoot).isDirectory())
|
|
79
|
+
refuseProjection("repository root is not a directory");
|
|
80
|
+
const plan = prepareProjectionWrite(canonicalRoot, artifacts);
|
|
81
|
+
for (const entry of plan.changed) replaceLeaf(canonicalRoot, entry);
|
|
82
|
+
for (const entry of plan.obsolete) removeObsolete(canonicalRoot, entry);
|
|
83
|
+
return plan.changed.length;
|
|
84
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
/** Shared canonical writers emit one complete, feature-local projection set. */
|
|
5
|
+
import { buildFeatureModels } from "./projection-model.mjs";
|
|
6
|
+
import {
|
|
7
|
+
STATIC_ARTIFACTS,
|
|
8
|
+
renderFeatureArtifacts,
|
|
9
|
+
} from "./projection-render.mjs";
|
|
10
|
+
import { validateProjectionTargets } from "./projection-paths.mjs";
|
|
11
|
+
import { writeProjectionArtifacts } from "./projection-write.mjs";
|
|
12
|
+
|
|
13
|
+
export { FEATURE_PROJECTION_SCHEMA } from "./projection-paths.mjs";
|
|
14
|
+
export { PROJECTION_INDEX_SCHEMA } from "./projection-render.mjs";
|
|
15
|
+
|
|
16
|
+
/** Reads actual evidence; building the artifact map itself never writes files. */
|
|
17
|
+
export function buildFeatureArtifacts(root, inputs) {
|
|
18
|
+
const artifacts = new Map(STATIC_ARTIFACTS);
|
|
19
|
+
for (const model of buildFeatureModels(root, inputs)) {
|
|
20
|
+
for (const [relative, body] of renderFeatureArtifacts(model))
|
|
21
|
+
artifacts.set(relative, body);
|
|
22
|
+
}
|
|
23
|
+
validateProjectionTargets(artifacts.keys());
|
|
24
|
+
return artifacts;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Preflight every desired/obsolete path, then write changed canonical bytes. */
|
|
28
|
+
export function writeFeatureArtifacts(root, inputs) {
|
|
29
|
+
return writeProjectionArtifacts(root, buildFeatureArtifacts(root, inputs));
|
|
30
|
+
}
|
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
/**
|
|
6
6
|
* bdd-matrix — the per-scenario traceability matrix.
|
|
7
7
|
*
|
|
8
|
-
* The human-readable companion to `check-bdd-coverage.mjs`.
|
|
9
|
-
* gate's
|
|
10
|
-
*
|
|
8
|
+
* The human-readable companion to `check-bdd-coverage.mjs`. Committed feature
|
|
9
|
+
* projections reuse the gate's evidence validator. The retained aggregate
|
|
10
|
+
* stdout/API view reports configured mapping declarations; use the gate's
|
|
11
|
+
* `--report` for evidence-validated coverage credit.
|
|
11
12
|
*
|
|
12
13
|
* Every row separates the five facts the gate keeps apart: the behavior
|
|
13
14
|
* DECLARED, where it came from, whether automation is MAPPED per runner
|
|
14
15
|
* (traceability), whether that automation RAN, and what it returned. A `✓` in
|
|
15
|
-
* the
|
|
16
|
-
* evidence string — never that
|
|
16
|
+
* the retained aggregate view reports a declared mapping without validating
|
|
17
|
+
* its evidence string — never that a test ran or passed.
|
|
17
18
|
*
|
|
18
19
|
* Usage: BDD_BASE_SHA="$(git merge-base HEAD origin/HEAD)" bun run bdd:matrix [--results <file>]
|
|
19
20
|
*
|
|
@@ -26,15 +27,19 @@ import { fileURLToPath } from "node:url";
|
|
|
26
27
|
import { waivedKeys } from "./bdd/baseline.mjs";
|
|
27
28
|
import { byCodeUnit, declaredPlatforms, trackerUrl } from "./bdd/contract.mjs";
|
|
28
29
|
import { loadScenarios } from "./bdd/parse.mjs";
|
|
30
|
+
import { writeFeatureArtifacts } from "./bdd/projection.mjs";
|
|
29
31
|
import { indexResults } from "./bdd/report.mjs";
|
|
30
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
loadExecutionResults,
|
|
34
|
+
parseArgs,
|
|
35
|
+
schemaDefect,
|
|
36
|
+
} from "./check-bdd-coverage.mjs";
|
|
31
37
|
import { invokedAsScript } from "./lib/invoked-as-script.mjs";
|
|
32
38
|
|
|
33
39
|
const PACKAGE_ROOT = path.resolve(
|
|
34
40
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
35
41
|
".."
|
|
36
42
|
);
|
|
37
|
-
const OUT_REL = path.join("docs", "bdd-scenario-matrix.md");
|
|
38
43
|
|
|
39
44
|
/**
|
|
40
45
|
* Index mappings by scenario and runner.
|
|
@@ -221,7 +226,7 @@ function preamble(scenarios, supplied) {
|
|
|
221
226
|
Every declared behavior, where it came from, and what actually proves it.
|
|
222
227
|
Generated by \`BDD_BASE_SHA="$(git merge-base HEAD origin/HEAD)" bun run bdd:matrix\`; never hand-edited.
|
|
223
228
|
|
|
224
|
-
- **Mapped** —
|
|
229
|
+
- **Mapped** — a mapping is declared for that runner and platform. This retained aggregate view does not validate its evidence string. Use \`check-bdd-coverage.mjs --report\` or the committed feature leaves for evidence-validated traceability; a check mark here never proves a test ran or passed.
|
|
225
230
|
- **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."}
|
|
226
231
|
- **Lifecycle** — \`required\` counts toward coverage; \`blocked\`, \`reference-only\`, and \`superseded\` are excluded until that changes.
|
|
227
232
|
|
|
@@ -237,19 +242,50 @@ ${scenarios.length} scenarios declared, ${required} required.
|
|
|
237
242
|
function main() {
|
|
238
243
|
const root = process.env.BDD_COVERAGE_ROOT || PACKAGE_ROOT;
|
|
239
244
|
const argv = process.argv.slice(2);
|
|
240
|
-
const resultFiles =
|
|
241
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
242
|
-
if (argv[index] === "--results" && argv[index + 1])
|
|
243
|
-
resultFiles.push(argv[index + 1]);
|
|
244
|
-
}
|
|
245
|
-
const body = renderMatrix(root, resultFiles);
|
|
245
|
+
const { resultFiles } = parseArgs(argv, process.env);
|
|
246
246
|
if (!argv.includes("--write")) {
|
|
247
|
-
console.log(
|
|
247
|
+
console.log(renderMatrix(root, resultFiles));
|
|
248
248
|
return;
|
|
249
249
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
250
|
+
writeMatrix(root, resultFiles);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Regenerate the same owned feature files as the coverage writer.
|
|
255
|
+
* @param {string} root - Project root.
|
|
256
|
+
* @param {readonly string[]} resultFiles - Explicit result inputs.
|
|
257
|
+
* @returns {void}
|
|
258
|
+
*/
|
|
259
|
+
function writeMatrix(root, resultFiles) {
|
|
260
|
+
try {
|
|
261
|
+
const contract = JSON.parse(
|
|
262
|
+
fs.readFileSync(path.join(root, "bdd", "coverage-map.json"), "utf8")
|
|
263
|
+
);
|
|
264
|
+
const unsupported = schemaDefect(contract);
|
|
265
|
+
if (unsupported)
|
|
266
|
+
throw new Error(`${unsupported.code}: ${unsupported.message}`);
|
|
267
|
+
const scenarios = loadScenarios(
|
|
268
|
+
root,
|
|
269
|
+
declaredPlatforms(contract.runnerPlatforms)
|
|
270
|
+
);
|
|
271
|
+
const execution = loadExecutionResults(root, resultFiles);
|
|
272
|
+
if (execution.defects.length) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
execution.defects
|
|
275
|
+
.map(item => `${item.code}: ${item.message}`)
|
|
276
|
+
.join("; ")
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
const written = writeFeatureArtifacts(root, {
|
|
280
|
+
contract,
|
|
281
|
+
scenarios,
|
|
282
|
+
runs: execution.runs,
|
|
283
|
+
});
|
|
284
|
+
console.log(`[bdd-matrix] wrote ${written} feature projection files`);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
console.error(`[bdd-matrix] report-write: ${error.message}`);
|
|
287
|
+
process.exitCode = 1;
|
|
288
|
+
}
|
|
253
289
|
}
|
|
254
290
|
|
|
255
291
|
if (invokedAsScript(import.meta.url)) main();
|
|
@@ -74,8 +74,9 @@ import {
|
|
|
74
74
|
missingDiscoveryDefects,
|
|
75
75
|
} from "./bdd/discover.mjs";
|
|
76
76
|
import { loadScenarios } from "./bdd/parse.mjs";
|
|
77
|
+
import { appendCiSummary } from "./bdd/ci-summary.mjs";
|
|
78
|
+
import { writeFeatureArtifacts } from "./bdd/projection.mjs";
|
|
77
79
|
import { buildReport } from "./bdd/report.mjs";
|
|
78
|
-
import { renderBurndown } from "./bdd/render.mjs";
|
|
79
80
|
import {
|
|
80
81
|
unresolvedEvidenceKeys,
|
|
81
82
|
validateMappings,
|
|
@@ -447,7 +448,12 @@ export function run(root, options) {
|
|
|
447
448
|
}),
|
|
448
449
|
...completenessDefects({ contract, scenarios, report, discovery }),
|
|
449
450
|
];
|
|
450
|
-
return result({
|
|
451
|
+
return result({
|
|
452
|
+
defects,
|
|
453
|
+
report,
|
|
454
|
+
contract,
|
|
455
|
+
projectionInputs: { contract, scenarios, runs: execution.runs, cache },
|
|
456
|
+
});
|
|
451
457
|
}
|
|
452
458
|
|
|
453
459
|
/**
|
|
@@ -474,7 +480,7 @@ function configFatals(loaded) {
|
|
|
474
480
|
* @param {object} contract - Parsed coverage map.
|
|
475
481
|
* @returns {object|null} The defect, or null.
|
|
476
482
|
*/
|
|
477
|
-
function schemaDefect(contract) {
|
|
483
|
+
export function schemaDefect(contract) {
|
|
478
484
|
const version = contract.schemaVersion;
|
|
479
485
|
if (SUPPORTED_MAP_SCHEMA_VERSIONS.includes(version)) return null;
|
|
480
486
|
return defect(
|
|
@@ -505,12 +511,13 @@ const INVALID_CODES = Object.freeze([
|
|
|
505
511
|
* @param {object} input - Defects, report, and the parsed contract.
|
|
506
512
|
* @returns {object} The internal result.
|
|
507
513
|
*/
|
|
508
|
-
function result({ defects, report, contract }) {
|
|
514
|
+
function result({ defects, report, contract, projectionInputs = null }) {
|
|
509
515
|
return {
|
|
510
516
|
status: statusFor({ defects, fatal: hasFatalDefect(defects), report }),
|
|
511
517
|
defects,
|
|
512
518
|
report,
|
|
513
519
|
contract,
|
|
520
|
+
projectionInputs,
|
|
514
521
|
};
|
|
515
522
|
}
|
|
516
523
|
|
|
@@ -646,10 +653,31 @@ async function main() {
|
|
|
646
653
|
return;
|
|
647
654
|
}
|
|
648
655
|
const options = parseArgs(process.argv.slice(2), process.env);
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
656
|
+
let gateRun = run(root, options);
|
|
657
|
+
let filesWritten = 0;
|
|
658
|
+
if (options.write && gateRun.report && gateRun.status !== "invalid") {
|
|
659
|
+
try {
|
|
660
|
+
filesWritten = writeFeatureArtifacts(root, gateRun.projectionInputs);
|
|
661
|
+
} catch (error) {
|
|
662
|
+
gateRun = result({
|
|
663
|
+
...gateRun,
|
|
664
|
+
defects: [...gateRun.defects, defect("report-write", error.message)],
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
let envelope = await sealEnvelope({ gateRun, options, filesWritten });
|
|
669
|
+
try {
|
|
670
|
+
appendCiSummary(
|
|
671
|
+
{ ...gateRun, status: envelope.status },
|
|
672
|
+
process.env.GITHUB_STEP_SUMMARY
|
|
673
|
+
);
|
|
674
|
+
} catch (error) {
|
|
675
|
+
gateRun = result({
|
|
676
|
+
...gateRun,
|
|
677
|
+
defects: [...gateRun.defects, defect("ci-summary-write", error.message)],
|
|
678
|
+
});
|
|
679
|
+
envelope = await sealEnvelope({ gateRun, options, filesWritten });
|
|
680
|
+
}
|
|
653
681
|
// Exactly ONE machine-readable object on stdout. `--report` is a diagnostic
|
|
654
682
|
// that swaps the envelope for the detailed report; it never adds a second
|
|
655
683
|
// document, because a stream carrying two shapes has no schema at all.
|
|
@@ -706,25 +734,6 @@ async function sealEnvelope({ gateRun, options, filesWritten }) {
|
|
|
706
734
|
}
|
|
707
735
|
}
|
|
708
736
|
|
|
709
|
-
/**
|
|
710
|
-
* Write the regenerated machine report and burndown.
|
|
711
|
-
* @param {string} root - Repo root.
|
|
712
|
-
* @param {object} report - The report.
|
|
713
|
-
* @returns {number} How many files were written, for the envelope's counters.
|
|
714
|
-
*/
|
|
715
|
-
function writeArtifacts(root, report) {
|
|
716
|
-
fs.writeFileSync(
|
|
717
|
-
path.join(root, "bdd", "coverage-report.json"),
|
|
718
|
-
`${JSON.stringify(report, null, 2)}\n`
|
|
719
|
-
);
|
|
720
|
-
fs.mkdirSync(path.join(root, "docs"), { recursive: true });
|
|
721
|
-
fs.writeFileSync(
|
|
722
|
-
path.join(root, "docs", "e2e-bdd-coverage.md"),
|
|
723
|
-
`${renderBurndown(report).trim()}\n`
|
|
724
|
-
);
|
|
725
|
-
return 2;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
737
|
/**
|
|
729
738
|
* Narrate the result on stderr, which the envelope contract reserves for
|
|
730
739
|
* humans so stdout stays exactly one machine-readable object.
|
package/package.json
CHANGED
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
"qs": "^6.16.0"
|
|
190
190
|
},
|
|
191
191
|
"name": "@codyswann/lisa",
|
|
192
|
-
"version": "4.74.
|
|
192
|
+
"version": "4.74.2",
|
|
193
193
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
194
194
|
"main": "dist/index.js",
|
|
195
195
|
"exports": {
|
|
@@ -345,7 +345,7 @@
|
|
|
345
345
|
"test": "tests"
|
|
346
346
|
},
|
|
347
347
|
"types": "./dist/index.d.ts",
|
|
348
|
-
"lisaReleaseCommit": "
|
|
349
|
-
"gitHead": "
|
|
350
|
-
"lisaReleaseTag": "v4.74.
|
|
348
|
+
"lisaReleaseCommit": "49bcce1aacaea1aba5ff900d13d3c6231fa54ca7",
|
|
349
|
+
"gitHead": "49bcce1aacaea1aba5ff900d13d3c6231fa54ca7",
|
|
350
|
+
"lisaReleaseTag": "v4.74.2"
|
|
351
351
|
}
|