peaks-loop 4.0.0-beta.14 → 4.0.0-beta.16
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/CHANGELOG.md +230 -148
- package/README-en.md +1 -1
- package/README.md +32 -1
- package/bin/peaks.js +0 -0
- package/dist/cli/cli-helpers.d.ts +1 -1
- package/dist/cli/cli-helpers.js +1 -1
- package/dist/cli/commands/adapter-commands-s2a.js +1 -1
- package/dist/cli/commands/asset-commands.js +33 -16
- package/dist/cli/commands/audit-commands.js +1 -1
- package/dist/cli/commands/bee-commands.js +1 -1
- package/dist/cli/commands/capability-commands.js +1 -1
- package/dist/cli/commands/classify-classify-commands.js +1 -1
- package/dist/cli/commands/code-commands.js +1 -1
- package/dist/cli/commands/code-review-commands.js +1 -1
- package/dist/cli/commands/codegraph-commands.js +1 -1
- package/dist/cli/commands/compact-command.js +1 -1
- package/dist/cli/commands/complexity-commands.js +1 -1
- package/dist/cli/commands/config-commands.js +1 -1
- package/dist/cli/commands/context-commands.js +1 -1
- package/dist/cli/commands/contract-commands.js +1 -1
- package/dist/cli/commands/core/artifacts-command.js +1 -1
- package/dist/cli/commands/core/binding-commands.js +1 -1
- package/dist/cli/commands/core/doctor-command.js +63 -3
- package/dist/cli/commands/core/memory-command.js +1 -1
- package/dist/cli/commands/core/profile-command.js +1 -1
- package/dist/cli/commands/core/proxy-command.js +1 -1
- package/dist/cli/commands/core/session-command.js +1 -1
- package/dist/cli/commands/core/skill-command.js +2 -2
- package/dist/cli/commands/core/standards-command.js +1 -1
- package/dist/cli/commands/dispatch-commands.js +2 -2
- package/dist/cli/commands/dispatch-from-dag.js +1 -1
- package/dist/cli/commands/doc-commands.js +1 -1
- package/dist/cli/commands/ecc-commands.js +2 -2
- package/dist/cli/commands/evolution-commands.js +1 -1
- package/dist/cli/commands/feedback-commands.js +1 -1
- package/dist/cli/commands/final-review-commands.js +1 -1
- package/dist/cli/commands/fixture-commands.js +1 -1
- package/dist/cli/commands/fork-commands.js +1 -1
- package/dist/cli/commands/gate-commands.js +1 -1
- package/dist/cli/commands/heartbeat-commands.js +1 -1
- package/dist/cli/commands/hook-handle.js +1 -1
- package/dist/cli/commands/hooks-commands.js +1 -1
- package/dist/cli/commands/ide-commands.js +1 -1
- package/dist/cli/commands/impact-commands.js +1 -1
- package/dist/cli/commands/job-commands.js +1 -1
- package/dist/cli/commands/legacy-commands.js +1 -1
- package/dist/cli/commands/log-commands.js +1 -1
- package/dist/cli/commands/loop-commands.js +1 -1
- package/dist/cli/commands/loop-eval-commands.js +1 -1
- package/dist/cli/commands/memory-commands.js +1 -1
- package/dist/cli/commands/migrate-1-4-1-command.js +1 -1
- package/dist/cli/commands/migrate-v2-10-to-v2-11-command.js +1 -1
- package/dist/cli/commands/mut-commands.d.ts +1 -1
- package/dist/cli/commands/mut-commands.js +1 -5
- package/dist/cli/commands/observability-commands.js +1 -1
- package/dist/cli/commands/openspec-commands.js +2 -2
- package/dist/cli/commands/perf-audit-commands.js +2 -2
- package/dist/cli/commands/perf-commands.js +1 -1
- package/dist/cli/commands/polyrepo-commands.js +1 -1
- package/dist/cli/commands/prd-blocks-commands.js +1 -1
- package/dist/cli/commands/prd-commands.js +1 -1
- package/dist/cli/commands/project-commands.js +1 -1
- package/dist/cli/commands/qa-business-review-commands.js +1 -1
- package/dist/cli/commands/qa-commands.d.ts +1 -1
- package/dist/cli/commands/qa-commands.js +2 -2
- package/dist/cli/commands/release-commands.js +1 -1
- package/dist/cli/commands/request-commands.js +1 -1
- package/dist/cli/commands/retrospective-commands.js +1 -1
- package/dist/cli/commands/reviewer-commands.js +1 -1
- package/dist/cli/commands/role-commands.js +1 -1
- package/dist/cli/commands/runtime-commands.js +1 -2
- package/dist/cli/commands/sc-commands.js +1 -1
- package/dist/cli/commands/scan-commands.js +1 -1
- package/dist/cli/commands/security-audit-commands.js +2 -2
- package/dist/cli/commands/share-commands.d.ts +19 -0
- package/dist/cli/commands/share-commands.js +121 -2
- package/dist/cli/commands/skill-conformance-commands.js +1 -1
- package/dist/cli/commands/skill-loop-engineering-readiness-commands.js +1 -1
- package/dist/cli/commands/skill-search-commands.js +1 -1
- package/dist/cli/commands/slice-commands.js +1 -1
- package/dist/cli/commands/slice-integrate-commands.js +1 -1
- package/dist/cli/commands/slice-review-commands.js +1 -1
- package/dist/cli/commands/smoke-commands.js +1 -1
- package/dist/cli/commands/sop-commands.js +1 -1
- package/dist/cli/commands/statusline-commands.js +1 -1
- package/dist/cli/commands/sub-agent-commands.js +2 -1
- package/dist/cli/commands/understand-commands.js +1 -1
- package/dist/cli/commands/upgrade-commands.js +1 -1
- package/dist/cli/commands/user-touchpoint-commands.js +1 -1
- package/dist/cli/commands/verdict-aggregate-command.js +1 -1
- package/dist/cli/commands/workflow-commands.js +1 -1
- package/dist/cli/commands/workflow-plan-commands.js +1 -1
- package/dist/cli/commands/workspace/init-command.js +1 -1
- package/dist/cli/commands/workspace/migrate-command.js +1 -1
- package/dist/cli/commands/workspace/reconcile-command.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/program.js +2 -2
- package/dist/services/artifacts/artifact-prerequisites.js +1 -1
- package/dist/services/artifacts/request-artifact-service.js +1 -1
- package/dist/services/artifacts/workspace-service.js +1 -1
- package/dist/services/code/auto-compact-orchestrator.js +5 -2
- package/dist/services/code-review/ocr-service.d.ts +1 -1
- package/dist/services/code-review/ocr-service.js +6 -3
- package/dist/services/config/config-types.js +1 -1
- package/dist/services/dashboard/project-dashboard-service.js +2 -2
- package/dist/services/dispatch/batch-counter.js +1 -1
- package/dist/services/dispatch/dispatch-record-writer.js +1 -1
- package/dist/services/job/job-progress-store.d.ts +2 -2
- package/dist/services/job/job-types.d.ts +27 -27
- package/dist/services/log/retention.js +1 -1
- package/dist/services/openspec/openspec-archive-service.js +1 -1
- package/dist/services/openspec/openspec-bridge-service.js +1 -1
- package/dist/services/openspec/openspec-init-service.js +1 -1
- package/dist/services/openspec/openspec-propose-from-doctor-service.js +1 -1
- package/dist/services/openspec/openspec-render-service.js +1 -1
- package/dist/services/openspec/openspec-scan-service.js +1 -1
- package/dist/services/openspec/openspec-validate-service.js +1 -1
- package/dist/services/scan/acceptance-coverage-service.js +1 -1
- package/dist/services/scan/archetype-service.js +1 -1
- package/dist/services/scan/existing-system-service.js +1 -1
- package/dist/services/scan/libraries-service.js +1 -1
- package/dist/services/skill/skill-search-service.d.ts +5 -5
- package/dist/services/skill/skill-search-service.js +2 -2
- package/dist/services/skills/skill-registry.js +2 -2
- package/dist/services/skills/skill-runbook-service.js +1 -1
- package/dist/services/slice/slice-check-service.js +1 -1
- package/dist/services/understand/understand-hybrid-service.js +1 -1
- package/dist/services/understand/understand-scan-service.js +2 -2
- package/dist/services/upgrade/upgrade-service.js +4 -3
- package/dist/services/verdict/envelopes.d.ts +2 -2
- package/dist/services/verdict/envelopes.js +2 -2
- package/dist/services/verdict/verdict-aggregator.d.ts +3 -3
- package/dist/services/workflow/autonomous-resume-writer.js +1 -1
- package/dist/services/workflow/plan-reader.d.ts +1 -1
- package/dist/services/workflow/plan-reader.js +1 -1
- package/dist/services/workflow/plan-refresher.d.ts +1 -1
- package/dist/services/workflow/plan-refresher.js +1 -1
- package/dist/services/workflow/plan-trigger-detector.d.ts +1 -1
- package/dist/services/workflow/plan-trigger-detector.js +1 -1
- package/dist/services/workflow/workflow-router-service.js +11 -9
- package/dist/services/workspace/migrate-service.js +1 -1
- package/dist/services/workspace/workspace-service.js +1 -1
- package/package.json +13 -8
- package/scripts/sync-version.mjs +8 -1
- package/skills/bee/peaks-rd/SKILL.md +14 -1
- package/skills/peaks-code/SKILL.md +44 -0
- package/skills/peaks-code/references/sub-agent-dispatch.md +30 -0
- package/dist/services/agent/ecc-cache-service.d.ts +0 -97
- package/dist/services/agent/ecc-cache-service.js +0 -491
- package/dist/services/audit-independent/perf-audit-service.d.ts +0 -151
- package/dist/services/audit-independent/perf-audit-service.js +0 -316
- package/dist/services/audit-independent/security-audit-service.d.ts +0 -155
- package/dist/services/audit-independent/security-audit-service.js +0 -316
- package/dist/services/context/dispatch-context-guard.d.ts +0 -27
- package/dist/services/context/dispatch-context-guard.js +0 -192
- package/dist/services/context/shared-channel.d.ts +0 -93
- package/dist/services/context/shared-channel.js +0 -308
- package/dist/services/crystallization/crystallization-service.d.ts +0 -239
- package/dist/services/crystallization/crystallization-service.js +0 -397
- package/dist/services/crystallization/crystallization-store.d.ts +0 -75
- package/dist/services/crystallization/crystallization-store.js +0 -193
- package/dist/services/crystallization/crystallization-types.d.ts +0 -313
- package/dist/services/crystallization/crystallization-types.js +0 -299
- package/dist/services/crystallization/evidence-brief-builder.d.ts +0 -237
- package/dist/services/crystallization/evidence-brief-builder.js +0 -240
- package/dist/services/doctor/doctor-service.d.ts +0 -149
- package/dist/services/doctor/doctor-service.js +0 -929
- package/dist/services/filesystem/file-lock.d.ts +0 -11
- package/dist/services/filesystem/file-lock.js +0 -152
- package/dist/services/final-review/final-review-service.d.ts +0 -12
- package/dist/services/final-review/final-review-service.js +0 -52
- package/dist/services/final-review/final-review-types.d.ts +0 -42
- package/dist/services/final-review/final-review-types.js +0 -18
- package/dist/services/job/job-resource-snapshot.d.ts +0 -7
- package/dist/services/job/job-resource-snapshot.js +0 -75
- package/dist/services/mut/assert-scanner.d.ts +0 -6
- package/dist/services/mut/assert-scanner.js +0 -116
- package/dist/services/mut/index.d.ts +0 -8
- package/dist/services/mut/index.js +0 -7
- package/dist/services/mut/mut-runner.d.ts +0 -39
- package/dist/services/mut/mut-runner.js +0 -29
- package/dist/services/mut/production-stryker.d.ts +0 -2
- package/dist/services/mut/production-stryker.js +0 -75
- package/dist/services/mut/report-builder.d.ts +0 -11
- package/dist/services/mut/report-builder.js +0 -110
- package/dist/services/mut/report-loader.d.ts +0 -31
- package/dist/services/mut/report-loader.js +0 -78
- package/dist/services/mut/thresholds.d.ts +0 -21
- package/dist/services/mut/thresholds.js +0 -22
- package/dist/services/mut/types.d.ts +0 -336
- package/dist/services/mut/types.js +0 -59
- package/dist/shared/fs.d.ts +0 -4
- package/dist/shared/fs.js +0 -26
- package/dist/shared/paths.d.ts +0 -6
- package/dist/shared/paths.js +0 -50
- package/dist/shared/result.d.ts +0 -22
- package/dist/shared/result.js +0 -46
- package/dist/shared/version.d.ts +0 -1
- package/dist/shared/version.js +0 -1
- package/examples/video-demo/README.md +0 -142
- package/examples/video-demo/out/en-closing-960.png +0 -0
- package/examples/video-demo/out/zh-closing-960.png +0 -0
- package/examples/video-demo/preview/peaks-loop-demo-en.mp4 +0 -0
- package/examples/video-demo/preview/peaks-loop-demo.mp4 +0 -0
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export declare class LockTimeoutError extends Error {
|
|
2
|
-
readonly code = "LOCK_TIMEOUT";
|
|
3
|
-
readonly lockPath: string;
|
|
4
|
-
constructor(lockPath: string, attempts: number);
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Run `fn` while holding an exclusive lock on a sidecar file at
|
|
8
|
-
* `<filePath>.lock`. The lock is released in `finally` (close fd +
|
|
9
|
-
* unlink lock file). Throws LockTimeoutError after MAX_LOCK_RETRIES.
|
|
10
|
-
*/
|
|
11
|
-
export declare function withFileLockSync<T>(filePath: string, fn: () => T): T;
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cross-process file lock — sync, atomic, no third-party deps.
|
|
3
|
-
*
|
|
4
|
-
* Used by writeSharedEntry (shared-channel.ts) and appendHeartbeat /
|
|
5
|
-
* markCompleted (dispatch-record-writer.ts) to guard the
|
|
6
|
-
* read-modify-write sequence against lost updates from concurrent
|
|
7
|
-
* writers (slice 2026-06-23-audit-3rd finding #2 + #3).
|
|
8
|
-
*
|
|
9
|
-
* Mechanism: write a sidecar `.lock` file next to the target via
|
|
10
|
-
* `fs.openSync(path, 'wx')`. The `wx` flag maps to O_CREAT|O_EXCL on
|
|
11
|
-
* POSIX (atomic) and `CreateFileW(... CREATE_NEW)` on Windows (also
|
|
12
|
-
* atomic). The second opener fails with EEXIST. We retry with capped
|
|
13
|
-
* exponential backoff up to MAX_LOCK_RETRIES, falling through to
|
|
14
|
-
* LOCK_TIMEOUT if the lock cannot be acquired.
|
|
15
|
-
*
|
|
16
|
-
* Stale-lock reaping: a crashed previous holder leaves a `.lock` file
|
|
17
|
-
* behind. We reap locks older than LOCK_STALE_MS by unlinking before
|
|
18
|
-
* retrying. This trades a one-time stale-lock window for bounded
|
|
19
|
-
* recovery without leaking orphan locks.
|
|
20
|
-
*
|
|
21
|
-
* Why not fs.flock? Node's `fs.flock` is POSIX-only; on Windows it
|
|
22
|
-
* throws ERR_FEATURE_UNAVAILABLE_ON_PLATFORM. The `.lock` file
|
|
23
|
-
* pattern works uniformly across platforms and survives process
|
|
24
|
-
* crashes (the file remains; the next acquirer reaps it).
|
|
25
|
-
*/
|
|
26
|
-
import { closeSync, existsSync, mkdirSync, openSync, statSync, unlinkSync } from 'node:fs';
|
|
27
|
-
import { dirname, resolve } from 'node:path';
|
|
28
|
-
const MAX_LOCK_RETRIES = 100;
|
|
29
|
-
const LOCK_RETRY_BASE_MS = 5;
|
|
30
|
-
const LOCK_RETRY_MAX_MS = 50;
|
|
31
|
-
const LOCK_STALE_MS = 30_000;
|
|
32
|
-
export class LockTimeoutError extends Error {
|
|
33
|
-
code = 'LOCK_TIMEOUT';
|
|
34
|
-
lockPath;
|
|
35
|
-
constructor(lockPath, attempts) {
|
|
36
|
-
super(`LOCK_TIMEOUT: failed to acquire ${lockPath} after ${attempts} retries ` +
|
|
37
|
-
`(a prior holder may be alive or the lock is stale; it will be reaped ` +
|
|
38
|
-
`after ${LOCK_STALE_MS}ms of inactivity)`);
|
|
39
|
-
this.lockPath = lockPath;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Run `fn` while holding an exclusive lock on a sidecar file at
|
|
44
|
-
* `<filePath>.lock`. The lock is released in `finally` (close fd +
|
|
45
|
-
* unlink lock file). Throws LockTimeoutError after MAX_LOCK_RETRIES.
|
|
46
|
-
*/
|
|
47
|
-
export function withFileLockSync(filePath, fn) {
|
|
48
|
-
const target = resolve(filePath);
|
|
49
|
-
const lockPath = `${target}.lock`;
|
|
50
|
-
// Ensure the parent dir exists before openSync('wx') — first-write
|
|
51
|
-
// callers (e.g. writeSharedEntry on a fresh batch) reach the lock
|
|
52
|
-
// BEFORE writeAtomic's mkdirSync(dir, { recursive: true }) runs.
|
|
53
|
-
const dir = dirname(target);
|
|
54
|
-
if (!existsSync(dir)) {
|
|
55
|
-
mkdirSync(dir, { recursive: true });
|
|
56
|
-
}
|
|
57
|
-
// Wall-clock guard (slice 014): in pathological slow-system cases, the
|
|
58
|
-
// retry backoff could push wall-clock above LOCK_STALE_MS. Cap the loop at
|
|
59
|
-
// LOCK_STALE_MS regardless of attempts; throw the existing LockTimeoutError.
|
|
60
|
-
//
|
|
61
|
-
// Slice 2026-07-12-fix note: the slice 014 implementation re-checked
|
|
62
|
-
// `isStaleLock(lockPath)` at the TOP of every loop iteration. Under
|
|
63
|
-
// vitest-on-Windows slowdown (each `spinSleep` actually waits hundreds of
|
|
64
|
-
// ms because of event-loop preemption), the wall clock would cross
|
|
65
|
-
// `LOCK_STALE_MS` mid-loop and the reaper would unlink a still-fresh
|
|
66
|
-
// lock — `file-lock.test.ts` "does NOT reap a fresh .lock (<30s old)"
|
|
67
|
-
// caught this: the lock was reaped at iter ~54 after 30s of retries,
|
|
68
|
-
// defeating the test that asserts the fresh lock should block. The fix
|
|
69
|
-
// is to check staleness ONCE, before the loop, and inside the loop
|
|
70
|
-
// only retry on EEXIST. A crashed previous holder is reaped at entry;
|
|
71
|
-
// after we begin the retry-backoff phase, the lock is by definition
|
|
72
|
-
// non-stale (or absent after a race with another reaper), so a stale
|
|
73
|
-
// check on each iteration only adds risk of spuriously reaping a lock
|
|
74
|
-
// we just decided was live.
|
|
75
|
-
const startedAt = Date.now();
|
|
76
|
-
let attempts = 0;
|
|
77
|
-
let fd = null;
|
|
78
|
-
// One-shot stale reap before the loop: if a previous holder crashed and
|
|
79
|
-
// left the lock older than LOCK_STALE_MS, unlink it now so the first
|
|
80
|
-
// `openSync(lockPath, 'wx')` attempt can succeed.
|
|
81
|
-
if (isStaleLock(lockPath)) {
|
|
82
|
-
try {
|
|
83
|
-
unlinkSync(lockPath);
|
|
84
|
-
}
|
|
85
|
-
catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
86
|
-
// race: another process reaped it; loop will try to acquire directly.
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
while (attempts < MAX_LOCK_RETRIES) {
|
|
90
|
-
// Wall-clock guard runs at the top of each iteration so a slow retry
|
|
91
|
-
// backoff (e.g. vitest on Windows) never lets us outlive the stale
|
|
92
|
-
// threshold and re-enter the reaper branch on a still-live lock.
|
|
93
|
-
if (Date.now() - startedAt > LOCK_STALE_MS) {
|
|
94
|
-
throw new LockTimeoutError(lockPath, attempts);
|
|
95
|
-
}
|
|
96
|
-
try {
|
|
97
|
-
fd = openSync(lockPath, 'wx');
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
catch (err) {
|
|
101
|
-
const code = err.code;
|
|
102
|
-
if (code !== 'EEXIST')
|
|
103
|
-
throw err;
|
|
104
|
-
attempts += 1;
|
|
105
|
-
// Exponential backoff capped at LOCK_RETRY_MAX_MS.
|
|
106
|
-
const wait = Math.min(LOCK_RETRY_BASE_MS * 2 ** Math.min(attempts, 6), LOCK_RETRY_MAX_MS);
|
|
107
|
-
spinSleep(wait);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (fd === null) {
|
|
111
|
-
throw new LockTimeoutError(lockPath, attempts);
|
|
112
|
-
}
|
|
113
|
-
try {
|
|
114
|
-
return fn();
|
|
115
|
-
}
|
|
116
|
-
finally {
|
|
117
|
-
try {
|
|
118
|
-
closeSync(fd);
|
|
119
|
-
}
|
|
120
|
-
catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
121
|
-
// fd may already be closed by the OS on a crash; ignore.
|
|
122
|
-
}
|
|
123
|
-
try {
|
|
124
|
-
unlinkSync(lockPath);
|
|
125
|
-
}
|
|
126
|
-
catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
127
|
-
// Best-effort: stale .lock is harmless; the next acquirer reaps.
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
function isStaleLock(lockPath) {
|
|
132
|
-
try {
|
|
133
|
-
const s = statSync(lockPath);
|
|
134
|
-
return Date.now() - s.mtimeMs > LOCK_STALE_MS;
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Synchronous sleep via busy-wait. Bounded by MAX_LOCK_RETRIES *
|
|
142
|
-
* LOCK_RETRY_MAX_MS = 5s worst case. The alternative — async sleep —
|
|
143
|
-
* would force every lock consumer to become async, which is a much
|
|
144
|
-
* larger refactor for a slice whose lock contention is rare (concurrent
|
|
145
|
-
* `peaks sub-agent share` for the same batch).
|
|
146
|
-
*/
|
|
147
|
-
function spinSleep(ms) {
|
|
148
|
-
const end = Date.now() + ms;
|
|
149
|
-
while (Date.now() < end) {
|
|
150
|
-
/* intentional spin */
|
|
151
|
-
}
|
|
152
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { LlmRunner } from '../audit/audit-goal-service.js';
|
|
2
|
-
import type { FinalReviewOutput } from './final-review-types.js';
|
|
3
|
-
export interface PrepareFinalReviewOptions {
|
|
4
|
-
readonly projectRoot: string;
|
|
5
|
-
readonly sessionId: string;
|
|
6
|
-
readonly llmRunner: LlmRunner;
|
|
7
|
-
}
|
|
8
|
-
export declare class IncompleteFinalReviewError extends Error {
|
|
9
|
-
readonly code: "INCOMPLETE_FINAL_REVIEW";
|
|
10
|
-
constructor(message: string);
|
|
11
|
-
}
|
|
12
|
-
export declare function prepareFinalReview(rid: string, opts: PrepareFinalReviewOptions): Promise<FinalReviewOutput>;
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
const REQUIRED_DIMENSIONS = [
|
|
4
|
-
'functional-completeness',
|
|
5
|
-
'problem-resolution',
|
|
6
|
-
'no-new-bugs',
|
|
7
|
-
'existing-functionality-intact'
|
|
8
|
-
];
|
|
9
|
-
const SYSTEM_PROMPT = `You are preparing a 4-dimension business review for human acceptance. Produce a JSON response with EXACTLY these fields:
|
|
10
|
-
- rid (string)
|
|
11
|
-
- generatedAt (ISO timestamp)
|
|
12
|
-
- dimensions (array of EXACTLY 4 objects, one per dimension: functional-completeness, problem-resolution, no-new-bugs, existing-functionality-intact; each with dimension, verdict (pass | fail | inconclusive), summary, evidence (list of {kind, description, [artifact], [link]}), confidence (high | medium | low))
|
|
13
|
-
- overallSummary (one paragraph)
|
|
14
|
-
- allPass (boolean)
|
|
15
|
-
- needsAttention (list of dimension names that need human attention)
|
|
16
|
-
|
|
17
|
-
Output ONLY valid JSON, no prose.`;
|
|
18
|
-
export class IncompleteFinalReviewError extends Error {
|
|
19
|
-
code = 'INCOMPLETE_FINAL_REVIEW';
|
|
20
|
-
constructor(message) {
|
|
21
|
-
super(message);
|
|
22
|
-
this.name = 'IncompleteFinalReviewError';
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
export async function prepareFinalReview(rid, opts) {
|
|
26
|
-
const auditGoalPath = join(opts.projectRoot, '.peaks', '_runtime', opts.sessionId, 'audit-goal', `${rid}.json`);
|
|
27
|
-
let approvedGoal;
|
|
28
|
-
try {
|
|
29
|
-
approvedGoal = JSON.parse(readFileSync(auditGoalPath, 'utf8'));
|
|
30
|
-
}
|
|
31
|
-
catch (err) {
|
|
32
|
-
throw new Error(`Cannot read approved goal from ${auditGoalPath}: ${err.message}`);
|
|
33
|
-
}
|
|
34
|
-
const userPrompt = `Approved goal's success criteria: ${JSON.stringify(approvedGoal.successCriteria)}\n\nPrepare the 4-dim review evidence.`;
|
|
35
|
-
const response = await opts.llmRunner.call(SYSTEM_PROMPT, userPrompt, {
|
|
36
|
-
maxTokens: 3000
|
|
37
|
-
});
|
|
38
|
-
let parsed;
|
|
39
|
-
try {
|
|
40
|
-
parsed = JSON.parse(response.output);
|
|
41
|
-
}
|
|
42
|
-
catch (err) {
|
|
43
|
-
throw new IncompleteFinalReviewError(`LLM output is not valid JSON: ${err.message}`);
|
|
44
|
-
}
|
|
45
|
-
const output = parsed;
|
|
46
|
-
const presentDimensions = new Set(output.dimensions.map((d) => d.dimension));
|
|
47
|
-
const missing = REQUIRED_DIMENSIONS.filter(d => !presentDimensions.has(d));
|
|
48
|
-
if (missing.length > 0) {
|
|
49
|
-
throw new IncompleteFinalReviewError(`Missing required dimensions: ${missing.join(', ')}`);
|
|
50
|
-
}
|
|
51
|
-
return output;
|
|
52
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `prepareFinalReview()` — the 4-dimension business review primitive that gates
|
|
3
|
-
* end-of-workflow human acceptance in the 10% human / 90% LLM workflow.
|
|
4
|
-
*
|
|
5
|
-
* Contract:
|
|
6
|
-
* - Reads the approved audit-goal JSON from
|
|
7
|
-
* `.peaks/_runtime/<sessionId>/audit-goal/<rid>.json`.
|
|
8
|
-
* - Calls an injected `LlmRunner` exactly once with a 4-dim review prompt.
|
|
9
|
-
* - Parses the LLM output as JSON and validates that the `dimensions`
|
|
10
|
-
* array covers ALL four required dimensions
|
|
11
|
-
* (functional-completeness, problem-resolution, no-new-bugs,
|
|
12
|
-
* existing-functionality-intact).
|
|
13
|
-
* - Throws `IncompleteFinalReviewError` if the JSON is malformed or any
|
|
14
|
-
* required dimension is missing. Callers MUST treat this as a gate
|
|
15
|
-
* failure (return to human for re-prompting) — autonomous work must
|
|
16
|
-
* never proceed on a partial review.
|
|
17
|
-
*/
|
|
18
|
-
export type DimensionKind = 'functional-completeness' | 'problem-resolution' | 'no-new-bugs' | 'existing-functionality-intact';
|
|
19
|
-
export type DimensionVerdict = 'pass' | 'fail' | 'inconclusive';
|
|
20
|
-
export type EvidenceKind = 'test-result' | 'test-coverage' | 'manual-spot-check' | 'pre-post-diff' | 'regression-suite' | 'ac-mapping';
|
|
21
|
-
export type DimensionConfidence = 'high' | 'medium' | 'low';
|
|
22
|
-
export interface EvidenceItem {
|
|
23
|
-
readonly kind: EvidenceKind;
|
|
24
|
-
readonly description: string;
|
|
25
|
-
readonly artifact?: string;
|
|
26
|
-
readonly link?: string;
|
|
27
|
-
}
|
|
28
|
-
export interface DimensionEvidence {
|
|
29
|
-
readonly dimension: DimensionKind;
|
|
30
|
-
readonly verdict: DimensionVerdict;
|
|
31
|
-
readonly summary: string;
|
|
32
|
-
readonly evidence: readonly EvidenceItem[];
|
|
33
|
-
readonly confidence: DimensionConfidence;
|
|
34
|
-
}
|
|
35
|
-
export interface FinalReviewOutput {
|
|
36
|
-
readonly rid: string;
|
|
37
|
-
readonly generatedAt: string;
|
|
38
|
-
readonly dimensions: readonly DimensionEvidence[];
|
|
39
|
-
readonly overallSummary: string;
|
|
40
|
-
readonly allPass: boolean;
|
|
41
|
-
readonly needsAttention: readonly DimensionKind[];
|
|
42
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `prepareFinalReview()` — the 4-dimension business review primitive that gates
|
|
3
|
-
* end-of-workflow human acceptance in the 10% human / 90% LLM workflow.
|
|
4
|
-
*
|
|
5
|
-
* Contract:
|
|
6
|
-
* - Reads the approved audit-goal JSON from
|
|
7
|
-
* `.peaks/_runtime/<sessionId>/audit-goal/<rid>.json`.
|
|
8
|
-
* - Calls an injected `LlmRunner` exactly once with a 4-dim review prompt.
|
|
9
|
-
* - Parses the LLM output as JSON and validates that the `dimensions`
|
|
10
|
-
* array covers ALL four required dimensions
|
|
11
|
-
* (functional-completeness, problem-resolution, no-new-bugs,
|
|
12
|
-
* existing-functionality-intact).
|
|
13
|
-
* - Throws `IncompleteFinalReviewError` if the JSON is malformed or any
|
|
14
|
-
* required dimension is missing. Callers MUST treat this as a gate
|
|
15
|
-
* failure (return to human for re-prompting) — autonomous work must
|
|
16
|
-
* never proceed on a partial review.
|
|
17
|
-
*/
|
|
18
|
-
export {};
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { ResourceSnapshot } from './job-types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Collects a coarse-grained snapshot of host + project resources.
|
|
4
|
-
* Numeric ranges are clamped to zod's domain — out-of-range values are clamped, not thrown,
|
|
5
|
-
* so callers can decide what to do.
|
|
6
|
-
*/
|
|
7
|
-
export declare function collectResourceSnapshot(jobDir: string): ResourceSnapshot;
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
|
-
import { readdirSync, statSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
/**
|
|
5
|
-
* Collects a coarse-grained snapshot of host + project resources.
|
|
6
|
-
* Numeric ranges are clamped to zod's domain — out-of-range values are clamped, not thrown,
|
|
7
|
-
* so callers can decide what to do.
|
|
8
|
-
*/
|
|
9
|
-
export function collectResourceSnapshot(jobDir) {
|
|
10
|
-
const cpus = os.cpus();
|
|
11
|
-
const loadAvg = (os.loadavg()[0] ?? 0) / (cpus.length || 1);
|
|
12
|
-
const cpuPercent = Math.max(0, Math.min(100, loadAvg * 100));
|
|
13
|
-
const totalMemMb = os.totalmem() / 1024 / 1024;
|
|
14
|
-
const freeMemMb = os.freemem() / 1024 / 1024;
|
|
15
|
-
const usedMemMb = Math.max(0, totalMemMb - freeMemMb);
|
|
16
|
-
const memMb = Math.round(usedMemMb);
|
|
17
|
-
const diskMb = dirSizeMb(jobDir);
|
|
18
|
-
// contextRatio is best-effort: pull from the same env-var the v2.13.0 auto-compact uses.
|
|
19
|
-
const envVal = process.env.CLAUDE_CONTEXT_USAGE_PERCENT;
|
|
20
|
-
const contextRatio = envVal ? Math.max(0, Math.min(1, Number(envVal) / 100)) : 0;
|
|
21
|
-
return {
|
|
22
|
-
capturedAt: new Date().toISOString(),
|
|
23
|
-
cpuPercent,
|
|
24
|
-
memMb,
|
|
25
|
-
diskMb,
|
|
26
|
-
contextRatio,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
// Non-enumerable internal attachment for test-time access via
|
|
30
|
-
// `collectResourceSnapshot.dirSizeMb`. NOT a public export — keeps
|
|
31
|
-
// the prod module surface clean per PRD while still allowing
|
|
32
|
-
// targeted AC-1/AC-2/AC-3 unit tests to drive `dirSizeMb` directly.
|
|
33
|
-
Object.defineProperty(collectResourceSnapshot, 'dirSizeMb', {
|
|
34
|
-
value: dirSizeMb,
|
|
35
|
-
enumerable: false,
|
|
36
|
-
writable: false,
|
|
37
|
-
configurable: false,
|
|
38
|
-
});
|
|
39
|
-
/**
|
|
40
|
-
* Best-effort, non-recursive size of one directory level, in rounded megabytes.
|
|
41
|
-
*
|
|
42
|
-
* - Missing directory → 0.
|
|
43
|
-
* - Per-entry stat failures (race with deletion, EBUSY on Windows) → silently skipped.
|
|
44
|
-
* - When `opts.maxEntries` > 0 and the directory has more entries than the cap,
|
|
45
|
-
* the returned value is a partial sum (a lower bound), and no further `stat`
|
|
46
|
-
* calls are issued past the cap. Callers must treat the value as a lower bound.
|
|
47
|
-
* - `opts.maxEntries` defaults to 0 (unbounded, legacy behaviour).
|
|
48
|
-
*
|
|
49
|
-
* NOTE: when maxEntries > 0 and the directory contains more entries than the
|
|
50
|
-
* cap, this is a partial sum. Callers must treat the value as a lower bound.
|
|
51
|
-
*/
|
|
52
|
-
function dirSizeMb(dir, opts) {
|
|
53
|
-
const cap = opts?.maxEntries ?? 0;
|
|
54
|
-
let total = 0;
|
|
55
|
-
let index = 0;
|
|
56
|
-
try {
|
|
57
|
-
for (const name of readdirSync(dir)) {
|
|
58
|
-
if (cap > 0 && index >= cap)
|
|
59
|
-
break;
|
|
60
|
-
try {
|
|
61
|
-
total += statSync(join(dir, name)).size;
|
|
62
|
-
}
|
|
63
|
-
catch (e) {
|
|
64
|
-
// best-effort: missing entries (race with deletion) are silently skipped
|
|
65
|
-
void e;
|
|
66
|
-
}
|
|
67
|
-
index++;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
catch (e) {
|
|
71
|
-
// best-effort: missing dir is treated as 0 bytes
|
|
72
|
-
void e;
|
|
73
|
-
}
|
|
74
|
-
return Math.round(total / 1024 / 1024);
|
|
75
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per spec §4.2 验收审计 + §7 阶段二 — 5-pattern weak-assertion AST scan.
|
|
3
|
-
*
|
|
4
|
-
* Hard constraints:
|
|
5
|
-
* H6 (CLI裁决): every detection is regex/AST-based, not LLM-judged.
|
|
6
|
-
* KISS: regex matchers on common expect() patterns. Production slice
|
|
7
|
-
* would migrate to TypeScript Compiler API; v1 ships regex for speed.
|
|
8
|
-
*
|
|
9
|
-
* Weak patterns (5):
|
|
10
|
-
* - toBeDefined()
|
|
11
|
-
* - toBeTruthy()
|
|
12
|
-
* - toEqual(x) where arg === receiver (toEqual-self)
|
|
13
|
-
* - expect.anything()
|
|
14
|
-
* - toBe(x) where arg === receiver (toBe-self)
|
|
15
|
-
*/
|
|
16
|
-
import { readFile } from 'node:fs/promises';
|
|
17
|
-
import { join } from 'node:path';
|
|
18
|
-
const WEAK_PATTERNS = [
|
|
19
|
-
{ pattern: 'toBeDefined', regex: /\.toBeDefined\s*\(\s*\)/g },
|
|
20
|
-
{ pattern: 'toBeTruthy', regex: /\.toBeTruthy\s*\(\s*\)/g },
|
|
21
|
-
{ pattern: 'expect-anything', regex: /expect\.anything\s*\(\s*\)/g },
|
|
22
|
-
// Self-equality patterns require matched-pair parsing; handled below.
|
|
23
|
-
];
|
|
24
|
-
const TO_EQUAL_SELF = /\.toEqual\s*\(\s*([a-zA-Z_$][\w$]*)\s*\)/g;
|
|
25
|
-
const TO_BE_SELF = /\.toBe\s*\(\s*([a-zA-Z_$][\w$]*)\s*\)/g;
|
|
26
|
-
async function countAssertions(content) {
|
|
27
|
-
// Approximate: count expect(...).method( occurrences.
|
|
28
|
-
// Handle nested parens by finding expect( and matching to a balanced close.
|
|
29
|
-
let count = 0;
|
|
30
|
-
const re = /expect\s*\(/g;
|
|
31
|
-
for (const m of content.matchAll(re)) {
|
|
32
|
-
let depth = 1;
|
|
33
|
-
let i = (m.index ?? 0) + m[0].length;
|
|
34
|
-
while (i < content.length && depth > 0) {
|
|
35
|
-
const ch = content[i];
|
|
36
|
-
if (ch === '(')
|
|
37
|
-
depth++;
|
|
38
|
-
else if (ch === ')')
|
|
39
|
-
depth--;
|
|
40
|
-
i++;
|
|
41
|
-
}
|
|
42
|
-
// After balanced expect(...), check for .method( pattern.
|
|
43
|
-
const tail = content.slice(i);
|
|
44
|
-
if (/^\s*\.\w+\s*\(/.test(tail)) {
|
|
45
|
-
count++;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return count;
|
|
49
|
-
}
|
|
50
|
-
function lineOf(content, offset) {
|
|
51
|
-
return content.slice(0, offset).split('\n').length;
|
|
52
|
-
}
|
|
53
|
-
export async function scanAssertions(input) {
|
|
54
|
-
let total = 0;
|
|
55
|
-
let weak = 0;
|
|
56
|
-
const byPattern = new Map();
|
|
57
|
-
for (const file of input.testFiles) {
|
|
58
|
-
const fullPath = join(input.project, file);
|
|
59
|
-
const content = await readFile(fullPath, 'utf8');
|
|
60
|
-
total += await countAssertions(content);
|
|
61
|
-
for (const { pattern, regex } of WEAK_PATTERNS) {
|
|
62
|
-
const matches = [...content.matchAll(regex)];
|
|
63
|
-
if (matches.length > 0) {
|
|
64
|
-
weak += matches.length;
|
|
65
|
-
const list = byPattern.get(pattern) ?? [];
|
|
66
|
-
for (const m of matches) {
|
|
67
|
-
const idx = m.index ?? 0;
|
|
68
|
-
list.push({ file, line: lineOf(content, idx), code: m[0] });
|
|
69
|
-
}
|
|
70
|
-
byPattern.set(pattern, list);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
// toEqual-self: needs receiver extraction.
|
|
74
|
-
const eqSelf = [...content.matchAll(TO_EQUAL_SELF)];
|
|
75
|
-
for (const m of eqSelf) {
|
|
76
|
-
const receiverMatch = content.slice(0, m.index ?? 0).match(/expect\s*\(\s*([a-zA-Z_$][\w$]*)\s*\)/g);
|
|
77
|
-
if (receiverMatch && receiverMatch.length > 0) {
|
|
78
|
-
const lastReceiver = receiverMatch[receiverMatch.length - 1];
|
|
79
|
-
const receiverName = lastReceiver?.match(/expect\s*\(\s*([a-zA-Z_$][\w$]*)\s*\)/)?.[1];
|
|
80
|
-
if (receiverName && receiverName === m[1]) {
|
|
81
|
-
weak += 1;
|
|
82
|
-
const idx = m.index ?? 0;
|
|
83
|
-
const list = byPattern.get('toEqual-self') ?? [];
|
|
84
|
-
list.push({ file, line: lineOf(content, idx), code: m[0] });
|
|
85
|
-
byPattern.set('toEqual-self', list);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
// toBe-self: same pattern.
|
|
90
|
-
const beSelf = [...content.matchAll(TO_BE_SELF)];
|
|
91
|
-
for (const m of beSelf) {
|
|
92
|
-
const receiverMatch = content.slice(0, m.index ?? 0).match(/expect\s*\(\s*([a-zA-Z_$][\w$]*)\s*\)/g);
|
|
93
|
-
if (receiverMatch && receiverMatch.length > 0) {
|
|
94
|
-
const lastReceiver = receiverMatch[receiverMatch.length - 1];
|
|
95
|
-
const receiverName = lastReceiver?.match(/expect\s*\(\s*([a-zA-Z_$][\w$]*)\s*\)/)?.[1];
|
|
96
|
-
if (receiverName && receiverName === m[1]) {
|
|
97
|
-
weak += 1;
|
|
98
|
-
const idx = m.index ?? 0;
|
|
99
|
-
const list = byPattern.get('toBe-self') ?? [];
|
|
100
|
-
list.push({ file, line: lineOf(content, idx), code: m[0] });
|
|
101
|
-
byPattern.set('toBe-self', list);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
const weakPatterns = [];
|
|
107
|
-
for (const [pattern, examples] of byPattern.entries()) {
|
|
108
|
-
weakPatterns.push({ pattern, count: examples.length, examples });
|
|
109
|
-
}
|
|
110
|
-
return {
|
|
111
|
-
totalAssertions: total,
|
|
112
|
-
weakAssertions: weak,
|
|
113
|
-
weakRate: total === 0 ? 0 : weak / total,
|
|
114
|
-
weakPatterns,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export { MutReportSchema } from './types.js';
|
|
2
|
-
export { scanAssertions, type ScanInput } from './assert-scanner.js';
|
|
3
|
-
export { runMutation, type RunMutationInput, type RunMutationOutput, type StrykerInvoker, type StrykerRawResult, } from './mut-runner.js';
|
|
4
|
-
export { createProductionStrykerInvoker } from './production-stryker.js';
|
|
5
|
-
export { buildMutReport, type BuildMutInput } from './report-builder.js';
|
|
6
|
-
export { loadMutReport, mutReportPath, MUT_REPORT_RELATIVE_PATH } from './report-loader.js';
|
|
7
|
-
export { DEFAULT_THRESHOLDS, evaluateThresholds, type Thresholds, type ThresholdEvaluation, type ThresholdViolation, type ThresholdViolationKind, } from './thresholds.js';
|
|
8
|
-
export type { MutReportJson, MutationReport, AssertionsReport, WeakPattern, WeakPatternCount, Followup, } from './types.js';
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { MutReportSchema } from './types.js';
|
|
2
|
-
export { scanAssertions } from './assert-scanner.js';
|
|
3
|
-
export { runMutation, } from './mut-runner.js';
|
|
4
|
-
export { createProductionStrykerInvoker } from './production-stryker.js';
|
|
5
|
-
export { buildMutReport } from './report-builder.js';
|
|
6
|
-
export { loadMutReport, mutReportPath, MUT_REPORT_RELATIVE_PATH } from './report-loader.js';
|
|
7
|
-
export { DEFAULT_THRESHOLDS, evaluateThresholds, } from './thresholds.js';
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Per spec §4.2 验收审计 — Stryker wrapper for TS mutation testing.
|
|
3
|
-
*
|
|
4
|
-
* Hard constraints:
|
|
5
|
-
* H6 (CLI裁决): invokeStryker is injected; production wires it to
|
|
6
|
-
* @stryker-mutator/core programmatic API.
|
|
7
|
-
* DRY: production implementation lives in this file; tests mock the
|
|
8
|
-
* invokeStryker injection point.
|
|
9
|
-
* KISS: keep the public surface narrow — input, invoker, output.
|
|
10
|
-
*/
|
|
11
|
-
import type { MutationReport } from './types.js';
|
|
12
|
-
export interface StrykerRawResult {
|
|
13
|
-
readonly mutantsTotal: number;
|
|
14
|
-
readonly mutantsKilled: number;
|
|
15
|
-
readonly mutantsSurvived: number;
|
|
16
|
-
readonly mutantsTimeout: number;
|
|
17
|
-
readonly perFile: ReadonlyArray<{
|
|
18
|
-
readonly file: string;
|
|
19
|
-
readonly killRate: number;
|
|
20
|
-
readonly survived: ReadonlyArray<{
|
|
21
|
-
line: number;
|
|
22
|
-
mutation: string;
|
|
23
|
-
survivedBecause: string;
|
|
24
|
-
}>;
|
|
25
|
-
}>;
|
|
26
|
-
}
|
|
27
|
-
export type StrykerInvoker = (opts: {
|
|
28
|
-
project: string;
|
|
29
|
-
testFiles: ReadonlyArray<string>;
|
|
30
|
-
}) => Promise<StrykerRawResult>;
|
|
31
|
-
export interface RunMutationInput {
|
|
32
|
-
readonly project: string;
|
|
33
|
-
readonly testFiles: ReadonlyArray<string>;
|
|
34
|
-
readonly invokeStryker: StrykerInvoker;
|
|
35
|
-
}
|
|
36
|
-
export interface RunMutationOutput {
|
|
37
|
-
readonly mutation: MutationReport;
|
|
38
|
-
}
|
|
39
|
-
export declare function runMutation(input: RunMutationInput): Promise<RunMutationOutput>;
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export async function runMutation(input) {
|
|
2
|
-
let raw;
|
|
3
|
-
try {
|
|
4
|
-
raw = await input.invokeStryker({
|
|
5
|
-
project: input.project,
|
|
6
|
-
testFiles: input.testFiles,
|
|
7
|
-
});
|
|
8
|
-
}
|
|
9
|
-
catch (err) {
|
|
10
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
11
|
-
throw new Error(`Stryker invocation failed: ${message}`);
|
|
12
|
-
}
|
|
13
|
-
const killRate = raw.mutantsTotal === 0 ? 0 : raw.mutantsKilled / raw.mutantsTotal;
|
|
14
|
-
return {
|
|
15
|
-
mutation: {
|
|
16
|
-
tool: 'stryker',
|
|
17
|
-
mutantsTotal: raw.mutantsTotal,
|
|
18
|
-
mutantsKilled: raw.mutantsKilled,
|
|
19
|
-
mutantsSurvived: raw.mutantsSurvived,
|
|
20
|
-
mutantsTimeout: raw.mutantsTimeout,
|
|
21
|
-
killRate,
|
|
22
|
-
byFile: raw.perFile.map((f) => ({
|
|
23
|
-
file: f.file,
|
|
24
|
-
killRate: f.killRate,
|
|
25
|
-
survived: f.survived,
|
|
26
|
-
})),
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Production Stryker invoker. Uses @stryker-mutator/core programmatic API.
|
|
3
|
-
* Note: this is a thin wrapper — actual Stryker invocation requires the
|
|
4
|
-
* Stryker config in `stryker.conf.js` to live at the project root.
|
|
5
|
-
*/
|
|
6
|
-
import { relative, isAbsolute } from 'node:path';
|
|
7
|
-
export function createProductionStrykerInvoker() {
|
|
8
|
-
return async ({ project, testFiles: _testFiles }) => {
|
|
9
|
-
// Lazy-load Stryker so unit tests don't need it installed.
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
11
|
-
const Stryker = (await import('@stryker-mutator/core')).Stryker;
|
|
12
|
-
// Mutation surface is owned by stryker.conf.js. testFiles is a runtime
|
|
13
|
-
// hint (consumed by the configured testRunner, not by Stryker's `mutate`
|
|
14
|
-
// field — `mutate` is source files to mutate, never test paths).
|
|
15
|
-
const stryker = new Stryker({});
|
|
16
|
-
const mutants = (await stryker.runMutationTest());
|
|
17
|
-
return normalize(mutants, project);
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
function normalize(mutants, project) {
|
|
21
|
-
let mutantsKilled = 0;
|
|
22
|
-
let mutantsSurvived = 0;
|
|
23
|
-
let mutantsTimeout = 0;
|
|
24
|
-
for (const m of mutants) {
|
|
25
|
-
if (m.status === 'Killed')
|
|
26
|
-
mutantsKilled++;
|
|
27
|
-
else if (m.status === 'Survived')
|
|
28
|
-
mutantsSurvived++;
|
|
29
|
-
else if (m.status === 'Timeout')
|
|
30
|
-
mutantsTimeout++;
|
|
31
|
-
}
|
|
32
|
-
// Bucket mutants by fileName to populate perFile. deriveFollowups iterates
|
|
33
|
-
// m.byFile to emit per-file followups (e.g. low_kill_rate) — leaving this
|
|
34
|
-
// empty (as v0 did) silently swallowed those followups in production.
|
|
35
|
-
const byFile = new Map();
|
|
36
|
-
for (const m of mutants) {
|
|
37
|
-
// Stryker 8 returns absolute paths in MutantResult.fileName. Strip the
|
|
38
|
-
// project root so byFile[].file is repo-relative (e.g. "src/services/loop/...").
|
|
39
|
-
// path.relative falls back to the absolute path when input is outside
|
|
40
|
-
// project, which is the safest failure mode.
|
|
41
|
-
const file = isAbsolute(m.fileName) && m.fileName.startsWith(project)
|
|
42
|
-
? relative(project, m.fileName)
|
|
43
|
-
: m.fileName;
|
|
44
|
-
let bucket = byFile.get(file);
|
|
45
|
-
if (!bucket) {
|
|
46
|
-
bucket = { killed: 0, survived: 0, survivedEntries: [] };
|
|
47
|
-
byFile.set(file, bucket);
|
|
48
|
-
}
|
|
49
|
-
if (m.status === 'Killed')
|
|
50
|
-
bucket.killed++;
|
|
51
|
-
else if (m.status === 'Survived') {
|
|
52
|
-
bucket.survived++;
|
|
53
|
-
bucket.survivedEntries.push({
|
|
54
|
-
line: m.location.start.line + 1, // Stryker is 0-based; reports are 1-based.
|
|
55
|
-
mutation: m.replacement,
|
|
56
|
-
survivedBecause: m.statusReason ?? '',
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
const perFile = [...byFile.entries()]
|
|
61
|
-
.map(([file, b]) => {
|
|
62
|
-
const total = b.killed + b.survived;
|
|
63
|
-
const killRate = total === 0 ? 0 : b.killed / total;
|
|
64
|
-
return { file, killRate, survived: b.survivedEntries };
|
|
65
|
-
})
|
|
66
|
-
// Stable order for MUT.sig / reports.
|
|
67
|
-
.sort((a, b) => a.file.localeCompare(b.file));
|
|
68
|
-
return {
|
|
69
|
-
mutantsTotal: mutants.length,
|
|
70
|
-
mutantsKilled,
|
|
71
|
-
mutantsSurvived,
|
|
72
|
-
mutantsTimeout,
|
|
73
|
-
perFile,
|
|
74
|
-
};
|
|
75
|
-
}
|