@codyswann/lisa 2.346.0 → 2.349.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.
Files changed (77) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +62 -4
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/expo/copy-overwrite/scripts/bdd/baseline.mjs +224 -0
  5. package/expo/copy-overwrite/scripts/bdd/contract.mjs +121 -0
  6. package/expo/copy-overwrite/scripts/bdd/envelope.mjs +231 -0
  7. package/expo/copy-overwrite/scripts/bdd/parse.mjs +220 -0
  8. package/expo/copy-overwrite/scripts/bdd/render.mjs +170 -0
  9. package/expo/copy-overwrite/scripts/bdd/report.mjs +361 -0
  10. package/expo/copy-overwrite/scripts/bdd/validate.mjs +282 -0
  11. package/expo/copy-overwrite/scripts/bdd/waivers.mjs +210 -0
  12. package/expo/copy-overwrite/scripts/bdd-matrix.mjs +278 -0
  13. package/expo/copy-overwrite/scripts/check-bdd-coverage.mjs +657 -0
  14. package/expo/create-only/.github/required-checks.json +52 -0
  15. package/expo/create-only/.github/workflows/nightly-e2e-bypass-reaper.yml +83 -0
  16. package/expo/create-only/.github/workflows/nightly-e2e-health.yml +140 -0
  17. package/expo/create-only/bdd/coverage-map.json +38 -0
  18. package/expo/create-only/bdd/features/.keep +5 -0
  19. package/expo/github-rulesets/{playwright.json → bdd-coverage.json} +3 -8
  20. package/expo/github-rulesets/nightly-e2e-health.json +33 -0
  21. package/expo/package-lisa/package.lisa.json +3 -0
  22. package/package.json +1 -1
  23. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  25. package/plugins/lisa-agy/plugin.json +1 -1
  26. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  29. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  35. package/plugins/lisa-expo-agy/plugin.json +1 -1
  36. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  46. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  51. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  55. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  56. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-rails-agy/plugin.json +1 -1
  61. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  65. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  66. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  70. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  71. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  73. package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +1516 -0
  74. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +580 -0
  75. package/typescript/copy-overwrite/scripts/nightly-e2e-suites.schema.json +85 -0
  76. package/typescript/create-only/.github/required-checks.json +28 -0
  77. package/typescript/package-lisa/package.lisa.json +4 -1
