mandrel 1.69.0 → 1.71.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 +7 -7
- package/.agents/docs/SDLC.md +4 -5
- package/.agents/docs/configuration.md +9 -9
- package/.agents/docs/workflows.md +4 -6
- package/.agents/schemas/qa-finding.schema.json +1 -1
- package/.agents/scripts/apply-quality-bootstrap.js +79 -0
- package/.agents/scripts/audit-labels-bootstrap.js +52 -30
- package/.agents/scripts/audit-to-stories.js +54 -0
- package/.agents/scripts/bootstrap.js +13 -3
- package/.agents/scripts/generate-config-docs.js +189 -94
- package/.agents/scripts/lib/audit-suite/findings.js +0 -4
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +99 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +13 -5
- package/.agents/scripts/lib/baseline-snapshot.js +163 -4
- package/.agents/scripts/lib/baselines/refresh-service.js +0 -4
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +1 -1
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +1 -1
- package/.agents/scripts/lib/config/baselines.js +0 -20
- package/.agents/scripts/lib/config/defaults.js +1 -1
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/config/temp-paths.js +0 -31
- package/.agents/scripts/lib/config-resolver.js +1 -1
- package/.agents/scripts/lib/crap-utils.js +281 -0
- package/.agents/scripts/lib/orchestration/dispatch-engine.js +0 -2
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +0 -84
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +3 -4
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -4
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +101 -70
- package/.agents/scripts/lib/orchestration/spec-renderer.js +42 -14
- package/.agents/scripts/lib/orchestration/ticket-lease.js +3 -0
- package/.agents/scripts/lib/qa/qa-context-hydrator.js +1 -1
- package/.agents/scripts/lib/qa/resolve-qa-contract.js +1 -1
- package/.agents/scripts/lib/story-body/story-body.js +110 -65
- package/.agents/scripts/lib/test-tiers.js +13 -7
- package/.agents/scripts/lib/wave-runner/tick.js +177 -53
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +226 -0
- package/.agents/scripts/mandrel-update-preflight.js +235 -0
- package/.agents/scripts/providers/github/issues.js +48 -0
- package/.agents/scripts/providers/github.js +1 -0
- package/.agents/scripts/sync-agentrc.js +2 -2
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +3 -3
- package/.agents/skills/stack/qa/qa-harness/SKILL.md +4 -4
- package/.agents/workflows/git-deliver.md +298 -0
- package/.agents/workflows/helpers/epic-testing.md +6 -6
- package/.agents/workflows/helpers/{agents-sync-config.md → mandrel-sync-config.md} +5 -4
- package/.agents/workflows/{agents-update.md → mandrel-update.md} +210 -33
- package/.agents/workflows/qa-explore.md +1 -1
- package/.agents/workflows/{qa-run-harness.md → qa-run.md} +5 -5
- package/README.md +40 -0
- package/docs/CHANGELOG.md +43 -0
- package/lib/cli/registry.js +49 -6
- package/lib/cli/update.js +335 -332
- package/package.json +16 -11
- package/.agents/workflows/git-commit-all.md +0 -15
- package/.agents/workflows/git-pr-all.md +0 -281
- package/.agents/workflows/git-push.md +0 -63
|
@@ -242,7 +242,7 @@ export function ensureQualityNpmScripts(ctx) {
|
|
|
242
242
|
* underneath the project config at read time, so a key whose intended
|
|
243
243
|
* value equals the framework default would be written redundantly — and
|
|
244
244
|
* would then be flagged `[REDUNDANT]` by the sync-agentrc helper on the
|
|
245
|
-
* next /
|
|
245
|
+
* next /mandrel-update. Default-aware seeding keeps the two helpers from
|
|
246
246
|
* contradicting each other.
|
|
247
247
|
*
|
|
248
248
|
* Returns `{ merged, addedKeys[] }` so the caller can report exactly
|
|
@@ -38,23 +38,3 @@ export function getBaselines(config) {
|
|
|
38
38
|
maintainability: merge('maintainability'),
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Legacy entry point retained for backward-compatible imports. Story #1737
|
|
44
|
-
* retired the standalone `baselines.*` block; this helper now translates
|
|
45
|
-
* `{ lint: { path }, ... }` input onto synthetic `gates.<tier>.baselinePath`.
|
|
46
|
-
*
|
|
47
|
-
* @param {object|undefined} userBlock
|
|
48
|
-
*/
|
|
49
|
-
export function resolveBaselines(userBlock) {
|
|
50
|
-
const block = userBlock ?? {};
|
|
51
|
-
return getBaselines({
|
|
52
|
-
quality: {
|
|
53
|
-
gates: {
|
|
54
|
-
lint: { baselinePath: block.lint?.path },
|
|
55
|
-
crap: { baselinePath: block.crap?.path },
|
|
56
|
-
maintainability: { baselinePath: block.maintainability?.path },
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* (tests/config/full-agentrc-runtime-parity.test.js) keeps the two in
|
|
10
10
|
* lockstep.
|
|
11
11
|
*
|
|
12
|
-
* Story #1995: the `/
|
|
12
|
+
* Story #1995: the `/mandrel-update` sync helper consults this module
|
|
13
13
|
* (not the template directly) to decide whether a project value is
|
|
14
14
|
* "just the default" and therefore safe to omit from `.agentrc.json`.
|
|
15
15
|
*
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* untouched. Operators who want a leaner config can prune the
|
|
17
17
|
* redundant keys by hand.
|
|
18
18
|
*
|
|
19
|
-
* Outcome: after `/
|
|
19
|
+
* Outcome: after `/mandrel-update`, `.agentrc.json` contains only keys
|
|
20
20
|
* that materially diverge from framework defaults, plus the
|
|
21
21
|
* consumer-identity keys with no sensible framework default (owner,
|
|
22
22
|
* repo, operatorHandle).
|
|
@@ -56,8 +56,6 @@
|
|
|
56
56
|
import { execFileSync } from 'node:child_process';
|
|
57
57
|
import path from 'node:path';
|
|
58
58
|
|
|
59
|
-
let _resolveConfig;
|
|
60
|
-
|
|
61
59
|
/**
|
|
62
60
|
* Cache the resolved main-checkout root per spawn cwd so the
|
|
63
61
|
* `git rev-parse` shell-out runs at most once per distinct working
|
|
@@ -136,20 +134,6 @@ function anchorTempRoot(tempRoot) {
|
|
|
136
134
|
return root ? path.join(root, tempRoot) : tempRoot;
|
|
137
135
|
}
|
|
138
136
|
|
|
139
|
-
/**
|
|
140
|
-
* Lazy import of `resolveConfig` to side-step a circular module graph
|
|
141
|
-
* (`config-resolver.js` re-exports from this directory and importing it
|
|
142
|
-
* eagerly would resolve `temp-paths.js` before `lib/config/limits.js` is
|
|
143
|
-
* ready). The resolver itself caches per-root, so the inner call is cheap.
|
|
144
|
-
*/
|
|
145
|
-
async function getResolveConfig() {
|
|
146
|
-
if (!_resolveConfig) {
|
|
147
|
-
const mod = await import('../config-resolver.js');
|
|
148
|
-
_resolveConfig = mod.resolveConfig;
|
|
149
|
-
}
|
|
150
|
-
return _resolveConfig;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
137
|
/**
|
|
154
138
|
* Synchronous tempRoot extraction. Accepts the canonical full resolved
|
|
155
139
|
* config (`{ project, ... }`) and reads `project.paths.tempRoot`.
|
|
@@ -172,21 +156,6 @@ export function tempRootFrom(config) {
|
|
|
172
156
|
: 'temp';
|
|
173
157
|
}
|
|
174
158
|
|
|
175
|
-
/**
|
|
176
|
-
* Async tempRoot resolver. When the caller cannot pass a config bag, this
|
|
177
|
-
* loads the project's `.agentrc.json` via `resolveConfig` (cached per
|
|
178
|
-
* root). Most `.agents/scripts` consumers should prefer the synchronous
|
|
179
|
-
* variant by threading their already-resolved config through.
|
|
180
|
-
*
|
|
181
|
-
* @param {{ cwd?: string }} [opts]
|
|
182
|
-
* @returns {Promise<string>}
|
|
183
|
-
*/
|
|
184
|
-
export async function tempRootAsync(opts) {
|
|
185
|
-
const resolveConfig = await getResolveConfig();
|
|
186
|
-
const resolved = resolveConfig({ cwd: opts?.cwd });
|
|
187
|
-
return tempRootFrom(resolved);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
159
|
const epicId = (id) => {
|
|
191
160
|
if (!Number.isInteger(id) || id <= 0) {
|
|
192
161
|
throw new Error(
|
|
@@ -199,7 +199,7 @@ function applyDefaults(raw) {
|
|
|
199
199
|
// `#/$defs/qa`). It needs no default-layering — the harness resolver
|
|
200
200
|
// (`resolveQaContract`) owns normalization and required-field
|
|
201
201
|
// enforcement — it only needs to survive the reshape so
|
|
202
|
-
// `/qa-run
|
|
202
|
+
// `/qa-run` Step 0 can read it off the resolved wrapper.
|
|
203
203
|
...(raw.qa !== undefined ? { qa: raw.qa } : {}),
|
|
204
204
|
};
|
|
205
205
|
}
|
|
@@ -13,6 +13,10 @@ import { scanDirectory } from './maintainability-utils.js';
|
|
|
13
13
|
import { resolveTsTranspilerVersion, transpileIfNeeded } from './transpile.js';
|
|
14
14
|
|
|
15
15
|
const CRAP_WORKER_URL = new URL('./workers/crap-worker.js', import.meta.url);
|
|
16
|
+
const COMBINED_MI_CRAP_WORKER_URL = new URL(
|
|
17
|
+
'./workers/combined-mi-crap-worker.js',
|
|
18
|
+
import.meta.url,
|
|
19
|
+
);
|
|
16
20
|
|
|
17
21
|
// Pool-vs-serial cutover — single-sourced in cpu-pool.js (see the
|
|
18
22
|
// POOL_SERIAL_THRESHOLD docstring for the tuning rationale).
|
|
@@ -472,3 +476,280 @@ async function scoreFilesViaPool(queue, coverage) {
|
|
|
472
476
|
return { item, result: r };
|
|
473
477
|
});
|
|
474
478
|
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* In-process combined scorer: parse `abs` exactly once via `analyzeOnce` and
|
|
482
|
+
* derive BOTH the module MI score and the per-method CRAP rows. The reference
|
|
483
|
+
* implementation for the combined worker, used directly below
|
|
484
|
+
* `SERIAL_THRESHOLD` (matching the serial fast paths of `calculateAll` and
|
|
485
|
+
* `scanAndScore`).
|
|
486
|
+
*
|
|
487
|
+
* Return shape mirrors `combined-mi-crap-worker.js`:
|
|
488
|
+
* - `miScore` — `null` on read failure (MI dropped by the host), `0` on
|
|
489
|
+
* transpile-null / parse-error (parity with `calculateForFile` /
|
|
490
|
+
* `calculateForSource`), otherwise the module maintainability index.
|
|
491
|
+
* - `crapRows` — `null` on read/transpile/parse failure (CRAP drops the
|
|
492
|
+
* file), `[]` when coverage-skipped, otherwise the scored method rows.
|
|
493
|
+
* - `skippedFileNoCoverage` / `skippedMethodsNoCoverage` — CRAP counters.
|
|
494
|
+
*/
|
|
495
|
+
function scoreFileCombinedSerial({ abs, relPath, requireCoverage }, coverage) {
|
|
496
|
+
const entry = findCoverageEntry(coverage, relPath);
|
|
497
|
+
let source;
|
|
498
|
+
try {
|
|
499
|
+
source = fs.readFileSync(abs, 'utf-8');
|
|
500
|
+
} catch {
|
|
501
|
+
return {
|
|
502
|
+
relPath,
|
|
503
|
+
miScore: null,
|
|
504
|
+
skippedFileNoCoverage: false,
|
|
505
|
+
crapRows: null,
|
|
506
|
+
skippedMethodsNoCoverage: 0,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
const prepared = transpileIfNeeded(abs, source);
|
|
510
|
+
if (prepared === null) {
|
|
511
|
+
return {
|
|
512
|
+
relPath,
|
|
513
|
+
miScore: 0,
|
|
514
|
+
skippedFileNoCoverage: false,
|
|
515
|
+
crapRows: null,
|
|
516
|
+
skippedMethodsNoCoverage: 0,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
const {
|
|
520
|
+
miScore,
|
|
521
|
+
crapRows: rawCrapRows,
|
|
522
|
+
parseError,
|
|
523
|
+
} = analyzeOnce(prepared, entry);
|
|
524
|
+
if (parseError) {
|
|
525
|
+
return {
|
|
526
|
+
relPath,
|
|
527
|
+
miScore: 0,
|
|
528
|
+
skippedFileNoCoverage: false,
|
|
529
|
+
crapRows: null,
|
|
530
|
+
skippedMethodsNoCoverage: 0,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
if (requireCoverage && entry === null) {
|
|
534
|
+
return {
|
|
535
|
+
relPath,
|
|
536
|
+
miScore,
|
|
537
|
+
skippedFileNoCoverage: true,
|
|
538
|
+
crapRows: [],
|
|
539
|
+
skippedMethodsNoCoverage: 0,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
const crapRows = [];
|
|
543
|
+
let skippedMethodsNoCoverage = 0;
|
|
544
|
+
for (const mr of rawCrapRows) {
|
|
545
|
+
if (mr.crap === null || mr.coverage === null) {
|
|
546
|
+
skippedMethodsNoCoverage += 1;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
crapRows.push({
|
|
550
|
+
method: mr.method,
|
|
551
|
+
startLine: mr.startLine,
|
|
552
|
+
cyclomatic: mr.cyclomatic,
|
|
553
|
+
coverage: mr.coverage,
|
|
554
|
+
crap: mr.crap,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
relPath,
|
|
559
|
+
miScore,
|
|
560
|
+
skippedFileNoCoverage: false,
|
|
561
|
+
crapRows,
|
|
562
|
+
skippedMethodsNoCoverage,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function scoreFilesCombinedViaPool(queue, coverage) {
|
|
567
|
+
const enrichedQueue = queue.map((item) => ({
|
|
568
|
+
...item,
|
|
569
|
+
coverageEntry: findCoverageEntry(coverage, item.relPath),
|
|
570
|
+
}));
|
|
571
|
+
const results = await runOnPool(COMBINED_MI_CRAP_WORKER_URL, enrichedQueue, {
|
|
572
|
+
workerData: {},
|
|
573
|
+
});
|
|
574
|
+
return results.map((r, i) => {
|
|
575
|
+
const item = queue[i];
|
|
576
|
+
if (!r || r.__cpuPoolError) {
|
|
577
|
+
Logger.warn(
|
|
578
|
+
`[crap-utils] combined worker pool error for ${item.relPath}: ${r?.message ?? 'unknown'}`,
|
|
579
|
+
);
|
|
580
|
+
return { item, result: null };
|
|
581
|
+
}
|
|
582
|
+
return { item, result: r };
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Combined MI + CRAP single-pass scan. Walks the shared `targetDirs` once (or
|
|
588
|
+
* reuses `preScannedFiles`), dispatches every file through ONE worker that
|
|
589
|
+
* calls `analyzeOnce` a single time, and returns BOTH the maintainability
|
|
590
|
+
* score map and the CRAP scan result.
|
|
591
|
+
*
|
|
592
|
+
* This collapses the two independent escomplex passes the full-tree baseline
|
|
593
|
+
* regenerator used to run (`calculateAll` → maintainability worker, then
|
|
594
|
+
* `scanAndScore` → CRAP worker) into one parse per file. The outputs are
|
|
595
|
+
* shaped to be drop-in equivalents of the two passes they replace, so the
|
|
596
|
+
* downstream envelope projection + writer logic stays byte-identical:
|
|
597
|
+
*
|
|
598
|
+
* - `miScores` — `Record<relPath, number>` keyed exactly as `calculateAll`
|
|
599
|
+
* keys its result (`path.relative(cwd, abs)`, POSIX-normalised), with
|
|
600
|
+
* read-failure files (`miScore === null`) dropped. Parity target:
|
|
601
|
+
* `calculateAll(files)`.
|
|
602
|
+
* - `crap` — `{ rows, scannedFiles, skippedFilesNoCoverage,
|
|
603
|
+
* skippedMethodsNoCoverage }`, identical in shape and content to
|
|
604
|
+
* `scanAndScore({ targetDirs, coverage, ... })`. The rows are
|
|
605
|
+
* CRAP-sorted (file → startLine → method) so the result matches
|
|
606
|
+
* `scanAndScore` even before the writer re-sorts.
|
|
607
|
+
*
|
|
608
|
+
* The `requireCoverage`, `scopeFiles`, `ignoreGlobs`, and `preScannedFiles`
|
|
609
|
+
* semantics match `scanAndScore` exactly — coverage gating, scope filtering,
|
|
610
|
+
* and the single-walk reuse path behave the same. Files dropped from CRAP by
|
|
611
|
+
* the coverage gate STILL contribute their MI score (the MI pass never
|
|
612
|
+
* required coverage), preserving the two-pass behaviour where MI scores every
|
|
613
|
+
* file in the target dirs.
|
|
614
|
+
*
|
|
615
|
+
* @param {{
|
|
616
|
+
* targetDirs: string[],
|
|
617
|
+
* coverage: object|null,
|
|
618
|
+
* requireCoverage?: boolean,
|
|
619
|
+
* cwd?: string,
|
|
620
|
+
* scopeFiles?: Set<string>|string[]|null,
|
|
621
|
+
* ignoreGlobs?: string[],
|
|
622
|
+
* preScannedFiles?: string[]|null,
|
|
623
|
+
* }} params
|
|
624
|
+
* @returns {Promise<{
|
|
625
|
+
* miScores: Record<string, number>,
|
|
626
|
+
* crap: {
|
|
627
|
+
* rows: Array<{
|
|
628
|
+
* file: string, method: string, startLine: number,
|
|
629
|
+
* cyclomatic: number, coverage: number, crap: number,
|
|
630
|
+
* }>,
|
|
631
|
+
* scannedFiles: number,
|
|
632
|
+
* skippedFilesNoCoverage: number,
|
|
633
|
+
* skippedMethodsNoCoverage: number,
|
|
634
|
+
* },
|
|
635
|
+
* }>}
|
|
636
|
+
*/
|
|
637
|
+
export async function scanAndScoreCombined({
|
|
638
|
+
targetDirs,
|
|
639
|
+
coverage,
|
|
640
|
+
requireCoverage = true,
|
|
641
|
+
cwd = process.cwd(),
|
|
642
|
+
scopeFiles = null,
|
|
643
|
+
ignoreGlobs = [],
|
|
644
|
+
preScannedFiles = null,
|
|
645
|
+
}) {
|
|
646
|
+
if (!Array.isArray(targetDirs)) {
|
|
647
|
+
throw new TypeError('scanAndScoreCombined: targetDirs must be an array');
|
|
648
|
+
}
|
|
649
|
+
const scopeSet =
|
|
650
|
+
scopeFiles == null
|
|
651
|
+
? null
|
|
652
|
+
: scopeFiles instanceof Set
|
|
653
|
+
? scopeFiles
|
|
654
|
+
: new Set(scopeFiles);
|
|
655
|
+
|
|
656
|
+
// Single directory walk (or reuse the caller's pre-walked list), mirroring
|
|
657
|
+
// scanAndScore so the file discovery is byte-identical between paths.
|
|
658
|
+
const files = preScannedFiles != null ? [...preScannedFiles] : [];
|
|
659
|
+
if (preScannedFiles == null) {
|
|
660
|
+
for (const dir of targetDirs) {
|
|
661
|
+
const abs = path.isAbsolute(dir) ? dir : path.resolve(cwd, dir);
|
|
662
|
+
scanDirectory(abs, files, { cwd, ignoreGlobs });
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
files.sort();
|
|
666
|
+
|
|
667
|
+
// Build the work queue. Each item carries both the canonicalised relPath
|
|
668
|
+
// (CRAP's key + scope filter, matching scanAndScore) and the raw relPath
|
|
669
|
+
// (MI's key, matching calculateAll's `path.relative(cwd, p)` shape).
|
|
670
|
+
const queue = [];
|
|
671
|
+
for (const abs of files) {
|
|
672
|
+
const rawRel = path.relative(cwd, abs).replace(/\\/g, '/');
|
|
673
|
+
const relPath = canonicalisePath(rawRel);
|
|
674
|
+
if (scopeSet && !scopeSet.has(relPath)) continue;
|
|
675
|
+
queue.push({ abs, relPath, miRel: rawRel, requireCoverage });
|
|
676
|
+
}
|
|
677
|
+
const scannedFiles = queue.length;
|
|
678
|
+
|
|
679
|
+
const perFile =
|
|
680
|
+
queue.length < SERIAL_THRESHOLD
|
|
681
|
+
? queue.map((item) => ({
|
|
682
|
+
item,
|
|
683
|
+
result: scoreFileCombinedSerial(item, coverage),
|
|
684
|
+
}))
|
|
685
|
+
: await scoreFilesCombinedViaPool(queue, coverage);
|
|
686
|
+
|
|
687
|
+
// MI assembly — mirror calculateAll: drop read-failure files (miScore
|
|
688
|
+
// null), key by the raw relative path, then sort ascending so the returned
|
|
689
|
+
// object is insertion-order-stable.
|
|
690
|
+
const miEntries = [];
|
|
691
|
+
// CRAP assembly — mirror scanAndScore: file-level skip counter, drop
|
|
692
|
+
// read/transpile/parse failures, accumulate method rows.
|
|
693
|
+
const crapRows = [];
|
|
694
|
+
let skippedFilesNoCoverage = 0;
|
|
695
|
+
let skippedMethodsNoCoverage = 0;
|
|
696
|
+
|
|
697
|
+
for (const { item, result } of perFile) {
|
|
698
|
+
if (!result) continue; // unrecoverable per-file failure: drop silently
|
|
699
|
+
|
|
700
|
+
// MI side.
|
|
701
|
+
if (result.miScore !== null) {
|
|
702
|
+
miEntries.push({ relPath: item.miRel, score: result.miScore });
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// CRAP side.
|
|
706
|
+
if (result.skippedFileNoCoverage) {
|
|
707
|
+
skippedFilesNoCoverage += 1;
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if (result.crapRows === null) {
|
|
711
|
+
if (result.error) {
|
|
712
|
+
Logger.warn(
|
|
713
|
+
`[crap-utils] failed to score ${item.relPath}: ${result.error}`,
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
skippedMethodsNoCoverage += result.skippedMethodsNoCoverage ?? 0;
|
|
719
|
+
for (const mr of result.crapRows) {
|
|
720
|
+
crapRows.push({
|
|
721
|
+
file: item.relPath,
|
|
722
|
+
method: mr.method,
|
|
723
|
+
startLine: mr.startLine,
|
|
724
|
+
cyclomatic: mr.cyclomatic,
|
|
725
|
+
coverage: mr.coverage,
|
|
726
|
+
crap: mr.crap,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
miEntries.sort((a, b) =>
|
|
732
|
+
a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0,
|
|
733
|
+
);
|
|
734
|
+
const miScores = {};
|
|
735
|
+
for (const { relPath, score } of miEntries) {
|
|
736
|
+
miScores[relPath] = score;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
crapRows.sort((a, b) => {
|
|
740
|
+
if (a.file !== b.file) return a.file < b.file ? -1 : 1;
|
|
741
|
+
if (a.startLine !== b.startLine) return a.startLine - b.startLine;
|
|
742
|
+
if (a.method !== b.method) return a.method < b.method ? -1 : 1;
|
|
743
|
+
return 0;
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
return {
|
|
747
|
+
miScores,
|
|
748
|
+
crap: {
|
|
749
|
+
rows: crapRows,
|
|
750
|
+
scannedFiles,
|
|
751
|
+
skippedFilesNoCoverage,
|
|
752
|
+
skippedMethodsNoCoverage,
|
|
753
|
+
},
|
|
754
|
+
};
|
|
755
|
+
}
|
|
@@ -27,8 +27,6 @@ import { buildManifest } from './manifest-builder.js';
|
|
|
27
27
|
import { STATE_LABELS } from './ticketing.js';
|
|
28
28
|
|
|
29
29
|
export const AGENT_DONE_LABEL = STATE_LABELS.DONE;
|
|
30
|
-
export const AGENT_EXECUTING_LABEL = STATE_LABELS.EXECUTING;
|
|
31
|
-
export const AGENT_READY_LABEL = STATE_LABELS.READY;
|
|
32
30
|
|
|
33
31
|
/* node:coverage ignore next */
|
|
34
32
|
export function ensureBranch(branchName, baseBranch) {
|
|
@@ -37,20 +37,6 @@ export function escapePipes(s) {
|
|
|
37
37
|
return String(s).replace(/\|/g, '\\|');
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/**
|
|
41
|
-
* Render a millisecond duration as a compact human-readable string. Used
|
|
42
|
-
* in the wave-elapsed header to keep the snapshot tight.
|
|
43
|
-
*/
|
|
44
|
-
export function formatElapsed(ms) {
|
|
45
|
-
const s = Math.max(0, Math.floor(ms / 1000));
|
|
46
|
-
const h = Math.floor(s / 3600);
|
|
47
|
-
const m = Math.floor((s % 3600) / 60);
|
|
48
|
-
const sec = s % 60;
|
|
49
|
-
if (h) return `${h}h ${m}m`;
|
|
50
|
-
if (m) return `${m}m ${sec}s`;
|
|
51
|
-
return `${sec}s`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
40
|
/**
|
|
55
41
|
* Derive the high-level state classification for a single ticket. Reads
|
|
56
42
|
* the canonical `agent::*` label set first, then falls back to the GitHub
|
|
@@ -69,35 +55,6 @@ export function deriveState(ticket, AGENT_LABELS) {
|
|
|
69
55
|
return 'unknown';
|
|
70
56
|
}
|
|
71
57
|
|
|
72
|
-
/**
|
|
73
|
-
* Build the markdown table for the per-poll progress snapshot. Switches
|
|
74
|
-
* between a 3-column (ID / State / Title) and a 4-column (Wave / ID /
|
|
75
|
-
* State / Title) form depending on whether any row carries a wave index —
|
|
76
|
-
* the wider form fires when the reporter has a plan set and is rendering
|
|
77
|
-
* every wave instead of just the current one.
|
|
78
|
-
*/
|
|
79
|
-
export function renderProgressTable(rows) {
|
|
80
|
-
const includeWaveCol = rows.some((r) => Number.isInteger(r.wave));
|
|
81
|
-
if (includeWaveCol) {
|
|
82
|
-
return [
|
|
83
|
-
'| Wave | ID | State | Title |',
|
|
84
|
-
'|---|---|---|---|',
|
|
85
|
-
...rows.map(
|
|
86
|
-
(r) =>
|
|
87
|
-
`| ${r.wave + 1} | #${r.id} | ${STATE_EMOJI[r.state] ?? ''} ${r.state} | ${escapePipes(r.title)} |`,
|
|
88
|
-
),
|
|
89
|
-
].join('\n');
|
|
90
|
-
}
|
|
91
|
-
return [
|
|
92
|
-
'| ID | State | Title |',
|
|
93
|
-
'|---|---|---|',
|
|
94
|
-
...rows.map(
|
|
95
|
-
(r) =>
|
|
96
|
-
`| #${r.id} | ${STATE_EMOJI[r.state] ?? ''} ${r.state} | ${escapePipes(r.title)} |`,
|
|
97
|
-
),
|
|
98
|
-
].join('\n');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
58
|
/**
|
|
102
59
|
* Declarative descriptor table that drives the Notable bullet block. Each
|
|
103
60
|
* descriptor names a row state, the emoji prefix to render, and the
|
|
@@ -196,47 +153,6 @@ export async function renderNotable({ rows, detectors = [], wave, logger }) {
|
|
|
196
153
|
return items.join('\n');
|
|
197
154
|
}
|
|
198
155
|
|
|
199
|
-
/**
|
|
200
|
-
* Compose the full per-poll snapshot body. Pure with respect to the
|
|
201
|
-
* supplied state — the caller passes the resolved `rows`, the plan/wave
|
|
202
|
-
* context, the optional aggregated phase-timings block, and the now()
|
|
203
|
-
* clock; the renderer assembles header + table + Notable + phase-timings.
|
|
204
|
-
*/
|
|
205
|
-
export async function renderProgressBody({
|
|
206
|
-
rows,
|
|
207
|
-
plan,
|
|
208
|
-
currentWave,
|
|
209
|
-
epicStartedAt,
|
|
210
|
-
now,
|
|
211
|
-
detectors,
|
|
212
|
-
phaseSummariesBlock,
|
|
213
|
-
logger,
|
|
214
|
-
}) {
|
|
215
|
-
const done = rows.filter((r) => r.state === 'done').length;
|
|
216
|
-
const total = rows.length;
|
|
217
|
-
const totalWaves = plan?.length ?? currentWave?.totalWaves ?? '?';
|
|
218
|
-
const currentWaveNum = currentWave
|
|
219
|
-
? currentWave.index + 1
|
|
220
|
-
: (plan?.length ?? '?');
|
|
221
|
-
const waveLabel = `Wave ${currentWaveNum}/${totalWaves}`;
|
|
222
|
-
const elapsedSrc = epicStartedAt ?? currentWave?.startedAt ?? null;
|
|
223
|
-
const elapsed = elapsedSrc
|
|
224
|
-
? ` · ${formatElapsed(now() - new Date(elapsedSrc))} elapsed`
|
|
225
|
-
: '';
|
|
226
|
-
|
|
227
|
-
const header = `### 📊 Progress — ${waveLabel} · ${done}/${total} closed${elapsed}`;
|
|
228
|
-
const table = renderProgressTable(rows);
|
|
229
|
-
const notable = await renderNotable({
|
|
230
|
-
rows,
|
|
231
|
-
detectors,
|
|
232
|
-
wave: currentWave,
|
|
233
|
-
logger,
|
|
234
|
-
});
|
|
235
|
-
const parts = [header, '', table, '', '**Notable**', notable];
|
|
236
|
-
if (phaseSummariesBlock) parts.push('', phaseSummariesBlock);
|
|
237
|
-
return parts.join('\n');
|
|
238
|
-
}
|
|
239
|
-
|
|
240
156
|
/**
|
|
241
157
|
* Render and upsert the rolled-up `epic-run-progress` comment on the Epic.
|
|
242
158
|
*
|
|
@@ -67,10 +67,9 @@ export const PHASE_ORDER = [
|
|
|
67
67
|
];
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
* Emoji prefix per high-level state.
|
|
71
|
-
* (`composition.
|
|
72
|
-
*
|
|
73
|
-
* in both surfaces.
|
|
70
|
+
* Emoji prefix per high-level state. Consumed by the rolled-up Epic table
|
|
71
|
+
* (`composition.upsertEpicRunProgress`) so operators see a consistent icon
|
|
72
|
+
* per state across the progress surface.
|
|
74
73
|
*/
|
|
75
74
|
export const STATE_EMOJI = {
|
|
76
75
|
done: '✅',
|