@codyswann/lisa 4.64.2 → 4.64.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/all/create-only/.github/workflows/continuous-gates.yml +42 -2
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +24 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +12 -10
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/create-only/stryker.conf.json +1 -0
- package/package.json +5 -5
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/threshold-ratchet-compare.mjs +50 -8
- package/plugins/lisa/hooks/threshold-ratchet-families.mjs +24 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/threshold-ratchet-compare.mjs +50 -8
- package/plugins/lisa-copilot/hooks/threshold-ratchet-families.mjs +24 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/threshold-ratchet-compare.mjs +50 -8
- package/plugins/lisa-cursor/hooks/threshold-ratchet-families.mjs +24 -0
- 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-compare.mjs +50 -8
- package/plugins/src/base/hooks/threshold-ratchet-families.mjs +24 -0
- package/rails/copy-overwrite/scripts/threshold-ratchet-compare.mjs +50 -8
- package/rails/copy-overwrite/scripts/threshold-ratchet-families.mjs +24 -0
- package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +297 -25
- package/typescript/copy-overwrite/scripts/threshold-ratchet-compare.mjs +50 -8
- package/typescript/copy-overwrite/scripts/threshold-ratchet-families.mjs +24 -0
- package/typescript/create-only/stryker.conf.json +1 -0
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
extractStrykerConstraints,
|
|
19
19
|
extractStrykerMutate,
|
|
20
20
|
familyFor,
|
|
21
|
+
globRoot,
|
|
21
22
|
parseJson,
|
|
22
23
|
} from "./threshold-ratchet-families.mjs";
|
|
23
24
|
|
|
@@ -154,15 +155,56 @@ function compareStryker(relPath, base, current) {
|
|
|
154
155
|
);
|
|
155
156
|
const baseMutate = extractStrykerMutate(base);
|
|
156
157
|
const currentMutate = extractStrykerMutate(current);
|
|
158
|
+
// Roots the gate already covered, and roots this change adds. An exclusion
|
|
159
|
+
// scoped to a newly added root narrows territory that was not being mutated
|
|
160
|
+
// a moment ago, so it cannot shrink the gate — the change is a net widening.
|
|
161
|
+
//
|
|
162
|
+
// Without this, repairing an INERT `mutate` list is unreachable without a
|
|
163
|
+
// human-approved ratchet exception: the repair necessarily adds source roots
|
|
164
|
+
// AND the test-file exclusions that belong with them, and every one of those
|
|
165
|
+
// exclusions read as a weakening. Measured on two repositories whose real
|
|
166
|
+
// effect was 0 -> 69 and 0 -> 75 files mutated (CodySwannGT/lisa#4243).
|
|
167
|
+
const baseRoots = [...baseMutate.positives].map(globRoot);
|
|
168
|
+
/**
|
|
169
|
+
* Whether an exclusion might touch any baseline positive.
|
|
170
|
+
*
|
|
171
|
+
* Both containment directions matter: excluding `src` removes a baseline
|
|
172
|
+
* rooted at `src/existing`, too. An empty prefix (wildcards, braces, classes
|
|
173
|
+
* or extglobs at the root) might cover everything. We grant an exemption
|
|
174
|
+
* only for provably disjoint territory, never from a guessed glob overlap.
|
|
175
|
+
* @param {string} root A rooted glob prefix
|
|
176
|
+
* @returns {boolean} True when overlap cannot be ruled out
|
|
177
|
+
*/
|
|
178
|
+
const overlapsBase = root =>
|
|
179
|
+
baseRoots.some(
|
|
180
|
+
base =>
|
|
181
|
+
root === "" ||
|
|
182
|
+
base === "" ||
|
|
183
|
+
root === base ||
|
|
184
|
+
root.startsWith(`${base}/`) ||
|
|
185
|
+
base.startsWith(`${root}/`)
|
|
186
|
+
);
|
|
187
|
+
const addedRoots = [...currentMutate.positives]
|
|
188
|
+
.filter(positive => !baseMutate.positives.has(positive))
|
|
189
|
+
.map(globRoot)
|
|
190
|
+
.filter(root => root !== "");
|
|
191
|
+
/**
|
|
192
|
+
* Whether a root lies inside territory THIS change is adding.
|
|
193
|
+
* @param {string} root A rooted glob prefix
|
|
194
|
+
* @returns {boolean} True when an added root equals or contains it
|
|
195
|
+
*/
|
|
196
|
+
const underAdded = root =>
|
|
197
|
+
addedRoots.some(added => root === added || root.startsWith(`${added}/`));
|
|
157
198
|
for (const negation of currentMutate.negations) {
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
199
|
+
if (baseMutate.negations.has(negation)) continue;
|
|
200
|
+
const root = globRoot(negation);
|
|
201
|
+
if (!overlapsBase(root) && underAdded(root)) continue;
|
|
202
|
+
findings.push({
|
|
203
|
+
file: relPath,
|
|
204
|
+
key: `mutate ${negation}`,
|
|
205
|
+
type: TYPE_EXEMPTION_ADDED,
|
|
206
|
+
message: `${relPath}: new mutation-testing exclusion "${negation}" — excluding files from a gate is a weakening.`,
|
|
207
|
+
});
|
|
166
208
|
}
|
|
167
209
|
for (const positive of baseMutate.positives) {
|
|
168
210
|
if (!currentMutate.positives.has(positive)) {
|
|
@@ -308,6 +308,30 @@ export function extractStrykerMutate(conf) {
|
|
|
308
308
|
return { positives, negations };
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/**
|
|
312
|
+
* The literal directory prefix a glob is rooted at, before any wildcard.
|
|
313
|
+
*
|
|
314
|
+
* `lib/**\/*.spec.ts` and `!lib/**\/*.d.ts` are both rooted at `lib`. This is
|
|
315
|
+
* what lets the ratchet tell territory a change ADDED from territory it
|
|
316
|
+
* already covered: an exclusion inside a newly added root cannot shrink a
|
|
317
|
+
* gate, because nothing there was being mutated a moment ago.
|
|
318
|
+
* @param {string} glob A stryker mutate entry, with or without a leading `!`
|
|
319
|
+
* @returns {string} The rooted prefix, or "" when the glob starts with a wildcard
|
|
320
|
+
*/
|
|
321
|
+
export function globRoot(glob) {
|
|
322
|
+
const bare = glob.startsWith("!") ? glob.slice(1) : glob;
|
|
323
|
+
// Backslash escapes and parent traversal make textual containment unsafe.
|
|
324
|
+
// An unknown prefix overlaps everything rather than granting an exemption.
|
|
325
|
+
if (bare.includes("\\") || bare.split("/").includes("..")) return "";
|
|
326
|
+
const rooted = [];
|
|
327
|
+
for (const segment of bare.split("/")) {
|
|
328
|
+
if (segment === "." || segment === "") continue;
|
|
329
|
+
if (["*", "?", "{", "[", "("].some(meta => segment.includes(meta))) break;
|
|
330
|
+
rooted.push(segment);
|
|
331
|
+
}
|
|
332
|
+
return rooted.join("/");
|
|
333
|
+
}
|
|
334
|
+
|
|
311
335
|
/**
|
|
312
336
|
* Parse one k6 threshold expression (`p(95)<1000`, `rate>=0.99`, …) into a
|
|
313
337
|
* ratchet constraint. Upper bounds (`<`, `<=`) may only decrease; lower
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
extractStrykerConstraints,
|
|
22
22
|
extractStrykerMutate,
|
|
23
23
|
familyFor,
|
|
24
|
+
globRoot,
|
|
24
25
|
parseJson,
|
|
25
26
|
} from "./threshold-ratchet-families.mjs";
|
|
26
27
|
|
|
@@ -157,15 +158,56 @@ function compareStryker(relPath, base, current) {
|
|
|
157
158
|
);
|
|
158
159
|
const baseMutate = extractStrykerMutate(base);
|
|
159
160
|
const currentMutate = extractStrykerMutate(current);
|
|
161
|
+
// Roots the gate already covered, and roots this change adds. An exclusion
|
|
162
|
+
// scoped to a newly added root narrows territory that was not being mutated
|
|
163
|
+
// a moment ago, so it cannot shrink the gate — the change is a net widening.
|
|
164
|
+
//
|
|
165
|
+
// Without this, repairing an INERT `mutate` list is unreachable without a
|
|
166
|
+
// human-approved ratchet exception: the repair necessarily adds source roots
|
|
167
|
+
// AND the test-file exclusions that belong with them, and every one of those
|
|
168
|
+
// exclusions read as a weakening. Measured on two repositories whose real
|
|
169
|
+
// effect was 0 -> 69 and 0 -> 75 files mutated (CodySwannGT/lisa#4243).
|
|
170
|
+
const baseRoots = [...baseMutate.positives].map(globRoot);
|
|
171
|
+
/**
|
|
172
|
+
* Whether an exclusion might touch any baseline positive.
|
|
173
|
+
*
|
|
174
|
+
* Both containment directions matter: excluding `src` removes a baseline
|
|
175
|
+
* rooted at `src/existing`, too. An empty prefix (wildcards, braces, classes
|
|
176
|
+
* or extglobs at the root) might cover everything. We grant an exemption
|
|
177
|
+
* only for provably disjoint territory, never from a guessed glob overlap.
|
|
178
|
+
* @param {string} root A rooted glob prefix
|
|
179
|
+
* @returns {boolean} True when overlap cannot be ruled out
|
|
180
|
+
*/
|
|
181
|
+
const overlapsBase = root =>
|
|
182
|
+
baseRoots.some(
|
|
183
|
+
base =>
|
|
184
|
+
root === "" ||
|
|
185
|
+
base === "" ||
|
|
186
|
+
root === base ||
|
|
187
|
+
root.startsWith(`${base}/`) ||
|
|
188
|
+
base.startsWith(`${root}/`)
|
|
189
|
+
);
|
|
190
|
+
const addedRoots = [...currentMutate.positives]
|
|
191
|
+
.filter(positive => !baseMutate.positives.has(positive))
|
|
192
|
+
.map(globRoot)
|
|
193
|
+
.filter(root => root !== "");
|
|
194
|
+
/**
|
|
195
|
+
* Whether a root lies inside territory THIS change is adding.
|
|
196
|
+
* @param {string} root A rooted glob prefix
|
|
197
|
+
* @returns {boolean} True when an added root equals or contains it
|
|
198
|
+
*/
|
|
199
|
+
const underAdded = root =>
|
|
200
|
+
addedRoots.some(added => root === added || root.startsWith(`${added}/`));
|
|
160
201
|
for (const negation of currentMutate.negations) {
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
202
|
+
if (baseMutate.negations.has(negation)) continue;
|
|
203
|
+
const root = globRoot(negation);
|
|
204
|
+
if (!overlapsBase(root) && underAdded(root)) continue;
|
|
205
|
+
findings.push({
|
|
206
|
+
file: relPath,
|
|
207
|
+
key: `mutate ${negation}`,
|
|
208
|
+
type: TYPE_EXEMPTION_ADDED,
|
|
209
|
+
message: `${relPath}: new mutation-testing exclusion "${negation}" — excluding files from a gate is a weakening.`,
|
|
210
|
+
});
|
|
169
211
|
}
|
|
170
212
|
for (const positive of baseMutate.positives) {
|
|
171
213
|
if (!currentMutate.positives.has(positive)) {
|
|
@@ -311,6 +311,30 @@ export function extractStrykerMutate(conf) {
|
|
|
311
311
|
return { positives, negations };
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
/**
|
|
315
|
+
* The literal directory prefix a glob is rooted at, before any wildcard.
|
|
316
|
+
*
|
|
317
|
+
* `lib/**\/*.spec.ts` and `!lib/**\/*.d.ts` are both rooted at `lib`. This is
|
|
318
|
+
* what lets the ratchet tell territory a change ADDED from territory it
|
|
319
|
+
* already covered: an exclusion inside a newly added root cannot shrink a
|
|
320
|
+
* gate, because nothing there was being mutated a moment ago.
|
|
321
|
+
* @param {string} glob A stryker mutate entry, with or without a leading `!`
|
|
322
|
+
* @returns {string} The rooted prefix, or "" when the glob starts with a wildcard
|
|
323
|
+
*/
|
|
324
|
+
export function globRoot(glob) {
|
|
325
|
+
const bare = glob.startsWith("!") ? glob.slice(1) : glob;
|
|
326
|
+
// Backslash escapes and parent traversal make textual containment unsafe.
|
|
327
|
+
// An unknown prefix overlaps everything rather than granting an exemption.
|
|
328
|
+
if (bare.includes("\\") || bare.split("/").includes("..")) return "";
|
|
329
|
+
const rooted = [];
|
|
330
|
+
for (const segment of bare.split("/")) {
|
|
331
|
+
if (segment === "." || segment === "") continue;
|
|
332
|
+
if (["*", "?", "{", "[", "("].some(meta => segment.includes(meta))) break;
|
|
333
|
+
rooted.push(segment);
|
|
334
|
+
}
|
|
335
|
+
return rooted.join("/");
|
|
336
|
+
}
|
|
337
|
+
|
|
314
338
|
/**
|
|
315
339
|
* Parse one k6 threshold expression (`p(95)<1000`, `rate>=0.99`, …) into a
|
|
316
340
|
* ratchet constraint. Upper bounds (`<`, `<=`) may only decrease; lower
|