@@ -0,0 +1,657 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * check-bdd-coverage — the BDD behavior-contract gate.
4
+ *
5
+ * Validates `bdd/features/*.feature` against `bdd/coverage-map.json` and
6
+ * reports five separate facts: behaviors DECLARED, obligations MAPPED to an
7
+ * automated test (traceability), mapped tests that RAN, what those runs
8
+ * returned, and what is WAIVED. Traceability coverage is not execution
9
+ * coverage and is never a pass rate.
10
+ *
11
+ * THREE-STATE ADOPTION (`BDD_MODE`, supplied by CI, never inferred):
12
+ *
13
+ * not-adopted The contract is not required. The gate reports and exits 0,
14
+ * and the check MUST NOT be a required ruleset context.
15
+ * bootstrap A visible, non-blocking check carrying a named owner and a
16
+ * hard expiry. Contract defects are warnings; a missing or
17
+ * passed expiry is a failure, so bootstrap cannot become
18
+ * permanent.
19
+ * enforced Absence fails. A missing config, a malformed manifest, zero
20
+ * scenarios, zero mappings, any contract defect, a floor
21
+ * regression, or a deleted scenario all fail loudly. Only in
22
+ * this state is the check a required ruleset context.
23
+ *
24
+ * A required context is NEVER auto-skipped: GitHub counts a skipped required
25
+ * check as passing, which is the exact anti-pattern this gate exists to
26
+ * avoid.
27
+ *
28
+ * Usage:
29
+ * node scripts/check-bdd-coverage.mjs [--write] [--json] [--results <file>]
30
+ *
31
+ * @module scripts/check-bdd-coverage
32
+ */
33
+ import * as fs from "node:fs";
34
+ import * as path from "node:path";
35
+ import { fileURLToPath, pathToFileURL } from "node:url";
36
+
37
+ import {
38
+ ADOPTION_STATES,
39
+ SUPPORTED_MAP_SCHEMA_VERSIONS,
40
+ declaredPlatforms,
41
+ } from "./bdd/contract.mjs";
42
+ import {
43
+ SUCCESS_STATUSES,
44
+ WARNABLE_DEFECT_CODES,
45
+ buildSummary,
46
+ contractVersion,
47
+ correlationId,
48
+ hasFatalDefect,
49
+ loadEnvelopeModule,
50
+ subjectFor,
51
+ } from "./bdd/envelope.mjs";
52
+ import { checkDeletions, checkRatchet, loadBaseline } from "./bdd/baseline.mjs";
53
+ import { loadScenarios } from "./bdd/parse.mjs";
54
+ import { buildReport } from "./bdd/report.mjs";
55
+ import { renderBurndown } from "./bdd/render.mjs";
56
+ import {
57
+ validateMappings,
58
+ validateScenarios,
59
+ validateTrackerTags,
60
+ } from "./bdd/validate.mjs";
61
+ import { validateWaivers } from "./bdd/waivers.mjs";
62
+
63
+ const PACKAGE_ROOT = path.resolve(
64
+ path.dirname(fileURLToPath(import.meta.url)),
65
+ ".."
66
+ );
67
+ const MAP_REL = "bdd/coverage-map.json";
68
+ const defect = (code, message) => ({ code, message });
69
+
70
+ /**
71
+ * Resolve the adoption state from the environment.
72
+ * @param {Record<string, string|undefined>} env - Process environment.
73
+ * @returns {{mode: string, error: string|null}} The resolved mode.
74
+ */
75
+ export function resolveMode(env) {
76
+ const raw = (env.BDD_MODE ?? "").trim();
77
+ if (raw === "") return { mode: "not-adopted", error: null };
78
+ if (!ADOPTION_STATES.includes(raw)) {
79
+ return {
80
+ mode: "not-adopted",
81
+ error: `BDD_MODE="${raw}" is not one of ${ADOPTION_STATES.join(", ")}`,
82
+ };
83
+ }
84
+ return { mode: raw, error: null };
85
+ }
86
+
87
+ /**
88
+ * Read and parse the coverage map, distinguishing "absent" from "malformed".
89
+ * @param {string} root - Repo root.
90
+ * @returns {{present: boolean, contract: object|null, error: string|null}} Load result.
91
+ */
92
+ export function loadContract(root) {
93
+ const file = path.join(root, MAP_REL);
94
+ if (!fs.existsSync(file))
95
+ return { present: false, contract: null, error: null };
96
+ try {
97
+ const contract = JSON.parse(fs.readFileSync(file, "utf8"));
98
+ if (
99
+ contract === null ||
100
+ typeof contract !== "object" ||
101
+ Array.isArray(contract)
102
+ ) {
103
+ return {
104
+ present: true,
105
+ contract: null,
106
+ error: `${MAP_REL} is not a JSON object`,
107
+ };
108
+ }
109
+ return { present: true, contract, error: null };
110
+ } catch (error) {
111
+ return {
112
+ present: true,
113
+ contract: null,
114
+ error: `${MAP_REL} is not valid JSON: ${error.message}`,
115
+ };
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Validate the manifest's own adoption block against the mode CI declared.
121
+ *
122
+ * The two must agree: CI is the authority (it survives deletion of `bdd/`),
123
+ * and the manifest is the self-describing record. A disagreement means an
124
+ * adoption was half-performed, which is exactly the state that produces a
125
+ * required check nobody is actually running.
126
+ * @param {object} contract - Parsed coverage map.
127
+ * @param {string} mode - Mode declared by CI.
128
+ * @param {string} today - ISO date to evaluate the expiry against.
129
+ * @returns {object[]} Defects found.
130
+ */
131
+ export function validateAdoption(contract, mode, today) {
132
+ const adoption = contract.adoption ?? {};
133
+ const defects = [];
134
+ if (adoption.state && adoption.state !== mode) {
135
+ defects.push(
136
+ defect(
137
+ "adoption-drift",
138
+ `bdd/coverage-map.json declares adoption.state "${adoption.state}" but CI passed BDD_MODE "${mode}". Adoption is one operation: change both, and the ruleset context, together.`
139
+ )
140
+ );
141
+ }
142
+ if (mode === "enforced" && !adoption.state) {
143
+ defects.push(
144
+ defect(
145
+ "adoption-drift",
146
+ `enforced mode requires adoption.state "enforced" in ${MAP_REL}`
147
+ )
148
+ );
149
+ }
150
+ if (mode !== "bootstrap") return defects;
151
+ return [...defects, ...bootstrapDefects(adoption, today)];
152
+ }
153
+
154
+ /**
155
+ * Bootstrap owes a named owner and a hard expiry, and dies at that expiry.
156
+ * @param {object} adoption - The manifest's adoption block.
157
+ * @param {string} today - ISO date to evaluate against.
158
+ * @returns {object[]} Defects found.
159
+ */
160
+ function bootstrapDefects(adoption, today) {
161
+ const defects = [];
162
+ if (!adoption.owner) {
163
+ defects.push(
164
+ defect(
165
+ "bootstrap-metadata",
166
+ "bootstrap requires adoption.owner (a named person, not a team)"
167
+ )
168
+ );
169
+ }
170
+ if (!adoption.expiresAt) {
171
+ defects.push(
172
+ defect(
173
+ "bootstrap-metadata",
174
+ "bootstrap requires adoption.expiresAt (an ISO date); a bootstrap with no time-box never ends"
175
+ )
176
+ );
177
+ } else if (!/^\d{4}-\d{2}-\d{2}$/.test(adoption.expiresAt)) {
178
+ defects.push(
179
+ defect(
180
+ "bootstrap-metadata",
181
+ "adoption.expiresAt must be an ISO date (YYYY-MM-DD)"
182
+ )
183
+ );
184
+ } else if (adoption.expiresAt < today) {
185
+ defects.push(
186
+ defect(
187
+ "bootstrap-expired",
188
+ `the BDD bootstrap expired on ${adoption.expiresAt} (owner: ${adoption.owner ?? "unnamed"}). Advance to enforced or re-authorize the time-box.`
189
+ )
190
+ );
191
+ }
192
+ return defects;
193
+ }
194
+
195
+ /**
196
+ * Read every supplied execution-result document.
197
+ * @param {string} root - Repo root.
198
+ * @param {readonly string[]} files - Repo-relative or absolute result paths.
199
+ * @returns {{runs: object[], defects: object[]}} Parsed runs and any read errors.
200
+ */
201
+ export function loadExecutionResults(root, files) {
202
+ const runs = [];
203
+ const defects = [];
204
+ for (const file of files) {
205
+ const resolved = path.resolve(root, file);
206
+ if (!fs.existsSync(resolved)) {
207
+ defects.push(
208
+ defect("execution-results", `execution results not found: ${file}`)
209
+ );
210
+ continue;
211
+ }
212
+ try {
213
+ const parsed = JSON.parse(fs.readFileSync(resolved, "utf8"));
214
+ for (const run of Array.isArray(parsed) ? parsed : [parsed]) {
215
+ if (!run.runner) {
216
+ defects.push(
217
+ defect(
218
+ "execution-results",
219
+ `${file}: each run must name its runner`
220
+ )
221
+ );
222
+ continue;
223
+ }
224
+ runs.push(run);
225
+ }
226
+ } catch (error) {
227
+ defects.push(defect("execution-results", `${file}: ${error.message}`));
228
+ }
229
+ }
230
+ return { runs, defects };
231
+ }
232
+
233
+ /**
234
+ * Run every contract validator plus the base-revision comparisons.
235
+ * @param {object} input - Root, contract, scenarios, platforms, and options.
236
+ * @returns {object[]} Defects found.
237
+ */
238
+ function validateAll({ root, contract, scenarios, platforms, options }) {
239
+ const defects = [
240
+ ...validateScenarios(scenarios, platforms),
241
+ ...validateTrackerTags(scenarios, contract.trackers),
242
+ ...validateMappings({ root, scenarios, contract }),
243
+ ...validateWaivers({ scenarios, contract, today: options.today }),
244
+ ];
245
+ if (!options.baseSha) return defects;
246
+ const baseline = loadBaseline(root, options.baseSha);
247
+ if (!baseline.available) {
248
+ return [
249
+ ...defects,
250
+ defect(
251
+ "baseline",
252
+ `base revision ${options.baseSha} is not readable; the ratchet and deletion checks could not run`
253
+ ),
254
+ ];
255
+ }
256
+ return [
257
+ ...defects,
258
+ ...checkRatchet({
259
+ baseContract: baseline.contract,
260
+ contract,
261
+ labels: options.labels,
262
+ }),
263
+ ...checkDeletions({
264
+ baseIds: baseline.scenarioIds,
265
+ scenarios,
266
+ contract,
267
+ labels: options.labels,
268
+ }),
269
+ ];
270
+ }
271
+
272
+ /**
273
+ * Defects that only exist in enforced mode, where absence must fail.
274
+ * @param {object} input - Contract, scenarios, report, and platforms.
275
+ * @returns {object[]} Defects found.
276
+ */
277
+ function enforcedDefects({ contract, scenarios, report, platforms }) {
278
+ const defects = [];
279
+ if (scenarios.length === 0) {
280
+ defects.push(
281
+ defect(
282
+ "empty-contract",
283
+ "enforced mode: bdd/features declares zero scenarios"
284
+ )
285
+ );
286
+ }
287
+ if ((contract.mappings ?? []).length === 0) {
288
+ defects.push(
289
+ defect(
290
+ "empty-contract",
291
+ "enforced mode: bdd/coverage-map.json declares zero test mappings"
292
+ )
293
+ );
294
+ }
295
+ if (Object.keys(contract.runnerPlatforms ?? {}).length === 0) {
296
+ defects.push(
297
+ defect(
298
+ "empty-contract",
299
+ "enforced mode: bdd/coverage-map.json declares no runnerPlatforms"
300
+ )
301
+ );
302
+ }
303
+ for (const platform of report.floor.unset) {
304
+ defects.push(
305
+ defect(
306
+ "floor-missing",
307
+ `enforced mode: no coverageFloor declared for platform ${platform}`
308
+ )
309
+ );
310
+ }
311
+ for (const [platform, value] of Object.entries(report.floor.byPlatform)) {
312
+ if (!value.ok) {
313
+ defects.push(
314
+ defect(
315
+ "floor-regression",
316
+ `${platform} traceability coverage ${value.actual}% is below its committed floor of ${value.floor}%`
317
+ )
318
+ );
319
+ }
320
+ }
321
+ void platforms;
322
+ return defects;
323
+ }
324
+
325
+ /**
326
+ * Evaluate the gate.
327
+ * @param {string} root - Repo root.
328
+ * @param {object} options - Mode, dates, labels, base SHA, and result files.
329
+ * @returns {object} The result envelope.
330
+ */
331
+ export function run(root, options) {
332
+ const { mode } = options;
333
+ const loaded = loadContract(root);
334
+ const fatal = configFatals(loaded, mode);
335
+ if (fatal)
336
+ return result({ mode, defects: [fatal], report: null, contract: null });
337
+ if (!loaded.present) {
338
+ return result({ mode, defects: [], report: null, contract: null });
339
+ }
340
+ const contract = loaded.contract;
341
+ const versionDefect = schemaDefect(contract);
342
+ const platforms = declaredPlatforms(contract.runnerPlatforms);
343
+ const scenarios = loadScenarios(root, platforms);
344
+ const execution = loadExecutionResults(root, options.resultFiles ?? []);
345
+ const report = buildReport({
346
+ scenarios,
347
+ contract,
348
+ runs: execution.runs,
349
+ platforms,
350
+ });
351
+ const defects = [
352
+ ...(versionDefect ? [versionDefect] : []),
353
+ ...validateAdoption(contract, mode, options.today),
354
+ ...execution.defects,
355
+ ...validateAll({ root, contract, scenarios, platforms, options }),
356
+ ...(mode === "enforced"
357
+ ? enforcedDefects({ contract, scenarios, report, platforms })
358
+ : []),
359
+ ];
360
+ return result({ mode, defects, report, contract });
361
+ }
362
+
363
+ /**
364
+ * Configuration problems that stop the gate before it can evaluate anything.
365
+ * @param {object} loaded - Result of {@link loadContract}.
366
+ * @param {string} mode - Adoption state.
367
+ * @returns {object|null} The fatal defect, or null.
368
+ */
369
+ function configFatals(loaded, mode) {
370
+ if (loaded.error) return defect("config-malformed", loaded.error);
371
+ if (loaded.present || mode === "not-adopted") return null;
372
+ return defect(
373
+ "config-absent",
374
+ `${mode} mode requires ${MAP_REL}, which does not exist. In ${mode} mode absence is a failure, never a skip.`
375
+ );
376
+ }
377
+
378
+ /**
379
+ * Reject a coverage map written against an unknown schema version.
380
+ * @param {object} contract - Parsed coverage map.
381
+ * @returns {object|null} The defect, or null.
382
+ */
383
+ function schemaDefect(contract) {
384
+ const version = contract.schemaVersion;
385
+ if (SUPPORTED_MAP_SCHEMA_VERSIONS.includes(version)) return null;
386
+ return defect(
387
+ "config-schema",
388
+ `bdd/coverage-map.json schemaVersion ${JSON.stringify(version)} is not supported (this gate reads ${SUPPORTED_MAP_SCHEMA_VERSIONS.join(", ")})`
389
+ );
390
+ }
391
+
392
+ /**
393
+ * Statuses that must NOT map onto `failed`, because they describe a bad
394
+ * request or an unreadable contract rather than a contract that failed.
395
+ * Source constants, deliberately: an unrecognized code falls through to
396
+ * `failed`, which is the closed direction.
397
+ */
398
+ const INVALID_CODES = Object.freeze([
399
+ "config-malformed",
400
+ "config-schema",
401
+ "config-absent",
402
+ ]);
403
+
404
+ /**
405
+ * Assemble the gate's internal result, with each defect's severity resolved.
406
+ *
407
+ * Severity is decided here, once, from the adoption state and the warnable
408
+ * allowlist, so the human output, the envelope findings and the exit code can
409
+ * never disagree about whether something was a warning.
410
+ * @param {object} input - Mode, defects, report, and the parsed contract.
411
+ * @returns {object} The internal result.
412
+ */
413
+ function result({ mode, defects, report, contract }) {
414
+ const fatal = hasFatalDefect(mode, defects);
415
+ const graded = defects.map(item => ({
416
+ ...item,
417
+ severity:
418
+ mode === "enforced" || !WARNABLE_DEFECT_CODES.includes(item.code)
419
+ ? "error"
420
+ : "warning",
421
+ }));
422
+ return {
423
+ adoptionState: mode,
424
+ status: statusFor({ mode, defects: graded, fatal, report }),
425
+ defects: graded,
426
+ report,
427
+ contract,
428
+ };
429
+ }
430
+
431
+ /**
432
+ * Map the run onto the standard envelope's status vocabulary.
433
+ * @param {object} input - Mode, graded defects, fatality, and the report.
434
+ * @returns {string} An envelope status.
435
+ */
436
+ function statusFor({ mode, defects, fatal, report }) {
437
+ if (defects.some(item => INVALID_CODES.includes(item.code))) return "invalid";
438
+ if (fatal) return "failed";
439
+ if (mode === "not-adopted" && !report) return "not-adopted";
440
+ return "completed";
441
+ }
442
+
443
+ /**
444
+ * One operator-readable line naming what the gate proved, and what it did not.
445
+ * @param {object} run - The internal result.
446
+ * @returns {string} Summary line.
447
+ */
448
+ function summaryLine(run) {
449
+ const head = `bdd-coverage ${run.adoptionState}: ${run.status}`;
450
+ if (!run.report) return `${head} (${run.defects.length} findings)`;
451
+ const trace = run.report.traceability.overall;
452
+ const execution = run.report.execution.supplied
453
+ ? `${run.report.execution.executed}/${run.report.execution.mappedTests} mapped tests executed, ${run.report.execution.passed} passed / ${run.report.execution.failed} failed / ${run.report.execution.skipped} skipped`
454
+ : "no execution evidence supplied";
455
+ return `${head}; ${run.report.scenarios.declared} scenarios declared, traceability ${trace.covered}/${trace.total} (${trace.percentage}%), ${execution}, ${run.report.waived.count} waived, ${run.defects.length} findings`;
456
+ }
457
+
458
+ /**
459
+ * The one-sentence `reason` the envelope requires for a non-success status.
460
+ * @param {object} run - The internal result.
461
+ * @returns {string} The reason.
462
+ */
463
+ function reasonFor(run) {
464
+ const first = run.defects.find(item => item.severity === "error");
465
+ return first
466
+ ? `${first.code}: ${first.message}`
467
+ : `bdd-coverage ${run.adoptionState} did not complete`;
468
+ }
469
+
470
+ /**
471
+ * Convert the internal result into Lisa's standard command envelope.
472
+ *
473
+ * `mode` is the ENVELOPE's mode — the gate really runs, so it is always
474
+ * `real`. The BDD adoption state is a different axis and rides in
475
+ * `summary.adoptionState`, with `status: "not-adopted"` carrying it for a
476
+ * repo that has not wired the contract.
477
+ * @param {object} input - The result, the environment, and CLI options.
478
+ * @returns {object} An envelope conforming to lisa-command-envelope.v1.
479
+ */
480
+ export function toCommandEnvelope({
481
+ run: gateRun,
482
+ env,
483
+ options,
484
+ filesWritten,
485
+ }) {
486
+ const fields = {
487
+ capability: "bdd-coverage",
488
+ mode: "real",
489
+ operation: "check",
490
+ environment: env.BDD_ENVIRONMENT || "local",
491
+ contractVersion: contractVersion(gateRun.contract),
492
+ dryRun: !options.write,
493
+ status: gateRun.status,
494
+ correlationId: correlationId(env.BDD_CORRELATION_ID, {
495
+ adoptionState: gateRun.adoptionState,
496
+ status: gateRun.status,
497
+ summary: summaryLine(gateRun),
498
+ }),
499
+ summary: {
500
+ ...buildSummary({
501
+ adoptionState: gateRun.adoptionState,
502
+ report: gateRun.report,
503
+ defects: gateRun.defects,
504
+ filesWritten,
505
+ }),
506
+ headline: summaryLine(gateRun),
507
+ },
508
+ findings: gateRun.defects.map(item => ({
509
+ code: item.code,
510
+ subject: subjectFor(item),
511
+ message: item.message,
512
+ severity: item.severity,
513
+ })),
514
+ };
515
+ return SUCCESS_STATUSES.includes(gateRun.status)
516
+ ? fields
517
+ : { ...fields, reason: reasonFor(gateRun) };
518
+ }
519
+
520
+ /**
521
+ * Parse argv into options.
522
+ * @param {readonly string[]} argv - Process arguments.
523
+ * @param {Record<string, string|undefined>} env - Process environment.
524
+ * @returns {object} Parsed options.
525
+ */
526
+ export function parseArgs(argv, env) {
527
+ const resultFiles = [];
528
+ for (let index = 0; index < argv.length; index += 1) {
529
+ if (argv[index] === "--results" && argv[index + 1]) {
530
+ resultFiles.push(argv[index + 1]);
531
+ index += 1;
532
+ }
533
+ }
534
+ if (env.BDD_EXECUTION_RESULTS) {
535
+ resultFiles.push(
536
+ ...env.BDD_EXECUTION_RESULTS.split(",")
537
+ .map(item => item.trim())
538
+ .filter(Boolean)
539
+ );
540
+ }
541
+ return {
542
+ write: argv.includes("--write"),
543
+ json: argv.includes("--json"),
544
+ report: argv.includes("--report"),
545
+ resultFiles,
546
+ baseSha: env.BDD_BASE_SHA || null,
547
+ labels: (env.BDD_PR_LABELS ?? "")
548
+ .split(",")
549
+ .map(item => item.trim())
550
+ .filter(Boolean),
551
+ today: env.BDD_TODAY || new Date().toISOString().slice(0, 10),
552
+ };
553
+ }
554
+
555
+ /**
556
+ * CLI entry point.
557
+ *
558
+ * stdout carries exactly one machine-readable result — the standard command
559
+ * envelope — and human narration goes to stderr, per the envelope contract.
560
+ * @returns {Promise<void>} Resolves once the exit code is set.
561
+ */
562
+ async function main() {
563
+ const root = process.env.BDD_COVERAGE_ROOT || PACKAGE_ROOT;
564
+ const resolved = resolveMode(process.env);
565
+ if (resolved.error) {
566
+ console.error(`[bdd-coverage] ${resolved.error}`);
567
+ process.exitCode = 2;
568
+ return;
569
+ }
570
+ const options = {
571
+ ...parseArgs(process.argv.slice(2), process.env),
572
+ mode: resolved.mode,
573
+ };
574
+ const gateRun = run(root, options);
575
+ const filesWritten =
576
+ options.write && gateRun.report ? writeArtifacts(root, gateRun.report) : 0;
577
+ const envelope = await sealEnvelope({ gateRun, options, filesWritten });
578
+ // Exactly ONE machine-readable object on stdout. `--report` is a diagnostic
579
+ // that swaps the envelope for the detailed report; it never adds a second
580
+ // document, because a stream carrying two shapes has no schema at all.
581
+ if (options.report) console.log(JSON.stringify(gateRun.report, null, 2));
582
+ else if (options.json) console.log(JSON.stringify(envelope, null, 2));
583
+ printHuman(gateRun, envelope);
584
+ process.exitCode = SUCCESS_STATUSES.includes(envelope.status) ? 0 : 1;
585
+ }
586
+
587
+ /**
588
+ * Build the envelope through the shared module when it is installed, so its
589
+ * validator — not this file — decides conformance.
590
+ *
591
+ * When the shared module is absent the same object is emitted unvalidated
592
+ * rather than nothing: a gate that produces no result is indistinguishable
593
+ * from one that passed.
594
+ * @param {object} input - The gate run, CLI options, and files written.
595
+ * @returns {Promise<object>} The envelope.
596
+ */
597
+ async function sealEnvelope({ gateRun, options, filesWritten }) {
598
+ const fields = toCommandEnvelope({
599
+ run: gateRun,
600
+ env: process.env,
601
+ options,
602
+ filesWritten,
603
+ });
604
+ const shared = await loadEnvelopeModule(
605
+ path.dirname(fileURLToPath(import.meta.url))
606
+ );
607
+ if (!shared) {
608
+ console.error(
609
+ "[bdd-coverage] scripts/lisa-command-envelope.mjs is not installed; emitting the envelope without validating it against the published schema."
610
+ );
611
+ return { schemaVersion: "lisa-command-envelope-v1", ...fields };
612
+ }
613
+ return shared.buildEnvelope(fields);
614
+ }
615
+
616
+ /**
617
+ * Write the regenerated machine report and burndown.
618
+ * @param {string} root - Repo root.
619
+ * @param {object} report - The report.
620
+ * @returns {number} How many files were written, for the envelope's counters.
621
+ */
622
+ function writeArtifacts(root, report) {
623
+ fs.writeFileSync(
624
+ path.join(root, "bdd", "coverage-report.json"),
625
+ `${JSON.stringify(report, null, 2)}\n`
626
+ );
627
+ fs.mkdirSync(path.join(root, "docs"), { recursive: true });
628
+ fs.writeFileSync(
629
+ path.join(root, "docs", "e2e-bdd-coverage.md"),
630
+ `${renderBurndown(report).trim()}\n`
631
+ );
632
+ return 2;
633
+ }
634
+
635
+ /**
636
+ * Narrate the result on stderr, which the envelope contract reserves for
637
+ * humans so stdout stays exactly one machine-readable object.
638
+ * @param {object} gateRun - The internal result.
639
+ * @param {object} envelope - The emitted envelope.
640
+ * @returns {void}
641
+ */
642
+ function printHuman(gateRun, envelope) {
643
+ for (const item of gateRun.defects) {
644
+ console.error(
645
+ `[bdd-coverage] ${item.severity}: ${item.code}: ${item.message}`
646
+ );
647
+ }
648
+ if (gateRun.adoptionState === "bootstrap" && gateRun.defects.length > 0) {
649
+ console.error(
650
+ "[bdd-coverage] bootstrap: warnings above are visible, not blockers, until this repo advances to enforced. Anything reported as `error` fails even here."
651
+ );
652
+ }
653
+ console.error(`[bdd-coverage] ${envelope.summary.headline}`);
654
+ }
655
+
656
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
657
+ main();
@@ -0,0 +1,52 @@
1
+ {
2
+ "_readme": [
3
+ "See typescript/create-only/.github/required-checks.json in Lisa for the full rationale. In short: GitHub counts a SKIPPED required status check as SATISFIED, so a `skip_jobs` token that silences a required context makes the merge gate decorative.",
4
+ "This expo seed declares the three tokens the shipped ci.yml skips. Every one of them is currently NOT ruleset-required, so nothing here is a false green — but the declaration exists so that the day one of them BECOMES required, the guard says so instead of the gate quietly going hollow.",
5
+ "Transcribe `required_contexts` byte for byte from your ruleset and keep it current with `--remote`.",
6
+ "The nightly-gate context is listed because Lisa ships expo/github-rulesets/nightly-e2e-health.json requiring it on dev. No skip_jobs token silences it today, so nothing is a false green — it is here so that the day one does, the guard says so instead of staying quiet."
7
+ ],
8
+ "ruleset": {
9
+ "repo": "",
10
+ "ids": [],
11
+ "baseline_fetched_at": ""
12
+ },
13
+ "workflows": [
14
+ ".github/workflows/ci.yml"
15
+ ],
16
+ "exemption_ticket_pattern": "^[A-Z][A-Z0-9]+-\\d+$",
17
+ "required_contexts": [
18
+ "🔍 Quality Checks / 🧹 Lint",
19
+ "🔍 Quality Checks / 🔍 Type Check",
20
+ "🔍 Quality Checks / 🏗️ Build",
21
+ "🔍 Quality Checks / 📐 Check Formatting",
22
+ "🔍 Quality Checks / 🔒 Security Scan",
23
+ "🔍 Quality Checks / 🧪 Run Unit Tests",
24
+ "🔍 Quality Checks / 🧪 Run Integration Tests",
25
+ "🔍 Quality Checks / 🔗 Work-Item Traceability",
26
+ "🌙 Nightly E2E Health / 🌙 Gate"
27
+ ],
28
+ "skip_job_declarations": {
29
+ "test:e2e": {
30
+ "suppressed_contexts": [
31
+ "🔍 Quality Checks / 🧪 Run E2E Tests"
32
+ ],
33
+ "ruleset_required": false,
34
+ "reason": "A fresh Expo project has no `test:e2e` script. Lisa's quality callee guards each job on script existence, so un-skipping it would produce a real run that succeeds having tested nothing — a worse false green than the skip. Not a required context, so skipping removes a check rather than faking one."
35
+ },
36
+ "playwright_e2e": {
37
+ "suppressed_contexts": [
38
+ "🔍 Quality Checks / 🎭 Playwright Shard Setup",
39
+ "🔍 Quality Checks / 🎭 Playwright E2E Tests"
40
+ ],
41
+ "ruleset_required": false,
42
+ "reason": "Note this ONE token silences TWO jobs. Neither is a required context. Browser e2e is gated by the nightly suite plus `🌙 Nightly E2E Health`, not by a PR-time Playwright run — which is exactly why the old `playwright` ruleset template was deleted: it required a context PRs skip, and GitHub counts skipped as satisfied."
43
+ },
44
+ "zap_baseline": {
45
+ "suppressed_contexts": [
46
+ "🔍 Quality Checks / 🕷️ OWASP ZAP Baseline"
47
+ ],
48
+ "ruleset_required": false,
49
+ "reason": "DAST needs a deployed target and is run against staging, not on every PR. Not a required context."
50
+ }
51
+ }
52
+ }