@codyswann/lisa 3.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +50 -23
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/copy-overwrite/scripts/bdd/baseline.mjs +211 -121
- package/expo/copy-overwrite/scripts/bdd/contract.mjs +10 -2
- package/expo/copy-overwrite/scripts/bdd/envelope.mjs +3 -2
- package/expo/copy-overwrite/scripts/bdd/render.mjs +2 -2
- package/expo/copy-overwrite/scripts/check-bdd-coverage.mjs +45 -8
- package/expo/copy-overwrite/scripts/classify-maestro-failures.mjs +775 -0
- package/expo/create-only/.github/workflows/nightly-e2e-report.yml +71 -0
- package/expo/create-only/.maestro/flake-classification.json +21 -0
- package/expo/create-only/bdd/coverage-map.json +1 -2
- package/expo/package-lisa/package.lisa.json +1 -0
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-atlassian-access/SKILL.md +75 -64
- package/plugins/lisa/.codex-plugin/skills/lisa-jam-access/SKILL.md +13 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-access/SKILL.md +30 -10
- package/plugins/lisa/.codex-plugin/skills/lisa-notion-access/SKILL.md +36 -23
- package/plugins/lisa/.codex-plugin/skills/lisa-posthog-access/SKILL.md +16 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +8 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-sentry-access/SKILL.md +16 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-sonarcloud-access/SKILL.md +7 -1
- package/plugins/lisa/hooks/threshold-ratchet-families.mjs +24 -0
- package/plugins/lisa/rules/eager/credential-substrate-precedence.md +52 -0
- package/plugins/lisa/rules/eager/integration-access-layer.md +7 -3
- package/plugins/lisa/rules/reference/bdd-e2e-coverage.md +19 -8
- package/plugins/lisa/rules/reference/credential-substrate-precedence.md +166 -0
- package/plugins/lisa/rules/reference/integration-access-layer.md +27 -15
- package/plugins/lisa/skills/lisa-atlassian-access/SKILL.md +76 -65
- package/plugins/lisa/skills/lisa-jam-access/SKILL.md +14 -6
- package/plugins/lisa/skills/lisa-linear-access/SKILL.md +31 -11
- package/plugins/lisa/skills/lisa-notion-access/SKILL.md +37 -24
- package/plugins/lisa/skills/lisa-posthog-access/SKILL.md +17 -7
- package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +8 -0
- package/plugins/lisa/skills/lisa-sentry-access/SKILL.md +17 -7
- package/plugins/lisa/skills/lisa-sonarcloud-access/SKILL.md +7 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-atlassian-access/SKILL.md +76 -65
- package/plugins/lisa-agy/skills/lisa-jam-access/SKILL.md +14 -6
- package/plugins/lisa-agy/skills/lisa-linear-access/SKILL.md +31 -11
- package/plugins/lisa-agy/skills/lisa-notion-access/SKILL.md +37 -24
- package/plugins/lisa-agy/skills/lisa-posthog-access/SKILL.md +17 -7
- package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +8 -0
- package/plugins/lisa-agy/skills/lisa-sentry-access/SKILL.md +17 -7
- package/plugins/lisa-agy/skills/lisa-sonarcloud-access/SKILL.md +7 -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/hooks/threshold-ratchet-families.mjs +24 -0
- package/plugins/lisa-copilot/rules/eager/credential-substrate-precedence.md +52 -0
- package/plugins/lisa-copilot/rules/eager/integration-access-layer.md +7 -3
- package/plugins/lisa-copilot/rules/reference/bdd-e2e-coverage.md +19 -8
- package/plugins/lisa-copilot/rules/reference/credential-substrate-precedence.md +166 -0
- package/plugins/lisa-copilot/rules/reference/integration-access-layer.md +27 -15
- package/plugins/lisa-copilot/skills/lisa-atlassian-access/SKILL.md +76 -65
- package/plugins/lisa-copilot/skills/lisa-jam-access/SKILL.md +14 -6
- package/plugins/lisa-copilot/skills/lisa-linear-access/SKILL.md +31 -11
- package/plugins/lisa-copilot/skills/lisa-notion-access/SKILL.md +37 -24
- package/plugins/lisa-copilot/skills/lisa-posthog-access/SKILL.md +17 -7
- package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +8 -0
- package/plugins/lisa-copilot/skills/lisa-sentry-access/SKILL.md +17 -7
- package/plugins/lisa-copilot/skills/lisa-sonarcloud-access/SKILL.md +7 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/threshold-ratchet-families.mjs +24 -0
- package/plugins/lisa-cursor/rules/bdd-e2e-coverage-reference.mdc +19 -8
- package/plugins/lisa-cursor/rules/credential-substrate-precedence-reference.mdc +171 -0
- package/plugins/lisa-cursor/rules/credential-substrate-precedence.mdc +57 -0
- package/plugins/lisa-cursor/rules/integration-access-layer-reference.mdc +27 -15
- package/plugins/lisa-cursor/rules/integration-access-layer.mdc +7 -3
- package/plugins/lisa-cursor/skills/lisa-atlassian-access/SKILL.md +76 -65
- package/plugins/lisa-cursor/skills/lisa-jam-access/SKILL.md +14 -6
- package/plugins/lisa-cursor/skills/lisa-linear-access/SKILL.md +31 -11
- package/plugins/lisa-cursor/skills/lisa-notion-access/SKILL.md +37 -24
- package/plugins/lisa-cursor/skills/lisa-posthog-access/SKILL.md +17 -7
- package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +8 -0
- package/plugins/lisa-cursor/skills/lisa-sentry-access/SKILL.md +17 -7
- package/plugins/lisa-cursor/skills/lisa-sonarcloud-access/SKILL.md +7 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/threshold-ratchet-families.mjs +24 -0
- package/plugins/src/base/rules/eager/credential-substrate-precedence.md +52 -0
- package/plugins/src/base/rules/eager/integration-access-layer.md +7 -3
- package/plugins/src/base/rules/reference/bdd-e2e-coverage.md +19 -8
- package/plugins/src/base/rules/reference/credential-substrate-precedence.md +166 -0
- package/plugins/src/base/rules/reference/integration-access-layer.md +27 -15
- package/plugins/src/base/skills/lisa-atlassian-access/SKILL.md +76 -65
- package/plugins/src/base/skills/lisa-jam-access/SKILL.md +14 -6
- package/plugins/src/base/skills/lisa-linear-access/SKILL.md +31 -11
- package/plugins/src/base/skills/lisa-notion-access/SKILL.md +37 -24
- package/plugins/src/base/skills/lisa-posthog-access/SKILL.md +17 -7
- package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +8 -0
- package/plugins/src/base/skills/lisa-sentry-access/SKILL.md +17 -7
- package/plugins/src/base/skills/lisa-sonarcloud-access/SKILL.md +7 -1
- package/rails/copy-overwrite/scripts/threshold-ratchet-families.mjs +24 -0
- package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +631 -5
- package/typescript/copy-overwrite/scripts/threshold-ratchet-families.mjs +24 -0
|
@@ -1,20 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Base-revision comparisons: the
|
|
3
|
-
* scenario-deletion check.
|
|
2
|
+
* Base-revision comparisons: the non-regression invariants.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* base revision to answer it, so they share one git read
|
|
4
|
+
* Three checks, one question — "did this change make the number look better by
|
|
5
|
+
* describing less of the product, rather than by covering more of it?" — and
|
|
6
|
+
* all three need a base revision to answer it, so they share one git read:
|
|
7
|
+
*
|
|
8
|
+
* 1. `coverage-regression` Coverage the repo already accepted cannot be
|
|
9
|
+
* given back.
|
|
10
|
+
* 2. `obligation-uncovered` Behavior that is NEW here is mapped or waived.
|
|
11
|
+
* 3. `scenario-deleted` A retired behavior is `@superseded` with a
|
|
12
|
+
* record, never quietly deleted.
|
|
13
|
+
*
|
|
14
|
+
* These replaced a numeric ratchet on `coverageFloor` (a floor that could only
|
|
15
|
+
* rise, whose reduction needed a `coverageFloorBaseline` record plus a
|
|
16
|
+
* maintainer label). The number is still a gate — `floor-regression` still
|
|
17
|
+
* fails a platform sitting below its committed floor, and `floor-invalid`
|
|
18
|
+
* still refuses a floor written so it cannot be evaluated — but the floor no
|
|
19
|
+
* longer carries the non-regression duty, because a number is a bad instrument
|
|
20
|
+
* for it. A percentage can hold steady while a specific accepted behavior
|
|
21
|
+
* loses its automation and an easier one gains some; and keeping a number
|
|
22
|
+
* honest costs a recurring "nudge the value" pull request that proves nothing.
|
|
23
|
+
* Checks 1 and 2 are strictly stronger: they are per obligation, they cannot
|
|
24
|
+
* be satisfied by an offsetting gain elsewhere, and they shrink to zero as
|
|
25
|
+
* waivers retire instead of accumulating.
|
|
8
26
|
*
|
|
9
27
|
* @module scripts/bdd/baseline
|
|
10
28
|
*/
|
|
11
29
|
import { spawnSync } from "node:child_process";
|
|
12
30
|
|
|
13
|
-
import {
|
|
31
|
+
import { declaredPlatforms } from "./contract.mjs";
|
|
32
|
+
import { parseFeatureSource, scenarioIdsIn } from "./parse.mjs";
|
|
14
33
|
|
|
15
34
|
const defect = (code, message) => ({ code, message });
|
|
16
35
|
|
|
17
|
-
/** The maintainer-applied PR label that authorizes
|
|
36
|
+
/** The maintainer-applied PR label that authorizes giving coverage back. */
|
|
18
37
|
export const BASELINE_LABEL = "bdd-floor-baseline";
|
|
19
38
|
|
|
20
39
|
/**
|
|
@@ -53,12 +72,19 @@ function featureFilesAt(root, revision) {
|
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
/**
|
|
56
|
-
* Load the base revision's contract and
|
|
75
|
+
* Load the base revision's contract and its parsed scenarios.
|
|
76
|
+
*
|
|
77
|
+
* The base is parsed against the UNION of the base's and the head's platform
|
|
78
|
+
* vocabularies. Using head's alone would make a base scenario's `@web` read as
|
|
79
|
+
* an unknown tag the moment a pull request deleted the web runner from
|
|
80
|
+
* `runnerPlatforms` — which would turn "I deleted the runner that proved this"
|
|
81
|
+
* into an invisible change rather than the coverage loss it is.
|
|
57
82
|
* @param {string} root - Repo root.
|
|
58
83
|
* @param {string} revision - Base commit-ish.
|
|
59
|
-
* @
|
|
84
|
+
* @param {ReadonlySet<string>} headPlatforms - The head contract's platforms.
|
|
85
|
+
* @returns {{available: boolean, contract: object|null, scenarios: object[], scenarioIds: Set<string>}} Base state.
|
|
60
86
|
*/
|
|
61
|
-
export function loadBaseline(root, revision) {
|
|
87
|
+
export function loadBaseline(root, revision, headPlatforms = new Set()) {
|
|
62
88
|
const raw = showAtRevision(root, revision, "bdd/coverage-map.json");
|
|
63
89
|
let contract = null;
|
|
64
90
|
if (raw !== null) {
|
|
@@ -68,145 +94,211 @@ export function loadBaseline(root, revision) {
|
|
|
68
94
|
contract = null;
|
|
69
95
|
}
|
|
70
96
|
}
|
|
71
|
-
const
|
|
72
|
-
.map(file => showAtRevision(root, revision, file))
|
|
73
|
-
.filter(
|
|
97
|
+
const documents = featureFilesAt(root, revision)
|
|
98
|
+
.map(file => ({ file, source: showAtRevision(root, revision, file) }))
|
|
99
|
+
.filter(document => document.source !== null);
|
|
100
|
+
const platforms = new Set([
|
|
101
|
+
...declaredPlatforms(contract?.runnerPlatforms),
|
|
102
|
+
...headPlatforms,
|
|
103
|
+
]);
|
|
74
104
|
return {
|
|
75
|
-
available: raw !== null ||
|
|
105
|
+
available: raw !== null || documents.length > 0,
|
|
76
106
|
contract,
|
|
77
|
-
|
|
107
|
+
scenarios: documents.flatMap(document =>
|
|
108
|
+
parseFeatureSource(document.source, document.file, platforms)
|
|
109
|
+
),
|
|
110
|
+
scenarioIds: scenarioIdsIn(documents.map(document => document.source)),
|
|
78
111
|
};
|
|
79
112
|
}
|
|
80
113
|
|
|
81
114
|
/**
|
|
82
|
-
*
|
|
115
|
+
* Every `SCENARIO:platform` a contract's mappings actually claim.
|
|
83
116
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* @
|
|
117
|
+
* Structural only — it deliberately does NOT ask whether each mapping's
|
|
118
|
+
* evidence string still resolves. Evidence rot is already its own defect
|
|
119
|
+
* (`mapping-evidence`) and already drops the obligation out of the reported
|
|
120
|
+
* percentage; counting it here too would report one act twice and would make
|
|
121
|
+
* these checks depend on reading files out of a git revision.
|
|
122
|
+
* @param {readonly object[]} scenarios - Parsed scenarios for that revision.
|
|
123
|
+
* @param {object|null} contract - That revision's coverage map.
|
|
124
|
+
* @returns {Set<string>} Accepted keys.
|
|
90
125
|
*/
|
|
91
|
-
export function
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
platform =>
|
|
104
|
-
!isUsableFloor(after[platform]) || after[platform] < before[platform]
|
|
105
|
-
)
|
|
106
|
-
.map(platform => ({
|
|
107
|
-
platform,
|
|
108
|
-
from: before[platform],
|
|
109
|
-
to: isUsableFloor(after[platform]) ? after[platform] : null,
|
|
110
|
-
malformed:
|
|
111
|
-
after[platform] !== undefined && !isUsableFloor(after[platform]),
|
|
112
|
-
}));
|
|
113
|
-
return drops.flatMap(drop => ratchetDefects(drop, contract, labels));
|
|
126
|
+
export function acceptedKeys(scenarios, contract) {
|
|
127
|
+
const required = new Set(
|
|
128
|
+
scenarios.filter(scenario => scenario.required).map(scenario => scenario.id)
|
|
129
|
+
);
|
|
130
|
+
const keys = new Set();
|
|
131
|
+
for (const mapping of contract?.mappings ?? []) {
|
|
132
|
+
if (!required.has(mapping.scenario)) continue;
|
|
133
|
+
for (const platform of mapping.platforms ?? []) {
|
|
134
|
+
keys.add(`${mapping.scenario}:${platform}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return keys;
|
|
114
138
|
}
|
|
115
139
|
|
|
116
140
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* @
|
|
122
|
-
|
|
141
|
+
* Every `SCENARIO:platform` a revision OWES — required scenarios expanded over
|
|
142
|
+
* the platforms they declare, less the ones a waiver removed.
|
|
143
|
+
* @param {readonly object[]} scenarios - Parsed scenarios for that revision.
|
|
144
|
+
* @param {object|null} contract - That revision's coverage map.
|
|
145
|
+
* @returns {Set<string>} Obligation keys.
|
|
146
|
+
*/
|
|
147
|
+
export function obligationKeys(scenarios, contract) {
|
|
148
|
+
const waived = waivedKeys(contract);
|
|
149
|
+
const keys = new Set();
|
|
150
|
+
for (const scenario of scenarios) {
|
|
151
|
+
if (!scenario.required) continue;
|
|
152
|
+
for (const platform of scenario.platforms) {
|
|
153
|
+
const key = `${scenario.id}:${platform}`;
|
|
154
|
+
if (!waived.has(key)) keys.add(key);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return keys;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Every `SCENARIO:platform` a waiver removes from the denominator.
|
|
162
|
+
* @param {object|null} contract - A coverage map.
|
|
163
|
+
* @returns {Set<string>} Waived keys.
|
|
123
164
|
*/
|
|
124
|
-
|
|
125
|
-
return (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
value <= 100
|
|
165
|
+
function waivedKeys(contract) {
|
|
166
|
+
return new Set(
|
|
167
|
+
(contract?.platformWaivers ?? []).flatMap(waiver =>
|
|
168
|
+
(waiver.platforms ?? []).map(platform => `${waiver.scenario}:${platform}`)
|
|
169
|
+
)
|
|
130
170
|
);
|
|
131
171
|
}
|
|
132
172
|
|
|
133
173
|
/**
|
|
134
|
-
*
|
|
135
|
-
* @param {
|
|
136
|
-
* @returns {string}
|
|
174
|
+
* Split a `SCENARIO:platform` key back into its parts.
|
|
175
|
+
* @param {string} key - The key.
|
|
176
|
+
* @returns {{scenario: string, platform: string}} Its parts.
|
|
137
177
|
*/
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
return drop.to === null
|
|
143
|
-
? `coverageFloor.${drop.platform} was removed (was ${drop.from})`
|
|
144
|
-
: `coverageFloor.${drop.platform} lowered ${drop.from} → ${drop.to}`;
|
|
178
|
+
function partsOf(key) {
|
|
179
|
+
const at = key.indexOf(":");
|
|
180
|
+
return { scenario: key.slice(0, at), platform: key.slice(at + 1) };
|
|
145
181
|
}
|
|
146
182
|
|
|
147
183
|
/**
|
|
148
|
-
*
|
|
149
|
-
* @
|
|
150
|
-
*
|
|
184
|
+
* Fields a retirement record owes whoever has to re-litigate it later.
|
|
185
|
+
* Shared with {@link checkDeletions} so the two routes out of the denominator
|
|
186
|
+
* — deleting the scenario and un-mapping it — cannot demand different proof.
|
|
187
|
+
*/
|
|
188
|
+
const RETIREMENT_FIELDS = ["reason", "ticket", "approvedBy", "recordedAt"];
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Whether a scenario has been retired the way the contract requires: a
|
|
192
|
+
* complete `retirements` record AND the maintainer-applied label. Either alone
|
|
193
|
+
* is not an authorization — that is the whole point of asking for two
|
|
194
|
+
* artifacts one author cannot produce by editing one file.
|
|
195
|
+
* @param {object|undefined} record - The retirements entry, if any.
|
|
151
196
|
* @param {readonly string[]} labels - PR labels.
|
|
197
|
+
* @returns {boolean} True when the retirement is authorized and complete.
|
|
198
|
+
*/
|
|
199
|
+
function isRetired(record, labels) {
|
|
200
|
+
if (!record || !labels.includes(BASELINE_LABEL)) return false;
|
|
201
|
+
return RETIREMENT_FIELDS.every(field => Boolean(record[field]));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Coverage the repo already accepted cannot be given back.
|
|
206
|
+
*
|
|
207
|
+
* This is the invariant that replaced the floor ratchet, and it is deliberately
|
|
208
|
+
* blind to the percentage: an obligation that was mapped at the base revision
|
|
209
|
+
* and is not mapped here is a regression even when the headline number went UP
|
|
210
|
+
* because easier behavior was covered in the same change.
|
|
211
|
+
*
|
|
212
|
+
* Keys whose scenario disappeared from the contract entirely are left to
|
|
213
|
+
* {@link checkDeletions}, which names that act precisely; reporting both would
|
|
214
|
+
* describe one deletion as two different failures.
|
|
215
|
+
* @param {object} input - Baseline, head contract and scenarios, and PR labels.
|
|
152
216
|
* @returns {object[]} Defects found.
|
|
153
217
|
*/
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
218
|
+
export function checkCoverageRegression({
|
|
219
|
+
baseline,
|
|
220
|
+
contract,
|
|
221
|
+
scenarios,
|
|
222
|
+
labels,
|
|
223
|
+
}) {
|
|
224
|
+
const before = acceptedKeys(baseline.scenarios, baseline.contract);
|
|
225
|
+
const after = acceptedKeys(scenarios, contract);
|
|
226
|
+
const present = new Set(scenarios.map(scenario => scenario.id));
|
|
227
|
+
const retired = new Map(
|
|
228
|
+
(contract.retirements ?? []).map(entry => [entry.scenario, entry])
|
|
161
229
|
);
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
230
|
+
const waived = waivedKeys(contract);
|
|
231
|
+
return [...before]
|
|
232
|
+
.filter(key => !after.has(key) && present.has(partsOf(key).scenario))
|
|
233
|
+
.sort()
|
|
234
|
+
.flatMap(key => regressionDefect(key, { retired, waived, labels }));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The defect for one obligation whose accepted coverage disappeared, or none
|
|
239
|
+
* when it left the denominator through an authorized route.
|
|
240
|
+
* @param {string} key - The `SCENARIO:platform` key.
|
|
241
|
+
* @param {object} input - Retirement records, waived keys, and PR labels.
|
|
242
|
+
* @returns {object[]} Zero or one defect.
|
|
243
|
+
*/
|
|
244
|
+
function regressionDefect(key, { retired, waived, labels }) {
|
|
245
|
+
const { scenario, platform } = partsOf(key);
|
|
246
|
+
if (isRetired(retired.get(scenario), labels)) return [];
|
|
247
|
+
if (waived.has(key) && labels.includes(BASELINE_LABEL)) return [];
|
|
248
|
+
return [
|
|
249
|
+
defect(
|
|
250
|
+
"coverage-regression",
|
|
251
|
+
`${scenario}:${platform} was covered at the base revision and is not covered here. Coverage this repository already accepted cannot be handed back quietly. Restore the mapping, or take one of the two recorded routes out — a retirements record (${RETIREMENT_FIELDS.join(", ")}) for a behavior the product no longer has, or a platformWaivers entry for a runner that cannot decide it — and get the maintainer-applied "${BASELINE_LABEL}" label on this pull request. ${routeTaken(key, { retired, waived, labels })}`
|
|
252
|
+
),
|
|
253
|
+
];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Say which half of the authorization is missing, so the operator is told what
|
|
258
|
+
* to do rather than only what went wrong.
|
|
259
|
+
* @param {string} key - The `SCENARIO:platform` key.
|
|
260
|
+
* @param {object} input - Retirement records, waived keys, labels, contract.
|
|
261
|
+
* @returns {string} A one-sentence next step.
|
|
262
|
+
*/
|
|
263
|
+
function routeTaken(key, { retired, waived, labels }) {
|
|
264
|
+
const { scenario } = partsOf(key);
|
|
265
|
+
const record = retired.get(scenario);
|
|
266
|
+
const authorized = labels.includes(BASELINE_LABEL);
|
|
267
|
+
if ((record || waived.has(key)) && !authorized) {
|
|
268
|
+
return `A record is present but the "${BASELINE_LABEL}" label is not: recording a reduction in the same pull request that makes it is not an authorization.`;
|
|
172
269
|
}
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
"floor-ratchet",
|
|
177
|
-
`${what}: requires the maintainer-applied "${BASELINE_LABEL}" label. Changing the floor in the same pull request that changes the code is not an authorization.`
|
|
178
|
-
)
|
|
179
|
-
);
|
|
270
|
+
if (record) {
|
|
271
|
+
const missing = RETIREMENT_FIELDS.filter(field => !record[field]);
|
|
272
|
+
return `Its retirements record is incomplete (no ${missing.join(", no ")}).`;
|
|
180
273
|
}
|
|
181
|
-
return
|
|
274
|
+
return "Neither a retirements record nor a waiver was found for it.";
|
|
182
275
|
}
|
|
183
276
|
|
|
184
277
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
278
|
+
* Behavior that is NEW here is mapped or waived.
|
|
279
|
+
*
|
|
280
|
+
* The companion to {@link checkCoverageRegression}: without it a repository
|
|
281
|
+
* could hold every accepted obligation and still let the product outrun its
|
|
282
|
+
* contract, one unmapped scenario at a time. Pre-existing gaps are untouched —
|
|
283
|
+
* an obligation that was already owed at the base revision is burndown, not a
|
|
284
|
+
* regression — which is what lets a brownfield project adopt `enforced`
|
|
285
|
+
* without first backfilling its whole history.
|
|
286
|
+
* @param {object} input - Baseline plus the head contract and scenarios.
|
|
190
287
|
* @returns {object[]} Defects found.
|
|
191
288
|
*/
|
|
192
|
-
function
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
`${what}: coverageFloorBaseline.runUrl must be an https URL`
|
|
206
|
-
)
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
return defects;
|
|
289
|
+
export function checkNewObligations({ baseline, contract, scenarios }) {
|
|
290
|
+
const before = obligationKeys(baseline.scenarios, baseline.contract);
|
|
291
|
+
const covered = acceptedKeys(scenarios, contract);
|
|
292
|
+
return [...obligationKeys(scenarios, contract)]
|
|
293
|
+
.filter(key => !before.has(key) && !covered.has(key))
|
|
294
|
+
.sort()
|
|
295
|
+
.map(key => {
|
|
296
|
+
const { scenario, platform } = partsOf(key);
|
|
297
|
+
return defect(
|
|
298
|
+
"obligation-uncovered",
|
|
299
|
+
`${scenario}:${platform} is new here and nothing covers it. New behavior arrives mapped to an automated test or waived with a dated, owned platformWaivers entry — the contract does not accept a third answer. (Behavior that was already uncovered before this change is burndown, not a defect, and is listed in the report's gaps.)`
|
|
300
|
+
);
|
|
301
|
+
});
|
|
210
302
|
}
|
|
211
303
|
|
|
212
304
|
/**
|
|
@@ -245,9 +337,7 @@ function deletionDefects(id, record, labels) {
|
|
|
245
337
|
),
|
|
246
338
|
];
|
|
247
339
|
}
|
|
248
|
-
const missing =
|
|
249
|
-
field => !record[field]
|
|
250
|
-
);
|
|
340
|
+
const missing = RETIREMENT_FIELDS.filter(field => !record[field]);
|
|
251
341
|
const defects = missing.map(field =>
|
|
252
342
|
defect("scenario-deleted", `${id}: retirements record has no ${field}`)
|
|
253
343
|
);
|
|
@@ -9,8 +9,16 @@
|
|
|
9
9
|
* @module scripts/bdd/contract
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Report/envelope schema emitted by the gate. See docs/bdd-coverage-schema.md.
|
|
14
|
+
*
|
|
15
|
+
* v3 retired the `floor-ratchet` defect code — defect codes are API, and the
|
|
16
|
+
* compatibility policy retires one only with a bump. The report's own fields
|
|
17
|
+
* are unchanged, and `coverage-map.schemaVersion` is untouched: a v2 map still
|
|
18
|
+
* reads exactly as before, its now-unused `coverageFloorBaseline` simply
|
|
19
|
+
* ignored.
|
|
20
|
+
*/
|
|
21
|
+
export const REPORT_SCHEMA_VERSION = 3;
|
|
14
22
|
|
|
15
23
|
/** Coverage-map schema versions this gate can read. */
|
|
16
24
|
export const SUPPORTED_MAP_SCHEMA_VERSIONS = [1, 2];
|
|
@@ -42,17 +42,17 @@ const ENVELOPE_MODULE_PATHS = Object.freeze([
|
|
|
42
42
|
* adoption-drift, config-*, discovery-invalid) are deliberately absent, so
|
|
43
43
|
* they always fail: a malformed discovery block would silently switch off the
|
|
44
44
|
* only check that can see an undeclared test, exactly as a quoted coverage
|
|
45
|
-
* floor silently switches off the
|
|
45
|
+
* floor silently switches off the bar (`floor-invalid`, likewise absent).
|
|
46
46
|
*/
|
|
47
47
|
export const WARNABLE_DEFECT_CODES = Object.freeze([
|
|
48
48
|
"baseline",
|
|
49
|
+
"coverage-regression",
|
|
49
50
|
"discovery-missing",
|
|
50
51
|
"empty-contract",
|
|
51
52
|
"exclusion-metadata",
|
|
52
53
|
"exclusion-stale",
|
|
53
54
|
"execution-results",
|
|
54
55
|
"floor-missing",
|
|
55
|
-
"floor-ratchet",
|
|
56
56
|
"floor-regression",
|
|
57
57
|
"mapping-duplicate",
|
|
58
58
|
"mapping-evidence",
|
|
@@ -60,6 +60,7 @@ export const WARNABLE_DEFECT_CODES = Object.freeze([
|
|
|
60
60
|
"mapping-orphan",
|
|
61
61
|
"mapping-platform",
|
|
62
62
|
"mapping-runner",
|
|
63
|
+
"obligation-uncovered",
|
|
63
64
|
"scenario-deleted",
|
|
64
65
|
"scenario-duplicate-id",
|
|
65
66
|
"scenario-id",
|
|
@@ -215,9 +215,9 @@ ${platformTable(report)}
|
|
|
215
215
|
|
|
216
216
|
${executionSection(report)}
|
|
217
217
|
|
|
218
|
-
## Coverage floor
|
|
218
|
+
## Coverage floor
|
|
219
219
|
|
|
220
|
-
The committed floor
|
|
220
|
+
The committed floor is an absolute bar — "is this platform below it right now" — and nothing else. It is **not** a ratchet and nobody has to nudge it upward: what protects coverage already earned is a separate, per-obligation check. Coverage accepted at the base revision cannot disappear, and behavior that is new must arrive mapped or waived; either reduction takes a recorded route (a \`retirements\` record or a \`platformWaivers\` entry) plus the maintainer-applied \`bdd-floor-baseline\` label — two artifacts one author cannot produce alone.
|
|
221
221
|
|
|
222
222
|
${floorTable(report)}
|
|
223
223
|
|
|
@@ -17,10 +17,18 @@
|
|
|
17
17
|
* passed expiry is a failure, so bootstrap cannot become
|
|
18
18
|
* permanent.
|
|
19
19
|
* enforced Absence fails. A missing config, a malformed manifest, zero
|
|
20
|
-
* scenarios, zero mappings, any contract defect, a
|
|
21
|
-
*
|
|
20
|
+
* scenarios, zero mappings, any contract defect, a platform
|
|
21
|
+
* below its committed floor, coverage given back, new behavior
|
|
22
|
+
* nobody mapped or waived, a deleted scenario, or a run with no
|
|
23
|
+
* base revision to compare against all fail loudly. Only in
|
|
22
24
|
* this state is the check a required ruleset context.
|
|
23
25
|
*
|
|
26
|
+
* The committed `coverageFloor` is an ABSOLUTE BAR, not a ratchet: it answers
|
|
27
|
+
* "is this platform below the bar right now", and nothing stops a project
|
|
28
|
+
* lowering it. Non-regression is a separate, deterministic job done per
|
|
29
|
+
* obligation in `bdd/baseline.mjs` — see that module for why a number was the
|
|
30
|
+
* wrong instrument for it.
|
|
31
|
+
*
|
|
24
32
|
* A required context is NEVER auto-skipped: GitHub counts a skipped required
|
|
25
33
|
* check as passing, which is the exact anti-pattern this gate exists to
|
|
26
34
|
* avoid.
|
|
@@ -49,7 +57,12 @@ import {
|
|
|
49
57
|
loadEnvelopeModule,
|
|
50
58
|
subjectFor,
|
|
51
59
|
} from "./bdd/envelope.mjs";
|
|
52
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
checkCoverageRegression,
|
|
62
|
+
checkDeletions,
|
|
63
|
+
checkNewObligations,
|
|
64
|
+
loadBaseline,
|
|
65
|
+
} from "./bdd/baseline.mjs";
|
|
53
66
|
import {
|
|
54
67
|
disclosureDefects,
|
|
55
68
|
discoverSpecs,
|
|
@@ -258,24 +271,26 @@ function validateAll({
|
|
|
258
271
|
...discovery.defects,
|
|
259
272
|
...disclosureDefects({ root, contract, discovery }),
|
|
260
273
|
];
|
|
261
|
-
if (!options.baseSha) return defects;
|
|
262
|
-
const baseline = loadBaseline(root, options.baseSha);
|
|
274
|
+
if (!options.baseSha) return [...defects, ...missingBaseDefects(options)];
|
|
275
|
+
const baseline = loadBaseline(root, options.baseSha, platforms);
|
|
263
276
|
if (!baseline.available) {
|
|
264
277
|
return [
|
|
265
278
|
...defects,
|
|
266
279
|
defect(
|
|
267
280
|
"baseline",
|
|
268
|
-
`base revision ${options.baseSha} is not readable
|
|
281
|
+
`base revision ${options.baseSha} is not readable, so the non-regression checks could not run. A gate that cannot compare against a base does not get to report that nothing regressed.`
|
|
269
282
|
),
|
|
270
283
|
];
|
|
271
284
|
}
|
|
272
285
|
return [
|
|
273
286
|
...defects,
|
|
274
|
-
...
|
|
275
|
-
|
|
287
|
+
...checkCoverageRegression({
|
|
288
|
+
baseline,
|
|
276
289
|
contract,
|
|
290
|
+
scenarios,
|
|
277
291
|
labels: options.labels,
|
|
278
292
|
}),
|
|
293
|
+
...checkNewObligations({ baseline, contract, scenarios }),
|
|
279
294
|
...checkDeletions({
|
|
280
295
|
baseIds: baseline.scenarioIds,
|
|
281
296
|
scenarios,
|
|
@@ -285,6 +300,28 @@ function validateAll({
|
|
|
285
300
|
];
|
|
286
301
|
}
|
|
287
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Enforced mode owes a base revision.
|
|
305
|
+
*
|
|
306
|
+
* Non-regression is the whole of what protects accepted coverage now that the
|
|
307
|
+
* floor is a plain bar rather than a ratchet, and every one of those checks
|
|
308
|
+
* needs a base. Running without one used to skip them in silence, which is a
|
|
309
|
+
* gate reporting a property it never evaluated. Bootstrap stays quiet — it is
|
|
310
|
+
* non-blocking by construction — and a local run outside CI is not making a
|
|
311
|
+
* merge decision, so neither is asked for a base it does not have.
|
|
312
|
+
* @param {object} options - Parsed CLI/environment options.
|
|
313
|
+
* @returns {object[]} Zero or one defect.
|
|
314
|
+
*/
|
|
315
|
+
function missingBaseDefects(options) {
|
|
316
|
+
if (options.mode !== "enforced") return [];
|
|
317
|
+
return [
|
|
318
|
+
defect(
|
|
319
|
+
"baseline",
|
|
320
|
+
"enforced mode requires BDD_BASE_SHA: without a base revision the gate cannot tell coverage that was given back from coverage that was never there, so it refuses to claim either."
|
|
321
|
+
),
|
|
322
|
+
];
|
|
323
|
+
}
|
|
324
|
+
|
|
288
325
|
/**
|
|
289
326
|
* A malformed coverage floor, in EVERY adopted state.
|
|
290
327
|
*
|