@ornexus/neocortex 4.60.73 → 4.60.75
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/release-source.json +1 -1
- package/dist/sbom.cdx.json +1216 -1210
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/package.json +2 -2
- package/packages/client/dist/adapters/adapter-registry.js +1 -108
- package/packages/client/dist/adapters/antigravity-adapter.js +2 -77
- package/packages/client/dist/adapters/claude-code-adapter.js +3 -79
- package/packages/client/dist/adapters/codex-adapter.js +2 -80
- package/packages/client/dist/adapters/cursor-adapter.js +4 -115
- package/packages/client/dist/adapters/gemini-adapter.js +2 -71
- package/packages/client/dist/adapters/grok-adapter.js +2 -42
- package/packages/client/dist/adapters/index.js +1 -22
- package/packages/client/dist/adapters/platform-detector.js +1 -212
- package/packages/client/dist/adapters/target-adapter.js +0 -12
- package/packages/client/dist/adapters/vscode-adapter.js +2 -72
- package/packages/client/dist/agent/refresh-stubs.js +2 -417
- package/packages/client/dist/agent/update-agent-yaml.js +1 -102
- package/packages/client/dist/agent/update-description.js +1 -251
- package/packages/client/dist/cache/crypto-utils.js +1 -78
- package/packages/client/dist/cache/encrypted-cache.d.ts +27 -0
- package/packages/client/dist/cache/encrypted-cache.js +1 -108
- package/packages/client/dist/cache/in-memory-asset-cache.js +1 -73
- package/packages/client/dist/cache/index.js +1 -13
- package/packages/client/dist/cache/protected-pi-boundary.js +1 -44
- package/packages/client/dist/checkpoint/checkpoint-client-reader.js +2 -127
- package/packages/client/dist/checkpoint/index.js +1 -11
- package/packages/client/dist/checkpoint/shared-checkpoint-types.js +1 -32
- package/packages/client/dist/cli.js +5 -358
- package/packages/client/dist/commands/activate.js +8 -394
- package/packages/client/dist/commands/architecture-answers.js +7 -274
- package/packages/client/dist/commands/architecture-memory-terminal.js +4 -253
- package/packages/client/dist/commands/architecture-non-ui-continuation.js +1 -100
- package/packages/client/dist/commands/architecture-root-binding.js +1 -51
- package/packages/client/dist/commands/cache-status.js +2 -112
- package/packages/client/dist/commands/config.js +1 -88
- package/packages/client/dist/commands/doctor.js +2 -635
- package/packages/client/dist/commands/invoke.d.ts +38 -1
- package/packages/client/dist/commands/invoke.js +143 -7200
- package/packages/client/dist/commands/legacy-epic-terminal.js +1 -71
- package/packages/client/dist/commands/legacy-story-branch.js +1 -96
- package/packages/client/dist/commands/legacy-story-pr.js +1 -276
- package/packages/client/dist/commands/mutation-qa-checkpoint.js +5 -285
- package/packages/client/dist/commands/mutation-qa-gate.js +1 -54
- package/packages/client/dist/commands/mutation-qa-git.js +1 -41
- package/packages/client/dist/commands/mutation-qa-policy.js +1 -102
- package/packages/client/dist/commands/mutation-qa-run.js +3 -219
- package/packages/client/dist/commands/mutation-qa-verdict.js +1 -149
- package/packages/client/dist/commands/mutation-qa.js +1 -220
- package/packages/client/dist/commands/native-agent.js +2 -136
- package/packages/client/dist/commands/planning-diagnosis.js +1 -43
- package/packages/client/dist/commands/planning-generation.js +1 -53
- package/packages/client/dist/commands/planning-memory.js +1 -40
- package/packages/client/dist/commands/planning-research.js +1 -42
- package/packages/client/dist/commands/private-session.js +1 -75
- package/packages/client/dist/commands/property-qa.js +1 -139
- package/packages/client/dist/commands/qa-control.js +1 -16
- package/packages/client/dist/commands/qa-scan-browser-launch.js +1 -55
- package/packages/client/dist/commands/qa-scan-browser-route.js +1 -28
- package/packages/client/dist/commands/qa-scan-capture.js +5 -663
- package/packages/client/dist/commands/qa-scan-discovery.js +3 -376
- package/packages/client/dist/commands/qa-scan-review.js +5 -826
- package/packages/client/dist/commands/qa-synthetic-backend-capture.js +1 -266
- package/packages/client/dist/commands/qa-synthetic-backend-review.js +1 -297
- package/packages/client/dist/commands/qa-synthetic-backend.js +2 -340
- package/packages/client/dist/commands/qa-synthetic-broker-port.js +1 -85
- package/packages/client/dist/commands/qa-synthetic-browser.js +1 -39
- package/packages/client/dist/commands/refactor-characterization.js +21 -671
- package/packages/client/dist/commands/refactor-commit.js +13 -399
- package/packages/client/dist/commands/refactor-control.js +3 -142
- package/packages/client/dist/commands/refactor-cpp-assertions.js +3 -93
- package/packages/client/dist/commands/refactor-review.js +5 -283
- package/packages/client/dist/commands/refactor-root.js +7 -588
- package/packages/client/dist/commands/refresh-memory.js +3 -156
- package/packages/client/dist/commands/shared-architecture-non-ui.js +1 -24
- package/packages/client/dist/commands/story-checkpoint.js +1 -171
- package/packages/client/dist/config/host-auth-broker.d.ts +31 -0
- package/packages/client/dist/config/host-auth-broker.js +1 -0
- package/packages/client/dist/config/host-auth-reference.d.ts +32 -0
- package/packages/client/dist/config/host-auth-reference.js +1 -0
- package/packages/client/dist/config/profile-directory.js +1 -13
- package/packages/client/dist/config/resolver-selection.js +1 -278
- package/packages/client/dist/config/secure-config.js +12 -281
- package/packages/client/dist/config.js +2 -223
- package/packages/client/dist/constants.js +1 -25
- package/packages/client/dist/context/context-collector.js +2 -222
- package/packages/client/dist/context/context-sanitizer.js +1 -145
- package/packages/client/dist/continuity/autonomous-governance.js +1 -285
- package/packages/client/dist/continuity/continuity-client-state-store.js +1 -1587
- package/packages/client/dist/continuity/invoke-hooks.js +1 -129
- package/packages/client/dist/continuity/migrations/001-initial-schema.js +2 -12
- package/packages/client/dist/continuity/migrations/002-processing-output-summary-indexes.js +2 -12
- package/packages/client/dist/continuity/migrations/003-headless-execution-attempts.js +2 -12
- package/packages/client/dist/continuity/migrations/004-outbox-delivery-state.js +2 -12
- package/packages/client/dist/continuity/migrations/005-bounded-headless-queue.js +2 -12
- package/packages/client/dist/continuity/migrations/006-native-agent-dispatches.js +2 -10
- package/packages/client/dist/continuity/native-agent-store.js +1 -69
- package/packages/client/dist/continuity/native-agent-types.d.ts +11 -0
- package/packages/client/dist/continuity/native-agent-types.js +1 -90
- package/packages/client/dist/continuity/outbox.js +1 -117
- package/packages/client/dist/continuity/runner-bootstrap-policy.js +1 -172
- package/packages/client/dist/continuity/sqlite-store.js +81 -2612
- package/packages/client/dist/effects/invoke-effect-gateways.js +1 -62
- package/packages/client/dist/errors/error-messages.d.ts +3 -0
- package/packages/client/dist/errors/error-messages.js +2 -228
- package/packages/client/dist/errors/frontmatter-diagnostics.js +1 -125
- package/packages/client/dist/evidence/collector.js +1 -347
- package/packages/client/dist/evidence/index.js +1 -1
- package/packages/client/dist/evidence/types.js +0 -2
- package/packages/client/dist/fs/atomic-state-replace.js +1 -152
- package/packages/client/dist/generated/command-registry.generated.js +1 -4076
- package/packages/client/dist/generated/public-projections.generated.js +1 -149
- package/packages/client/dist/git/direct-yolo-c01-retry.js +4 -138
- package/packages/client/dist/git/direct-yolo-checkpoint.d.ts +11 -1
- package/packages/client/dist/git/direct-yolo-checkpoint.js +4 -1234
- package/packages/client/dist/git/epic-operational-state.js +2 -672
- package/packages/client/dist/git/git-lifecycle-step-ids.js +1 -14
- package/packages/client/dist/git/git-lifecycle-store.d.ts +23 -0
- package/packages/client/dist/git/git-lifecycle-store.js +10 -1981
- package/packages/client/dist/git/legacy-story-branch-checkpoint.js +9 -259
- package/packages/client/dist/git/legacy-story-branch-setup.js +4 -348
- package/packages/client/dist/git/legacy-story-pr.js +70 -3538
- package/packages/client/dist/git/secure-filesystem-boundary.js +5 -182
- package/packages/client/dist/graph-retrieval/pre-command-hook.js +1 -284
- package/packages/client/dist/graph-retrieval/shared-graph-retrieval-contract.js +1 -237
- package/packages/client/dist/i18n/first-run.js +2 -60
- package/packages/client/dist/index.d.ts +2 -0
- package/packages/client/dist/index.js +1 -47
- package/packages/client/dist/invocation/invoke-input.js +1 -24
- package/packages/client/dist/license/host-auth-recovery.d.ts +158 -0
- package/packages/client/dist/license/host-auth-recovery.js +1 -0
- package/packages/client/dist/license/index.js +1 -5
- package/packages/client/dist/license/license-client.d.ts +10 -1
- package/packages/client/dist/license/license-client.js +1 -304
- package/packages/client/dist/machine/fingerprint.js +2 -163
- package/packages/client/dist/machine/index.js +1 -5
- package/packages/client/dist/memory/project-memory-path-scope.js +1 -62
- package/packages/client/dist/memory/project-memory-writer.js +40 -1007
- package/packages/client/dist/memory/shared-project-memory-types.js +2 -253
- package/packages/client/dist/migrations/epic-lifecycle-backfill.js +3 -1060
- package/packages/client/dist/migrations/project-identity-capability.js +1 -50
- package/packages/client/dist/migrations/project-identity.js +7 -493
- package/packages/client/dist/migrations/state-json.js +4 -393
- package/packages/client/dist/policy/architecture-policy.js +2 -184
- package/packages/client/dist/policy/index.js +1 -7
- package/packages/client/dist/policy/shared-policy-types.js +0 -38
- package/packages/client/dist/policy/subagent-approval-policy.js +1 -55
- package/packages/client/dist/private-execution/clean-baseline.js +2 -279
- package/packages/client/dist/private-execution/codex-executor.js +4 -574
- package/packages/client/dist/private-execution/codex-host-runtime.js +1 -137
- package/packages/client/dist/private-execution/codex-lifecycle-guard.js +1 -97
- package/packages/client/dist/private-execution/codex-scoped-permissions.js +1 -128
- package/packages/client/dist/private-execution/delivery.js +13 -410
- package/packages/client/dist/private-execution/diagnosis-root-review.js +2 -294
- package/packages/client/dist/private-execution/frozen-handoff.js +6 -19
- package/packages/client/dist/private-execution/implementation-validation.js +2 -106
- package/packages/client/dist/private-execution/isolated-admission.js +23 -1432
- package/packages/client/dist/private-execution/isolated-session.js +5 -725
- package/packages/client/dist/private-execution/isolated-workspace.js +1 -122
- package/packages/client/dist/private-execution/pact-consumer-runtime.js +3 -409
- package/packages/client/dist/private-execution/pact-test-source.js +1 -258
- package/packages/client/dist/private-execution/pact-toolchain.js +3 -290
- package/packages/client/dist/private-execution/pact-write-set.js +1 -172
- package/packages/client/dist/private-execution/planning-diagnosis-checkpoint.js +5 -406
- package/packages/client/dist/private-execution/planning-generation-checkpoint.js +5 -462
- package/packages/client/dist/private-execution/planning-memory-checkpoint.js +3 -139
- package/packages/client/dist/private-execution/planning-research-checkpoint.js +4 -325
- package/packages/client/dist/private-execution/private-report-store.js +1 -42
- package/packages/client/dist/private-execution/property-test-candidate.js +1 -103
- package/packages/client/dist/private-execution/property-test-choices.js +3 -147
- package/packages/client/dist/private-execution/property-test-plan.js +7 -271
- package/packages/client/dist/private-execution/property-test-review.js +4 -258
- package/packages/client/dist/private-execution/property-test-runtime.js +9 -372
- package/packages/client/dist/private-execution/property-test-source.js +1 -139
- package/packages/client/dist/private-execution/root-checkpoint.js +1 -80
- package/packages/client/dist/private-execution/root-memory-git-evidence.js +3 -135
- package/packages/client/dist/private-execution/root-memory-recovery.js +1 -93
- package/packages/client/dist/private-execution/root-memory-request.js +1 -29
- package/packages/client/dist/private-execution/root-memory-scope.js +1 -178
- package/packages/client/dist/private-execution/root-memory-writer.js +1 -102
- package/packages/client/dist/private-execution/session-lease.js +1 -63
- package/packages/client/dist/private-execution/shared-private-admission-types.js +1 -50
- package/packages/client/dist/private-execution/shared-project-validation.js +1 -66
- package/packages/client/dist/private-execution/shared-root-memory-types.js +1 -61
- package/packages/client/dist/private-execution/ux-source-validation.js +1 -372
- package/packages/client/dist/private-execution/ux-write-set.js +2 -191
- package/packages/client/dist/registry/registry-reconciliation.js +1 -234
- package/packages/client/dist/resilience/circuit-breaker.js +1 -170
- package/packages/client/dist/resilience/degradation-manager.js +1 -164
- package/packages/client/dist/resilience/freshness-indicator.js +1 -100
- package/packages/client/dist/resilience/index.js +1 -8
- package/packages/client/dist/resilience/recovery-detector.js +1 -74
- package/packages/client/dist/resolvers/asset-resolver.js +0 -13
- package/packages/client/dist/resolvers/local-resolver.js +8 -255
- package/packages/client/dist/resolvers/remote-resolver.js +1 -233
- package/packages/client/dist/runner/binary.js +1 -404
- package/packages/client/dist/runner/cli.js +17 -1571
- package/packages/client/dist/runner/epic-background-dispatcher.d.ts +61 -0
- package/packages/client/dist/runner/epic-background-dispatcher.js +1 -0
- package/packages/client/dist/runner/headless/adapters.js +1 -49
- package/packages/client/dist/runner/headless/background-root.d.ts +65 -0
- package/packages/client/dist/runner/headless/background-root.js +1 -0
- package/packages/client/dist/runner/headless/claude-code-adapter.js +1 -79
- package/packages/client/dist/runner/headless/continuous-story-workspace.js +11 -1628
- package/packages/client/dist/runner/headless/epic-git-reconciliation-inventory.js +1 -69
- package/packages/client/dist/runner/headless/epic-merge-candidate-qa.d.ts +1 -1
- package/packages/client/dist/runner/headless/epic-merge-candidate-qa.js +2 -110
- package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.d.ts +2 -0
- package/packages/client/dist/runner/headless/epic-terminal-git-cli-adapter.js +10 -1236
- package/packages/client/dist/runner/headless/epic-terminal-git-runner.d.ts +27 -2
- package/packages/client/dist/runner/headless/epic-terminal-git-runner.js +2 -318
- package/packages/client/dist/runner/headless/fake-adapter.js +1 -62
- package/packages/client/dist/runner/headless/managed-environment-cleanup.d.ts +95 -0
- package/packages/client/dist/runner/headless/managed-environment-cleanup.js +1 -0
- package/packages/client/dist/runner/headless/managed-profile.js +1 -132
- package/packages/client/dist/runner/headless/opencode-adapter.js +2 -145
- package/packages/client/dist/runner/headless/prepared-story-bundle.js +1 -43
- package/packages/client/dist/runner/headless/resolver.js +1 -231
- package/packages/client/dist/runner/headless/run-yolo.js +1 -885
- package/packages/client/dist/runner/headless/run-yoloop.d.ts +1 -0
- package/packages/client/dist/runner/headless/run-yoloop.js +2 -2574
- package/packages/client/dist/runner/headless/types.js +2 -574
- package/packages/client/dist/runner/local-console.js +13 -396
- package/packages/client/dist/runner/portal-sync-worker.js +1 -1487
- package/packages/client/dist/runner/project-identity.js +1 -106
- package/packages/client/dist/runner/scheduler.js +7 -995
- package/packages/client/dist/runner-cli.js +2 -18
- package/packages/client/dist/state/invocation-shape.js +1 -13
- package/packages/client/dist/state/project-state-snapshot.d.ts +7 -0
- package/packages/client/dist/state/project-state-snapshot.js +1 -1731
- package/packages/client/dist/state/state-json-repair.js +3 -785
- package/packages/client/dist/state/structural-dag-projection.js +1 -118
- package/packages/client/dist/telemetry/index.js +1 -5
- package/packages/client/dist/telemetry/offline-queue.js +1 -131
- package/packages/client/dist/tier/index.js +1 -5
- package/packages/client/dist/tier/tier-aware-client.js +1 -405
- package/packages/client/dist/types/index.js +1 -38
- package/packages/client/dist/version.js +1 -27
- package/packages/client/dist/yolo/change-evidence-collector.d.ts +21 -1
- package/packages/client/dist/yolo/change-evidence-collector.js +1 -126
- package/packages/client/dist/yolo/invoke-envelope-capability.js +1 -62
- package/packages/client/dist/yolo/spec-envelope-collector.js +5 -184
- package/packages/client/dist/yoloop/discovery-hook.js +2 -300
- package/packages/client/dist/yoloop/index.js +1 -8
- package/packages/client/dist/yoloop/invoke-hooks.js +12 -669
- package/packages/client/dist/yoloop/shared-discover-epics.js +1 -1007
- package/packages/client/dist/yoloop/shared-yoloop-types.js +1 -33
- package/packages/client/dist/yoloop/target-normalization.js +1 -180
- package/packages/client/dist/yoloop/yoloop-client-state-store.js +1 -1920
- package/packages/shared/dist/invocation/explicit-intent-recovery.d.ts +83 -0
- package/packages/shared/dist/invocation/explicit-intent-recovery.js +187 -0
- package/packages/shared/dist/invocation/index.d.ts +7 -0
- package/packages/shared/dist/invocation/index.js +22 -0
- package/packages/shared/dist/yolo/change-evidence.js +1 -1
- package/packages/shared/dist/yoloop/epic-terminal-git.js +144 -2
- package/targets-stubs/antigravity/gemini.md +1 -1
- package/targets-stubs/antigravity/skill/SKILL.md +1 -1
- package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex-root.md +2 -2
- package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex.md +2 -2
- package/targets-stubs/codex/AGENTS.md +24 -21
- package/targets-stubs/codex/README.md +6 -1
- package/targets-stubs/codex/neocortex.toml +7 -2
- package/targets-stubs/cursor/agent.md +2 -2
- package/targets-stubs/gemini-cli/agent.md +2 -2
- package/targets-stubs/opencode/neocortex-root.md +16 -1
- package/targets-stubs/opencode/neocortex.md +6 -0
- package/targets-stubs/vscode/neocortex.agent.md +2 -2
|
@@ -1,219 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
-
import { homedir, tmpdir } from 'node:os';
|
|
6
|
-
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
7
|
-
import { mutationQaGit } from './mutation-qa-git.js';
|
|
8
|
-
import { planMutationQa } from './mutation-qa.js';
|
|
9
|
-
import { evaluateMutationQaReport } from './mutation-qa-verdict.js';
|
|
10
|
-
const REPORT_RELATIVE_PATH = 'reports/mutation/mutation.json';
|
|
11
|
-
const MAX_REPORT_BYTES = 8 * 1024 * 1024;
|
|
12
|
-
const MAX_PROCESS_MS = 30 * 60 * 1000;
|
|
13
|
-
const MAX_PREPARE_MS = 5 * 60 * 1000;
|
|
14
|
-
function ensureInside(root, path) {
|
|
15
|
-
const rel = relative(root, path);
|
|
16
|
-
return rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
17
|
-
}
|
|
18
|
-
function readSafeReport(root) {
|
|
19
|
-
const path = join(root, REPORT_RELATIVE_PATH);
|
|
20
|
-
if (!existsSync(path))
|
|
21
|
-
return null;
|
|
22
|
-
const real = realpathSync(path);
|
|
23
|
-
if (real !== path || !ensureInside(root, real))
|
|
24
|
-
throw new Error('mutation-qa-report-path-unsafe');
|
|
25
|
-
const stat = lstatSync(path);
|
|
26
|
-
if (!stat.isFile() || stat.size === 0 || stat.size > MAX_REPORT_BYTES) {
|
|
27
|
-
throw new Error('mutation-qa-report-size-invalid');
|
|
28
|
-
}
|
|
29
|
-
return readFileSync(path);
|
|
30
|
-
}
|
|
31
|
-
function assertPhysicalAfterRun(root, plan) {
|
|
32
|
-
const head = mutationQaGit(root, ['rev-parse', '--verify', 'HEAD']).toString('utf8').trim();
|
|
33
|
-
const baseRevision = plan.baseRef.startsWith('sha:') ? plan.baseSha : plan.baseRef;
|
|
34
|
-
const base = mutationQaGit(root, ['rev-parse', '--verify', '--end-of-options', `${baseRevision}^{commit}`])
|
|
35
|
-
.toString('utf8').trim();
|
|
36
|
-
if (head !== plan.headSha || base !== plan.baseSha)
|
|
37
|
-
throw new Error('mutation-qa-physical-drift');
|
|
38
|
-
const entries = mutationQaGit(root, ['status', '--porcelain=v1', '-z', '--untracked-files=all'])
|
|
39
|
-
.toString('utf8').split('\0').filter(Boolean);
|
|
40
|
-
if (entries.some((entry) => !entry.startsWith('?? ')
|
|
41
|
-
|| !(entry.slice(3).startsWith('node_modules/')
|
|
42
|
-
|| entry.slice(3).startsWith('reports/mutation/')
|
|
43
|
-
|| entry.slice(3).startsWith('.stryker-tmp/')))) {
|
|
44
|
-
throw new Error('mutation-qa-checkout-drift');
|
|
45
|
-
}
|
|
46
|
-
for (const file of plan.eligibleFiles) {
|
|
47
|
-
const path = resolve(root, file.path);
|
|
48
|
-
if (!ensureInside(root, path) || realpathSync(path) !== path || !lstatSync(path).isFile()
|
|
49
|
-
|| mutationQaGit(root, ['hash-object', '--', file.path]).toString('utf8').trim() !== file.blobSha) {
|
|
50
|
-
throw new Error('mutation-qa-source-drift');
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
function trustedTool(name) {
|
|
55
|
-
for (const directory of (process.env.PATH ?? '').split(':')) {
|
|
56
|
-
if (!isAbsolute(directory))
|
|
57
|
-
continue;
|
|
58
|
-
const candidate = join(directory, name);
|
|
59
|
-
if (!existsSync(candidate))
|
|
60
|
-
continue;
|
|
61
|
-
try {
|
|
62
|
-
const tool = realpathSync(candidate);
|
|
63
|
-
if ((tool.startsWith('/usr/') || tool.startsWith('/home/linuxbrew/.linuxbrew/'))
|
|
64
|
-
&& lstatSync(tool).isFile() && (name !== 'npm' || tool.endsWith('.js')))
|
|
65
|
-
return tool;
|
|
66
|
-
}
|
|
67
|
-
catch { /* The first matching tool on PATH is not trusted. */ }
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
function sandboxArgs(sandbox, privateHome, hostsPath, sourceEnv, cachePath) {
|
|
73
|
-
const executable = realpathSync(process.execPath);
|
|
74
|
-
const brewRoot = '/home/linuxbrew/.linuxbrew';
|
|
75
|
-
if (!executable.startsWith('/usr/') && !executable.startsWith(`${brewRoot}/`))
|
|
76
|
-
return null;
|
|
77
|
-
const args = ['--die-with-parent', '--unshare-net', '--unshare-pid', '--new-session',
|
|
78
|
-
'--ro-bind', '/usr', '/usr'];
|
|
79
|
-
for (const path of ['/bin', '/lib', '/lib64']) {
|
|
80
|
-
if (existsSync(path))
|
|
81
|
-
args.push('--ro-bind', path, path);
|
|
82
|
-
}
|
|
83
|
-
if (executable.startsWith(`${brewRoot}/`)) {
|
|
84
|
-
if (!existsSync(brewRoot))
|
|
85
|
-
return null;
|
|
86
|
-
args.push('--dir', '/home', '--dir', '/home/linuxbrew', '--ro-bind', brewRoot, brewRoot);
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
args.push('--dir', '/home');
|
|
90
|
-
}
|
|
91
|
-
args.push('--bind', privateHome, '/home/worker');
|
|
92
|
-
if (cachePath)
|
|
93
|
-
args.push('--dir', '/npm-cache', '--overlay-src', cachePath, '--tmp-overlay', '/npm-cache');
|
|
94
|
-
args.push('--dir', '/etc', '--ro-bind', hostsPath, '/etc/hosts', '--proc', '/proc', '--dev', '/dev', '--tmpfs', '/tmp', '--dir', '/work', '--bind', sandbox, '/work', '--chdir', '/work', '--clearenv');
|
|
95
|
-
for (const [key, value] of Object.entries(sourceEnv)) {
|
|
96
|
-
args.push('--setenv', key, value);
|
|
97
|
-
}
|
|
98
|
-
return args;
|
|
99
|
-
}
|
|
100
|
-
/** Installs only from the local npm cache in a disposable clone; never mutates the source checkout. */
|
|
101
|
-
export function runMutationQa(projectRoot, baseRef, expectedPlanSha256) {
|
|
102
|
-
if (!/^[a-f0-9]{64}$/.test(expectedPlanSha256))
|
|
103
|
-
throw new Error('mutation-qa-plan-digest-invalid');
|
|
104
|
-
const root = resolve(projectRoot);
|
|
105
|
-
const plan = planMutationQa(root, baseRef);
|
|
106
|
-
if (plan.planSha256 !== expectedPlanSha256)
|
|
107
|
-
throw new Error('mutation-qa-plan-drift');
|
|
108
|
-
const base = { schemaVersion: 1, kind: 'mutation-qa-run',
|
|
109
|
-
planSha256: plan.planSha256, headSha: plan.headSha,
|
|
110
|
-
checkpointAccepted: false, localOnly: true,
|
|
111
|
-
executionEnvironment: 'os-sandbox-clone' };
|
|
112
|
-
if (plan.outcome === 'skipped-no-files')
|
|
113
|
-
return Object.freeze({ ...base, outcome: 'skipped-no-files' });
|
|
114
|
-
if (!existsSync(join(root, 'package-lock.json'))) {
|
|
115
|
-
return Object.freeze({ ...base, outcome: 'skipped-dependencies-unavailable' });
|
|
116
|
-
}
|
|
117
|
-
const tempRoot = mkdtempSync(join(tmpdir(), 'mutation-qa-run-'));
|
|
118
|
-
const sandbox = join(tempRoot, 'checkout');
|
|
119
|
-
const privateHome = join(tempRoot, 'home');
|
|
120
|
-
mkdirSync(privateHome, { mode: 0o700 });
|
|
121
|
-
const hostsPath = join(tempRoot, 'hosts');
|
|
122
|
-
writeFileSync(hostsPath, '127.0.0.1 localhost\n::1 localhost\n', { mode: 0o600 });
|
|
123
|
-
const environment = { PATH: process.env.PATH ?? '/usr/bin:/bin', HOME: '/home/worker',
|
|
124
|
-
TMPDIR: '/tmp', CI: 'true', LANG: 'C.UTF-8', NO_COLOR: '1',
|
|
125
|
-
npm_config_offline: 'true', npm_config_yes: 'false', npm_config_ignore_scripts: 'true',
|
|
126
|
-
npm_config_audit: 'false', npm_config_fund: 'false',
|
|
127
|
-
npm_config_userconfig: '/dev/null', npm_config_globalconfig: '/tmp/npm-global.npmrc' };
|
|
128
|
-
try {
|
|
129
|
-
mutationQaGit(root, ['clone', '--local', '--no-hardlinks', '--no-checkout', root, sandbox]);
|
|
130
|
-
mutationQaGit(sandbox, ['checkout', '--detach', plan.headSha]);
|
|
131
|
-
if (!plan.baseRef.startsWith('sha:')) {
|
|
132
|
-
mutationQaGit(sandbox, ['update-ref', plan.baseRef, plan.baseSha]);
|
|
133
|
-
}
|
|
134
|
-
const npmCli = trustedTool('npm');
|
|
135
|
-
const bwrap = trustedTool('bwrap');
|
|
136
|
-
const npmCache = join(homedir(), '.npm');
|
|
137
|
-
if (!npmCli || !bwrap || !existsSync(npmCache)) {
|
|
138
|
-
return Object.freeze({ ...base, outcome: 'skipped-host-isolation-unavailable' });
|
|
139
|
-
}
|
|
140
|
-
const confined = sandboxArgs(sandbox, privateHome, hostsPath, environment);
|
|
141
|
-
const installConfined = sandboxArgs(sandbox, privateHome, hostsPath, {
|
|
142
|
-
...environment, npm_config_cache: '/npm-cache', npm_config_logs_dir: '/tmp/npm-logs',
|
|
143
|
-
}, realpathSync(npmCache));
|
|
144
|
-
if (!confined || !installConfined) {
|
|
145
|
-
return Object.freeze({ ...base, outcome: 'skipped-host-isolation-unavailable' });
|
|
146
|
-
}
|
|
147
|
-
const executable = realpathSync(process.execPath);
|
|
148
|
-
const sandboxProbe = spawnSync(bwrap, [...confined, '--', executable, '-e', 'process.exit(0)'], {
|
|
149
|
-
cwd: sandbox, timeout: 10_000, maxBuffer: 4096,
|
|
150
|
-
stdio: ['ignore', 'pipe', 'pipe'], env: { PATH: process.env.PATH ?? '/usr/bin:/bin' },
|
|
151
|
-
});
|
|
152
|
-
if (sandboxProbe.error || sandboxProbe.signal || sandboxProbe.status !== 0) {
|
|
153
|
-
return Object.freeze({ ...base, outcome: 'skipped-host-isolation-unavailable' });
|
|
154
|
-
}
|
|
155
|
-
const install = spawnSync(bwrap, [...installConfined, '--', executable, npmCli,
|
|
156
|
-
'ci', '--offline', '--ignore-scripts', '--no-audit', '--no-fund'], {
|
|
157
|
-
cwd: sandbox, timeout: MAX_PREPARE_MS, maxBuffer: 2 * 1024 * 1024,
|
|
158
|
-
stdio: ['ignore', 'pipe', 'pipe'], env: { PATH: process.env.PATH ?? '/usr/bin:/bin' },
|
|
159
|
-
});
|
|
160
|
-
if (install.error || install.signal || install.status !== 0) {
|
|
161
|
-
return Object.freeze({ ...base, outcome: 'skipped-dependencies-unavailable' });
|
|
162
|
-
}
|
|
163
|
-
const binary = join(sandbox, 'node_modules/.bin/stryker');
|
|
164
|
-
if (!existsSync(binary))
|
|
165
|
-
return Object.freeze({ ...base, outcome: 'skipped-stryker-missing' });
|
|
166
|
-
const realBinary = realpathSync(binary);
|
|
167
|
-
if (!ensureInside(sandbox, realBinary) || !lstatSync(realBinary).isFile()) {
|
|
168
|
-
throw new Error('mutation-qa-stryker-path-unsafe');
|
|
169
|
-
}
|
|
170
|
-
const binaryInside = `/work/${relative(sandbox, realBinary).split(sep).join('/')}`;
|
|
171
|
-
const priorReport = readSafeReport(sandbox);
|
|
172
|
-
const priorDigest = priorReport ? createHash('sha256').update(priorReport).digest('hex') : null;
|
|
173
|
-
const previousUmask = process.umask(0o077);
|
|
174
|
-
let exitStatus = null;
|
|
175
|
-
let processError = false;
|
|
176
|
-
let hostIsolationUnavailable = false;
|
|
177
|
-
try {
|
|
178
|
-
const result = spawnSync(bwrap, [...confined, '--', executable, binaryInside, 'run', '--mutate',
|
|
179
|
-
plan.eligibleFiles.map((file) => file.path).join(','), '--reporters', 'json',
|
|
180
|
-
'--timeoutMS', '600000', '--concurrency', '4', '--coverageAnalysis', 'off'], {
|
|
181
|
-
cwd: sandbox, timeout: MAX_PROCESS_MS, maxBuffer: 2 * 1024 * 1024,
|
|
182
|
-
stdio: ['ignore', 'pipe', 'pipe'], env: { PATH: process.env.PATH ?? '/usr/bin:/bin' },
|
|
183
|
-
});
|
|
184
|
-
exitStatus = result.status;
|
|
185
|
-
processError = !!result.error || !!result.signal;
|
|
186
|
-
hostIsolationUnavailable = result.error?.code === 'ENOENT';
|
|
187
|
-
}
|
|
188
|
-
finally {
|
|
189
|
-
process.umask(previousUmask);
|
|
190
|
-
}
|
|
191
|
-
assertPhysicalAfterRun(sandbox, plan);
|
|
192
|
-
if (planMutationQa(root, baseRef).planSha256 !== plan.planSha256) {
|
|
193
|
-
throw new Error('mutation-qa-physical-drift');
|
|
194
|
-
}
|
|
195
|
-
if (hostIsolationUnavailable) {
|
|
196
|
-
return Object.freeze({ ...base, outcome: 'skipped-host-isolation-unavailable' });
|
|
197
|
-
}
|
|
198
|
-
if (processError || (exitStatus !== 0 && exitStatus !== 1)) {
|
|
199
|
-
return Object.freeze({ ...base, outcome: 'skipped-stryker-error' });
|
|
200
|
-
}
|
|
201
|
-
const rawReport = readSafeReport(sandbox);
|
|
202
|
-
if (!rawReport || createHash('sha256').update(rawReport).digest('hex') === priorDigest) {
|
|
203
|
-
if (exitStatus === 1)
|
|
204
|
-
return Object.freeze({ ...base, outcome: 'skipped-stryker-error' });
|
|
205
|
-
throw new Error('mutation-qa-report-stale');
|
|
206
|
-
}
|
|
207
|
-
const verdict = evaluateMutationQaReport(plan, rawReport);
|
|
208
|
-
// StrykerJS may exit 1 only because its configured break threshold was
|
|
209
|
-
// crossed. Never infer that from an exit code alone: require a fresh,
|
|
210
|
-
// fully validated report whose own score proves that exact condition.
|
|
211
|
-
if (exitStatus === 1 && !verdict.runnerBreakCrossed) {
|
|
212
|
-
return Object.freeze({ ...base, outcome: 'skipped-stryker-error' });
|
|
213
|
-
}
|
|
214
|
-
return Object.freeze({ ...base, outcome: 'verified', verdict });
|
|
215
|
-
}
|
|
216
|
-
finally {
|
|
217
|
-
rmSync(tempRoot, { recursive: true, force: true });
|
|
218
|
-
}
|
|
219
|
-
}
|
|
1
|
+
import{spawnSync as E}from"node:child_process";import{createHash as q}from"node:crypto";import{existsSync as p,lstatSync as k,mkdirSync as G,mkdtempSync as V,readFileSync as Y,realpathSync as l,rmSync as J,writeFileSync as K}from"node:fs";import{homedir as Z,tmpdir as ee}from"node:os";import{isAbsolute as M,join as u,relative as F,resolve as C,sep as B}from"node:path";import{mutationQaGit as f}from"./mutation-qa-git.js";import{planMutationQa as I}from"./mutation-qa.js";import{evaluateMutationQaReport as te}from"./mutation-qa-verdict.js";const re="reports/mutation/mutation.json",oe=8*1024*1024,ne=1800*1e3,ie=300*1e3;function x(n,t){const a=F(n,t);return a!==".."&&!a.startsWith(`..${B}`)&&!M(a)}function $(n){const t=u(n,re);if(!p(t))return null;const a=l(t);if(a!==t||!x(n,a))throw new Error("mutation-qa-report-path-unsafe");const r=k(t);if(!r.isFile()||r.size===0||r.size>oe)throw new Error("mutation-qa-report-size-invalid");return Y(t)}function se(n,t){const a=f(n,["rev-parse","--verify","HEAD"]).toString("utf8").trim(),r=t.baseRef.startsWith("sha:")?t.baseSha:t.baseRef,i=f(n,["rev-parse","--verify","--end-of-options",`${r}^{commit}`]).toString("utf8").trim();if(a!==t.headSha||i!==t.baseSha)throw new Error("mutation-qa-physical-drift");if(f(n,["status","--porcelain=v1","-z","--untracked-files=all"]).toString("utf8").split("\0").filter(Boolean).some(o=>!o.startsWith("?? ")||!(o.slice(3).startsWith("node_modules/")||o.slice(3).startsWith("reports/mutation/")||o.slice(3).startsWith(".stryker-tmp/"))))throw new Error("mutation-qa-checkout-drift");for(const o of t.eligibleFiles){const e=C(n,o.path);if(!x(n,e)||l(e)!==e||!k(e).isFile()||f(n,["hash-object","--",o.path]).toString("utf8").trim()!==o.blobSha)throw new Error("mutation-qa-source-drift")}}function N(n){for(const t of(process.env.PATH??"").split(":")){if(!M(t))continue;const a=u(t,n);if(p(a)){try{const r=l(a);if((r.startsWith("/usr/")||r.startsWith("/home/linuxbrew/.linuxbrew/"))&&k(r).isFile()&&(n!=="npm"||r.endsWith(".js")))return r}catch{}return null}}return null}function Q(n,t,a,r,i){const s=l(process.execPath),o="/home/linuxbrew/.linuxbrew";if(!s.startsWith("/usr/")&&!s.startsWith(`${o}/`))return null;const e=["--die-with-parent","--unshare-net","--unshare-pid","--new-session","--ro-bind","/usr","/usr"];for(const c of["/bin","/lib","/lib64"])p(c)&&e.push("--ro-bind",c,c);if(s.startsWith(`${o}/`)){if(!p(o))return null;e.push("--dir","/home","--dir","/home/linuxbrew","--ro-bind",o,o)}else e.push("--dir","/home");e.push("--bind",t,"/home/worker"),i&&e.push("--dir","/npm-cache","--overlay-src",i,"--tmp-overlay","/npm-cache"),e.push("--dir","/etc","--ro-bind",a,"/etc/hosts","--proc","/proc","--dev","/dev","--tmpfs","/tmp","--dir","/work","--bind",n,"/work","--chdir","/work","--clearenv");for(const[c,m]of Object.entries(r))e.push("--setenv",c,m);return e}function de(n,t,a){if(!/^[a-f0-9]{64}$/.test(a))throw new Error("mutation-qa-plan-digest-invalid");const r=C(n),i=I(r,t);if(i.planSha256!==a)throw new Error("mutation-qa-plan-drift");const s={schemaVersion:1,kind:"mutation-qa-run",planSha256:i.planSha256,headSha:i.headSha,checkpointAccepted:!1,localOnly:!0,executionEnvironment:"os-sandbox-clone"};if(i.outcome==="skipped-no-files")return Object.freeze({...s,outcome:"skipped-no-files"});if(!p(u(r,"package-lock.json")))return Object.freeze({...s,outcome:"skipped-dependencies-unavailable"});const o=V(u(ee(),"mutation-qa-run-")),e=u(o,"checkout"),c=u(o,"home");G(c,{mode:448});const m=u(o,"hosts");K(m,`127.0.0.1 localhost
|
|
2
|
+
::1 localhost
|
|
3
|
+
`,{mode:384});const A={PATH:process.env.PATH??"/usr/bin:/bin",HOME:"/home/worker",TMPDIR:"/tmp",CI:"true",LANG:"C.UTF-8",NO_COLOR:"1",npm_config_offline:"true",npm_config_yes:"false",npm_config_ignore_scripts:"true",npm_config_audit:"false",npm_config_fund:"false",npm_config_userconfig:"/dev/null",npm_config_globalconfig:"/tmp/npm-global.npmrc"};try{f(r,["clone","--local","--no-hardlinks","--no-checkout",r,e]),f(e,["checkout","--detach",i.headSha]),i.baseRef.startsWith("sha:")||f(e,["update-ref",i.baseRef,i.baseSha]);const O=N("npm"),d=N("bwrap"),j=u(Z(),".npm");if(!O||!d||!p(j))return Object.freeze({...s,outcome:"skipped-host-isolation-unavailable"});const v=Q(e,c,m,A),R=Q(e,c,m,{...A,npm_config_cache:"/npm-cache",npm_config_logs_dir:"/tmp/npm-logs"},l(j));if(!v||!R)return Object.freeze({...s,outcome:"skipped-host-isolation-unavailable"});const w=l(process.execPath),g=E(d,[...v,"--",w,"-e","process.exit(0)"],{cwd:e,timeout:1e4,maxBuffer:4096,stdio:["ignore","pipe","pipe"],env:{PATH:process.env.PATH??"/usr/bin:/bin"}});if(g.error||g.signal||g.status!==0)return Object.freeze({...s,outcome:"skipped-host-isolation-unavailable"});const y=E(d,[...R,"--",w,O,"ci","--offline","--ignore-scripts","--no-audit","--no-fund"],{cwd:e,timeout:ie,maxBuffer:2*1024*1024,stdio:["ignore","pipe","pipe"],env:{PATH:process.env.PATH??"/usr/bin:/bin"}});if(y.error||y.signal||y.status!==0)return Object.freeze({...s,outcome:"skipped-dependencies-unavailable"});const P=u(e,"node_modules/.bin/stryker");if(!p(P))return Object.freeze({...s,outcome:"skipped-stryker-missing"});const S=l(P);if(!x(e,S)||!k(S).isFile())throw new Error("mutation-qa-stryker-path-unsafe");const D=`/work/${F(e,S).split(B).join("/")}`,T=$(e),L=T?q("sha256").update(T).digest("hex"):null,U=process.umask(63);let h=null,z=!1,H=!1;try{const b=E(d,[...v,"--",w,D,"run","--mutate",i.eligibleFiles.map(X=>X.path).join(","),"--reporters","json","--timeoutMS","600000","--concurrency","4","--coverageAnalysis","off"],{cwd:e,timeout:ne,maxBuffer:2097152,stdio:["ignore","pipe","pipe"],env:{PATH:process.env.PATH??"/usr/bin:/bin"}});h=b.status,z=!!b.error||!!b.signal,H=b.error?.code==="ENOENT"}finally{process.umask(U)}if(se(e,i),I(r,t).planSha256!==i.planSha256)throw new Error("mutation-qa-physical-drift");if(H)return Object.freeze({...s,outcome:"skipped-host-isolation-unavailable"});if(z||h!==0&&h!==1)return Object.freeze({...s,outcome:"skipped-stryker-error"});const _=$(e);if(!_||q("sha256").update(_).digest("hex")===L){if(h===1)return Object.freeze({...s,outcome:"skipped-stryker-error"});throw new Error("mutation-qa-report-stale")}const W=te(i,_);return h===1&&!W.runnerBreakCrossed?Object.freeze({...s,outcome:"skipped-stryker-error"}):Object.freeze({...s,outcome:"verified",verdict:W})}finally{J(o,{recursive:!0,force:!0})}}export{de as runMutationQa};
|
|
@@ -1,149 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
function score(killed, survived, noCoverage, timeout) {
|
|
4
|
-
const valid = killed + survived + noCoverage + timeout;
|
|
5
|
-
return valid === 0 ? 0 : Math.round(((killed + timeout) / valid) * 10_000) / 100;
|
|
6
|
-
}
|
|
7
|
-
function gitBlobSha(source, objectFormat) {
|
|
8
|
-
const content = Buffer.from(source, 'utf8');
|
|
9
|
-
return createHash(objectFormat).update(`blob ${content.byteLength}\0`).update(content).digest('hex');
|
|
10
|
-
}
|
|
11
|
-
function areaThreshold(path, thresholds) {
|
|
12
|
-
const area = path.match(/^src\/([^/]+)\//)?.[1];
|
|
13
|
-
if (area && thresholds.perAreaThreshold[area] !== undefined)
|
|
14
|
-
return thresholds.perAreaThreshold[area];
|
|
15
|
-
if (area === 'adapters' && thresholds.perAreaThreshold.infrastructure !== undefined) {
|
|
16
|
-
return thresholds.perAreaThreshold.infrastructure;
|
|
17
|
-
}
|
|
18
|
-
return thresholds.defaultThreshold;
|
|
19
|
-
}
|
|
20
|
-
/** Reject partial/foreign reports; never project file names, source, mutants or test details. */
|
|
21
|
-
export function evaluateMutationQaReport(plan, rawReport) {
|
|
22
|
-
const thresholds = plan.policy.thresholds;
|
|
23
|
-
if (plan.outcome !== 'ready' || plan.eligibleFiles.length === 0) {
|
|
24
|
-
throw new Error('mutation-qa-plan-not-ready');
|
|
25
|
-
}
|
|
26
|
-
if (rawReport.byteLength === 0 || rawReport.byteLength > 8 * 1024 * 1024) {
|
|
27
|
-
throw new Error('mutation-qa-report-size-invalid');
|
|
28
|
-
}
|
|
29
|
-
if ([thresholds.defaultThreshold, thresholds.blockThreshold,
|
|
30
|
-
...Object.values(thresholds.perAreaThreshold)].some((value) => !Number.isFinite(value)
|
|
31
|
-
|| value < 0 || value > 100)
|
|
32
|
-
|| thresholds.blockThreshold > thresholds.defaultThreshold) {
|
|
33
|
-
throw new Error('mutation-qa-thresholds-invalid');
|
|
34
|
-
}
|
|
35
|
-
let parsed;
|
|
36
|
-
try {
|
|
37
|
-
parsed = JSON.parse(rawReport.toString('utf8'));
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
throw new Error('mutation-qa-report-invalid');
|
|
41
|
-
}
|
|
42
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
43
|
-
throw new Error('mutation-qa-report-invalid');
|
|
44
|
-
}
|
|
45
|
-
const record = parsed;
|
|
46
|
-
if (typeof record.schemaVersion !== 'string' || !/^[12](?:\.\d+){0,2}$/.test(record.schemaVersion)
|
|
47
|
-
|| !record.thresholds || typeof record.thresholds !== 'object' || Array.isArray(record.thresholds)
|
|
48
|
-
|| !record.files || typeof record.files !== 'object'
|
|
49
|
-
|| Array.isArray(record.files))
|
|
50
|
-
throw new Error('mutation-qa-report-invalid');
|
|
51
|
-
const runnerThresholds = record.thresholds;
|
|
52
|
-
const runnerHigh = runnerThresholds.high;
|
|
53
|
-
const runnerLow = runnerThresholds.low;
|
|
54
|
-
const runnerBreakThreshold = runnerThresholds.break === undefined || runnerThresholds.break === null
|
|
55
|
-
? null : runnerThresholds.break;
|
|
56
|
-
if (typeof runnerHigh !== 'number' || !Number.isFinite(runnerHigh)
|
|
57
|
-
|| runnerHigh < 0 || runnerHigh > 100
|
|
58
|
-
|| typeof runnerLow !== 'number' || !Number.isFinite(runnerLow)
|
|
59
|
-
|| runnerLow < 0 || runnerLow > runnerHigh
|
|
60
|
-
|| runnerBreakThreshold !== null && (typeof runnerBreakThreshold !== 'number'
|
|
61
|
-
|| !Number.isFinite(runnerBreakThreshold) || runnerBreakThreshold < 0
|
|
62
|
-
|| runnerBreakThreshold > runnerLow))
|
|
63
|
-
throw new Error('mutation-qa-report-invalid');
|
|
64
|
-
const files = record.files;
|
|
65
|
-
const expected = plan.eligibleFiles.map((file) => file.path).sort();
|
|
66
|
-
const actual = Object.keys(files).sort();
|
|
67
|
-
if (expected.length !== actual.length || expected.some((path, index) => path !== actual[index])) {
|
|
68
|
-
throw new Error('mutation-qa-report-scope-mismatch');
|
|
69
|
-
}
|
|
70
|
-
let killed = 0;
|
|
71
|
-
let survived = 0;
|
|
72
|
-
let noCoverage = 0;
|
|
73
|
-
let timeout = 0;
|
|
74
|
-
let invalid = 0;
|
|
75
|
-
let belowAreaThresholdCount = 0;
|
|
76
|
-
for (const path of expected) {
|
|
77
|
-
const file = files[path];
|
|
78
|
-
if (!file || typeof file !== 'object' || Array.isArray(file)
|
|
79
|
-
|| typeof file.language !== 'string'
|
|
80
|
-
|| typeof file.source !== 'string'
|
|
81
|
-
|| !Array.isArray(file.mutants)) {
|
|
82
|
-
throw new Error('mutation-qa-report-invalid');
|
|
83
|
-
}
|
|
84
|
-
const expectedBlob = plan.eligibleFiles.find((entry) => entry.path === path)?.blobSha;
|
|
85
|
-
if (!expectedBlob || gitBlobSha(file.source, expectedBlob.length === 64 ? 'sha256' : 'sha1') !== expectedBlob) {
|
|
86
|
-
throw new Error('mutation-qa-report-source-mismatch');
|
|
87
|
-
}
|
|
88
|
-
const mutants = file.mutants;
|
|
89
|
-
if (mutants.length === 0 || mutants.length > 100_000)
|
|
90
|
-
throw new Error('mutation-qa-report-invalid');
|
|
91
|
-
let fileKilled = 0;
|
|
92
|
-
let fileSurvived = 0;
|
|
93
|
-
let fileNoCoverage = 0;
|
|
94
|
-
let fileTimeout = 0;
|
|
95
|
-
for (const mutant of mutants) {
|
|
96
|
-
if (!mutant || typeof mutant !== 'object' || Array.isArray(mutant)) {
|
|
97
|
-
throw new Error('mutation-qa-report-invalid');
|
|
98
|
-
}
|
|
99
|
-
const record = mutant;
|
|
100
|
-
if (typeof record.id !== 'string' || !record.id
|
|
101
|
-
|| typeof record.mutatorName !== 'string' || !record.mutatorName
|
|
102
|
-
|| !record.location || typeof record.location !== 'object' || Array.isArray(record.location)) {
|
|
103
|
-
throw new Error('mutation-qa-report-invalid');
|
|
104
|
-
}
|
|
105
|
-
switch (mutant.status) {
|
|
106
|
-
case 'Killed':
|
|
107
|
-
killed++;
|
|
108
|
-
fileKilled++;
|
|
109
|
-
break;
|
|
110
|
-
case 'Survived':
|
|
111
|
-
survived++;
|
|
112
|
-
fileSurvived++;
|
|
113
|
-
break;
|
|
114
|
-
case 'NoCoverage':
|
|
115
|
-
noCoverage++;
|
|
116
|
-
fileNoCoverage++;
|
|
117
|
-
break;
|
|
118
|
-
case 'Timeout':
|
|
119
|
-
timeout++;
|
|
120
|
-
fileTimeout++;
|
|
121
|
-
break;
|
|
122
|
-
case 'RuntimeError':
|
|
123
|
-
case 'CompileError':
|
|
124
|
-
invalid++;
|
|
125
|
-
break;
|
|
126
|
-
case 'Ignored': break;
|
|
127
|
-
default: throw new Error('mutation-qa-report-status-unsupported');
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if (fileKilled + fileSurvived + fileNoCoverage + fileTimeout === 0) {
|
|
131
|
-
throw new Error('mutation-qa-report-no-scored-mutants');
|
|
132
|
-
}
|
|
133
|
-
if (score(fileKilled, fileSurvived, fileNoCoverage, fileTimeout) < areaThreshold(path, thresholds)) {
|
|
134
|
-
belowAreaThresholdCount++;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
const mutationScore = score(killed, survived, noCoverage, timeout);
|
|
138
|
-
const runnerBreakCrossed = runnerBreakThreshold !== null
|
|
139
|
-
&& (killed + timeout) * 100
|
|
140
|
-
< runnerBreakThreshold * (killed + survived + noCoverage + timeout);
|
|
141
|
-
const outcome = mutationScore < thresholds.blockThreshold ? 'failed'
|
|
142
|
-
: mutationScore < thresholds.defaultThreshold || belowAreaThresholdCount > 0 || invalid > 0
|
|
143
|
-
? 'warned' : 'passed';
|
|
144
|
-
return Object.freeze({ schemaVersion: 1, kind: 'mutation-qa-verdict', planSha256: plan.planSha256,
|
|
145
|
-
reportSha256: createHash('sha256').update(rawReport).digest('hex'),
|
|
146
|
-
runnerBreakThreshold, runnerBreakCrossed,
|
|
147
|
-
outcome, mutationScore, killed, survived, noCoverage, timeout, invalid, belowAreaThresholdCount,
|
|
148
|
-
checkpointAccepted: false, localOnly: true });
|
|
149
|
-
}
|
|
1
|
+
import{createHash as F}from"node:crypto";function C(t,r,e,i){const o=t+r+e+i;return o===0?0:Math.round((t+i)/o*1e4)/100}function O(t,r){const e=Buffer.from(t,"utf8");return F(r).update(`blob ${e.byteLength}\0`).update(e).digest("hex")}function L(t,r){const e=t.match(/^src\/([^/]+)\//)?.[1];return e&&r.perAreaThreshold[e]!==void 0?r.perAreaThreshold[e]:e==="adapters"&&r.perAreaThreshold.infrastructure!==void 0?r.perAreaThreshold.infrastructure:r.defaultThreshold}function z(t,r){const e=t.policy.thresholds;if(t.outcome!=="ready"||t.eligibleFiles.length===0)throw new Error("mutation-qa-plan-not-ready");if(r.byteLength===0||r.byteLength>8*1024*1024)throw new Error("mutation-qa-report-size-invalid");if([e.defaultThreshold,e.blockThreshold,...Object.values(e.perAreaThreshold)].some(a=>!Number.isFinite(a)||a<0||a>100)||e.blockThreshold>e.defaultThreshold)throw new Error("mutation-qa-thresholds-invalid");let i;try{i=JSON.parse(r.toString("utf8"))}catch{throw new Error("mutation-qa-report-invalid")}if(!i||typeof i!="object"||Array.isArray(i))throw new Error("mutation-qa-report-invalid");const o=i;if(typeof o.schemaVersion!="string"||!/^[12](?:\.\d+){0,2}$/.test(o.schemaVersion)||!o.thresholds||typeof o.thresholds!="object"||Array.isArray(o.thresholds)||!o.files||typeof o.files!="object"||Array.isArray(o.files))throw new Error("mutation-qa-report-invalid");const u=o.thresholds,c=u.high,d=u.low,s=u.break===void 0||u.break===null?null:u.break;if(typeof c!="number"||!Number.isFinite(c)||c<0||c>100||typeof d!="number"||!Number.isFinite(d)||d<0||d>c||s!==null&&(typeof s!="number"||!Number.isFinite(s)||s<0||s>d))throw new Error("mutation-qa-report-invalid");const j=o.files,y=t.eligibleFiles.map(a=>a.path).sort(),N=Object.keys(j).sort();if(y.length!==N.length||y.some((a,n)=>a!==N[n]))throw new Error("mutation-qa-report-scope-mismatch");let f=0,p=0,b=0,m=0,w=0,g=0;for(const a of y){const n=j[a];if(!n||typeof n!="object"||Array.isArray(n)||typeof n.language!="string"||typeof n.source!="string"||!Array.isArray(n.mutants))throw new Error("mutation-qa-report-invalid");const A=t.eligibleFiles.find(l=>l.path===a)?.blobSha;if(!A||O(n.source,A.length===64?"sha256":"sha1")!==A)throw new Error("mutation-qa-report-source-mismatch");const T=n.mutants;if(T.length===0||T.length>1e5)throw new Error("mutation-qa-report-invalid");let k=0,E=0,q=0,S=0;for(const l of T){if(!l||typeof l!="object"||Array.isArray(l))throw new Error("mutation-qa-report-invalid");const h=l;if(typeof h.id!="string"||!h.id||typeof h.mutatorName!="string"||!h.mutatorName||!h.location||typeof h.location!="object"||Array.isArray(h.location))throw new Error("mutation-qa-report-invalid");switch(l.status){case"Killed":f++,k++;break;case"Survived":p++,E++;break;case"NoCoverage":b++,q++;break;case"Timeout":m++,S++;break;case"RuntimeError":case"CompileError":w++;break;case"Ignored":break;default:throw new Error("mutation-qa-report-status-unsupported")}}if(k+E+q+S===0)throw new Error("mutation-qa-report-no-scored-mutants");C(k,E,q,S)<L(a,e)&&g++}const v=C(f,p,b,m),x=s!==null&&(f+m)*100<s*(f+p+b+m),B=v<e.blockThreshold?"failed":v<e.defaultThreshold||g>0||w>0?"warned":"passed";return Object.freeze({schemaVersion:1,kind:"mutation-qa-verdict",planSha256:t.planSha256,reportSha256:F("sha256").update(r).digest("hex"),runnerBreakThreshold:s,runnerBreakCrossed:x,outcome:B,mutationScore:v,killed:f,survived:p,noCoverage:b,timeout:m,invalid:w,belowAreaThresholdCount:g,checkpointAccepted:!1,localOnly:!0})}export{z as evaluateMutationQaReport};
|
|
@@ -1,220 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import { lstatSync, realpathSync } from 'node:fs';
|
|
4
|
-
import { resolve } from 'node:path';
|
|
5
|
-
import { mutationQaGit } from './mutation-qa-git.js';
|
|
6
|
-
import { loadMutationQaPolicy, policyExcludesPath } from './mutation-qa-policy.js';
|
|
7
|
-
function value(root, args) {
|
|
8
|
-
return mutationQaGit(root, args).toString('utf8').trim();
|
|
9
|
-
}
|
|
10
|
-
function eligiblePath(path) {
|
|
11
|
-
return path.length > 0 && path.length <= 240
|
|
12
|
-
&& /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*$/.test(path)
|
|
13
|
-
&& !path.split('/').some((part) => part === '.' || part === '..')
|
|
14
|
-
&& /\.(?:ts|tsx|js|jsx)$/.test(path)
|
|
15
|
-
&& !/\.d\.ts$|\.(?:test|spec)\.[jt]sx?$|(?:^|\/)__tests__\/|(?:^|\/)migrations\//.test(path);
|
|
16
|
-
}
|
|
17
|
-
function isSourceCandidate(path) {
|
|
18
|
-
return /\.(?:ts|tsx|js|jsx)$/.test(path)
|
|
19
|
-
&& !/\.d\.ts$|\.(?:test|spec)\.[jt]sx?$|(?:^|\/)__tests__\/|(?:^|\/)migrations\//.test(path);
|
|
20
|
-
}
|
|
21
|
-
/** No test execution, package installation, network, project write or QA verdict. */
|
|
22
|
-
export function planMutationQa(projectRoot, baseRef) {
|
|
23
|
-
const exactBaseSha = /^sha:([a-f0-9]{40}|[a-f0-9]{64})$/.exec(baseRef)?.[1];
|
|
24
|
-
const remoteRef = /^refs\/remotes\/[A-Za-z0-9][A-Za-z0-9_-]{0,63}\/[A-Za-z0-9][A-Za-z0-9/_-]{0,127}$/.test(baseRef)
|
|
25
|
-
&& !baseRef.includes('..') && !baseRef.includes('//') && !baseRef.endsWith('/');
|
|
26
|
-
if (!exactBaseSha && !remoteRef) {
|
|
27
|
-
throw new Error('mutation-qa-base-ref-invalid');
|
|
28
|
-
}
|
|
29
|
-
const root = resolve(projectRoot);
|
|
30
|
-
try {
|
|
31
|
-
if (realpathSync(root) !== root || !lstatSync(root).isDirectory()
|
|
32
|
-
|| value(root, ['rev-parse', '--show-toplevel']) !== root) {
|
|
33
|
-
throw new Error('mutation-qa-project-root-invalid');
|
|
34
|
-
}
|
|
35
|
-
const headSha = value(root, ['rev-parse', '--verify', 'HEAD']);
|
|
36
|
-
const baseRevision = exactBaseSha ?? baseRef;
|
|
37
|
-
const baseSha = value(root, ['rev-parse', '--verify', '--end-of-options', `${baseRevision}^{commit}`]);
|
|
38
|
-
if (exactBaseSha) {
|
|
39
|
-
try {
|
|
40
|
-
mutationQaGit(root, ['merge-base', '--is-ancestor', baseSha, headSha]);
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
throw new Error('mutation-qa-base-not-ancestor');
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
const mergeBaseSha = value(root, ['merge-base', baseSha, headSha]);
|
|
47
|
-
if (![headSha, baseSha, mergeBaseSha].every((sha) => /^[a-f0-9]{40,64}$/.test(sha))) {
|
|
48
|
-
throw new Error('mutation-qa-revision-invalid');
|
|
49
|
-
}
|
|
50
|
-
if (exactBaseSha && (baseSha !== exactBaseSha || mergeBaseSha !== baseSha)) {
|
|
51
|
-
throw new Error('mutation-qa-base-not-ancestor');
|
|
52
|
-
}
|
|
53
|
-
if (mutationQaGit(root, ['status', '--porcelain=v1', '-z', '--untracked-files=all']).byteLength !== 0) {
|
|
54
|
-
throw new Error('mutation-qa-checkout-dirty');
|
|
55
|
-
}
|
|
56
|
-
const policy = loadMutationQaPolicy(root);
|
|
57
|
-
const changedPaths = mutationQaGit(root, ['diff', '--name-only', '-z', '--diff-filter=ACMR', mergeBaseSha, headSha])
|
|
58
|
-
.toString('utf8').split('\0').filter(Boolean);
|
|
59
|
-
if (changedPaths.length > 512)
|
|
60
|
-
throw new Error('mutation-qa-scope-too-large');
|
|
61
|
-
const sourcePaths = changedPaths.filter(isSourceCandidate);
|
|
62
|
-
if (sourcePaths.some((path) => !eligiblePath(path)))
|
|
63
|
-
throw new Error('mutation-qa-source-path-unsafe');
|
|
64
|
-
if (sourcePaths.length > 128)
|
|
65
|
-
throw new Error('mutation-qa-scope-too-large');
|
|
66
|
-
const eligibleFiles = sourcePaths.filter((path) => !policyExcludesPath(policy, path)).sort().map((path) => {
|
|
67
|
-
const entry = value(root, ['ls-tree', 'HEAD', '--', path]);
|
|
68
|
-
const match = entry.match(/^(100644|100755) blob ([a-f0-9]{40,64})\t(.+)$/);
|
|
69
|
-
if (!match || match[3] !== path)
|
|
70
|
-
throw new Error('mutation-qa-source-not-regular');
|
|
71
|
-
return Object.freeze({ path, blobSha: match[2] });
|
|
72
|
-
});
|
|
73
|
-
if (value(root, ['rev-parse', '--verify', 'HEAD']) !== headSha
|
|
74
|
-
|| value(root, ['rev-parse', '--verify', '--end-of-options', `${baseRevision}^{commit}`]) !== baseSha
|
|
75
|
-
|| mutationQaGit(root, ['status', '--porcelain=v1', '-z', '--untracked-files=all']).byteLength !== 0) {
|
|
76
|
-
throw new Error('mutation-qa-physical-drift');
|
|
77
|
-
}
|
|
78
|
-
const fields = { schemaVersion: 1, kind: 'mutation-qa-plan',
|
|
79
|
-
outcome: eligibleFiles.length > 0 ? 'ready' : 'skipped-no-files',
|
|
80
|
-
baseRef, baseSha, headSha, mergeBaseSha, policy, eligibleFiles,
|
|
81
|
-
checkpointAccepted: false, localOnly: true };
|
|
82
|
-
return Object.freeze({ ...fields,
|
|
83
|
-
planSha256: createHash('sha256').update('mutation-qa-plan-v1\0')
|
|
84
|
-
.update(JSON.stringify(fields)).digest('hex'),
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
if (error instanceof Error && error.message.startsWith('mutation-qa-'))
|
|
89
|
-
throw error;
|
|
90
|
-
throw new Error('mutation-qa-plan-unavailable');
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
export async function mutationQaCliHandler(args) {
|
|
94
|
-
let projectRoot = process.cwd();
|
|
95
|
-
let baseRef = '';
|
|
96
|
-
let baseSha = '';
|
|
97
|
-
let planSha = '';
|
|
98
|
-
let authorizeRun = false;
|
|
99
|
-
let recordCheckpoint = false;
|
|
100
|
-
let format = 'plain';
|
|
101
|
-
const seen = new Set();
|
|
102
|
-
try {
|
|
103
|
-
if (args[0] !== 'plan' && args[0] !== 'run' && args[0] !== 'execute' && args[0] !== 'checkpoint'
|
|
104
|
-
&& args[0] !== 'gate') {
|
|
105
|
-
throw new Error('mutation-qa-action-invalid');
|
|
106
|
-
}
|
|
107
|
-
for (let index = 1; index < args.length; index++) {
|
|
108
|
-
const arg = args[index];
|
|
109
|
-
if (seen.has(arg))
|
|
110
|
-
throw new Error('mutation-qa-argument-duplicate');
|
|
111
|
-
seen.add(arg);
|
|
112
|
-
const next = () => args[++index] ?? '';
|
|
113
|
-
if (arg === '--project-root')
|
|
114
|
-
projectRoot = next();
|
|
115
|
-
else if (arg === '--base-ref')
|
|
116
|
-
baseRef = next();
|
|
117
|
-
else if (arg === '--base-sha')
|
|
118
|
-
baseSha = next();
|
|
119
|
-
else if (arg === '--plan-sha')
|
|
120
|
-
planSha = next();
|
|
121
|
-
else if (arg === '--authorize-run')
|
|
122
|
-
authorizeRun = true;
|
|
123
|
-
else if (arg === '--record-checkpoint')
|
|
124
|
-
recordCheckpoint = true;
|
|
125
|
-
else if (arg === '--format') {
|
|
126
|
-
const selected = next();
|
|
127
|
-
if (selected !== 'plain' && selected !== 'json')
|
|
128
|
-
throw new Error('mutation-qa-format-invalid');
|
|
129
|
-
format = selected;
|
|
130
|
-
}
|
|
131
|
-
else
|
|
132
|
-
throw new Error('mutation-qa-argument-invalid');
|
|
133
|
-
}
|
|
134
|
-
if (!projectRoot || Boolean(baseRef) === Boolean(baseSha)) {
|
|
135
|
-
throw new Error('mutation-qa-target-required');
|
|
136
|
-
}
|
|
137
|
-
if (baseSha && !/^([a-f0-9]{40}|[a-f0-9]{64})$/.test(baseSha)) {
|
|
138
|
-
throw new Error('mutation-qa-base-ref-invalid');
|
|
139
|
-
}
|
|
140
|
-
const baseSelector = baseSha ? `sha:${baseSha}` : baseRef;
|
|
141
|
-
if (args[0] === 'execute') {
|
|
142
|
-
if (!authorizeRun || !planSha || recordCheckpoint) {
|
|
143
|
-
throw new Error('mutation-qa-execute-authorization-required');
|
|
144
|
-
}
|
|
145
|
-
const { executeMutationQaPlan } = await import('./mutation-qa-gate.js');
|
|
146
|
-
const execution = executeMutationQaPlan(projectRoot, baseSelector, planSha);
|
|
147
|
-
if (format === 'json')
|
|
148
|
-
console.log(JSON.stringify({ ok: true, execution }));
|
|
149
|
-
else
|
|
150
|
-
console.log(`mutation-qa execute: ${execution.decision.status} head=${execution.headSha} debt=${execution.followUpDebt ? 'recorded' : 'none'} qaPassed=${execution.decision.qaPassed}`);
|
|
151
|
-
// Exit 2 is a recorded-debt continuation signal, never a shell-level QA pass.
|
|
152
|
-
return execution.decision.safeToContinueLocalQa ? 0
|
|
153
|
-
: execution.continuationAuthorized ? 2 : 1;
|
|
154
|
-
}
|
|
155
|
-
if (args[0] === 'gate') {
|
|
156
|
-
if (!planSha || authorizeRun || recordCheckpoint) {
|
|
157
|
-
throw new Error('mutation-qa-gate-options-invalid');
|
|
158
|
-
}
|
|
159
|
-
const { classifyMutationQaGate } = await import('./mutation-qa-gate.js');
|
|
160
|
-
const decision = classifyMutationQaGate(projectRoot, baseSelector, planSha);
|
|
161
|
-
if (format === 'json')
|
|
162
|
-
console.log(JSON.stringify({ ok: true, decision }));
|
|
163
|
-
else
|
|
164
|
-
console.log(`mutation-qa gate: ${decision.status} head=${decision.headSha}`);
|
|
165
|
-
// This action is also a shell/CI gate. A typed decision is not a pass:
|
|
166
|
-
// warned and unobserved still need a separate durable debt checkpoint.
|
|
167
|
-
return decision.safeToContinueLocalQa ? 0 : 1;
|
|
168
|
-
}
|
|
169
|
-
if (args[0] === 'checkpoint') {
|
|
170
|
-
if (!planSha || authorizeRun || recordCheckpoint) {
|
|
171
|
-
throw new Error('mutation-qa-checkpoint-options-invalid');
|
|
172
|
-
}
|
|
173
|
-
const { readMutationQaCheckpoint } = await import('./mutation-qa-checkpoint.js');
|
|
174
|
-
const checkpoint = readMutationQaCheckpoint(projectRoot, baseSelector, planSha);
|
|
175
|
-
if (format === 'json')
|
|
176
|
-
console.log(JSON.stringify({ ok: true, checkpoint: checkpoint ?? null }));
|
|
177
|
-
else
|
|
178
|
-
console.log(`mutation-qa checkpoint: ${checkpoint ? checkpoint.verdict : 'none'}`);
|
|
179
|
-
return 0;
|
|
180
|
-
}
|
|
181
|
-
if (args[0] === 'run') {
|
|
182
|
-
if (!authorizeRun || !planSha)
|
|
183
|
-
throw new Error('mutation-qa-run-authorization-required');
|
|
184
|
-
if (recordCheckpoint) {
|
|
185
|
-
const { runAndRecordMutationQa } = await import('./mutation-qa-checkpoint.js');
|
|
186
|
-
const { result, checkpoint } = runAndRecordMutationQa(projectRoot, baseSelector, planSha);
|
|
187
|
-
if (format === 'json')
|
|
188
|
-
console.log(JSON.stringify({ ok: true, result, checkpoint: checkpoint ?? null }));
|
|
189
|
-
else
|
|
190
|
-
console.log(`mutation-qa run: ${result.outcome} head=${result.headSha} checkpoint=${checkpoint ? 'accepted' : 'none'}`);
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
const { runMutationQa } = await import('./mutation-qa-run.js');
|
|
194
|
-
const result = runMutationQa(projectRoot, baseSelector, planSha);
|
|
195
|
-
if (format === 'json')
|
|
196
|
-
console.log(JSON.stringify({ ok: true, result }));
|
|
197
|
-
else
|
|
198
|
-
console.log(`mutation-qa run: ${result.outcome} head=${result.headSha}`);
|
|
199
|
-
}
|
|
200
|
-
return 0;
|
|
201
|
-
}
|
|
202
|
-
if (authorizeRun || planSha || recordCheckpoint)
|
|
203
|
-
throw new Error('mutation-qa-plan-options-invalid');
|
|
204
|
-
const plan = planMutationQa(projectRoot, baseSelector);
|
|
205
|
-
if (format === 'json')
|
|
206
|
-
console.log(JSON.stringify({ ok: true, plan }));
|
|
207
|
-
else
|
|
208
|
-
console.log(`mutation-qa plan: ${plan.outcome} files=${plan.eligibleFiles.length} head=${plan.headSha}`);
|
|
209
|
-
return 0;
|
|
210
|
-
}
|
|
211
|
-
catch (error) {
|
|
212
|
-
const reason = error instanceof Error && /^mutation-qa-[a-z0-9-]+$/.test(error.message)
|
|
213
|
-
? error.message : 'mutation-qa-unavailable';
|
|
214
|
-
if (format === 'json')
|
|
215
|
-
console.log(JSON.stringify({ ok: false, reasonCode: reason }));
|
|
216
|
-
else
|
|
217
|
-
console.error(`mutation-qa: ${reason}`);
|
|
218
|
-
return 1;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
1
|
+
import{createHash as q}from"node:crypto";import{lstatSync as y,realpathSync as $}from"node:fs";import{resolve as v}from"node:path";import{mutationQaGit as w}from"./mutation-qa-git.js";import{loadMutationQaPolicy as E,policyExcludesPath as S}from"./mutation-qa-policy.js";function p(o,n){return w(o,n).toString("utf8").trim()}function k(o){return o.length>0&&o.length<=240&&/^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*$/.test(o)&&!o.split("/").some(n=>n==="."||n==="..")&&/\.(?:ts|tsx|js|jsx)$/.test(o)&&!/\.d\.ts$|\.(?:test|spec)\.[jt]sx?$|(?:^|\/)__tests__\/|(?:^|\/)migrations\//.test(o)}function b(o){return/\.(?:ts|tsx|js|jsx)$/.test(o)&&!/\.d\.ts$|\.(?:test|spec)\.[jt]sx?$|(?:^|\/)__tests__\/|(?:^|\/)migrations\//.test(o)}function x(o,n){const f=/^sha:([a-f0-9]{40}|[a-f0-9]{64})$/.exec(n)?.[1],m=/^refs\/remotes\/[A-Za-z0-9][A-Za-z0-9_-]{0,63}\/[A-Za-z0-9][A-Za-z0-9/_-]{0,127}$/.test(n)&&!n.includes("..")&&!n.includes("//")&&!n.endsWith("/");if(!f&&!m)throw new Error("mutation-qa-base-ref-invalid");const e=v(o);try{if($(e)!==e||!y(e).isDirectory()||p(e,["rev-parse","--show-toplevel"])!==e)throw new Error("mutation-qa-project-root-invalid");const a=p(e,["rev-parse","--verify","HEAD"]),h=f??n,i=p(e,["rev-parse","--verify","--end-of-options",`${h}^{commit}`]);if(f)try{w(e,["merge-base","--is-ancestor",i,a])}catch{throw new Error("mutation-qa-base-not-ancestor")}const d=p(e,["merge-base",i,a]);if(![a,i,d].every(c=>/^[a-f0-9]{40,64}$/.test(c)))throw new Error("mutation-qa-revision-invalid");if(f&&(i!==f||d!==i))throw new Error("mutation-qa-base-not-ancestor");if(w(e,["status","--porcelain=v1","-z","--untracked-files=all"]).byteLength!==0)throw new Error("mutation-qa-checkout-dirty");const s=E(e),u=w(e,["diff","--name-only","-z","--diff-filter=ACMR",d,a]).toString("utf8").split("\0").filter(Boolean);if(u.length>512)throw new Error("mutation-qa-scope-too-large");const r=u.filter(b);if(r.some(c=>!k(c)))throw new Error("mutation-qa-source-path-unsafe");if(r.length>128)throw new Error("mutation-qa-scope-too-large");const t=r.filter(c=>!S(s,c)).sort().map(c=>{const g=p(e,["ls-tree","HEAD","--",c]).match(/^(100644|100755) blob ([a-f0-9]{40,64})\t(.+)$/);if(!g||g[3]!==c)throw new Error("mutation-qa-source-not-regular");return Object.freeze({path:c,blobSha:g[2]})});if(p(e,["rev-parse","--verify","HEAD"])!==a||p(e,["rev-parse","--verify","--end-of-options",`${h}^{commit}`])!==i||w(e,["status","--porcelain=v1","-z","--untracked-files=all"]).byteLength!==0)throw new Error("mutation-qa-physical-drift");const l={schemaVersion:1,kind:"mutation-qa-plan",outcome:t.length>0?"ready":"skipped-no-files",baseRef:n,baseSha:i,headSha:a,mergeBaseSha:d,policy:s,eligibleFiles:t,checkpointAccepted:!1,localOnly:!0};return Object.freeze({...l,planSha256:q("sha256").update("mutation-qa-plan-v1\0").update(JSON.stringify(l)).digest("hex")})}catch(a){throw a instanceof Error&&a.message.startsWith("mutation-qa-")?a:new Error("mutation-qa-plan-unavailable")}}async function C(o){let n=process.cwd(),f="",m="",e="",a=!1,h=!1,i="plain";const d=new Set;try{if(o[0]!=="plan"&&o[0]!=="run"&&o[0]!=="execute"&&o[0]!=="checkpoint"&&o[0]!=="gate")throw new Error("mutation-qa-action-invalid");for(let r=1;r<o.length;r++){const t=o[r];if(d.has(t))throw new Error("mutation-qa-argument-duplicate");d.add(t);const l=()=>o[++r]??"";if(t==="--project-root")n=l();else if(t==="--base-ref")f=l();else if(t==="--base-sha")m=l();else if(t==="--plan-sha")e=l();else if(t==="--authorize-run")a=!0;else if(t==="--record-checkpoint")h=!0;else if(t==="--format"){const c=l();if(c!=="plain"&&c!=="json")throw new Error("mutation-qa-format-invalid");i=c}else throw new Error("mutation-qa-argument-invalid")}if(!n||!!f==!!m)throw new Error("mutation-qa-target-required");if(m&&!/^([a-f0-9]{40}|[a-f0-9]{64})$/.test(m))throw new Error("mutation-qa-base-ref-invalid");const s=m?`sha:${m}`:f;if(o[0]==="execute"){if(!a||!e||h)throw new Error("mutation-qa-execute-authorization-required");const{executeMutationQaPlan:r}=await import("./mutation-qa-gate.js"),t=r(n,s,e);return console.log(i==="json"?JSON.stringify({ok:!0,execution:t}):`mutation-qa execute: ${t.decision.status} head=${t.headSha} debt=${t.followUpDebt?"recorded":"none"} qaPassed=${t.decision.qaPassed}`),t.decision.safeToContinueLocalQa?0:t.continuationAuthorized?2:1}if(o[0]==="gate"){if(!e||a||h)throw new Error("mutation-qa-gate-options-invalid");const{classifyMutationQaGate:r}=await import("./mutation-qa-gate.js"),t=r(n,s,e);return console.log(i==="json"?JSON.stringify({ok:!0,decision:t}):`mutation-qa gate: ${t.status} head=${t.headSha}`),t.safeToContinueLocalQa?0:1}if(o[0]==="checkpoint"){if(!e||a||h)throw new Error("mutation-qa-checkpoint-options-invalid");const{readMutationQaCheckpoint:r}=await import("./mutation-qa-checkpoint.js"),t=r(n,s,e);return console.log(i==="json"?JSON.stringify({ok:!0,checkpoint:t??null}):`mutation-qa checkpoint: ${t?t.verdict:"none"}`),0}if(o[0]==="run"){if(!a||!e)throw new Error("mutation-qa-run-authorization-required");if(h){const{runAndRecordMutationQa:r}=await import("./mutation-qa-checkpoint.js"),{result:t,checkpoint:l}=r(n,s,e);console.log(i==="json"?JSON.stringify({ok:!0,result:t,checkpoint:l??null}):`mutation-qa run: ${t.outcome} head=${t.headSha} checkpoint=${l?"accepted":"none"}`)}else{const{runMutationQa:r}=await import("./mutation-qa-run.js"),t=r(n,s,e);console.log(i==="json"?JSON.stringify({ok:!0,result:t}):`mutation-qa run: ${t.outcome} head=${t.headSha}`)}return 0}if(a||e||h)throw new Error("mutation-qa-plan-options-invalid");const u=x(n,s);return console.log(i==="json"?JSON.stringify({ok:!0,plan:u}):`mutation-qa plan: ${u.outcome} files=${u.eligibleFiles.length} head=${u.headSha}`),0}catch(s){const u=s instanceof Error&&/^mutation-qa-[a-z0-9-]+$/.test(s.message)?s.message:"mutation-qa-unavailable";return i==="json"?console.log(JSON.stringify({ok:!1,reasonCode:u})):console.error(`mutation-qa: ${u}`),1}}export{C as mutationQaCliHandler,x as planMutationQa};
|