@codyswann/lisa 4.74.0 → 4.74.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 (72) hide show
  1. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  2. package/dist/core/lisa-owned-hash-ledger.js +48 -0
  3. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  4. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  5. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  6. package/dist/core/upstream-evidence-manifest.js +36 -4
  7. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  8. package/expo/copy-overwrite/scripts/bdd/ci-summary.mjs +37 -0
  9. package/expo/copy-overwrite/scripts/bdd/projection-model.mjs +157 -0
  10. package/expo/copy-overwrite/scripts/bdd/projection-ownership.mjs +300 -0
  11. package/expo/copy-overwrite/scripts/bdd/projection-paths.mjs +152 -0
  12. package/expo/copy-overwrite/scripts/bdd/projection-render.mjs +303 -0
  13. package/expo/copy-overwrite/scripts/bdd/projection-write.mjs +84 -0
  14. package/expo/copy-overwrite/scripts/bdd/projection.mjs +30 -0
  15. package/expo/copy-overwrite/scripts/bdd-matrix.mjs +54 -18
  16. package/expo/copy-overwrite/scripts/check-bdd-coverage.mjs +36 -27
  17. package/package.json +4 -4
  18. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  20. package/plugins/lisa/rules/reference/bdd-e2e-coverage.md +9 -7
  21. package/plugins/lisa-agy/plugin.json +1 -1
  22. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  25. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-copilot/rules/reference/bdd-e2e-coverage.md +9 -7
  29. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cursor/rules/bdd-e2e-coverage-reference.mdc +9 -7
  31. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa-expo-agy/plugin.json +1 -1
  34. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  44. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  49. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  54. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-rails-agy/plugin.json +1 -1
  59. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  64. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  69. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/src/base/rules/reference/bdd-e2e-coverage.md +9 -7
  72. 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, "&lt;")
44
+ .replace(/>/g, "&gt;")
45
+ .replace(/`/g, "&#96;")
46
+ .replace(/\[/g, "&#91;")
47
+ .replace(/\]/g, "&#93;")
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`. It reuses that
9
- * gate's parser and the same `bdd/coverage-map.json`, so the two can never
10
- * disagree about what is covered.
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 mapped column asserts an aligned test exists and still contains its
16
- * evidence string — never that it passed.
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 { loadExecutionResults } from "./check-bdd-coverage.mjs";
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** — 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 \`✓\`.
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(body);
247
+ console.log(renderMatrix(root, resultFiles));
248
248
  return;
249
249
  }
250
- fs.mkdirSync(path.join(root, "docs"), { recursive: true });
251
- fs.writeFileSync(path.join(root, OUT_REL), body);
252
- console.log(`[bdd-matrix] wrote ${OUT_REL}`);
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({ defects, report, contract });
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
- const gateRun = run(root, options);
650
- const filesWritten =
651
- options.write && gateRun.report ? writeArtifacts(root, gateRun.report) : 0;
652
- const envelope = await sealEnvelope({ gateRun, options, filesWritten });
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.0",
192
+ "version": "4.74.1",
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": "fb7ed7c4d51c760b1855d4a68321a96ec0daf050",
349
- "gitHead": "fb7ed7c4d51c760b1855d4a68321a96ec0daf050",
350
- "lisaReleaseTag": "v4.74.0"
348
+ "lisaReleaseCommit": "cc1eae7783ceef7fb72a4883139a4df03d37e732",
349
+ "gitHead": "cc1eae7783ceef7fb72a4883139a4df03d37e732",
350
+ "lisaReleaseTag": "v4.74.1"
351
351
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.74.0",
3
+ "version": "4.74.1",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.74.0",
3
+ "version": "4.74.1",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"