@codyswann/lisa 4.64.2 → 4.64.3
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/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 +10 -9
- 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
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"timeoutMS": 60000,
|
|
18
18
|
"_dryRunTimeoutMinutesComment": "Whole-suite liveness bound, set by dryRunTimeoutMinutes, for the single un-mutated run that measures every test before any mutant is generated. Longer than the TypeScript template's 20 because this stack is the slowest runner shipped here: jest, a React Native transform on every file, and a cold Babel cache on the first run of the day. Stryker's 5-minute default cannot survive that on modest hardware, and when it blows there is no score to report — the gate simply dies. A genuinely hung dry run still fails, just later, and CI's own job timeout still caps it. Absolute BY NECESSITY: this bounds the run that PRODUCES the timing measurement, so there is nothing yet measured to scale it against. It cannot be made adaptive the way a per-mutant budget can. The claim it makes is deliberately weak — not \"your dry run finishes in this long\" but \"a dry run still going at this point is wedged rather than slow\" — which is what lets one number hold across hardware nobody here has seen. It fails only where a HEALTHY dry run genuinely exceeds it, which takes a pathological environment rather than a slow one (a saturated temp directory can make a single mkdtemp cost seconds). In that case the environment is the bug and raising this number hides it — which is why the gate now reports a blown budget as `dry-run-timeout` naming this value, rather than as a mutation score. The 30 is reasoned from this stack's shape — jest, a React Native transform on every file, a cold Babel cache — not measured against a reference Expo project, so treat it as a ceiling chosen to be safely loose rather than as a calibration.",
|
|
19
19
|
"dryRunTimeoutMinutes": 30,
|
|
20
|
+
"_mutateComment": "Initial source-layout guess. Lisa mutation runs report when these patterns select no tracked files and derive source roots when quality.mutation.sourceDirs is absent. Declare that key in .lisa.config.json to set authoritative roots; it takes precedence in diff and --all runs. This create-only file stays project-owned.",
|
|
20
21
|
"mutate": [
|
|
21
22
|
"src/**/*.ts",
|
|
22
23
|
"src/**/*.tsx",
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"build:dist:in-place": "node scripts/lib/worktree-dependencies.mjs && tsc && node scripts/copy-codex-scripts.mjs && node scripts/copy-opencode-plugin-templates.mjs",
|
|
6
6
|
"lisa-test-run": "node scripts/lib/worktree-dependencies.mjs && bun run build && node dist/cli/lisa-test-run.js --profile lisa",
|
|
7
7
|
"test:integration": "node scripts/lib/worktree-dependencies.mjs && $npm_execpath run lisa-test-run -- --adapter vitest -- vitest run tests/integration",
|
|
8
|
-
"test:integration:push": "node scripts/lib/worktree-dependencies.mjs && $npm_execpath run lisa-test-run -- --adapter vitest -- vitest run tests/integration --exclude='**/mutation-gate-bite.test.ts' --exclude='**/mutation-gate-diff-bite.test.ts' --exclude='**/mutation-sigterm-control.test.ts' --exclude='**/mutation-performance-fixture.test.ts' --exclude='**/mutation-performance-packed-apply.test.ts'",
|
|
8
|
+
"test:integration:push": "node scripts/lib/worktree-dependencies.mjs && $npm_execpath run lisa-test-run -- --adapter vitest -- vitest run tests/integration --exclude='**/mutation-gate-bite.test.ts' --exclude='**/mutation-gate-diff-bite.test.ts' --exclude='**/mutation-sigterm-control.test.ts' --exclude='**/mutation-performance-fixture.test.ts' --exclude='**/mutation-performance-packed-apply.test.ts' --exclude='**/mutation-source-roots.test.ts'",
|
|
9
9
|
"postinstall": "bash ./scripts/install-claude-plugins.sh || true; node ./scripts/install-generated-artifact-merge-driver.mjs || true; [ ! -f tsconfig.json ] || tsc -p tsconfig.json || true",
|
|
10
10
|
"pretest": "bun run build",
|
|
11
11
|
"test": "node scripts/lib/worktree-dependencies.mjs && $npm_execpath run lisa-test-run -- --adapter vitest -- vitest run",
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
"zod-validation-error": "^4.0.0"
|
|
188
188
|
},
|
|
189
189
|
"name": "@codyswann/lisa",
|
|
190
|
-
"version": "4.64.
|
|
190
|
+
"version": "4.64.3",
|
|
191
191
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
192
192
|
"main": "dist/index.js",
|
|
193
193
|
"exports": {
|
|
@@ -336,7 +336,7 @@
|
|
|
336
336
|
"test": "tests"
|
|
337
337
|
},
|
|
338
338
|
"types": "./dist/index.d.ts",
|
|
339
|
-
"lisaReleaseCommit": "
|
|
340
|
-
"gitHead": "
|
|
341
|
-
"lisaReleaseTag": "v4.64.
|
|
339
|
+
"lisaReleaseCommit": "83db3d28c60571ef6da43370c6f409876084dffe",
|
|
340
|
+
"gitHead": "83db3d28c60571ef6da43370c6f409876084dffe",
|
|
341
|
+
"lisaReleaseTag": "v4.64.3"
|
|
342
342
|
}
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.64.
|
|
3
|
+
"version": "4.64.3",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.64.
|
|
3
|
+
"version": "4.64.3",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.64.
|
|
3
|
+
"version": "4.64.3",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.64.
|
|
3
|
+
"version": "4.64.3",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.64.
|
|
3
|
+
"version": "4.64.3",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|