mandrel 2.24.0 → 2.26.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/agents/acceptance-critic.md +19 -21
- package/.agents/agents/auditor.md +16 -19
- package/.agents/agents/story-worker.md +41 -54
- package/.agents/audit-checklists/architecture.md +5 -1
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/audit-checklists/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +2 -1
- package/.agents/docs/configuration.md +2 -1
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +4 -2
- package/.agents/instructions.md +1 -1
- package/.agents/rules/ci-remediation.md +68 -3
- package/.agents/schemas/agentrc.schema.json +6 -1
- package/.agents/schemas/audit-rules.json +15 -0
- package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +8 -0
- package/.agents/schemas/baselines/crap.schema.json +13 -1
- package/.agents/schemas/crap-report.schema.json +37 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +124 -15
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/audit-labels-bootstrap.js +16 -95
- package/.agents/scripts/audit-to-stories.js +33 -3
- package/.agents/scripts/check-arch-cycles.js +12 -93
- package/.agents/scripts/check-baseline-drift.js +16 -3
- package/.agents/scripts/check-baselines.js +19 -3
- package/.agents/scripts/check-context-budget.js +95 -14
- package/.agents/scripts/check-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +121 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -0
- package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
- package/.agents/scripts/deliver-light.js +3 -4
- package/.agents/scripts/deliver-recover.js +13 -0
- package/.agents/scripts/diagnose-friction.js +85 -19
- package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
- package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
- package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
- package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
- package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
- package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
- package/.agents/scripts/lib/audit-baselines/read.js +87 -0
- package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
- package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
- package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
- package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
- package/.agents/scripts/lib/audit-suite/index.js +0 -5
- package/.agents/scripts/lib/audit-suite/selector.js +9 -62
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +177 -0
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
- package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
- package/.agents/scripts/lib/baseline-loader.js +0 -0
- package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
- package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
- package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
- package/.agents/scripts/lib/baselines/git-base.js +0 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +499 -57
- package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
- package/.agents/scripts/lib/baselines/reader.js +10 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
- package/.agents/scripts/lib/baselines/scope.js +39 -90
- package/.agents/scripts/lib/baselines/writer.js +16 -7
- package/.agents/scripts/lib/changed-files.js +8 -1
- package/.agents/scripts/lib/cli-args.js +115 -1
- package/.agents/scripts/lib/close-validation/runner.js +70 -25
- package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
- package/.agents/scripts/lib/crap-engine.js +124 -27
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +86 -13
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/env-loader.js +46 -16
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
- package/.agents/scripts/lib/findings/route-finding.js +123 -17
- package/.agents/scripts/lib/findings/severity.js +80 -2
- package/.agents/scripts/lib/git/cached-fetch.js +0 -0
- package/.agents/scripts/lib/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/label-constants.js +17 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
- package/.agents/scripts/lib/observability/source-classifier.js +175 -2
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +548 -0
- package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +71 -20
- package/.agents/scripts/lib/orchestration/light-backstop.js +20 -3
- package/.agents/scripts/lib/orchestration/light-escalation.js +82 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +103 -5
- package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +216 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +61 -13
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
- package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
- package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +125 -37
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +267 -125
- package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
- package/.agents/scripts/lib/reserved-test-ids.js +77 -0
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/story-plan.js +137 -42
- package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/ticket-body-sections.js +1 -1
- package/.agents/scripts/lib/util/concurrent-map.js +17 -0
- package/.agents/scripts/lib/util/parse-id-list.js +103 -0
- package/.agents/scripts/lib/util/poll-loop.js +18 -2
- package/.agents/scripts/lib/wave-runner/live-probe.js +29 -13
- package/.agents/scripts/lib/wave-runner/ready-set.js +254 -43
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +10 -13
- package/.agents/scripts/lib/workers/crap-worker.js +8 -13
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
- package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
- package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
- package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
- package/.agents/scripts/lib/worktree-manager.js +14 -0
- package/.agents/scripts/plan-run-epilogue.js +17 -5
- package/.agents/scripts/pr-watch-with-update.js +568 -213
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +137 -28
- package/.agents/scripts/resolve-stories.js +4 -1
- package/.agents/scripts/run-coverage.js +86 -35
- package/.agents/scripts/run-lint.js +20 -0
- package/.agents/scripts/run-tests.js +36 -36
- package/.agents/scripts/single-story-close.js +34 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/single-story-init.js +7 -0
- package/.agents/scripts/stories-wave-tick.js +308 -47
- package/.agents/scripts/story-plan.js +65 -9
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-crap-baseline.js +42 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/update-maintainability-baseline.js +31 -4
- package/.agents/scripts/validate-docs-freshness.js +1 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
- package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/templates/single-story-body.md +16 -8
- package/.agents/workflows/audit-architecture.md +69 -13
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/audit-quality.md +26 -0
- package/.agents/workflows/audit-to-stories.md +30 -4
- package/.agents/workflows/deliver.md +92 -97
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/audit-lens-core.md +25 -4
- package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
- package/.agents/workflows/helpers/deliver-digest.md +41 -21
- package/.agents/workflows/helpers/deliver-reference.md +95 -4
- package/.agents/workflows/helpers/deliver-story-reference.md +174 -52
- package/.agents/workflows/helpers/deliver-story.md +104 -119
- package/.agents/workflows/helpers/plan-reference.md +49 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +62 -75
- package/README.md +13 -6
- package/docs/CHANGELOG.md +93 -0
- package/lib/cli/update.js +14 -11
- package/lib/cli/version-check.js +9 -1
- package/package.json +9 -4
- package/.agents/schemas/friction-event.schema.json +0 -56
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* lib/wave-runner/live-probe.js — the state-probing adapter that feeds the
|
|
3
3
|
* ready-set kernel from live GitHub state.
|
|
4
4
|
*
|
|
5
|
-
* `
|
|
5
|
+
* `planReadySet` (`./ready-set.js`) is deliberately a pure, side-effect-free
|
|
6
6
|
* kernel: callers hand it the live Story records, the done set, and the
|
|
7
7
|
* in-flight count, and it decides. Until now the only adapter was the
|
|
8
8
|
* flag-driven one (`stories-wave-tick.js --dag/--done/--in-flight`), which
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
* "waiting" (Story #4601).
|
|
42
42
|
*
|
|
43
43
|
* It is an **adapter, not a kernel change**: it gathers inputs and hands them
|
|
44
|
-
* to `
|
|
44
|
+
* to `planReadySet` unchanged. The kernel stays pure and flag-driven, and
|
|
45
45
|
* the legacy flag mode stays byte-compatible.
|
|
46
46
|
*
|
|
47
47
|
* The graph resolution is **not** reimplemented here — it reuses
|
|
@@ -217,7 +217,7 @@ export function createProbeContext({
|
|
|
217
217
|
|
|
218
218
|
/**
|
|
219
219
|
* Probe live state for a set of Story ids and return the exact inputs
|
|
220
|
-
* `
|
|
220
|
+
* `planReadySet` consumes.
|
|
221
221
|
*
|
|
222
222
|
* Mirrors `resolve-stories.js`'s two-pass envelope build: a provisional pass
|
|
223
223
|
* yields the DAG whose foreign dependency ids are then resolved against live
|
|
@@ -239,14 +239,15 @@ export function createProbeContext({
|
|
|
239
239
|
* skipped (the probe never fails closed).
|
|
240
240
|
* @param {(msg: string) => void} [args.warn]
|
|
241
241
|
* Each returned node carries its **live labels**. That is load-bearing, not
|
|
242
|
-
* decoration: `
|
|
242
|
+
* decoration: `planReadySet` classifies from labels, so a node stripped of
|
|
243
243
|
* them reads as `ready` and an `agent::executing` Story gets re-dispatched
|
|
244
244
|
* onto a second branch while its first run is still going. The resolver's DAG
|
|
245
245
|
* projection (`{id, dependsOn, files}`) drops labels because flag mode's
|
|
246
246
|
* caller tracked in-flight itself; probe mode must put them back.
|
|
247
247
|
*
|
|
248
248
|
* @returns {Promise<{
|
|
249
|
-
* nodes: Array<{id: number, dependsOn: number[], files: string[], labels: string[]}>,
|
|
249
|
+
* nodes: Array<{id: number, dependsOn: number[], files: string[], body: string, labels: string[]}>,
|
|
250
|
+
* inFlightRecords: Array<{id: number, dependsOn: number[], files: string[], body: string, labels: string[]}>,
|
|
250
251
|
* doneIds: Set<number>,
|
|
251
252
|
* inFlight: number,
|
|
252
253
|
* blockedIds: number[],
|
|
@@ -282,6 +283,11 @@ export async function probeLiveState({
|
|
|
282
283
|
});
|
|
283
284
|
|
|
284
285
|
const labelsById = new Map(stories.map((s) => [s.id, s.labels ?? []]));
|
|
286
|
+
// Bodies ride along with the node so the co-dispatch guard can widen a
|
|
287
|
+
// declared footprint from the paths a Story's own text names (Story #4875).
|
|
288
|
+
// They are consumed in-process by `planReadySet` and never serialized into
|
|
289
|
+
// the beat envelope, which stays a list of ids.
|
|
290
|
+
const bodyById = new Map(stories.map((s) => [s.id, s.body ?? '']));
|
|
285
291
|
const inFlightIds = deriveInFlightIds(stories, dispatched);
|
|
286
292
|
// A Story another operator's lease holds occupies a (global) dispatch slot
|
|
287
293
|
// just like an in-flight one: fold it into the in-flight set so it is both
|
|
@@ -289,14 +295,24 @@ export async function probeLiveState({
|
|
|
289
295
|
// never dispatched by this run.
|
|
290
296
|
const foreignHeld = deriveForeignHeld(stories, self, warn);
|
|
291
297
|
for (const id of foreignHeld.keys()) inFlightIds.add(id);
|
|
298
|
+
const nodes = envelope.dag.map((node) => ({
|
|
299
|
+
...node,
|
|
300
|
+
body: bodyById.get(node.id) ?? '',
|
|
301
|
+
labels: projectInFlightLabels(
|
|
302
|
+
labelsById.get(node.id) ?? [],
|
|
303
|
+
inFlightIds.has(node.id),
|
|
304
|
+
),
|
|
305
|
+
}));
|
|
292
306
|
return {
|
|
293
|
-
nodes
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
307
|
+
nodes,
|
|
308
|
+
// The same nodes, narrowed to the Stories occupying a slot. `inFlight` is
|
|
309
|
+
// only a count, so it can shrink capacity but can never stop a Story
|
|
310
|
+
// admitted now from sharing files with one dispatched on an earlier beat
|
|
311
|
+
// and still implementing. These records carry the `files[]` and `body` the
|
|
312
|
+
// co-dispatch guard needs, so the kernel can RESERVE those footprints
|
|
313
|
+
// rather than merely count them (Story #4950). No extra fetch: this is a
|
|
314
|
+
// projection of what the probe already read.
|
|
315
|
+
inFlightRecords: nodes.filter((node) => inFlightIds.has(node.id)),
|
|
300
316
|
doneIds: new Set(envelope.done),
|
|
301
317
|
inFlight: inFlightIds.size,
|
|
302
318
|
blockedIds: deriveBlockedIds(stories),
|
|
@@ -310,7 +326,7 @@ export async function probeLiveState({
|
|
|
310
326
|
*
|
|
311
327
|
* This is the load-bearing half of the dispatch-window fix, and it is why
|
|
312
328
|
* `inFlight` alone is not enough. The two inputs do **different** jobs inside
|
|
313
|
-
* `
|
|
329
|
+
* `planReadySet`:
|
|
314
330
|
*
|
|
315
331
|
* - `inFlight` is only a **count**. It reserves capacity (`slots = cap −
|
|
316
332
|
* inFlight`) and nothing more.
|
|
@@ -19,20 +19,36 @@
|
|
|
19
19
|
* `stories-wave-tick.js` adapter wires this core; this module does not
|
|
20
20
|
* modify that CLI surface.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
22
|
+
* The scheduling surface:
|
|
23
23
|
* - `classifyStory(story)` — live-label classifier mapping a Story
|
|
24
24
|
* record's labels + issue state to one of `done | blocked | executing |
|
|
25
25
|
* ready`. Mirrors the done-predicate this module uses
|
|
26
26
|
* (`agent::done` OR closed issue) so a Story closed manually through
|
|
27
27
|
* the GitHub UI is recognised as done.
|
|
28
|
-
* - `storiesOverlap(a, b)` — the file-overlap co-dispatch
|
|
29
|
-
* when two Stories'
|
|
30
|
-
* that would touch the same file MUST NOT be
|
|
31
|
-
* `story-<id>` branches
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
28
|
+
* - `storiesOverlap(a, b)` — the **beat-local** file-overlap co-dispatch
|
|
29
|
+
* guard: true when two Stories' file footprints intersect. Two Stories
|
|
30
|
+
* that would touch the same file MUST NOT be admitted onto parallel
|
|
31
|
+
* `story-<id>` branches on the same beat (they would race the same path
|
|
32
|
+
* and produce a merge conflict at close). The comparison runs over
|
|
33
|
+
* the **widened** footprint (`storyWidenedFootprint`) — a declared
|
|
34
|
+
* `changes[]` is treated as a lower bound and widened from the paths the
|
|
35
|
+
* Story's own text names, because a guard that trusts a prediction cannot
|
|
36
|
+
* prevent the collision nobody predicted (Story #4875).
|
|
37
|
+
* - `planReadySet({ stories, doneIds, inFlight, globalCap,
|
|
38
|
+
* inFlightRecords })` — the scheduler. Returns the deterministic,
|
|
39
|
+
* overlap-free dispatch set (capped at `globalCap − inFlight`) **and**
|
|
40
|
+
* the Stories it withheld because a **concrete** path in their footprint
|
|
41
|
+
* is reserved by a Story still in flight from an earlier beat.
|
|
42
|
+
*
|
|
43
|
+
* The two guards deliberately draw the glob/UNKNOWN class differently
|
|
44
|
+
* (Story #4960). Within a beat an unknown-width footprint overlaps
|
|
45
|
+
* everything, because admitting two Stories whose real widths are unknown is
|
|
46
|
+
* the collision the guard exists to prevent. Across beats it reserves
|
|
47
|
+
* nothing: an in-flight Story's window spans its whole implementation, so a
|
|
48
|
+
* glob that reserved cross-beat would withhold every other Story for
|
|
49
|
+
* minutes-to-hours — and `resolve-stories.js` substitutes the UNKNOWN
|
|
50
|
+
* sentinel for any body it cannot parse, so a single malformed Story body
|
|
51
|
+
* would collapse an N-Story run to fully serial.
|
|
36
52
|
*
|
|
37
53
|
* Adjacency is re-derived from the supplied records via the shared
|
|
38
54
|
* `buildStoryAdjacency` builder (`lib/story-adjacency.js`) — the same
|
|
@@ -169,37 +185,109 @@ function isGlobPath(path) {
|
|
|
169
185
|
}
|
|
170
186
|
|
|
171
187
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
188
|
+
* Repo-relative file paths as they appear in Story prose: at least one `/`
|
|
189
|
+
* separator and a short file extension. Deliberately narrow — a token has to
|
|
190
|
+
* look like a real path before it can widen a footprint and withhold a Story.
|
|
191
|
+
*/
|
|
192
|
+
const PROSE_PATH_RE = /(?:[\w.@~-]+\/)+[\w.@-]+\.[A-Za-z0-9]{1,6}/g;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Scrape file paths a Story's **text** mentions but its `changes[]` never
|
|
196
|
+
* declared (Story #4875).
|
|
197
|
+
*
|
|
198
|
+
* The declared footprint is a planner's *prediction*, and it is systematically
|
|
199
|
+
* a lower bound: a Story's `## Spec` names the module it must also touch, its
|
|
200
|
+
* acceptance criteria name the caller that must be updated, and none of that
|
|
201
|
+
* reaches `changes[]`. The overlap guard exists to stop two Stories racing the
|
|
202
|
+
* same file, so trusting the declaration outright means the guard is blind to
|
|
203
|
+
* precisely the collisions nobody predicted.
|
|
204
|
+
*
|
|
205
|
+
* Evidence is only ever **added** — nothing here can shrink a declared
|
|
206
|
+
* footprint, so widening can withhold a Story for a beat but can never
|
|
207
|
+
* co-dispatch one the declared comparison would have caught.
|
|
208
|
+
*
|
|
209
|
+
* @param {StoryRecord} story
|
|
210
|
+
* @returns {Set<string>}
|
|
211
|
+
*/
|
|
212
|
+
function storyEvidencePaths(story) {
|
|
213
|
+
const out = new Set();
|
|
214
|
+
for (const field of [story?.title, story?.body, story?.spec]) {
|
|
215
|
+
if (typeof field !== 'string' || field === '') continue;
|
|
216
|
+
for (const match of field.matchAll(PROSE_PATH_RE)) {
|
|
217
|
+
const trimmed = match[0].trim();
|
|
218
|
+
if (trimmed) out.add(trimmed);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* A Story's footprint **widened from observable evidence** — the set the
|
|
226
|
+
* co-dispatch guard actually compares (Story #4875).
|
|
227
|
+
*
|
|
228
|
+
* `declared ∪ scraped-from-prose`. See {@link storyEvidencePaths} for why the
|
|
229
|
+
* declaration is treated as a lower bound rather than the answer.
|
|
230
|
+
*
|
|
231
|
+
* @param {StoryRecord} story
|
|
232
|
+
* @returns {Set<string>}
|
|
233
|
+
*/
|
|
234
|
+
function storyWidenedFootprint(story) {
|
|
235
|
+
const out = storyFootprint(story);
|
|
236
|
+
for (const path of storyEvidencePaths(story)) out.add(path);
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Both Stories' widened footprints, or `null` when either is empty.
|
|
242
|
+
*
|
|
243
|
+
* **An empty footprint means "no known overlap"**, so both guards below
|
|
244
|
+
* short-circuit to `false` on one. This is permissive by necessity: a Story
|
|
245
|
+
* with no declared footprint and no path evidence in its text carries no
|
|
246
|
+
* information, and withholding on absence would serialize every run.
|
|
247
|
+
*
|
|
248
|
+
* @param {StoryRecord} a
|
|
249
|
+
* @param {StoryRecord} b
|
|
250
|
+
* @returns {[Set<string>, Set<string>]|null}
|
|
251
|
+
*/
|
|
252
|
+
function widenedFootprintPair(a, b) {
|
|
253
|
+
const fa = storyWidenedFootprint(a);
|
|
254
|
+
if (fa.size === 0) return null;
|
|
255
|
+
const fb = storyWidenedFootprint(b);
|
|
256
|
+
if (fb.size === 0) return null;
|
|
257
|
+
return [fa, fb];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* **Beat-local** file-overlap co-dispatch guard. Returns `true` when two
|
|
262
|
+
* Stories' file footprints intersect — meaning they would race the same file
|
|
263
|
+
* if both were admitted onto parallel `story-<id>` branches on this beat.
|
|
264
|
+
*
|
|
265
|
+
* Comparison runs over the **widened** footprint
|
|
266
|
+
* ({@link storyWidenedFootprint}), not the declaration: a declared `changes[]`
|
|
267
|
+
* is a lower bound on what a Story will touch, and two Stories whose real edits
|
|
268
|
+
* collide were being co-dispatched whenever the collision was not predicted
|
|
269
|
+
* (Story #4875).
|
|
270
|
+
*
|
|
271
|
+
* **A glob footprint overlaps EVERYTHING** → `true` (Story #4539/#4540).
|
|
272
|
+
* Comparison is exact-string, so a Story declaring `.agents/scripts/lib/**`
|
|
273
|
+
* would not match another declaring `.agents/scripts/lib/story-adjacency.js` —
|
|
274
|
+
* the guard would silently pass two Stories that genuinely race. Unknown width
|
|
275
|
+
* is not the same as no width: fail safe by serializing the rest of the beat.
|
|
276
|
+
*
|
|
277
|
+
* That fail-safe is **beat-local and stays that way**. The cross-beat
|
|
278
|
+
* reservation uses {@link reservesConcretePath} instead, because a beat is a
|
|
279
|
+
* moment and an in-flight window is an implementation (Story #4960).
|
|
190
280
|
*
|
|
191
281
|
* @param {StoryRecord} a
|
|
192
282
|
* @param {StoryRecord} b
|
|
193
283
|
* @returns {boolean}
|
|
194
284
|
*/
|
|
195
285
|
export function storiesOverlap(a, b) {
|
|
196
|
-
const
|
|
197
|
-
if (
|
|
198
|
-
const fb =
|
|
199
|
-
if (fb.size === 0) return false;
|
|
286
|
+
const pair = widenedFootprintPair(a, b);
|
|
287
|
+
if (pair === null) return false;
|
|
288
|
+
const [fa, fb] = pair;
|
|
200
289
|
for (const path of fa) {
|
|
201
|
-
if (isGlobPath(path)) return true;
|
|
202
|
-
if (fb.has(path)) return true;
|
|
290
|
+
if (isGlobPath(path) || fb.has(path)) return true;
|
|
203
291
|
}
|
|
204
292
|
for (const path of fb) {
|
|
205
293
|
if (isGlobPath(path)) return true;
|
|
@@ -207,6 +295,42 @@ export function storiesOverlap(a, b) {
|
|
|
207
295
|
return false;
|
|
208
296
|
}
|
|
209
297
|
|
|
298
|
+
/**
|
|
299
|
+
* **Cross-beat** reservation guard: `true` only when the two widened
|
|
300
|
+
* footprints share a **concrete** (non-glob) path.
|
|
301
|
+
*
|
|
302
|
+
* A Story dispatched on an earlier beat holds its footprint for its entire
|
|
303
|
+
* implementation window, not for a moment, so the two guards cannot share a
|
|
304
|
+
* predicate (Story #4960):
|
|
305
|
+
*
|
|
306
|
+
* - A **concrete** shared path is a real, named collision — two branches
|
|
307
|
+
* editing `lib/foo.js` conflict at close whether the peer was admitted
|
|
308
|
+
* alongside or hours ago. It reserves, exactly as Story #4950 shipped.
|
|
309
|
+
* - A **glob** — or the UNKNOWN sentinel `resolve-stories.js` substitutes
|
|
310
|
+
* for an unparseable body — names no file. Reserving on it withheld every
|
|
311
|
+
* eligible Story against a single unknown-width blocker for that blocker's
|
|
312
|
+
* whole window, which is strictly worse than the serial run the
|
|
313
|
+
* reservation was meant to speed up. Glob paths are therefore skipped on
|
|
314
|
+
* both sides here; the beat-local {@link storiesOverlap} fail-safe is
|
|
315
|
+
* unchanged and still serializes them within a beat.
|
|
316
|
+
*
|
|
317
|
+
* Set intersection is symmetric, so scanning the reserving side alone finds
|
|
318
|
+
* every shared concrete path.
|
|
319
|
+
*
|
|
320
|
+
* @param {StoryRecord} held The in-flight Story holding the reservation.
|
|
321
|
+
* @param {StoryRecord} candidate The Story being considered for admission.
|
|
322
|
+
* @returns {boolean}
|
|
323
|
+
*/
|
|
324
|
+
function reservesConcretePath(held, candidate) {
|
|
325
|
+
const pair = widenedFootprintPair(held, candidate);
|
|
326
|
+
if (pair === null) return false;
|
|
327
|
+
const [fa, fb] = pair;
|
|
328
|
+
for (const path of fa) {
|
|
329
|
+
if (!isGlobPath(path) && fb.has(path)) return true;
|
|
330
|
+
}
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
210
334
|
/**
|
|
211
335
|
* Select the set of Stories safe to dispatch on this beat.
|
|
212
336
|
*
|
|
@@ -240,10 +364,12 @@ export function storiesOverlap(a, b) {
|
|
|
240
364
|
* already occupying a slot (executing / closing / dispatched-not-yet-
|
|
241
365
|
* labelled). When `slots <= 0`, the result is empty.
|
|
242
366
|
* 5. **Overlap guard.** Greedily admit eligible Stories in ascending-id
|
|
243
|
-
* order, skipping any whose file footprint overlaps
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
367
|
+
* order, skipping any whose file footprint overlaps a Story **already
|
|
368
|
+
* admitted this beat** ({@link storiesOverlap}) or shares a **concrete**
|
|
369
|
+
* path with a Story **still in flight from an earlier beat**
|
|
370
|
+
* (`inFlightRecords`, {@link reservesConcretePath}). A withheld Story
|
|
371
|
+
* stays eligible and is naturally re-considered on the next beat once
|
|
372
|
+
* the Story reserving its files has cleared.
|
|
247
373
|
*
|
|
248
374
|
* The result is deterministic: eligible Stories are considered in
|
|
249
375
|
* ascending-id order, so the same inputs always yield the same set.
|
|
@@ -261,22 +387,37 @@ export function storiesOverlap(a, b) {
|
|
|
261
387
|
* step 1 above). `false` keeps a foreign dependency as a gate
|
|
262
388
|
* (standalone / operator-DAG semantics); `true` prunes foreign edges so
|
|
263
389
|
* the DAG stays closed over the scheduled set (Epic semantics).
|
|
264
|
-
* @
|
|
265
|
-
*
|
|
390
|
+
* @param {StoryRecord[]} [args.inFlightRecords=[]] Records for the Stories
|
|
391
|
+
* already in flight, whose **concrete** footprint paths this beat must
|
|
392
|
+
* **reserve** rather than merely count. `inFlight` is a number and can only
|
|
393
|
+
* shrink capacity; without the records a Story admitted now can share files
|
|
394
|
+
* with one dispatched on an earlier beat and still implementing — a
|
|
395
|
+
* guaranteed merge conflict at close (Story #4950). A glob / UNKNOWN
|
|
396
|
+
* footprint reserves nothing (Story #4960). Callers that hold only ids (the
|
|
397
|
+
* `--dag`/`--in-flight` flag mode) pass nothing and get the pre-#4950
|
|
398
|
+
* same-beat-only behaviour.
|
|
399
|
+
* @returns {{ selected: StoryRecord[], withheldByInFlight: Array<{id: number, blockedBy: number}> }}
|
|
400
|
+
* `selected` is the dispatch set: a subset of `stories`, ascending by id,
|
|
401
|
+
* overlap-free, length ≤ `globalCap − inFlight`. `withheldByInFlight`
|
|
402
|
+
* names each eligible Story a reservation held back and the in-flight
|
|
403
|
+
* Story that holds it.
|
|
266
404
|
*/
|
|
267
|
-
export function
|
|
405
|
+
export function planReadySet({
|
|
268
406
|
stories,
|
|
269
407
|
doneIds = [],
|
|
270
408
|
inFlight = 0,
|
|
271
409
|
globalCap,
|
|
272
410
|
dropForeign = false,
|
|
411
|
+
inFlightRecords = [],
|
|
273
412
|
} = {}) {
|
|
274
413
|
const records = Array.isArray(stories) ? stories : [];
|
|
275
414
|
const cap = Number.isInteger(globalCap) ? globalCap : 0;
|
|
276
415
|
const inFlightCount =
|
|
277
416
|
Number.isInteger(inFlight) && inFlight > 0 ? inFlight : 0;
|
|
278
417
|
const slots = Math.max(0, cap - inFlightCount);
|
|
279
|
-
if (slots <= 0 || records.length === 0)
|
|
418
|
+
if (slots <= 0 || records.length === 0) {
|
|
419
|
+
return { selected: [], withheldByInFlight: [] };
|
|
420
|
+
}
|
|
280
421
|
|
|
281
422
|
// Step 1 — adjacency keyed by id. The `dropForeign` policy decides whether
|
|
282
423
|
// a dependency on an id outside the supplied set gates the dependent
|
|
@@ -309,13 +450,83 @@ export function selectReadySet({
|
|
|
309
450
|
}
|
|
310
451
|
|
|
311
452
|
// Steps 4 + 5 — greedily admit up to `slots`, skipping file-overlap
|
|
312
|
-
// collisions against the already-admitted set
|
|
453
|
+
// collisions against the already-admitted set AND against the footprints
|
|
454
|
+
// reserved by Stories still in flight from an earlier beat.
|
|
455
|
+
return admitStories({
|
|
456
|
+
eligibleIds,
|
|
457
|
+
byId,
|
|
458
|
+
slots,
|
|
459
|
+
reserved: Array.isArray(inFlightRecords) ? inFlightRecords : [],
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Greedily admit eligible Stories in ascending-id order under two distinct
|
|
465
|
+
* withholding rules, and report which ones a reservation held back.
|
|
466
|
+
*
|
|
467
|
+
* The two rules are distinct predicates, not one applied twice: the
|
|
468
|
+
* cross-beat reservation matches only a shared **concrete** path
|
|
469
|
+
* ({@link reservesConcretePath}), while the same-beat guard also serializes
|
|
470
|
+
* unknown-width footprints ({@link storiesOverlap}). See both for why.
|
|
471
|
+
*
|
|
472
|
+
* The reservation check runs **first**, so a Story racing both an in-flight
|
|
473
|
+
* Story and a same-beat peer is reported against the in-flight one: that is
|
|
474
|
+
* the longer-lived and more informative blocker (a Story that has been
|
|
475
|
+
* implementing for beats, not one merely admitted a moment ago), and checking
|
|
476
|
+
* it first is what makes the report complete — every withheld-by-reservation
|
|
477
|
+
* Story appears in it. Ordering cannot change `selected`: a candidate either
|
|
478
|
+
* rule rejects is skipped whichever runs first; only which list it is
|
|
479
|
+
* reported in depends on the order.
|
|
480
|
+
*
|
|
481
|
+
* @param {object} args
|
|
482
|
+
* @param {number[]} args.eligibleIds Ascending eligible Story ids.
|
|
483
|
+
* @param {Map<number, StoryRecord>} args.byId
|
|
484
|
+
* @param {number} args.slots Remaining dispatch capacity.
|
|
485
|
+
* @param {StoryRecord[]} args.reserved In-flight Story records.
|
|
486
|
+
* @returns {{ selected: StoryRecord[], withheldByInFlight: Array<{id: number, blockedBy: number}> }}
|
|
487
|
+
*/
|
|
488
|
+
function admitStories({ eligibleIds, byId, slots, reserved }) {
|
|
313
489
|
const selected = [];
|
|
490
|
+
const withheldByInFlight = [];
|
|
314
491
|
for (const id of eligibleIds) {
|
|
315
492
|
if (selected.length >= slots) break;
|
|
316
493
|
const rec = byId.get(id);
|
|
494
|
+
const blockedBy = findInFlightBlocker(rec, id, reserved);
|
|
495
|
+
if (blockedBy !== null) {
|
|
496
|
+
withheldByInFlight.push({ id, blockedBy });
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
317
499
|
if (selected.some((picked) => storiesOverlap(picked, rec))) continue;
|
|
318
500
|
selected.push(rec);
|
|
319
501
|
}
|
|
320
|
-
return selected;
|
|
502
|
+
return { selected, withheldByInFlight };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The id of the in-flight Story whose widened footprint reserves a concrete
|
|
507
|
+
* path the candidate would race, or `null` when none does.
|
|
508
|
+
*
|
|
509
|
+
* Two records are skipped rather than treated as blockers:
|
|
510
|
+
*
|
|
511
|
+
* - **The candidate itself.** Probe mode hands the whole record set to both
|
|
512
|
+
* arguments, and an in-flight Story classifies `executing` rather than
|
|
513
|
+
* `ready`, so a candidate can never legitimately appear here — but a
|
|
514
|
+
* caller that double-lists one Story must not have it withhold itself.
|
|
515
|
+
* - **An unidentifiable record.** A withholding this function cannot name
|
|
516
|
+
* is one the envelope cannot explain, and an unexplained unfilled slot is
|
|
517
|
+
* the exact operator-facing failure this reservation exists to remove.
|
|
518
|
+
* Probe-mode records always carry an integer id, so this is defensive.
|
|
519
|
+
*
|
|
520
|
+
* @param {StoryRecord} candidate
|
|
521
|
+
* @param {number} candidateId
|
|
522
|
+
* @param {StoryRecord[]} reserved
|
|
523
|
+
* @returns {number|null}
|
|
524
|
+
*/
|
|
525
|
+
function findInFlightBlocker(candidate, candidateId, reserved) {
|
|
526
|
+
for (const held of reserved) {
|
|
527
|
+
const heldId = storyIdOf(held);
|
|
528
|
+
if (heldId === null || heldId === candidateId) continue;
|
|
529
|
+
if (reservesConcretePath(held, candidate)) return heldId;
|
|
530
|
+
}
|
|
531
|
+
return null;
|
|
321
532
|
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*
|
|
25
25
|
* Message contract — see lib/cpu-pool.js:
|
|
26
26
|
* IN : { item: { abs: string, relPath: string, requireCoverage: boolean,
|
|
27
|
-
* coverageEntry: object | null } }
|
|
27
|
+
* coverageAvailable?: boolean, coverageEntry: object | null } }
|
|
28
28
|
* { exit: true }
|
|
29
29
|
* OUT : { ok: true, result: {
|
|
30
30
|
* relPath,
|
|
@@ -47,6 +47,7 @@ import { parentPort } from 'node:worker_threads';
|
|
|
47
47
|
import { finalizeMethodRows } from '../crap-engine.js';
|
|
48
48
|
import { analyzeOnce } from '../crap-utils.js';
|
|
49
49
|
import { prepareSourceForScoring } from '../transpile.js';
|
|
50
|
+
import { serveWorkerMessages } from './serve-worker-messages.js';
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
53
|
* Pure handler for a single inbound worker message. Exported so unit tests
|
|
@@ -88,7 +89,7 @@ export function handleCombinedMiCrapWorkerMessage(msg, deps = {}) {
|
|
|
88
89
|
},
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
|
-
const { abs, relPath, requireCoverage } = item;
|
|
92
|
+
const { abs, relPath, requireCoverage, coverageAvailable = true } = item;
|
|
92
93
|
const prepare = deps.prepare ?? prepareSourceForScoring;
|
|
93
94
|
const analyze = deps.analyze ?? analyzeOnce;
|
|
94
95
|
|
|
@@ -150,7 +151,10 @@ export function handleCombinedMiCrapWorkerMessage(msg, deps = {}) {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
const { rows, skippedMethodsNoCoverage, resolvedMethods, totalMethods } =
|
|
153
|
-
finalizeMethodRows(rawCrapRows, {
|
|
154
|
+
finalizeMethodRows(rawCrapRows, {
|
|
155
|
+
requireCoverage,
|
|
156
|
+
coverageAvailable,
|
|
157
|
+
});
|
|
154
158
|
return reply({
|
|
155
159
|
miScore,
|
|
156
160
|
crapRows: rows,
|
|
@@ -160,13 +164,6 @@ export function handleCombinedMiCrapWorkerMessage(msg, deps = {}) {
|
|
|
160
164
|
});
|
|
161
165
|
}
|
|
162
166
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (out.kind === 'exit') {
|
|
167
|
-
parentPort.close();
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
parentPort.postMessage(out.message);
|
|
171
|
-
});
|
|
172
|
-
}
|
|
167
|
+
serveWorkerMessages(parentPort, (msg) =>
|
|
168
|
+
handleCombinedMiCrapWorkerMessage(msg),
|
|
169
|
+
);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Message contract — see lib/cpu-pool.js:
|
|
13
13
|
* IN : { item: { abs: string, relPath: string, requireCoverage: boolean,
|
|
14
|
-
* coverageEntry: object | null } }
|
|
14
|
+
* coverageAvailable?: boolean, coverageEntry: object | null } }
|
|
15
15
|
* { exit: true }
|
|
16
16
|
* OUT : { ok: true, result: {
|
|
17
17
|
* relPath,
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
import { parentPort } from 'node:worker_threads';
|
|
33
33
|
import { calculateCrapForSource, finalizeMethodRows } from '../crap-engine.js';
|
|
34
34
|
import { prepareSourceForScoring } from '../transpile.js';
|
|
35
|
+
import { serveWorkerMessages } from './serve-worker-messages.js';
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* Pure handler for a single inbound worker message. Exported so unit
|
|
@@ -80,7 +81,7 @@ export function handleCrapWorkerMessage(msg, _coverage, deps = {}) {
|
|
|
80
81
|
},
|
|
81
82
|
};
|
|
82
83
|
}
|
|
83
|
-
const { abs, relPath, requireCoverage } = item;
|
|
84
|
+
const { abs, relPath, requireCoverage, coverageAvailable = true } = item;
|
|
84
85
|
|
|
85
86
|
// Coverage entry is pre-resolved on the host and attached to the item.
|
|
86
87
|
// `item.coverageEntry` may be explicitly `null` when the file has no
|
|
@@ -143,7 +144,10 @@ export function handleCrapWorkerMessage(msg, _coverage, deps = {}) {
|
|
|
143
144
|
);
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
const finalized = finalizeMethodRows(methodRows, {
|
|
147
|
+
const finalized = finalizeMethodRows(methodRows, {
|
|
148
|
+
requireCoverage,
|
|
149
|
+
coverageAvailable,
|
|
150
|
+
});
|
|
147
151
|
return {
|
|
148
152
|
kind: 'reply',
|
|
149
153
|
message: {
|
|
@@ -158,13 +162,4 @@ export function handleCrapWorkerMessage(msg, _coverage, deps = {}) {
|
|
|
158
162
|
};
|
|
159
163
|
}
|
|
160
164
|
|
|
161
|
-
|
|
162
|
-
parentPort.on('message', (msg) => {
|
|
163
|
-
const out = handleCrapWorkerMessage(msg, null);
|
|
164
|
-
if (out.kind === 'exit') {
|
|
165
|
-
parentPort.close();
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
parentPort.postMessage(out.message);
|
|
169
|
-
});
|
|
170
|
-
}
|
|
165
|
+
serveWorkerMessages(parentPort, (msg) => handleCrapWorkerMessage(msg, null));
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
calculateReportForFile,
|
|
41
41
|
} from '../maintainability-engine.js';
|
|
42
42
|
import { transpileIfNeeded } from '../transpile.js';
|
|
43
|
+
import { serveWorkerMessages } from './serve-worker-messages.js';
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* Score a pre-sourced content string (Story #3696). Mirrors
|
|
@@ -125,13 +126,6 @@ export function handleMaintainabilityReportWorkerMessage(msg, deps = {}) {
|
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (out.kind === 'exit') {
|
|
132
|
-
parentPort.close();
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
parentPort.postMessage(out.message);
|
|
136
|
-
});
|
|
137
|
-
}
|
|
129
|
+
serveWorkerMessages(parentPort, (msg) =>
|
|
130
|
+
handleMaintainabilityReportWorkerMessage(msg),
|
|
131
|
+
);
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { parentPort } from 'node:worker_threads';
|
|
23
23
|
import { scoreFile } from '../maintainability-engine.js';
|
|
24
|
+
import { serveWorkerMessages } from './serve-worker-messages.js';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Pure handler for a single inbound worker message. Exported so unit
|
|
@@ -72,13 +73,6 @@ export function handleMaintainabilityWorkerMessage(msg, deps = {}) {
|
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (out.kind === 'exit') {
|
|
79
|
-
parentPort.close();
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
parentPort.postMessage(out.message);
|
|
83
|
-
});
|
|
84
|
-
}
|
|
76
|
+
serveWorkerMessages(parentPort, (msg) =>
|
|
77
|
+
handleMaintainabilityWorkerMessage(msg),
|
|
78
|
+
);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/workers/serve-worker-messages.js — the shared CPU-pool worker skeleton.
|
|
3
|
+
*
|
|
4
|
+
* Every worker under `lib/workers/` is a pure message handler plus the same
|
|
5
|
+
* eight-line `parentPort` wiring: subscribe, run the handler, close on an
|
|
6
|
+
* `exit` verdict, post the reply otherwise. That wiring was copied into all
|
|
7
|
+
* four workers; it lives here once instead (Story #4926).
|
|
8
|
+
*
|
|
9
|
+
* The handler contract is unchanged — see `lib/cpu-pool.js`:
|
|
10
|
+
* IN : `{ item: … }` — one unit of work
|
|
11
|
+
* `{ exit: true }` — drain & terminate
|
|
12
|
+
* OUT : `{ kind: 'exit' }` | `{ kind: 'reply', message: object }`
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Wire a pure worker message handler onto a `worker_threads` port.
|
|
17
|
+
*
|
|
18
|
+
* A falsy `port` is a no-op, so a worker module imported directly by a unit
|
|
19
|
+
* test (where `parentPort` is `null`) installs no listener and stays pure.
|
|
20
|
+
*
|
|
21
|
+
* @param {import('node:worker_threads').MessagePort|null|undefined} port
|
|
22
|
+
* @param {(msg: unknown) => {kind: 'exit'} | {kind: 'reply', message: object}} handle
|
|
23
|
+
* @returns {void}
|
|
24
|
+
*/
|
|
25
|
+
export function serveWorkerMessages(port, handle) {
|
|
26
|
+
if (!port) return;
|
|
27
|
+
port.on('message', (msg) => {
|
|
28
|
+
const out = handle(msg);
|
|
29
|
+
if (out.kind === 'exit') {
|
|
30
|
+
port.close();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
port.postMessage(out.message);
|
|
34
|
+
});
|
|
35
|
+
}
|