mandrel 1.58.0 → 1.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +89 -87
- package/.agents/docs/SDLC.md +11 -7
- package/.agents/docs/workflows.md +2 -1
- package/.agents/schemas/audit-rules.json +20 -0
- package/.agents/scripts/acceptance-eval.js +20 -3
- package/.agents/scripts/assert-branch.js +1 -3
- package/.agents/scripts/bootstrap.js +1 -1
- package/.agents/scripts/check-arch-cycles.js +360 -0
- package/.agents/scripts/coverage-capture.js +24 -3
- package/.agents/scripts/epic-deliver-preflight.js +5 -3
- package/.agents/scripts/epic-deliver-prepare.js +12 -4
- package/.agents/scripts/epic-execute-record-wave.js +1 -1
- package/.agents/scripts/evidence-gate.js +1 -1
- package/.agents/scripts/git-rebase-and-resolve.js +1 -1
- package/.agents/scripts/hierarchy-gate.js +34 -14
- package/.agents/scripts/lib/baselines/kinds/coverage.js +33 -149
- package/.agents/scripts/lib/baselines/kinds/duplication.js +27 -116
- package/.agents/scripts/lib/baselines/kinds/kind-factory.js +192 -0
- package/.agents/scripts/lib/baselines/kinds/lighthouse.js +34 -133
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +31 -124
- package/.agents/scripts/lib/baselines/kinds/mutation.js +25 -111
- package/.agents/scripts/lib/baselines/maintainability-baseline-io.js +59 -0
- package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +37 -0
- package/.agents/scripts/lib/baselines/writer.js +1 -1
- package/.agents/scripts/lib/close-validation/commands.js +188 -0
- package/.agents/scripts/lib/close-validation/gates.js +235 -0
- package/.agents/scripts/lib/close-validation/process.js +101 -0
- package/.agents/scripts/lib/close-validation/projections/maintainability.js +1 -1
- package/.agents/scripts/lib/close-validation/runner.js +325 -0
- package/.agents/scripts/lib/close-validation/telemetry.js +70 -0
- package/.agents/scripts/lib/config/quality.js +6 -6
- package/.agents/scripts/lib/config-resolver.js +2 -5
- package/.agents/scripts/lib/coverage-capture.js +147 -4
- package/.agents/scripts/lib/cpu-pool.js +14 -0
- package/.agents/scripts/lib/crap-utils.js +6 -11
- package/.agents/scripts/lib/dynamic-workflow/documentation-report-contract.js +87 -0
- package/.agents/scripts/lib/git-utils.js +24 -22
- package/.agents/scripts/lib/maintainability-engine.js +1 -1
- package/.agents/scripts/lib/maintainability-utils.js +4 -187
- package/.agents/scripts/lib/observability/perf-report-readers.js +32 -23
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +80 -6
- package/.agents/scripts/lib/orchestration/code-review.js +90 -77
- package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +5 -12
- package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +14 -14
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +184 -49
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +26 -2
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +26 -6
- package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +7 -20
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +1 -2
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +0 -6
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +103 -0
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +22 -64
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +38 -76
- package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +4 -16
- package/.agents/scripts/lib/orchestration/file-assumptions.js +4 -3
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +144 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +2 -2
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +7 -7
- package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +3 -3
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +7 -7
- package/.agents/scripts/lib/orchestration/preflight-cache.js +35 -12
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +5 -60
- package/.agents/scripts/lib/orchestration/review-providers/native.js +7 -6
- package/.agents/scripts/lib/orchestration/review-providers/parse-findings.js +105 -0
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +7 -59
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +2 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -4
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +32 -35
- package/.agents/scripts/lib/orchestration/skill-capsule-loader.js +1 -2
- package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +451 -503
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +8 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +47 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +2 -2
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +358 -54
- package/.agents/scripts/lib/orchestration/story-close/phases/close.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +3 -2
- package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +30 -3
- package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +5 -18
- package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +3 -3
- package/.agents/scripts/lib/orchestration/story-close-recovery.js +33 -16
- package/.agents/scripts/lib/orchestration/story-reachability.js +47 -0
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +2 -33
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +42 -64
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +9 -0
- package/.agents/scripts/lib/orchestration/ticketing/state.js +50 -436
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +471 -0
- package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
- package/.agents/scripts/lib/orchestration/wave-record-notifications.js +1 -1
- package/.agents/scripts/lib/orchestration/wave-record-projection.js +1 -7
- package/.agents/scripts/lib/project-root.js +17 -0
- package/.agents/scripts/lib/story-adjacency.js +76 -0
- package/.agents/scripts/lib/story-lifecycle.js +1 -1
- package/.agents/scripts/lib/transpile.js +93 -0
- package/.agents/scripts/lib/wave-runner/tick.js +4 -153
- package/.agents/scripts/lib/workers/crap-worker.js +1 -1
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +20 -2
- package/.agents/scripts/lib/worktree/lifecycle/force-drain.js +90 -0
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +26 -8
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +74 -0
- package/.agents/scripts/providers/github/tickets.js +110 -6
- package/.agents/scripts/run-lint.js +9 -0
- package/.agents/scripts/run-tests.js +24 -4
- package/.agents/scripts/stories-wave-tick.js +8 -5
- package/.agents/scripts/story-init.js +149 -10
- package/.agents/scripts/sync-branch-from-base.js +1 -1
- package/.agents/skills/stack/qa/lighthouse-baseline/SKILL.md +1 -1
- package/.agents/workflows/audit-documentation.md +226 -0
- package/.agents/workflows/epic-deliver.md +16 -23
- package/.agents/workflows/epic-plan.md +1 -1
- package/.agents/workflows/helpers/epic-deliver-story.md +17 -28
- package/.agents/workflows/helpers/single-story-deliver.md +2 -1
- package/.agents/workflows/onboard.md +4 -3
- package/.agents/workflows/story-deliver.md +1 -1
- package/README.md +13 -8
- package/lib/cli/init.js +336 -0
- package/package.json +2 -1
- package/.agents/scripts/lib/auto-refresh-baselines.js +0 -308
- package/.agents/scripts/lib/close-validation.js +0 -897
- package/.agents/scripts/lib/orchestration/cascade-grouping.js +0 -275
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter.js +0 -69
- package/.agents/scripts/lib/orchestration/story-close/format-autofix-scoped.js +0 -221
- package/.agents/scripts/lib/orchestration/story-close/format-autofix-shared.js +0 -123
- package/.agents/scripts/lib/task-utils.js +0 -26
- package/.agents/scripts/story-deliver-prepare.js +0 -267
|
@@ -6,22 +6,17 @@ import {
|
|
|
6
6
|
coverageForMethodInEntry,
|
|
7
7
|
findCoverageEntry,
|
|
8
8
|
} from './coverage-utils.js';
|
|
9
|
-
import { runOnPool } from './cpu-pool.js';
|
|
9
|
+
import { POOL_SERIAL_THRESHOLD, runOnPool } from './cpu-pool.js';
|
|
10
10
|
import { crapFormula } from './crap-engine.js';
|
|
11
11
|
import { Logger } from './Logger.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
scanDirectory,
|
|
15
|
-
transpileIfNeeded,
|
|
16
|
-
} from './maintainability-utils.js';
|
|
12
|
+
import { scanDirectory } from './maintainability-utils.js';
|
|
13
|
+
import { resolveTsTranspilerVersion, transpileIfNeeded } from './transpile.js';
|
|
17
14
|
|
|
18
15
|
const CRAP_WORKER_URL = new URL('./workers/crap-worker.js', import.meta.url);
|
|
19
16
|
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
// stay serial and remain byte-identical to the pre-pool output.
|
|
24
|
-
const SERIAL_THRESHOLD = 8;
|
|
17
|
+
// Pool-vs-serial cutover — single-sourced in cpu-pool.js (see the
|
|
18
|
+
// POOL_SERIAL_THRESHOLD docstring for the tuning rationale).
|
|
19
|
+
const SERIAL_THRESHOLD = POOL_SERIAL_THRESHOLD;
|
|
25
20
|
// 1.1.0 — TypeScript support landed in 5.29.0. Bumped from 1.0.0 because
|
|
26
21
|
// the scanner now emits CRAP rows for TS/TSX paths that the previous
|
|
27
22
|
// kernel could never reach. The CRAP formula and per-method scoring
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// .agents/scripts/lib/dynamic-workflow/documentation-report-contract.js
|
|
2
|
+
import { assertSectionsContract } from './report-contract-core.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The `audit-documentation` report contract (Story #4024).
|
|
6
|
+
*
|
|
7
|
+
* This is the **single source of truth** for the report shape that BOTH the
|
|
8
|
+
* sequential lens (`.agents/workflows/audit-documentation.md` Step 3) and the
|
|
9
|
+
* orchestrated dynamic-workflow path
|
|
10
|
+
* (`.claude/workflows/audit-documentation.workflow.js`) MUST emit to
|
|
11
|
+
* `{{auditOutputDir}}/audit-documentation-results.md`. Keeping it here lets
|
|
12
|
+
* the contract-tier test assert report conformance against one definition
|
|
13
|
+
* rather than re-deriving headings from prose in two places.
|
|
14
|
+
*
|
|
15
|
+
* @module dynamic-workflow/documentation-report-contract
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** The artifact filename the lens writes under `auditOutputDir`. */
|
|
19
|
+
export const REPORT_ARTIFACT_BASENAME = 'audit-documentation-results.md';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The required top-level (`##`) section headings, in document order, that the
|
|
23
|
+
* lens markdown's Step 3 template defines. A conformant report MUST contain
|
|
24
|
+
* each of these headings; the orchestrated path assembles its verified
|
|
25
|
+
* findings into exactly this skeleton.
|
|
26
|
+
*/
|
|
27
|
+
export const REQUIRED_SECTIONS = Object.freeze([
|
|
28
|
+
'Executive Summary',
|
|
29
|
+
'Target Set Coverage',
|
|
30
|
+
'Detailed Findings',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
/** The H1 title the report opens with. */
|
|
34
|
+
export const REPORT_TITLE = 'Documentation Audit Report';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The required field labels inside each `### <finding>` block under
|
|
38
|
+
* `## Detailed Findings`. Mirrors the strict per-finding structure in the
|
|
39
|
+
* lens template.
|
|
40
|
+
*/
|
|
41
|
+
export const FINDING_FIELDS = Object.freeze([
|
|
42
|
+
'Category',
|
|
43
|
+
'Impact',
|
|
44
|
+
'Current State',
|
|
45
|
+
'Recommendation & Rationale',
|
|
46
|
+
'Agent Prompt',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The finding categories the per-finding `Category` field draws from. These
|
|
51
|
+
* mirror the lens's Step 3 template enumeration: deterministic-gate findings
|
|
52
|
+
* (Generator Drift, Link Integrity) plus semantic-verification findings
|
|
53
|
+
* (Broken Instruction, Stale Description, Missing Coverage).
|
|
54
|
+
*/
|
|
55
|
+
export const FINDING_CATEGORIES = Object.freeze([
|
|
56
|
+
'Broken Instruction',
|
|
57
|
+
'Stale Description',
|
|
58
|
+
'Missing Coverage',
|
|
59
|
+
'Generator Drift',
|
|
60
|
+
'Link Integrity',
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The impact buckets the Executive Summary and per-finding `Impact` field draw
|
|
65
|
+
* from. The lens speaks in High/Medium/Low; this list is the canonical
|
|
66
|
+
* taxonomy the downstream `audit-to-stories` consumer maps onto Story
|
|
67
|
+
* priority.
|
|
68
|
+
*/
|
|
69
|
+
export const IMPACT_LEVELS = Object.freeze(['High', 'Medium', 'Low']);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Assert that a rendered markdown report conforms to the contract: it has the
|
|
73
|
+
* H1 title and every required `##` section heading. Returns a structured
|
|
74
|
+
* result rather than throwing, so callers (tests, the orchestrated path's
|
|
75
|
+
* self-check) can report precisely which sections are missing.
|
|
76
|
+
*
|
|
77
|
+
* Pure function — string analysis only.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} markdown - The rendered report body.
|
|
80
|
+
* @returns {{ conformant: boolean, missingSections: string[], hasTitle: boolean }}
|
|
81
|
+
*/
|
|
82
|
+
export function assertReportContract(markdown) {
|
|
83
|
+
return assertSectionsContract(markdown, {
|
|
84
|
+
title: REPORT_TITLE,
|
|
85
|
+
requiredSections: REQUIRED_SECTIONS,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -231,25 +231,28 @@ export function __setSleep(fn, opts = {}) {
|
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* immediately
|
|
234
|
+
* Shared bounded retry loop for git commands that can hit packed-refs lock
|
|
235
|
+
* contention. Only contention signatures trigger a retry — non-contention
|
|
236
|
+
* failures surface immediately, and success short-circuits the loop.
|
|
237
237
|
*
|
|
238
238
|
* Backoff schedule: 250ms, 500ms, 1000ms (3 retries → 4 attempts total).
|
|
239
239
|
* Deliberately no global lock — a mutex would erase the parallelism the
|
|
240
|
-
* worktree-isolation model is designed to enable.
|
|
240
|
+
* worktree-isolation model is designed to enable. The schedule and the
|
|
241
|
+
* jitter policy (`_sleep` / `_jitterFactor` seams) live only here so a
|
|
242
|
+
* backoff tuning change has a single point of application.
|
|
241
243
|
*
|
|
242
244
|
* @param {string} cwd
|
|
243
|
-
* @param {
|
|
245
|
+
* @param {string[]} argvPrefix - Leading git argv (e.g. `['fetch']`).
|
|
246
|
+
* @param {string[]} args - Trailing arguments (e.g. `['origin']`).
|
|
244
247
|
* @returns {Promise<{ status: number, stdout: string, stderr: string, attempts: number }>}
|
|
245
248
|
*/
|
|
246
|
-
|
|
249
|
+
async function gitWithContentionRetry(cwd, argvPrefix, args) {
|
|
247
250
|
const backoff = [250, 500, 1000];
|
|
248
251
|
let attempt = 0;
|
|
249
252
|
let last;
|
|
250
253
|
for (;;) {
|
|
251
254
|
attempt++;
|
|
252
|
-
last = gitSpawn(cwd,
|
|
255
|
+
last = gitSpawn(cwd, ...argvPrefix, ...args);
|
|
253
256
|
if (last.status === 0) return { ...last, attempts: attempt };
|
|
254
257
|
if (!isPackedRefsContention(last.stderr))
|
|
255
258
|
return { ...last, attempts: attempt };
|
|
@@ -260,6 +263,18 @@ export async function gitFetchWithRetry(cwd, ...args) {
|
|
|
260
263
|
}
|
|
261
264
|
}
|
|
262
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Run `git fetch …` with the bounded packed-refs-contention retry loop
|
|
268
|
+
* (see `gitWithContentionRetry`).
|
|
269
|
+
*
|
|
270
|
+
* @param {string} cwd
|
|
271
|
+
* @param {...string} args - Arguments after `fetch` (e.g. `'origin'`).
|
|
272
|
+
* @returns {Promise<{ status: number, stdout: string, stderr: string, attempts: number }>}
|
|
273
|
+
*/
|
|
274
|
+
export function gitFetchWithRetry(cwd, ...args) {
|
|
275
|
+
return gitWithContentionRetry(cwd, ['fetch'], args);
|
|
276
|
+
}
|
|
277
|
+
|
|
263
278
|
/**
|
|
264
279
|
* Run `git pull --rebase …` with the same bounded retry loop as
|
|
265
280
|
* `gitFetchWithRetry`. Packed-refs contention can occur during pulls
|
|
@@ -269,21 +284,8 @@ export async function gitFetchWithRetry(cwd, ...args) {
|
|
|
269
284
|
* @param {...string} args - Arguments after `pull --rebase` (e.g. `'origin', 'main'`).
|
|
270
285
|
* @returns {Promise<{ status: number, stdout: string, stderr: string, attempts: number }>}
|
|
271
286
|
*/
|
|
272
|
-
export
|
|
273
|
-
|
|
274
|
-
let attempt = 0;
|
|
275
|
-
let last;
|
|
276
|
-
for (;;) {
|
|
277
|
-
attempt++;
|
|
278
|
-
last = gitSpawn(cwd, 'pull', '--rebase', ...args);
|
|
279
|
-
if (last.status === 0) return { ...last, attempts: attempt };
|
|
280
|
-
if (!isPackedRefsContention(last.stderr))
|
|
281
|
-
return { ...last, attempts: attempt };
|
|
282
|
-
if (attempt > backoff.length) return { ...last, attempts: attempt };
|
|
283
|
-
const base = backoff[attempt - 1];
|
|
284
|
-
const jitter = Math.floor(Math.random() * base * _jitterFactor);
|
|
285
|
-
await _sleep(base + jitter);
|
|
286
|
-
}
|
|
287
|
+
export function gitPullWithRetry(cwd, ...args) {
|
|
288
|
+
return gitWithContentionRetry(cwd, ['pull', '--rebase'], args);
|
|
287
289
|
}
|
|
288
290
|
|
|
289
291
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import escomplex from 'typhonjs-escomplex';
|
|
3
|
-
import { transpileIfNeeded } from './
|
|
3
|
+
import { transpileIfNeeded } from './transpile.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Calculates the maintainability score of a JavaScript source file or string.
|
|
@@ -1,119 +1,24 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { minimatch } from 'minimatch';
|
|
5
4
|
import { canonicalise as canonicalisePath } from './baselines/path-canon.js';
|
|
6
|
-
import {
|
|
7
|
-
write as writeBaselineEnvelope,
|
|
8
|
-
writeFile as writeBaselineFile,
|
|
9
|
-
} from './baselines/writer.js';
|
|
10
|
-
import { runOnPool } from './cpu-pool.js';
|
|
5
|
+
import { POOL_SERIAL_THRESHOLD, runOnPool } from './cpu-pool.js';
|
|
11
6
|
import { Logger } from './Logger.js';
|
|
12
7
|
import { calculateForFile } from './maintainability-engine.js';
|
|
13
8
|
|
|
14
|
-
const require = createRequire(import.meta.url);
|
|
15
|
-
|
|
16
9
|
const MAINTAINABILITY_WORKER_URL = new URL(
|
|
17
10
|
'./workers/maintainability-worker.js',
|
|
18
11
|
import.meta.url,
|
|
19
12
|
);
|
|
20
13
|
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
// fixtures (n=2 stays serial; the full repo n≈470 takes the pool path).
|
|
25
|
-
const SERIAL_THRESHOLD = 8;
|
|
14
|
+
// Pool-vs-serial cutover — single-sourced in cpu-pool.js (see the
|
|
15
|
+
// POOL_SERIAL_THRESHOLD docstring for the tuning rationale).
|
|
16
|
+
const SERIAL_THRESHOLD = POOL_SERIAL_THRESHOLD;
|
|
26
17
|
|
|
27
18
|
const JS_EXTS = new Set(['.js', '.mjs', '.cjs']);
|
|
28
19
|
const TS_EXTS = new Set(['.ts', '.tsx', '.mts', '.cts']);
|
|
29
20
|
const SUPPORTED_EXTS = new Set([...JS_EXTS, ...TS_EXTS]);
|
|
30
21
|
|
|
31
|
-
let _ts = null;
|
|
32
|
-
let _tsLoadFailed = false;
|
|
33
|
-
|
|
34
|
-
function loadTypeScript() {
|
|
35
|
-
if (_ts) return _ts;
|
|
36
|
-
if (_tsLoadFailed) return null;
|
|
37
|
-
try {
|
|
38
|
-
_ts = require('typescript');
|
|
39
|
-
return _ts;
|
|
40
|
-
} catch {
|
|
41
|
-
_tsLoadFailed = true;
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Resolve the `typescript` package version, used to stamp baselines so
|
|
48
|
-
* consumers can detect transpiler drift. Returns `'0.0.0'` when the
|
|
49
|
-
* dependency is unresolvable — callers treat that sentinel as "unknown
|
|
50
|
-
* environment" and may refuse to persist a baseline that includes TS rows.
|
|
51
|
-
*
|
|
52
|
-
* @returns {string}
|
|
53
|
-
*/
|
|
54
|
-
export function resolveTsTranspilerVersion() {
|
|
55
|
-
const ts = loadTypeScript();
|
|
56
|
-
if (ts && typeof ts.version === 'string') return ts.version;
|
|
57
|
-
return '0.0.0';
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function isTypeScriptPath(filePath) {
|
|
61
|
-
return TS_EXTS.has(path.extname(String(filePath)).toLowerCase());
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Pre-transpile TypeScript or TSX sources to JavaScript that the
|
|
66
|
-
* Esprima-based escomplex kernel can parse. Returns the input unchanged
|
|
67
|
-
* for `.js` / `.mjs` / `.cjs` paths.
|
|
68
|
-
*
|
|
69
|
-
* Type annotations introduce no control flow, so the transpiled output
|
|
70
|
-
* scores identically to the original TS for cyclomatic complexity,
|
|
71
|
-
* Halstead volume, and the maintainability index. `.tsx` uses the
|
|
72
|
-
* `react-jsx` emit so JSX expressions become function calls escomplex
|
|
73
|
-
* can read; `.preserve` would leave JSX in the output and Esprima would
|
|
74
|
-
* choke on it.
|
|
75
|
-
*
|
|
76
|
-
* On transpile failure the helper returns `null` — callers treat that
|
|
77
|
-
* as "skip this file" rather than crashing the scan.
|
|
78
|
-
*
|
|
79
|
-
* @param {string} filePath
|
|
80
|
-
* @param {string} source
|
|
81
|
-
* @returns {string|null}
|
|
82
|
-
*/
|
|
83
|
-
export function transpileIfNeeded(filePath, source) {
|
|
84
|
-
if (!isTypeScriptPath(filePath)) return source;
|
|
85
|
-
const ts = loadTypeScript();
|
|
86
|
-
if (!ts) {
|
|
87
|
-
Logger.warn(
|
|
88
|
-
`[Maintainability] ⚠ typescript package not resolvable; cannot score ${filePath}. ` +
|
|
89
|
-
"Install with 'npm install --save-dev typescript' (peer dep, >=5.0.0).",
|
|
90
|
-
);
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
try {
|
|
94
|
-
const result = ts.transpileModule(source, {
|
|
95
|
-
compilerOptions: {
|
|
96
|
-
target: ts.ScriptTarget.ESNext,
|
|
97
|
-
module: ts.ModuleKind.ESNext,
|
|
98
|
-
isolatedModules: true,
|
|
99
|
-
noEmitHelpers: true,
|
|
100
|
-
importHelpers: false,
|
|
101
|
-
removeComments: false,
|
|
102
|
-
jsx: ts.JsxEmit.ReactJSX,
|
|
103
|
-
sourceMap: false,
|
|
104
|
-
},
|
|
105
|
-
fileName: path.basename(filePath),
|
|
106
|
-
reportDiagnostics: false,
|
|
107
|
-
});
|
|
108
|
-
return result.outputText;
|
|
109
|
-
} catch (err) {
|
|
110
|
-
Logger.warn(
|
|
111
|
-
`[Maintainability] ⚠ TS transpile failed for ${filePath}: ${err?.message ?? err}; skipping.`,
|
|
112
|
-
);
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
22
|
/**
|
|
118
23
|
* @returns {boolean} True when the path's extension is one the engines score.
|
|
119
24
|
*/
|
|
@@ -121,94 +26,6 @@ function isSupportedSourceFile(filePath) {
|
|
|
121
26
|
return SUPPORTED_EXTS.has(path.extname(String(filePath)).toLowerCase());
|
|
122
27
|
}
|
|
123
28
|
|
|
124
|
-
/**
|
|
125
|
-
* Loads the current maintainability baseline from disk. The on-disk path is
|
|
126
|
-
* resolved by the caller via {@link getBaselines}; passing it explicitly
|
|
127
|
-
* removes the silent-default behaviour the framework dropped in Epic #730
|
|
128
|
-
* Story 5.5.
|
|
129
|
-
*
|
|
130
|
-
* @param {string} baselinePath Repo-relative or absolute path to the baseline
|
|
131
|
-
* JSON. Required.
|
|
132
|
-
* @returns {Record<string, number>}
|
|
133
|
-
*/
|
|
134
|
-
/**
|
|
135
|
-
* Story #1895: project the canonical maintainability envelope back to the
|
|
136
|
-
* legacy flat `{ path: mi }` map so existing gate consumers keep working
|
|
137
|
-
* without churn — Story #1912 will replace this shim with the shared
|
|
138
|
-
* reader. Returns the parsed input unchanged when it doesn't look like an
|
|
139
|
-
* envelope (legacy flat shape stays flat).
|
|
140
|
-
*/
|
|
141
|
-
function projectMaintainabilityEnvelopeToFlat(parsed) {
|
|
142
|
-
if (
|
|
143
|
-
!parsed ||
|
|
144
|
-
typeof parsed !== 'object' ||
|
|
145
|
-
Array.isArray(parsed) ||
|
|
146
|
-
!Array.isArray(parsed.rows) ||
|
|
147
|
-
typeof parsed.$schema !== 'string'
|
|
148
|
-
) {
|
|
149
|
-
return parsed;
|
|
150
|
-
}
|
|
151
|
-
const flat = {};
|
|
152
|
-
for (const row of parsed.rows) {
|
|
153
|
-
if (row && typeof row.path === 'string' && typeof row.mi === 'number') {
|
|
154
|
-
flat[row.path] = row.mi;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return flat;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function getBaseline(baselinePath) {
|
|
161
|
-
if (typeof baselinePath !== 'string' || baselinePath.length === 0) {
|
|
162
|
-
throw new TypeError(
|
|
163
|
-
'maintainability-utils.getBaseline: baselinePath is required (Epic #730 ' +
|
|
164
|
-
'Story 5.5 — callers resolve the path via getBaselines(config).maintainability.path).',
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
const abs = path.isAbsolute(baselinePath)
|
|
168
|
-
? baselinePath
|
|
169
|
-
: path.resolve(process.cwd(), baselinePath);
|
|
170
|
-
if (!fs.existsSync(abs)) return {};
|
|
171
|
-
try {
|
|
172
|
-
const parsed = JSON.parse(fs.readFileSync(abs, 'utf-8'));
|
|
173
|
-
return projectMaintainabilityEnvelopeToFlat(parsed);
|
|
174
|
-
} catch (err) {
|
|
175
|
-
Logger.warn(`[Maintainability] Failed to parse baseline: ${err.message}`);
|
|
176
|
-
return {};
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Saves a new maintainability baseline to disk at `baselinePath`.
|
|
182
|
-
*
|
|
183
|
-
* Accepts the legacy flat `{ path: mi }` shape for backwards compatibility
|
|
184
|
-
* with existing callers (`regenerateMainFromTree`, refresh helpers). The
|
|
185
|
-
* map is transformed into the canonical envelope shape (`$schema`,
|
|
186
|
-
* `kernelVersion`, `generatedAt`, `rollup`, `rows`) via the shared
|
|
187
|
-
* `lib/baselines/writer.js` pipeline before being persisted, so every
|
|
188
|
-
* write produces a file that round-trips through `lib/baselines/reader.js`
|
|
189
|
-
* without schema errors.
|
|
190
|
-
*
|
|
191
|
-
* @param {Record<string, number>} baseline path→MI flat map.
|
|
192
|
-
* @param {string} baselinePath Required — caller supplies via getBaselines().
|
|
193
|
-
*/
|
|
194
|
-
export function saveBaseline(baseline, baselinePath) {
|
|
195
|
-
if (typeof baselinePath !== 'string' || baselinePath.length === 0) {
|
|
196
|
-
throw new TypeError(
|
|
197
|
-
'maintainability-utils.saveBaseline: baselinePath is required.',
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
const abs = path.isAbsolute(baselinePath)
|
|
201
|
-
? baselinePath
|
|
202
|
-
: path.resolve(process.cwd(), baselinePath);
|
|
203
|
-
|
|
204
|
-
const rows = Object.entries(baseline ?? {}).map(([p, mi]) => ({
|
|
205
|
-
path: p,
|
|
206
|
-
mi,
|
|
207
|
-
}));
|
|
208
|
-
const envelope = writeBaselineEnvelope({ kind: 'maintainability', rows });
|
|
209
|
-
writeBaselineFile(abs, envelope);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
29
|
const IGNORED_DIRS = new Set([
|
|
213
30
|
'node_modules',
|
|
214
31
|
'.git',
|
|
@@ -16,6 +16,7 @@ import { storyArtifactPath } from '../config/temp-paths.js';
|
|
|
16
16
|
import { gitSpawn } from '../git-utils.js';
|
|
17
17
|
import { Logger } from '../Logger.js';
|
|
18
18
|
import { read as readSignals } from '../signals/read.js';
|
|
19
|
+
import { concurrentMap } from '../util/concurrent-map.js';
|
|
19
20
|
import { extractStoryPerfSummaryFromComment } from './perf-report-render.js';
|
|
20
21
|
import { forEachLine } from './signals-writer.js';
|
|
21
22
|
|
|
@@ -154,30 +155,38 @@ export async function collectStorySummaries(provider, epicId, logger) {
|
|
|
154
155
|
),
|
|
155
156
|
);
|
|
156
157
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
158
|
+
// Bounded-parallel comment fetch (Story #3990). Each Story's comment
|
|
159
|
+
// thread is an independent paginated REST read through a fresh `gh`
|
|
160
|
+
// spawn, so a serial loop pays N sequential round-trips. concurrentMap
|
|
161
|
+
// preserves input→output index, keeping summaries in Story order; the
|
|
162
|
+
// per-Story try/catch stays inside the mapper so one failed fetch
|
|
163
|
+
// degrades to warn-and-skip without aborting the batch (mirrors
|
|
164
|
+
// verifySingleResult in lib/orchestration/wave-record-io.js, #3024).
|
|
165
|
+
const perStory = await concurrentMap(
|
|
166
|
+
storyTickets,
|
|
167
|
+
async (ticket) => {
|
|
168
|
+
const id = Number(ticket.id ?? ticket.number);
|
|
169
|
+
if (!Number.isInteger(id) || id < 1) return null;
|
|
170
|
+
let comments;
|
|
171
|
+
try {
|
|
172
|
+
comments = (await provider.getTicketComments(id)) ?? [];
|
|
173
|
+
} catch (err) {
|
|
174
|
+
logger.warn?.(
|
|
175
|
+
`[analyze-execution] getTicketComments(${id}) failed: ${
|
|
176
|
+
err instanceof Error ? err.message : String(err)
|
|
177
|
+
}`,
|
|
178
|
+
);
|
|
179
|
+
return null;
|
|
177
180
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
for (const c of comments) {
|
|
182
|
+
const parsed = extractStoryPerfSummaryFromComment(c?.body);
|
|
183
|
+
if (parsed) return parsed;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
},
|
|
187
|
+
{ concurrency: 4 },
|
|
188
|
+
);
|
|
189
|
+
return perStory.filter((s) => s !== null);
|
|
181
190
|
}
|
|
182
191
|
|
|
183
192
|
/**
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Acceptance self-eval decision core (Story #3819).
|
|
3
3
|
*
|
|
4
|
-
* Pure
|
|
5
|
-
*
|
|
4
|
+
* Pure reducer that turns one round's critic verdict plus the resolved
|
|
5
|
+
* round cap into the loop's next action. The CLI wrapper
|
|
6
6
|
* (`acceptance-eval.js`) owns the file reads, schema validation, signal
|
|
7
7
|
* emission, and ticket transitions; this module owns the *decision* so it
|
|
8
8
|
* can be unit-tested in isolation.
|
|
9
9
|
*
|
|
10
|
+
* ## Round derivation (Story #4019)
|
|
11
|
+
*
|
|
12
|
+
* The round number is **derived from the signals ledger**, not from the
|
|
13
|
+
* critic's self-reported `verdict.round`: every prior round appended one
|
|
14
|
+
* `acceptance-eval` signal to the Story's `signals.ndjson`, so the
|
|
15
|
+
* current round is `count(prior signals) + 1`. This survives a subagent
|
|
16
|
+
* restart (the ledger is on disk) and removes the critic's scratch value
|
|
17
|
+
* from the cap enforcement path — a critic that always reports `round: 1`
|
|
18
|
+
* can no longer defeat the bounded-loop guarantee.
|
|
19
|
+
*
|
|
10
20
|
* ## The three terminal actions
|
|
11
21
|
*
|
|
12
22
|
* - `proceed` — every criterion is `met`. The Story may flip to
|
|
@@ -29,6 +39,10 @@
|
|
|
29
39
|
* possible action is `block`.
|
|
30
40
|
*/
|
|
31
41
|
|
|
42
|
+
import { readFileSync } from 'node:fs';
|
|
43
|
+
|
|
44
|
+
import { signalsFile } from '../config/temp-paths.js';
|
|
45
|
+
|
|
32
46
|
/**
|
|
33
47
|
* Verdicts that clear a criterion. Anything else (`partial`, `unmet`, or
|
|
34
48
|
* an unrecognised value) is treated as not-yet-met and triggers rework.
|
|
@@ -87,11 +101,16 @@ function partitionCriteria(criteria) {
|
|
|
87
101
|
* Decide the next loop action from a single round's verdict.
|
|
88
102
|
*
|
|
89
103
|
* @param {object} args
|
|
90
|
-
* @param {{
|
|
104
|
+
* @param {{ criteria?: Array<object> }} args.verdict
|
|
91
105
|
* A verdict already validated against the acceptance-eval-verdict schema.
|
|
106
|
+
* Its `round` field, when present, is ignored — the round is supplied by
|
|
107
|
+
* the caller (derived from the signals ledger; Story #4019).
|
|
92
108
|
* @param {number} args.maxRounds
|
|
93
109
|
* The resolved (already-clamped) redraft ceiling from
|
|
94
110
|
* `getAcceptanceEval(config).maxRounds`.
|
|
111
|
+
* @param {number} [args.round]
|
|
112
|
+
* The current round number, derived via `deriveAcceptanceEvalRound`.
|
|
113
|
+
* Defaults to 1 when absent or invalid.
|
|
95
114
|
* @returns {{
|
|
96
115
|
* decision: 'proceed' | 'redraft' | 'block',
|
|
97
116
|
* round: number,
|
|
@@ -102,10 +121,9 @@ function partitionCriteria(criteria) {
|
|
|
102
121
|
* capReached: boolean,
|
|
103
122
|
* }}
|
|
104
123
|
*/
|
|
105
|
-
export function decideAcceptanceEval({ verdict, maxRounds }) {
|
|
124
|
+
export function decideAcceptanceEval({ verdict, maxRounds, round: roundIn }) {
|
|
106
125
|
const cap = effectiveCap(maxRounds);
|
|
107
|
-
const round =
|
|
108
|
-
Number.isInteger(verdict?.round) && verdict.round >= 1 ? verdict.round : 1;
|
|
126
|
+
const round = Number.isInteger(roundIn) && roundIn >= 1 ? roundIn : 1;
|
|
109
127
|
const { metCount, notMet } = partitionCriteria(verdict?.criteria);
|
|
110
128
|
const totalCriteria = metCount + notMet.length;
|
|
111
129
|
const allMet = notMet.length === 0;
|
|
@@ -171,3 +189,59 @@ export function buildAcceptanceEvalSignal({
|
|
|
171
189
|
},
|
|
172
190
|
};
|
|
173
191
|
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Derive the current acceptance-eval round for a Story by counting the
|
|
195
|
+
* `acceptance-eval` signals already appended to the Story's
|
|
196
|
+
* `signals.ndjson` (Story #4019). Round = prior-signal count + 1, so the
|
|
197
|
+
* first run reports round 1 and each completed round (which appends one
|
|
198
|
+
* signal via `acceptance-eval.js`) advances the derived round by one.
|
|
199
|
+
*
|
|
200
|
+
* The derivation is restart-safe: the ledger lives on disk, so a subagent
|
|
201
|
+
* that dies mid-loop and restarts still observes every prior round. A
|
|
202
|
+
* missing or malformed ledger degrades to round 1 (no prior rounds), and
|
|
203
|
+
* malformed lines are skipped — observability corruption never wedges the
|
|
204
|
+
* gate.
|
|
205
|
+
*
|
|
206
|
+
* @param {object} args
|
|
207
|
+
* @param {number|null} args.epicId Parent Epic ID, or `null` for a
|
|
208
|
+
* standalone Story (routes to `<tempRoot>/standalone/stories/...`).
|
|
209
|
+
* @param {number} args.storyId
|
|
210
|
+
* @param {object} [args.config] Resolved config (tempRoot resolution).
|
|
211
|
+
* @param {(p: string) => string} [args.readFile] Injectable reader (tests).
|
|
212
|
+
* @param {(eid: number|null, sid: number, config?: object) => string} [args.signalsPathResolver]
|
|
213
|
+
* Injectable path resolver (tests). Defaults to `signalsFile`.
|
|
214
|
+
* @returns {number} The 1-based current round.
|
|
215
|
+
*/
|
|
216
|
+
export function deriveAcceptanceEvalRound({
|
|
217
|
+
epicId,
|
|
218
|
+
storyId,
|
|
219
|
+
config,
|
|
220
|
+
readFile = (p) => readFileSync(p, 'utf8'),
|
|
221
|
+
signalsPathResolver = signalsFile,
|
|
222
|
+
}) {
|
|
223
|
+
let text;
|
|
224
|
+
try {
|
|
225
|
+
text = readFile(signalsPathResolver(epicId ?? null, storyId, config));
|
|
226
|
+
} catch (_err) {
|
|
227
|
+
// No ledger yet → no prior rounds.
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let priorRounds = 0;
|
|
232
|
+
for (const line of String(text).split('\n')) {
|
|
233
|
+
const trimmed = line.trim();
|
|
234
|
+
if (trimmed === '') continue;
|
|
235
|
+
let record;
|
|
236
|
+
try {
|
|
237
|
+
record = JSON.parse(trimmed);
|
|
238
|
+
} catch (_err) {
|
|
239
|
+
continue; // Malformed line — skip, never throw.
|
|
240
|
+
}
|
|
241
|
+
if (!record || typeof record !== 'object') continue;
|
|
242
|
+
if (record.kind !== 'acceptance-eval') continue;
|
|
243
|
+
if (record.storyId !== storyId) continue;
|
|
244
|
+
priorRounds += 1;
|
|
245
|
+
}
|
|
246
|
+
return priorRounds + 1;
|
|
247
|
+
}
|