swarm-orchestrator 7.0.0-alpha.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/LICENSE +15 -0
- package/README.md +199 -0
- package/agents/reviewer.agent.md +50 -0
- package/agents/worker.agent.md +41 -0
- package/config/default-agents.yaml +56 -0
- package/config/pm-agent.yaml +36 -0
- package/config/quality-gates.yaml +88 -0
- package/config/repair-agent.yaml +34 -0
- package/config/semgrep-rules/complexity-mismatch.yaml +6 -0
- package/config/semgrep-rules/exception-swallowing.yaml +13 -0
- package/config/semgrep-rules/hardcoded-answer.yaml +6 -0
- package/config/semgrep-rules/mock-mutation.yaml +6 -0
- package/config/semgrep-rules/test-modification.yaml +6 -0
- package/config/user-agents.yaml +23 -0
- package/dist/src/adapters/adapter-factory.js +29 -0
- package/dist/src/adapters/agent-adapter.js +32 -0
- package/dist/src/adapters/claude-code-adapter.js +126 -0
- package/dist/src/adapters/claude-code-teams.js +119 -0
- package/dist/src/adapters/codex-adapter.js +118 -0
- package/dist/src/adapters/copilot-adapter.js +224 -0
- package/dist/src/adapters/fatal-error-classifier.js +88 -0
- package/dist/src/adapters/index.js +44 -0
- package/dist/src/adapters/persistent-session.js +204 -0
- package/dist/src/adapters/process-supervisor.js +199 -0
- package/dist/src/agents-exporter.js +398 -0
- package/dist/src/analytics-log.js +161 -0
- package/dist/src/baseline-scanner.js +65 -0
- package/dist/src/bootstrap-evidence.js +88 -0
- package/dist/src/bootstrap-orchestrator.js +205 -0
- package/dist/src/bootstrap-types.js +5 -0
- package/dist/src/branch-merger.js +427 -0
- package/dist/src/cli/attest-handlers.js +44 -0
- package/dist/src/cli/cost-prompt.js +51 -0
- package/dist/src/cli/demo-handlers.js +274 -0
- package/dist/src/cli/flags.js +152 -0
- package/dist/src/cli/index.js +57 -0
- package/dist/src/cli/live-status.js +186 -0
- package/dist/src/cli/misc-handlers.js +242 -0
- package/dist/src/cli/plan-handlers.js +350 -0
- package/dist/src/cli/share-handlers.js +163 -0
- package/dist/src/cli/status-handlers.js +457 -0
- package/dist/src/cli/swarm-handlers.js +638 -0
- package/dist/src/cli/usage.js +72 -0
- package/dist/src/cli.js +217 -0
- package/dist/src/commit-pattern-detector.js +143 -0
- package/dist/src/commit-quality-analyzer.js +38 -0
- package/dist/src/config-loader.js +339 -0
- package/dist/src/context-broker.js +358 -0
- package/dist/src/copilot-cli-wrapper.js +258 -0
- package/dist/src/cost-estimator.js +241 -0
- package/dist/src/defaults.js +18 -0
- package/dist/src/demo-mode.js +180 -0
- package/dist/src/deployment-handler.js +78 -0
- package/dist/src/deployment-manager.js +228 -0
- package/dist/src/execution-queue.js +196 -0
- package/dist/src/external-tool-manager.js +184 -0
- package/dist/src/gate-prompt-builder.js +117 -0
- package/dist/src/gate-remediation.js +173 -0
- package/dist/src/github-issues-ingester.js +152 -0
- package/dist/src/github-mcp-integrator.js +164 -0
- package/dist/src/hook-generator.js +229 -0
- package/dist/src/index.js +48 -0
- package/dist/src/knowledge-base.js +300 -0
- package/dist/src/logger.js +111 -0
- package/dist/src/meta-analyzer.js +208 -0
- package/dist/src/metrics-collector.js +209 -0
- package/dist/src/metrics-types.js +5 -0
- package/dist/src/multi-repo-coordinator.js +179 -0
- package/dist/src/orchestrator/async-meta-analysis.js +84 -0
- package/dist/src/orchestrator/fatal-run-error.js +43 -0
- package/dist/src/orchestrator/final-gates-remediation.js +258 -0
- package/dist/src/orchestrator/git-state-utils.js +119 -0
- package/dist/src/orchestrator/pause-controller.js +55 -0
- package/dist/src/orchestrator/replan-runner.js +258 -0
- package/dist/src/orchestrator/step-executor.js +391 -0
- package/dist/src/orchestrator/wave-scheduler-loop.js +204 -0
- package/dist/src/owasp-mapper.js +153 -0
- package/dist/src/owasp-report-renderer.js +39 -0
- package/dist/src/plan-files.js +142 -0
- package/dist/src/plan-generator.js +1207 -0
- package/dist/src/pm-agent.js +296 -0
- package/dist/src/post-run-reporter.js +203 -0
- package/dist/src/pr-automation.js +103 -0
- package/dist/src/pr-manager.js +318 -0
- package/dist/src/presenter/index.js +209 -0
- package/dist/src/prompt-builder.js +173 -0
- package/dist/src/quality-gates/config-loader.js +196 -0
- package/dist/src/quality-gates/default-config.js +243 -0
- package/dist/src/quality-gates/file-utils.js +152 -0
- package/dist/src/quality-gates/gate-runner.js +171 -0
- package/dist/src/quality-gates/gates/accessibility.js +355 -0
- package/dist/src/quality-gates/gates/duplicate-blocks.js +144 -0
- package/dist/src/quality-gates/gates/hardcoded-config.js +52 -0
- package/dist/src/quality-gates/gates/readme-claims.js +62 -0
- package/dist/src/quality-gates/gates/runtime-checks.js +265 -0
- package/dist/src/quality-gates/gates/scaffold-defaults.js +108 -0
- package/dist/src/quality-gates/gates/test-coverage.js +240 -0
- package/dist/src/quality-gates/gates/test-file-protection.js +85 -0
- package/dist/src/quality-gates/gates/test-isolation.js +74 -0
- package/dist/src/quality-gates/index.js +17 -0
- package/dist/src/quality-gates/registry.js +205 -0
- package/dist/src/quality-gates/types.js +2 -0
- package/dist/src/quick-fix-mode.js +302 -0
- package/dist/src/recipe-loader.js +122 -0
- package/dist/src/repair-agent.js +396 -0
- package/dist/src/repo-analyzer.js +342 -0
- package/dist/src/report-generator.js +146 -0
- package/dist/src/report-renderer.js +90 -0
- package/dist/src/requirement-filter.js +52 -0
- package/dist/src/sarif-formatter.js +140 -0
- package/dist/src/scheduling/dependency-analyzer.js +178 -0
- package/dist/src/scheduling/work-stealing-queue.js +123 -0
- package/dist/src/secret-redactor.js +90 -0
- package/dist/src/session-executor.js +587 -0
- package/dist/src/session-manager.js +221 -0
- package/dist/src/share/transcript-verification.js +128 -0
- package/dist/src/share-parser.js +477 -0
- package/dist/src/spinner.js +146 -0
- package/dist/src/step-runner.js +365 -0
- package/dist/src/swarm-orchestrator.js +686 -0
- package/dist/src/task-classifier.js +134 -0
- package/dist/src/test-command-discovery.js +123 -0
- package/dist/src/text-similarity.js +24 -0
- package/dist/src/tier-maps.js +158 -0
- package/dist/src/types.js +5 -0
- package/dist/src/url-shortener.js +135 -0
- package/dist/src/verification/attestation.js +166 -0
- package/dist/src/verification/cheat-detector.js +240 -0
- package/dist/src/verification/command-runner.js +70 -0
- package/dist/src/verification/composite-score.js +157 -0
- package/dist/src/verification/cosign-attestation.js +137 -0
- package/dist/src/verification/diff-analysis.js +89 -0
- package/dist/src/verification/differential-gate.js +173 -0
- package/dist/src/verification/index.js +41 -0
- package/dist/src/verification/mutation-gate.js +242 -0
- package/dist/src/verification/property-gate.js +201 -0
- package/dist/src/verification/test-synthesizer.js +243 -0
- package/dist/src/verifier/outcome-checks.js +396 -0
- package/dist/src/verifier/transcript-checks.js +207 -0
- package/dist/src/verifier/verification-reporters.js +148 -0
- package/dist/src/verifier-engine.js +491 -0
- package/dist/src/wave-resizer.js +131 -0
- package/dist/src/wave-scheduler.js +44 -0
- package/dist/src/worktree-manager.js +368 -0
- package/dist/src/worktree-reserved-paths.js +97 -0
- package/package.json +80 -0
- package/plugin/agents/.gitkeep +0 -0
- package/plugin/agents/backend_master.agent.md +72 -0
- package/plugin/agents/devops_pro.agent.md +45 -0
- package/plugin/agents/frontend_expert.agent.md +59 -0
- package/plugin/agents/integrator_finalizer.agent.md +44 -0
- package/plugin/agents/meta_reviewer.agent.md +44 -0
- package/plugin/agents/security_auditor.agent.md +43 -0
- package/plugin/agents/tester_elite.agent.md +43 -0
- package/plugin/hooks/hooks.json +19 -0
- package/plugin/hooks/post-tool-use.json +9 -0
- package/plugin/hooks/pre-tool-use.json +9 -0
- package/plugin/plugin.json +47 -0
- package/plugin/skills/swarm-gates/SKILL.md +53 -0
- package/plugin/skills/swarm-orchestrate/SKILL.md +34 -0
- package/plugin/skills/swarm-verify/SKILL.md +44 -0
- package/templates/README.md +35 -0
- package/templates/cli-tool.json +31 -0
- package/templates/fullstack.json +52 -0
- package/templates/library.json +38 -0
- package/templates/react-app.json +45 -0
- package/templates/recipes/add-api-docs.json +35 -0
- package/templates/recipes/add-auth.json +47 -0
- package/templates/recipes/add-ci.json +39 -0
- package/templates/recipes/add-tests.json +39 -0
- package/templates/recipes/migrate-to-ts.json +42 -0
- package/templates/recipes/refactor-modularize.json +44 -0
- package/templates/recipes/security-audit.json +35 -0
- package/templates/rest-api.json +38 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.signWithCosign = signWithCosign;
|
|
37
|
+
exports.signWithCosignKey = signWithCosignKey;
|
|
38
|
+
exports.verifyCosignSignature = verifyCosignSignature;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const command_runner_1 = require("./command-runner");
|
|
43
|
+
function shellQuote(value) {
|
|
44
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
45
|
+
}
|
|
46
|
+
function writeEnvelope(prefix, envelope) {
|
|
47
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
48
|
+
const envelopePath = path.join(tmp, 'envelope.json');
|
|
49
|
+
fs.writeFileSync(envelopePath, JSON.stringify(envelope, null, 2), 'utf8');
|
|
50
|
+
return { tmp, envelopePath };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Sign an attestation envelope with cosign keyless signing.
|
|
54
|
+
*
|
|
55
|
+
* @param envelope - In-toto statement to sign.
|
|
56
|
+
* @param repoPath - Repository root for temporary signing files.
|
|
57
|
+
* @returns Cosign signature metadata.
|
|
58
|
+
*/
|
|
59
|
+
async function signWithCosign(envelope, repoPath) {
|
|
60
|
+
const { tmp, envelopePath } = writeEnvelope('swarm-attest-', envelope);
|
|
61
|
+
const bundlePath = path.join(tmp, 'bundle.json');
|
|
62
|
+
const command = `cosign sign-blob --yes --bundle ${shellQuote(bundlePath)} ${shellQuote(envelopePath)}`;
|
|
63
|
+
const result = await (0, command_runner_1.runVerificationCommand)(command, repoPath, 300_000);
|
|
64
|
+
if (result.exitCode !== 0) {
|
|
65
|
+
throw new Error(`cosign sign-blob failed: ${(result.stderr || result.stdout).trim()}`);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
kind: 'cosign',
|
|
69
|
+
signature: result.stdout.trim(),
|
|
70
|
+
bundlePath,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Sign an attestation envelope with a local cosign key pair.
|
|
75
|
+
*
|
|
76
|
+
* This is intended for offline integration tests and controlled environments.
|
|
77
|
+
* Production attestations should use keyless `signWithCosign`.
|
|
78
|
+
*
|
|
79
|
+
* @param envelope - In-toto statement to sign.
|
|
80
|
+
* @param repoPath - Repository root for temporary signing files.
|
|
81
|
+
* @param options - Cosign key paths and optional password.
|
|
82
|
+
* @returns Cosign key signature metadata.
|
|
83
|
+
*/
|
|
84
|
+
async function signWithCosignKey(envelope, repoPath, options) {
|
|
85
|
+
const { tmp, envelopePath } = writeEnvelope('swarm-attest-key-', envelope);
|
|
86
|
+
const signaturePath = path.join(tmp, 'signature.sig');
|
|
87
|
+
const password = options.password ?? '';
|
|
88
|
+
const command = `COSIGN_PASSWORD=${shellQuote(password)} cosign sign-blob --key ${shellQuote(options.privateKeyPath)} --output-signature ${shellQuote(signaturePath)} ${shellQuote(envelopePath)}`;
|
|
89
|
+
const result = await (0, command_runner_1.runVerificationCommand)(command, repoPath, 300_000);
|
|
90
|
+
if (result.exitCode !== 0) {
|
|
91
|
+
throw new Error(`cosign key sign-blob failed: ${(result.stderr || result.stdout).trim()}`);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
kind: 'cosign',
|
|
95
|
+
signature: fs.readFileSync(signaturePath, 'utf8').trim(),
|
|
96
|
+
signaturePath,
|
|
97
|
+
publicKeyPath: options.publicKeyPath,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function missingCosignMetadata(attestation) {
|
|
101
|
+
return {
|
|
102
|
+
found: true,
|
|
103
|
+
verified: false,
|
|
104
|
+
reason: 'cosign bundle or key signature metadata missing',
|
|
105
|
+
attestation,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Verify a cosign-backed attestation signature.
|
|
110
|
+
*
|
|
111
|
+
* @param repoPath - Repository root.
|
|
112
|
+
* @param attestation - Parsed signed attestation.
|
|
113
|
+
* @returns Verification result.
|
|
114
|
+
*/
|
|
115
|
+
async function verifyCosignSignature(repoPath, attestation) {
|
|
116
|
+
const { tmp, envelopePath } = writeEnvelope('swarm-attest-verify-', attestation.envelope);
|
|
117
|
+
const sig = attestation.signature;
|
|
118
|
+
const command = sig.bundlePath
|
|
119
|
+
? `cosign verify-blob --bundle ${shellQuote(sig.bundlePath)} ${shellQuote(envelopePath)}`
|
|
120
|
+
: sig.signaturePath && sig.publicKeyPath
|
|
121
|
+
? `cosign verify-blob --key ${shellQuote(sig.publicKeyPath)} --signature ${shellQuote(sig.signaturePath)} ${shellQuote(envelopePath)}`
|
|
122
|
+
: '';
|
|
123
|
+
if (!command) {
|
|
124
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
125
|
+
return missingCosignMetadata(attestation);
|
|
126
|
+
}
|
|
127
|
+
const result = await (0, command_runner_1.runVerificationCommand)(command, repoPath, 300_000);
|
|
128
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
129
|
+
return {
|
|
130
|
+
found: true,
|
|
131
|
+
verified: result.exitCode === 0,
|
|
132
|
+
reason: result.exitCode === 0
|
|
133
|
+
? (sig.bundlePath ? 'cosign signature verified' : 'cosign key signature verified')
|
|
134
|
+
: (result.stderr || result.stdout).trim(),
|
|
135
|
+
attestation,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isTestFilePath = isTestFilePath;
|
|
4
|
+
exports.parseUnifiedDiff = parseUnifiedDiff;
|
|
5
|
+
exports.extractLiterals = extractLiterals;
|
|
6
|
+
const TEST_FILE_RE = /(^|\/)(test|tests|__tests__)(\/|$)|(?:\.test|\.spec)\.[cm]?[jt]sx?$|(^|\/)test_.*\.py$|_test\.py$/;
|
|
7
|
+
function parsePath(raw) {
|
|
8
|
+
return raw === '/dev/null' ? raw : raw.replace(/^[ab]\//, '');
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Return true when a repo-relative path is a common test file path.
|
|
12
|
+
*
|
|
13
|
+
* @param filePath - Repo-relative file path.
|
|
14
|
+
* @returns Whether the file is test-owned.
|
|
15
|
+
*/
|
|
16
|
+
function isTestFilePath(filePath) {
|
|
17
|
+
return TEST_FILE_RE.test(filePath);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Parse a unified git diff into per-file line records.
|
|
21
|
+
*
|
|
22
|
+
* @param diffText - Unified diff text.
|
|
23
|
+
* @returns Parsed file sections with added and removed line numbers.
|
|
24
|
+
*/
|
|
25
|
+
function parseUnifiedDiff(diffText) {
|
|
26
|
+
const files = [];
|
|
27
|
+
let current;
|
|
28
|
+
let oldLine = 0;
|
|
29
|
+
let newLine = 0;
|
|
30
|
+
for (const line of diffText.split('\n')) {
|
|
31
|
+
const header = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
32
|
+
if (header?.[1] && header[2]) {
|
|
33
|
+
current = { oldPath: header[1], newPath: header[2], lines: [] };
|
|
34
|
+
files.push(current);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!current)
|
|
38
|
+
continue;
|
|
39
|
+
if (line.startsWith('--- ')) {
|
|
40
|
+
current.oldPath = parsePath(line.slice(4).trim());
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (line.startsWith('+++ ')) {
|
|
44
|
+
current.newPath = parsePath(line.slice(4).trim());
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const hunk = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
48
|
+
if (hunk?.[1] && hunk[2]) {
|
|
49
|
+
oldLine = Number.parseInt(hunk[1], 10);
|
|
50
|
+
newLine = Number.parseInt(hunk[2], 10);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (line.startsWith('+++') || line.startsWith('---') || line.startsWith('\\')) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (line.startsWith('+')) {
|
|
57
|
+
current.lines.push({ kind: 'add', content: line.slice(1), newLine });
|
|
58
|
+
newLine += 1;
|
|
59
|
+
}
|
|
60
|
+
else if (line.startsWith('-')) {
|
|
61
|
+
current.lines.push({ kind: 'remove', content: line.slice(1), oldLine });
|
|
62
|
+
oldLine += 1;
|
|
63
|
+
}
|
|
64
|
+
else if (line.startsWith(' ')) {
|
|
65
|
+
current.lines.push({ kind: 'context', content: line.slice(1), oldLine, newLine });
|
|
66
|
+
oldLine += 1;
|
|
67
|
+
newLine += 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return files;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Extract simple string and numeric literals from one source line.
|
|
74
|
+
*
|
|
75
|
+
* @param line - Source line.
|
|
76
|
+
* @returns Literal values without quotes.
|
|
77
|
+
*/
|
|
78
|
+
function extractLiterals(line) {
|
|
79
|
+
const literals = new Set();
|
|
80
|
+
for (const match of line.matchAll(/(['"`])([^'"`]{2,})\1/g)) {
|
|
81
|
+
if (match[2])
|
|
82
|
+
literals.add(match[2]);
|
|
83
|
+
}
|
|
84
|
+
for (const match of line.matchAll(/\b\d+(?:\.\d+)?\b/g)) {
|
|
85
|
+
if (match[0])
|
|
86
|
+
literals.add(match[0]);
|
|
87
|
+
}
|
|
88
|
+
return [...literals];
|
|
89
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runDifferentialGate = runDifferentialGate;
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const command_runner_1 = require("./command-runner");
|
|
42
|
+
function git(repoPath, args) {
|
|
43
|
+
return (0, child_process_1.execFileSync)('git', args, {
|
|
44
|
+
cwd: repoPath,
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
47
|
+
env: {
|
|
48
|
+
...process.env,
|
|
49
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
50
|
+
},
|
|
51
|
+
}).trim();
|
|
52
|
+
}
|
|
53
|
+
function addDetachedWorktree(repoPath, worktreePath, ref) {
|
|
54
|
+
(0, child_process_1.execFileSync)('git', ['worktree', 'add', '--detach', worktreePath, ref], {
|
|
55
|
+
cwd: repoPath,
|
|
56
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
57
|
+
env: {
|
|
58
|
+
...process.env,
|
|
59
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function removeWorktree(repoPath, worktreePath) {
|
|
64
|
+
try {
|
|
65
|
+
(0, child_process_1.execFileSync)('git', ['worktree', 'remove', '--force', worktreePath], {
|
|
66
|
+
cwd: repoPath,
|
|
67
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
(0, child_process_1.execFileSync)('git', ['worktree', 'prune'], {
|
|
75
|
+
cwd: repoPath,
|
|
76
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Cleanup is best-effort after the target worktree is already gone.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function setupFailure(started, reason) {
|
|
84
|
+
return {
|
|
85
|
+
status: 'FAIL',
|
|
86
|
+
reason,
|
|
87
|
+
durationMs: Date.now() - started,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Run a differential intent gate for one synthesized or SWE-bench test.
|
|
92
|
+
*
|
|
93
|
+
* The gate runs the same command against a detached worktree at `baseCommit`
|
|
94
|
+
* and a detached worktree at `agentBranch`. The test must fail at the base
|
|
95
|
+
* and pass at the patch. If it passes at the base, the test is invalid because
|
|
96
|
+
* it does not prove the stated bug or feature.
|
|
97
|
+
*
|
|
98
|
+
* @param input - Repository path, test command, base commit, and patch branch.
|
|
99
|
+
* @returns Structured base/patch command evidence and gate status.
|
|
100
|
+
*/
|
|
101
|
+
async function runDifferentialGate(input) {
|
|
102
|
+
const started = Date.now();
|
|
103
|
+
const repoPath = path.resolve(input.repoPath);
|
|
104
|
+
const timeoutMs = input.timeoutMs ?? 120_000;
|
|
105
|
+
const root = input.worktreeRoot
|
|
106
|
+
? path.resolve(input.worktreeRoot)
|
|
107
|
+
: fs.mkdtempSync(path.join(os.tmpdir(), 'swarm-diff-gate-'));
|
|
108
|
+
const baseWorktree = path.join(root, 'base');
|
|
109
|
+
const patchWorktree = path.join(root, 'patch');
|
|
110
|
+
if (!input.testCommand.trim()) {
|
|
111
|
+
return setupFailure(started, 'differential gate requires a non-empty test command');
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
git(repoPath, ['rev-parse', '--is-inside-work-tree']);
|
|
115
|
+
git(repoPath, ['rev-parse', '--verify', input.baseCommit]);
|
|
116
|
+
git(repoPath, ['rev-parse', '--verify', input.agentBranch]);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
120
|
+
return setupFailure(started, `differential gate git setup failed: ${message}`);
|
|
121
|
+
}
|
|
122
|
+
let base;
|
|
123
|
+
let patch;
|
|
124
|
+
try {
|
|
125
|
+
fs.mkdirSync(root, { recursive: true });
|
|
126
|
+
addDetachedWorktree(repoPath, baseWorktree, input.baseCommit);
|
|
127
|
+
base = await (0, command_runner_1.runVerificationCommand)(input.testCommand, baseWorktree, timeoutMs);
|
|
128
|
+
if (base.exitCode === 0) {
|
|
129
|
+
return {
|
|
130
|
+
status: 'INVALID_TEST',
|
|
131
|
+
reason: 'test command passed against the base commit',
|
|
132
|
+
base,
|
|
133
|
+
durationMs: Date.now() - started,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
addDetachedWorktree(repoPath, patchWorktree, input.agentBranch);
|
|
137
|
+
patch = await (0, command_runner_1.runVerificationCommand)(input.testCommand, patchWorktree, timeoutMs);
|
|
138
|
+
if (patch.exitCode === 0) {
|
|
139
|
+
return {
|
|
140
|
+
status: 'PASS',
|
|
141
|
+
reason: 'test failed at base and passed at patch',
|
|
142
|
+
base,
|
|
143
|
+
patch,
|
|
144
|
+
durationMs: Date.now() - started,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
status: 'FAIL',
|
|
149
|
+
reason: 'test command failed against the patch',
|
|
150
|
+
base,
|
|
151
|
+
patch,
|
|
152
|
+
durationMs: Date.now() - started,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
157
|
+
return {
|
|
158
|
+
status: 'FAIL',
|
|
159
|
+
reason: `differential gate execution failed: ${message}`,
|
|
160
|
+
...(base ? { base } : {}),
|
|
161
|
+
...(patch ? { patch } : {}),
|
|
162
|
+
durationMs: Date.now() - started,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
if (fs.existsSync(baseWorktree))
|
|
167
|
+
removeWorktree(repoPath, baseWorktree);
|
|
168
|
+
if (fs.existsSync(patchWorktree))
|
|
169
|
+
removeWorktree(repoPath, patchWorktree);
|
|
170
|
+
if (!input.worktreeRoot)
|
|
171
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadCompositeScoreConfig = exports.computeCompositeScore = exports.DEFAULT_COMPOSITE_CONFIG = exports.signWithCosignKey = exports.verifyAttestation = exports.unsignedTestSigner = exports.signWithCosign = exports.readAttestationNote = exports.generateSignedAttestation = exports.createAttestationEnvelope = exports.attachAttestationNote = exports.runPropertyGate = exports.discoverPropertyTargets = exports.parseUnifiedDiff = exports.isTestFilePath = exports.extractLiterals = exports.runCheatDetector = exports.runMutationGate = exports.parseMutationOutput = exports.loadMutationThresholds = exports.evaluateMutationScore = exports.detectMutationLanguages = exports.buildMutationCommand = exports.DEFAULT_MUTATION_THRESHOLDS = exports.validateSynthesizedTestCandidate = exports.synthesizeRegressionTest = exports.runDifferentialGate = exports.runVerificationCommand = void 0;
|
|
4
|
+
var command_runner_1 = require("./command-runner");
|
|
5
|
+
Object.defineProperty(exports, "runVerificationCommand", { enumerable: true, get: function () { return command_runner_1.runVerificationCommand; } });
|
|
6
|
+
var differential_gate_1 = require("./differential-gate");
|
|
7
|
+
Object.defineProperty(exports, "runDifferentialGate", { enumerable: true, get: function () { return differential_gate_1.runDifferentialGate; } });
|
|
8
|
+
var test_synthesizer_1 = require("./test-synthesizer");
|
|
9
|
+
Object.defineProperty(exports, "synthesizeRegressionTest", { enumerable: true, get: function () { return test_synthesizer_1.synthesizeRegressionTest; } });
|
|
10
|
+
Object.defineProperty(exports, "validateSynthesizedTestCandidate", { enumerable: true, get: function () { return test_synthesizer_1.validateSynthesizedTestCandidate; } });
|
|
11
|
+
var mutation_gate_1 = require("./mutation-gate");
|
|
12
|
+
Object.defineProperty(exports, "DEFAULT_MUTATION_THRESHOLDS", { enumerable: true, get: function () { return mutation_gate_1.DEFAULT_MUTATION_THRESHOLDS; } });
|
|
13
|
+
Object.defineProperty(exports, "buildMutationCommand", { enumerable: true, get: function () { return mutation_gate_1.buildMutationCommand; } });
|
|
14
|
+
Object.defineProperty(exports, "detectMutationLanguages", { enumerable: true, get: function () { return mutation_gate_1.detectMutationLanguages; } });
|
|
15
|
+
Object.defineProperty(exports, "evaluateMutationScore", { enumerable: true, get: function () { return mutation_gate_1.evaluateMutationScore; } });
|
|
16
|
+
Object.defineProperty(exports, "loadMutationThresholds", { enumerable: true, get: function () { return mutation_gate_1.loadMutationThresholds; } });
|
|
17
|
+
Object.defineProperty(exports, "parseMutationOutput", { enumerable: true, get: function () { return mutation_gate_1.parseMutationOutput; } });
|
|
18
|
+
Object.defineProperty(exports, "runMutationGate", { enumerable: true, get: function () { return mutation_gate_1.runMutationGate; } });
|
|
19
|
+
var cheat_detector_1 = require("./cheat-detector");
|
|
20
|
+
Object.defineProperty(exports, "runCheatDetector", { enumerable: true, get: function () { return cheat_detector_1.runCheatDetector; } });
|
|
21
|
+
var diff_analysis_1 = require("./diff-analysis");
|
|
22
|
+
Object.defineProperty(exports, "extractLiterals", { enumerable: true, get: function () { return diff_analysis_1.extractLiterals; } });
|
|
23
|
+
Object.defineProperty(exports, "isTestFilePath", { enumerable: true, get: function () { return diff_analysis_1.isTestFilePath; } });
|
|
24
|
+
Object.defineProperty(exports, "parseUnifiedDiff", { enumerable: true, get: function () { return diff_analysis_1.parseUnifiedDiff; } });
|
|
25
|
+
var property_gate_1 = require("./property-gate");
|
|
26
|
+
Object.defineProperty(exports, "discoverPropertyTargets", { enumerable: true, get: function () { return property_gate_1.discoverPropertyTargets; } });
|
|
27
|
+
Object.defineProperty(exports, "runPropertyGate", { enumerable: true, get: function () { return property_gate_1.runPropertyGate; } });
|
|
28
|
+
var attestation_1 = require("./attestation");
|
|
29
|
+
Object.defineProperty(exports, "attachAttestationNote", { enumerable: true, get: function () { return attestation_1.attachAttestationNote; } });
|
|
30
|
+
Object.defineProperty(exports, "createAttestationEnvelope", { enumerable: true, get: function () { return attestation_1.createAttestationEnvelope; } });
|
|
31
|
+
Object.defineProperty(exports, "generateSignedAttestation", { enumerable: true, get: function () { return attestation_1.generateSignedAttestation; } });
|
|
32
|
+
Object.defineProperty(exports, "readAttestationNote", { enumerable: true, get: function () { return attestation_1.readAttestationNote; } });
|
|
33
|
+
Object.defineProperty(exports, "signWithCosign", { enumerable: true, get: function () { return attestation_1.signWithCosign; } });
|
|
34
|
+
Object.defineProperty(exports, "unsignedTestSigner", { enumerable: true, get: function () { return attestation_1.unsignedTestSigner; } });
|
|
35
|
+
Object.defineProperty(exports, "verifyAttestation", { enumerable: true, get: function () { return attestation_1.verifyAttestation; } });
|
|
36
|
+
var cosign_attestation_1 = require("./cosign-attestation");
|
|
37
|
+
Object.defineProperty(exports, "signWithCosignKey", { enumerable: true, get: function () { return cosign_attestation_1.signWithCosignKey; } });
|
|
38
|
+
var composite_score_1 = require("./composite-score");
|
|
39
|
+
Object.defineProperty(exports, "DEFAULT_COMPOSITE_CONFIG", { enumerable: true, get: function () { return composite_score_1.DEFAULT_COMPOSITE_CONFIG; } });
|
|
40
|
+
Object.defineProperty(exports, "computeCompositeScore", { enumerable: true, get: function () { return composite_score_1.computeCompositeScore; } });
|
|
41
|
+
Object.defineProperty(exports, "loadCompositeScoreConfig", { enumerable: true, get: function () { return composite_score_1.loadCompositeScoreConfig; } });
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_MUTATION_THRESHOLDS = void 0;
|
|
37
|
+
exports.loadMutationThresholds = loadMutationThresholds;
|
|
38
|
+
exports.detectMutationLanguages = detectMutationLanguages;
|
|
39
|
+
exports.buildMutationCommand = buildMutationCommand;
|
|
40
|
+
exports.parseMutationOutput = parseMutationOutput;
|
|
41
|
+
exports.evaluateMutationScore = evaluateMutationScore;
|
|
42
|
+
exports.runMutationGate = runMutationGate;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const yaml = __importStar(require("js-yaml"));
|
|
46
|
+
const command_runner_1 = require("./command-runner");
|
|
47
|
+
exports.DEFAULT_MUTATION_THRESHOLDS = {
|
|
48
|
+
failBelow: 0.6,
|
|
49
|
+
warnBelow: 0.8,
|
|
50
|
+
};
|
|
51
|
+
function shellQuote(value) {
|
|
52
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
53
|
+
}
|
|
54
|
+
function isRecord(value) {
|
|
55
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
function readNumber(record, key) {
|
|
58
|
+
const value = record[key];
|
|
59
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Load mutation thresholds from `.swarm/gates.yaml`, falling back to defaults.
|
|
63
|
+
*
|
|
64
|
+
* @param projectRoot - Target repository root.
|
|
65
|
+
* @returns Configured mutation score thresholds.
|
|
66
|
+
*/
|
|
67
|
+
function loadMutationThresholds(projectRoot) {
|
|
68
|
+
const configPath = path.join(projectRoot, '.swarm', 'gates.yaml');
|
|
69
|
+
if (!fs.existsSync(configPath))
|
|
70
|
+
return { ...exports.DEFAULT_MUTATION_THRESHOLDS };
|
|
71
|
+
const parsed = yaml.load(fs.readFileSync(configPath, 'utf8'));
|
|
72
|
+
if (!isRecord(parsed))
|
|
73
|
+
return { ...exports.DEFAULT_MUTATION_THRESHOLDS };
|
|
74
|
+
const verification = parsed.verification;
|
|
75
|
+
if (!isRecord(verification))
|
|
76
|
+
return { ...exports.DEFAULT_MUTATION_THRESHOLDS };
|
|
77
|
+
const mutation = verification.mutation;
|
|
78
|
+
if (!isRecord(mutation))
|
|
79
|
+
return { ...exports.DEFAULT_MUTATION_THRESHOLDS };
|
|
80
|
+
return {
|
|
81
|
+
failBelow: readNumber(mutation, 'failBelow') ?? exports.DEFAULT_MUTATION_THRESHOLDS.failBelow,
|
|
82
|
+
warnBelow: readNumber(mutation, 'warnBelow') ?? exports.DEFAULT_MUTATION_THRESHOLDS.warnBelow,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Group changed files by mutation tool language.
|
|
87
|
+
*
|
|
88
|
+
* @param changedFiles - Repo-relative changed file paths.
|
|
89
|
+
* @returns Mutation targets for supported languages.
|
|
90
|
+
*/
|
|
91
|
+
function detectMutationLanguages(changedFiles) {
|
|
92
|
+
const jsTs = changedFiles.filter(file => /\.(?:[cm]?js|jsx|ts|tsx)$/.test(file) && !file.endsWith('.d.ts'));
|
|
93
|
+
const python = changedFiles.filter(file => file.endsWith('.py'));
|
|
94
|
+
const java = changedFiles.filter(file => file.endsWith('.java'));
|
|
95
|
+
const targets = [];
|
|
96
|
+
if (jsTs.length > 0)
|
|
97
|
+
targets.push({ language: 'javascript-typescript', files: jsTs });
|
|
98
|
+
if (python.length > 0)
|
|
99
|
+
targets.push({ language: 'python', files: python });
|
|
100
|
+
if (java.length > 0)
|
|
101
|
+
targets.push({ language: 'java', files: java });
|
|
102
|
+
return targets;
|
|
103
|
+
}
|
|
104
|
+
function javaClassGlobs(files) {
|
|
105
|
+
return files.map(file => {
|
|
106
|
+
const normalized = file.replace(/\\/g, '/').replace(/\.java$/, '');
|
|
107
|
+
const marker = '/src/main/java/';
|
|
108
|
+
const idx = normalized.indexOf(marker);
|
|
109
|
+
const classPath = idx >= 0 ? normalized.slice(idx + marker.length) : normalized;
|
|
110
|
+
return classPath.replace(/\//g, '.');
|
|
111
|
+
}).join(',');
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Build the mutation tool command for one language target.
|
|
115
|
+
*
|
|
116
|
+
* @param projectRoot - Target repository root.
|
|
117
|
+
* @param target - Language target to mutate.
|
|
118
|
+
* @returns Shell command scoped to the changed files.
|
|
119
|
+
*/
|
|
120
|
+
function buildMutationCommand(projectRoot, target) {
|
|
121
|
+
if (target.language === 'javascript-typescript') {
|
|
122
|
+
return `npx stryker run --mutate ${shellQuote(target.files.join(','))} --reporters clear-text`;
|
|
123
|
+
}
|
|
124
|
+
if (target.language === 'python') {
|
|
125
|
+
return `python -m mutmut run --paths-to-mutate ${target.files.map(shellQuote).join(' ')}`;
|
|
126
|
+
}
|
|
127
|
+
const classGlobs = javaClassGlobs(target.files);
|
|
128
|
+
if (fs.existsSync(path.join(projectRoot, 'gradlew'))) {
|
|
129
|
+
return `./gradlew pitest -PtargetClasses=${shellQuote(classGlobs)}`;
|
|
130
|
+
}
|
|
131
|
+
return `mvn org.pitest:pitest-maven:mutationCoverage -DtargetClasses=${shellQuote(classGlobs)}`;
|
|
132
|
+
}
|
|
133
|
+
function parseCount(text, labels) {
|
|
134
|
+
for (const label of labels) {
|
|
135
|
+
const pattern = new RegExp(`${label}\\s*[:=]?\\s*(\\d+)`, 'i');
|
|
136
|
+
const match = text.match(pattern);
|
|
137
|
+
if (match?.[1])
|
|
138
|
+
return Number.parseInt(match[1], 10);
|
|
139
|
+
const leadingPattern = new RegExp(`(\\d+)\\s+(?:mutant\\(s\\)|mutants?)\\s+${label}`, 'i');
|
|
140
|
+
const leadingMatch = text.match(leadingPattern);
|
|
141
|
+
if (leadingMatch?.[1])
|
|
142
|
+
return Number.parseInt(leadingMatch[1], 10);
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Parse common Stryker, mutmut, and PITest output into mutation metrics.
|
|
148
|
+
*
|
|
149
|
+
* @param stdout - Tool stdout.
|
|
150
|
+
* @param stderr - Tool stderr.
|
|
151
|
+
* @returns Parsed mutant counts and score.
|
|
152
|
+
*/
|
|
153
|
+
function parseMutationOutput(stdout, stderr = '') {
|
|
154
|
+
const text = `${stdout}\n${stderr}`;
|
|
155
|
+
const scoreMatch = text.match(/(?:mutation\s+score|score)\s*[:=]?\s*(\d+(?:\.\d+)?)\s*%/i);
|
|
156
|
+
const killed = parseCount(text, ['killed mutants', 'killed']);
|
|
157
|
+
const survived = parseCount(text, ['survived mutants', 'survived', 'survivors']);
|
|
158
|
+
const total = parseCount(text, ['total mutants', 'mutant\\(s\\) generated', 'generated', 'total']);
|
|
159
|
+
const derivedTotal = total ?? (killed !== undefined && survived !== undefined ? killed + survived : undefined);
|
|
160
|
+
const mutationScore = scoreMatch?.[1]
|
|
161
|
+
? Number.parseFloat(scoreMatch[1]) / 100
|
|
162
|
+
: derivedTotal && killed !== undefined
|
|
163
|
+
? killed / derivedTotal
|
|
164
|
+
: 0;
|
|
165
|
+
return {
|
|
166
|
+
totalMutants: derivedTotal ?? 0,
|
|
167
|
+
killedMutants: killed ?? 0,
|
|
168
|
+
survivedMutants: survived ?? Math.max((derivedTotal ?? 0) - (killed ?? 0), 0),
|
|
169
|
+
mutationScore,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Classify a mutation score against hard-fail and warning thresholds.
|
|
174
|
+
*
|
|
175
|
+
* @param score - Mutation score from 0.0 to 1.0.
|
|
176
|
+
* @param thresholds - Configured mutation score thresholds.
|
|
177
|
+
* @returns Gate status for the score.
|
|
178
|
+
*/
|
|
179
|
+
function evaluateMutationScore(score, thresholds = exports.DEFAULT_MUTATION_THRESHOLDS) {
|
|
180
|
+
if (score < thresholds.failBelow)
|
|
181
|
+
return 'FAIL';
|
|
182
|
+
if (score < thresholds.warnBelow)
|
|
183
|
+
return 'WARNING';
|
|
184
|
+
return 'PASS';
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Run mutation testing against files changed by an agent patch.
|
|
188
|
+
*
|
|
189
|
+
* @param input - Target repo, changed files, thresholds, and optional runner.
|
|
190
|
+
* @returns Structured mutation score evidence.
|
|
191
|
+
*/
|
|
192
|
+
async function runMutationGate(input) {
|
|
193
|
+
const thresholds = input.thresholds ?? loadMutationThresholds(input.targetRepoPath);
|
|
194
|
+
const targets = detectMutationLanguages(input.changedFiles);
|
|
195
|
+
if (targets.length === 0) {
|
|
196
|
+
return {
|
|
197
|
+
status: 'SKIP',
|
|
198
|
+
reason: 'no changed files use a supported mutation testing language',
|
|
199
|
+
mutationScore: 1,
|
|
200
|
+
thresholds,
|
|
201
|
+
totalMutants: 0,
|
|
202
|
+
killedMutants: 0,
|
|
203
|
+
survivedMutants: 0,
|
|
204
|
+
results: [],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const runner = input.commandRunner ?? command_runner_1.runVerificationCommand;
|
|
208
|
+
const results = [];
|
|
209
|
+
for (const target of targets) {
|
|
210
|
+
const command = buildMutationCommand(input.targetRepoPath, target);
|
|
211
|
+
const commandResult = await runner(command, input.targetRepoPath, input.timeoutMs ?? 600_000);
|
|
212
|
+
const metrics = parseMutationOutput(commandResult.stdout, commandResult.stderr);
|
|
213
|
+
results.push({
|
|
214
|
+
language: target.language,
|
|
215
|
+
files: target.files,
|
|
216
|
+
command,
|
|
217
|
+
exitCode: commandResult.exitCode,
|
|
218
|
+
stdout: commandResult.stdout,
|
|
219
|
+
stderr: commandResult.stderr,
|
|
220
|
+
durationMs: commandResult.durationMs,
|
|
221
|
+
...metrics,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const totalMutants = results.reduce((sum, result) => sum + result.totalMutants, 0);
|
|
225
|
+
const killedMutants = results.reduce((sum, result) => sum + result.killedMutants, 0);
|
|
226
|
+
const survivedMutants = results.reduce((sum, result) => sum + result.survivedMutants, 0);
|
|
227
|
+
const mutationScore = totalMutants > 0 ? killedMutants / totalMutants : 0;
|
|
228
|
+
const toolFailed = results.some(result => result.exitCode !== 0 && result.totalMutants === 0);
|
|
229
|
+
const status = toolFailed ? 'FAIL' : evaluateMutationScore(mutationScore, thresholds);
|
|
230
|
+
return {
|
|
231
|
+
status,
|
|
232
|
+
reason: toolFailed
|
|
233
|
+
? 'mutation tool failed before producing a mutation score'
|
|
234
|
+
: `mutation score ${mutationScore.toFixed(3)} produced ${status}`,
|
|
235
|
+
mutationScore,
|
|
236
|
+
thresholds,
|
|
237
|
+
totalMutants,
|
|
238
|
+
killedMutants,
|
|
239
|
+
survivedMutants,
|
|
240
|
+
results,
|
|
241
|
+
};
|
|
242
|
+
}
|