mandrel 2.55.0 → 2.57.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/plan-critic.md +13 -18
- package/.agents/agents/story-worker.md +25 -34
- package/.agents/docs/agentrc-reference.json +4 -30
- package/.agents/docs/configuration.md +11 -28
- package/.agents/docs/execution-reference.md +5 -5
- package/.agents/docs/quality-gates.md +8 -7
- package/.agents/instructions.md +9 -10
- package/.agents/rules/ci-remediation.md +39 -21
- package/.agents/schemas/agentrc.schema.json +28 -185
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +107 -17
- package/.agents/scripts/audit-to-stories.js +222 -75
- package/.agents/scripts/ceremony-derive.js +191 -0
- package/.agents/scripts/check-context-budget.js +28 -33
- package/.agents/scripts/check-cyclomatic.js +4 -3
- package/.agents/scripts/deliver-light.js +31 -94
- package/.agents/scripts/file-ci-gap.js +306 -0
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +25 -1
- package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +40 -52
- package/.agents/scripts/lib/audit-to-stories/finding-adapter.js +5 -1
- package/.agents/scripts/lib/audit-to-stories/issue-corpus.js +162 -0
- package/.agents/scripts/lib/audit-to-stories/issues-file.js +121 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/ledger-record.js +126 -0
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +11 -0
- package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
- package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
- package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
- package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
- package/.agents/scripts/lib/close-validation/gates.js +52 -1
- package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
- package/.agents/scripts/lib/config/delivery-routing.js +7 -33
- package/.agents/scripts/lib/config/explain.js +0 -19
- package/.agents/scripts/lib/config/limits.js +18 -78
- package/.agents/scripts/lib/config/quality.js +6 -3
- package/.agents/scripts/lib/config/runners.js +3 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
- package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
- package/.agents/scripts/lib/config-settings-schema.js +49 -143
- package/.agents/scripts/lib/crap-engine.js +35 -4
- package/.agents/scripts/lib/crap-utils.js +17 -1
- package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +53 -13
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +71 -25
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +18 -25
- package/.agents/scripts/lib/{audit-to-stories/ledger.js → findings/audit-ledger.js} +131 -24
- package/.agents/scripts/lib/findings/route-finding.js +38 -0
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/github/framework-repo.js +148 -2
- package/.agents/scripts/lib/label-constants.js +6 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +2 -0
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
- package/.agents/scripts/lib/orchestration/ci-gap-intake.js +605 -0
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +13 -8
- package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
- package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
- package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
- package/.agents/scripts/lib/orchestration/plan-context.js +181 -387
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
- package/.agents/scripts/lib/orchestration/plan-persist/audit-provenance.js +197 -0
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +300 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +131 -168
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +133 -299
- package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +30 -139
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
- package/.agents/scripts/lib/orchestration/run-epilogue.js +4 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
- package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +32 -20
- package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
- package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
- package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
- package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
- package/.agents/scripts/lib/story-body/story-body.js +17 -237
- package/.agents/scripts/lib/templates/decomposer-prompts.js +84 -121
- package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
- package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
- package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
- package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
- package/.agents/scripts/lib/test-run-credit.js +266 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
- package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
- package/.agents/scripts/lib/workers/crap-worker.js +32 -41
- package/.agents/scripts/plan-context.js +7 -9
- package/.agents/scripts/plan-critics.js +28 -54
- package/.agents/scripts/plan-persist.js +25 -68
- package/.agents/scripts/pr-watch-with-update.js +3 -2
- package/.agents/scripts/quality-preview.js +51 -0
- package/.agents/scripts/run-tests.js +12 -0
- package/.agents/scripts/stories-wave-tick.js +23 -45
- package/.agents/scripts/test-isolate.js +13 -180
- package/.agents/scripts/update-coverage-baseline.js +25 -70
- package/.agents/scripts/update-crap-baseline.js +19 -123
- package/.agents/skills/core/scope-triage/SKILL.md +3 -3
- package/.agents/workflows/audit-clean-code.md +4 -3
- package/.agents/workflows/audit-to-stories.md +63 -27
- package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
- package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
- package/.agents/workflows/helpers/code-review.md +2 -3
- package/.agents/workflows/helpers/deliver-digest.md +41 -57
- package/.agents/workflows/helpers/deliver-light.md +40 -105
- package/.agents/workflows/helpers/deliver-reference.md +1 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +56 -62
- package/.agents/workflows/helpers/deliver-story.md +9 -13
- package/.agents/workflows/helpers/plan-reference.md +132 -196
- package/.agents/workflows/mandrel-plan.md +28 -41
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +33 -0
- package/lib/migrations/index.js +4 -0
- package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
- package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
- package/package.json +1 -1
- package/.agents/scripts/lib/framework-version.js +0 -39
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
- package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
- package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
|
@@ -4,80 +4,42 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Split out of `ready-set.js` (Story #5044), which is the *scheduling* kernel:
|
|
6
6
|
* eligibility, capacity, admission order. Deciding whether two Stories collide
|
|
7
|
-
* is a separate question with its own rules — what counts as a declaration
|
|
8
|
-
* what
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* is a separate question with its own rules — what counts as a declaration and
|
|
8
|
+
* what a glob means — and it had grown large enough inside the scheduler to
|
|
9
|
+
* obscure both.
|
|
10
|
+
*
|
|
11
|
+
* ## The footprint is the declaration (Story #5313)
|
|
12
|
+
*
|
|
13
|
+
* Between Story #4875 and Story #5313 the footprint compared here was the
|
|
14
|
+
* declared `changes[]` **widened** by every repo-relative path scraped out of
|
|
15
|
+
* the Story's title, spec and body, on the theory that a declaration is a
|
|
16
|
+
* lower bound. Measured against real cohorts the widening manufactured far
|
|
17
|
+
* more serialisation than it prevented: audit provenance footers, markdown
|
|
18
|
+
* citations, `## Verify` gate commands and `## Non-Goals` prose all read as
|
|
19
|
+
* edit intent, and three narrowing passes (#5044, #5265) were spent teaching
|
|
20
|
+
* the scrape what a path is not. The delivery diet removes the scrape: two
|
|
21
|
+
* Stories collide only when **both declare** a path (or one declares a glob),
|
|
22
|
+
* so the `scraped-overlap` class, the per-path attribution and the field
|
|
23
|
+
* labels are gone with it. What a Story edits beyond its declaration is the
|
|
24
|
+
* close-time merge's business, not a dispatch guess.
|
|
11
25
|
*
|
|
12
26
|
* The layer has exactly one job: given two Story records, say whether their
|
|
13
|
-
*
|
|
14
|
-
*
|
|
27
|
+
* declared footprints intersect and name the paths. It reads nothing and
|
|
28
|
+
* mutates nothing.
|
|
15
29
|
*
|
|
16
30
|
* @module lib/wave-runner/footprint
|
|
17
31
|
*/
|
|
18
32
|
|
|
19
33
|
/**
|
|
20
|
-
* Why two footprints collided.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* two Stories that both list `baselines/maintainability.json` really do have to
|
|
25
|
-
* be serialized, and no amount of scrape-narrowing should change that.
|
|
26
|
-
*
|
|
27
|
-
* `scraped-overlap` — every colliding path reached the comparison through the
|
|
28
|
-
* evidence widening rather than a declaration. Still a real signal (Story
|
|
29
|
-
* #4875 exists because declarations are systematically a lower bound), but it
|
|
30
|
-
* is the class where a false positive is possible, so it is the one an operator
|
|
31
|
-
* should be able to see and — via `footprintGuard: 'advisory'` — choose not to
|
|
32
|
-
* enforce.
|
|
34
|
+
* Why two footprints collided. Since Story #5313 there is exactly one class:
|
|
35
|
+
* a path both Stories declared (or a declared glob). The value is kept on the
|
|
36
|
+
* envelope so a consumer keyed on `source` does not have to learn a new
|
|
37
|
+
* vocabulary, and so a future second class has a home.
|
|
33
38
|
*/
|
|
34
39
|
export const OVERLAP_SOURCES = Object.freeze({
|
|
35
40
|
DECLARED: 'declared-overlap',
|
|
36
|
-
SCRAPED: 'scraped-overlap',
|
|
37
41
|
});
|
|
38
42
|
|
|
39
|
-
/**
|
|
40
|
-
* Repo-relative file paths as they appear in Story prose: at least one `/`
|
|
41
|
-
* separator and a short file extension. Deliberately narrow — a token has to
|
|
42
|
-
* look like a real path before it can widen a footprint and withhold a Story.
|
|
43
|
-
*/
|
|
44
|
-
const PROSE_PATH_RE = /(?:[\w.@~-]+\/)+[\w.@-]+\.[A-Za-z0-9]{1,6}/g;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* The **machine-generated audit provenance footers** — and nothing else.
|
|
48
|
-
*
|
|
49
|
-
* `/audit-to-stories` and `plan-persist` stamp `<!-- audit-fingerprints: … -->`
|
|
50
|
-
* and `<!-- audit-semantic-keys: … -->` onto a Story body as dedup identity.
|
|
51
|
-
* A semantic key is `area␟primaryFile`, and that `␟` (U+241F) separator is
|
|
52
|
-
* outside {@link PROSE_PATH_RE}'s character class, so the `primaryFile` half
|
|
53
|
-
* matches as a standalone path token. `plan-persist` carries the **sweep-wide
|
|
54
|
-
* union** of those footers onto every sibling of an audit-derived plan, so
|
|
55
|
-
* every pair of that plan shared path-shaped tokens neither Story would edit —
|
|
56
|
-
* measured at 10/10 colliding pairs, 0/10 once these blocks are ignored
|
|
57
|
-
* (issue #5040).
|
|
58
|
-
*
|
|
59
|
-
* **This is surgical on purpose: a blanket HTML-comment strip would be wrong.**
|
|
60
|
-
* `.agents/instructions.md` § 7 puts a complexity decomposition's numbered
|
|
61
|
-
* sub-steps inside a `<!-- DECOMPOSITION -->` block, and the paths a sub-step
|
|
62
|
-
* names are exactly the edit intent this layer exists to read. Only the two
|
|
63
|
-
* provenance markers below are removed; every other comment stays evidence.
|
|
64
|
-
*/
|
|
65
|
-
const PROVENANCE_FOOTER_RE =
|
|
66
|
-
/<!--\s*audit-(?:fingerprints|semantic-keys)\s*:[\s\S]*?-->/g;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* The URL interior of a markdown inline link (`](…)`), with an optional title.
|
|
70
|
-
* A link target is a *citation* — "see [the spec](docs/architecture.md)" — not
|
|
71
|
-
* a declaration that this Story will edit that file, and generated bodies cite
|
|
72
|
-
* the same source report from every sibling. The link **text** is left intact:
|
|
73
|
-
* a human writing "the caller in [`bin/mandrel.js`](bin/mandrel.js)" is naming
|
|
74
|
-
* an edit target in the prose half, and that half still counts.
|
|
75
|
-
*/
|
|
76
|
-
const MARKDOWN_LINK_URL_RE = /\]\(\s*[^)\s]*(?:\s+"[^"]*")?\s*\)/g;
|
|
77
|
-
|
|
78
|
-
/** Default gitignored scratch root when no `project.paths.tempRoot` is threaded. */
|
|
79
|
-
const DEFAULT_TEMP_ROOT = 'temp';
|
|
80
|
-
|
|
81
43
|
/**
|
|
82
44
|
* Extract a Story's declared file footprint as a normalized set of path
|
|
83
45
|
* strings. Accepts the three footprint shapes a Story record can carry:
|
|
@@ -125,236 +87,6 @@ function isGlobPath(path) {
|
|
|
125
87
|
return path.includes('*') || path.includes('?') || path.includes('{');
|
|
126
88
|
}
|
|
127
89
|
|
|
128
|
-
/**
|
|
129
|
-
* Is this path inside the gitignored temp root?
|
|
130
|
-
*
|
|
131
|
-
* `project.paths.tempRoot` is scratch space by contract
|
|
132
|
-
* ([`.agents/instructions.md`](../../../instructions.md) § 6): nothing under it
|
|
133
|
-
* is ever committed, so it can never be a delivery write target, and two
|
|
134
|
-
* Stories naming the same `temp/audits/audit-<lens>-results.md` source report
|
|
135
|
-
* are not racing anything. The match is rooted, not a substring test, so a real
|
|
136
|
-
* deliverable like `lib/temperature.js` is untouched.
|
|
137
|
-
*
|
|
138
|
-
* @param {string} path
|
|
139
|
-
* @param {string} tempRoot
|
|
140
|
-
* @returns {boolean}
|
|
141
|
-
*/
|
|
142
|
-
function isUnderTempRoot(path, tempRoot) {
|
|
143
|
-
if (!tempRoot) return false;
|
|
144
|
-
const normalized = path.replace(/^\.\//, '');
|
|
145
|
-
return normalized === tempRoot || normalized.startsWith(`${tempRoot}/`);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Scrape file paths a Story's **text** mentions but its `changes[]` never
|
|
150
|
-
* declared (Story #4875), **narrowed to text that expresses edit intent**
|
|
151
|
-
* (Story #5044).
|
|
152
|
-
*
|
|
153
|
-
* The declared footprint is a planner's *prediction*, and it is systematically
|
|
154
|
-
* a lower bound: a Story's `## Spec` names the module it must also touch, its
|
|
155
|
-
* acceptance criteria name the caller that must be updated, and none of that
|
|
156
|
-
* reaches `changes[]`. The overlap guard exists to stop two Stories racing the
|
|
157
|
-
* same file, so trusting the declaration outright means the guard is blind to
|
|
158
|
-
* precisely the collisions nobody predicted.
|
|
159
|
-
*
|
|
160
|
-
* But the converse failure is just as real: a path-shaped token that no human
|
|
161
|
-
* wrote as intent manufactures a collision, and a manufactured collision
|
|
162
|
-
* serializes a run that had no reason to be serial. Three token sources are
|
|
163
|
-
* therefore excluded before the scrape, each because it is *structurally*
|
|
164
|
-
* incapable of naming an edit target:
|
|
165
|
-
*
|
|
166
|
-
* 1. **Audit provenance footers** ({@link PROVENANCE_FOOTER_RE}) — machine-
|
|
167
|
-
* stamped dedup identity, unioned sweep-wide across siblings.
|
|
168
|
-
* 2. **Markdown-link URLs** ({@link MARKDOWN_LINK_URL_RE}) — citations.
|
|
169
|
-
* 3. **Paths under the temp root** ({@link isUnderTempRoot}) — gitignored
|
|
170
|
-
* scratch, never a write target.
|
|
171
|
-
*
|
|
172
|
-
* Evidence is only ever **added** to the declaration — nothing here can shrink
|
|
173
|
-
* a declared footprint, so `changes[]` remains a lower bound (Story #4875) and
|
|
174
|
-
* narrowing the scrape can never co-dispatch a pair the declared comparison
|
|
175
|
-
* would have caught.
|
|
176
|
-
*
|
|
177
|
-
* Each path is returned **with the field it was scraped from** (Story #5265).
|
|
178
|
-
* "This pair collided on a path neither declared" is only half an
|
|
179
|
-
* explanation: the operator's next question is always *where did that path
|
|
180
|
-
* come from*, and until they can answer it they cannot tell an unpredicted
|
|
181
|
-
* edit target from a citation the guard read as one. The measured case is a
|
|
182
|
-
* gate script every Story merely **runs** in `verify[]` — attribution turns
|
|
183
|
-
* that from an unexplained serialisation into a one-glance verdict.
|
|
184
|
-
*
|
|
185
|
-
* @param {object} story
|
|
186
|
-
* @param {object} [options]
|
|
187
|
-
* @param {string} [options.tempRoot='temp'] Resolved `project.paths.tempRoot`.
|
|
188
|
-
* @returns {Map<string, Set<string>>} Path → the field label(s) that named it.
|
|
189
|
-
*/
|
|
190
|
-
function storyEvidencePaths(story, { tempRoot = DEFAULT_TEMP_ROOT } = {}) {
|
|
191
|
-
const out = new Map();
|
|
192
|
-
for (const [field, text] of attributedSegments(story)) {
|
|
193
|
-
for (const [token] of text.matchAll(PROSE_PATH_RE)) {
|
|
194
|
-
if (isUnderTempRoot(token, tempRoot)) continue;
|
|
195
|
-
const fields = out.get(token);
|
|
196
|
-
if (fields) fields.add(field);
|
|
197
|
-
else out.set(token, new Set([field]));
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return out;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* A markdown section heading in a serialized Story body — the attribution
|
|
205
|
-
* grain (Story #5265).
|
|
206
|
-
*
|
|
207
|
-
* `body` alone would be a true but useless label: a Story body is the whole
|
|
208
|
-
* document, so every scraped path would report the same field. The section is
|
|
209
|
-
* where the distinction actually lives — a path under `## Changes` is a
|
|
210
|
-
* declaration restated, one under `## Verify` is a command line, one under
|
|
211
|
-
* `## Non-Goals` is explicitly *not* an edit target.
|
|
212
|
-
*/
|
|
213
|
-
const BODY_SECTION_RE = /^#{2,6}[ \t]+(\S.*?)[ \t]*$/gm;
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Strip the two token sources that are structurally incapable of naming an
|
|
217
|
-
* edit target. Applied to the whole field **before** segmentation, so the
|
|
218
|
-
* scanned text is byte-identical to what the pre-attribution scrape read and
|
|
219
|
-
* a stripped footer can never be mistaken for a section boundary.
|
|
220
|
-
*
|
|
221
|
-
* @param {string} text
|
|
222
|
-
* @returns {string}
|
|
223
|
-
*/
|
|
224
|
-
function stripNonIntentTokens(text) {
|
|
225
|
-
return text
|
|
226
|
-
.replace(PROVENANCE_FOOTER_RE, ' ')
|
|
227
|
-
.replace(MARKDOWN_LINK_URL_RE, ']()');
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Split a Story body into `[label, text]` segments at its `##` headings.
|
|
232
|
-
*
|
|
233
|
-
* The heading line stays with the section it opens rather than being consumed
|
|
234
|
-
* as a delimiter: a heading can itself name a path, and dropping that text
|
|
235
|
-
* would *narrow* the footprint — the one direction this layer must never move
|
|
236
|
-
* (Story #4875 / #5265 AC-8). Text before the first heading keeps the bare
|
|
237
|
-
* `body` label.
|
|
238
|
-
*
|
|
239
|
-
* @param {string} body
|
|
240
|
-
* @returns {Array<[string, string]>}
|
|
241
|
-
*/
|
|
242
|
-
function bodySegments(body) {
|
|
243
|
-
const out = [];
|
|
244
|
-
let cursor = 0;
|
|
245
|
-
let label = 'body';
|
|
246
|
-
for (const match of body.matchAll(BODY_SECTION_RE)) {
|
|
247
|
-
if (match.index > cursor)
|
|
248
|
-
out.push([label, body.slice(cursor, match.index)]);
|
|
249
|
-
label = `body:${match[1].trim()}`;
|
|
250
|
-
cursor = match.index;
|
|
251
|
-
}
|
|
252
|
-
out.push([label, body.slice(cursor)]);
|
|
253
|
-
return out;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Every scannable `[fieldLabel, text]` pair on a Story record: `title` and
|
|
258
|
-
* `spec` whole, `body` split by section.
|
|
259
|
-
*
|
|
260
|
-
* @param {object} story
|
|
261
|
-
* @returns {Array<[string, string]>}
|
|
262
|
-
*/
|
|
263
|
-
function attributedSegments(story) {
|
|
264
|
-
const out = [];
|
|
265
|
-
if (typeof story?.title === 'string') {
|
|
266
|
-
out.push(['title', stripNonIntentTokens(story.title)]);
|
|
267
|
-
}
|
|
268
|
-
if (typeof story?.body === 'string') {
|
|
269
|
-
out.push(...bodySegments(stripNonIntentTokens(story.body)));
|
|
270
|
-
}
|
|
271
|
-
if (typeof story?.spec === 'string') {
|
|
272
|
-
out.push(['spec', stripNonIntentTokens(story.spec)]);
|
|
273
|
-
}
|
|
274
|
-
return out;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* A Story's declared footprint **and** the evidence-widened one.
|
|
279
|
-
*
|
|
280
|
-
* Both are returned because they answer different questions. `widened` decides
|
|
281
|
-
* *whether* two Stories collide; `declared` decides *how to describe* the
|
|
282
|
-
* collision — a path both Stories declared is intended serialization (two
|
|
283
|
-
* Stories that really do rewrite the same generated baseline), while one only
|
|
284
|
-
* the scrape produced may be an artifact of how a body was worded. An operator
|
|
285
|
-
* reading an unfilled slot needs to tell those apart.
|
|
286
|
-
*
|
|
287
|
-
* `evidence` carries the third answer (Story #5265): *which field* produced
|
|
288
|
-
* each scraped path, so a collision can say where the token was written
|
|
289
|
-
* rather than only that nobody declared it.
|
|
290
|
-
*
|
|
291
|
-
* @param {object} story
|
|
292
|
-
* @param {object} [options]
|
|
293
|
-
* @returns {{ declared: Set<string>, widened: Set<string>, evidence: Map<string, Set<string>> }}
|
|
294
|
-
*/
|
|
295
|
-
function storyFootprints(story, options) {
|
|
296
|
-
const declared = storyFootprint(story);
|
|
297
|
-
const evidence = storyEvidencePaths(story, options);
|
|
298
|
-
const widened = new Set(declared);
|
|
299
|
-
for (const path of evidence.keys()) widened.add(path);
|
|
300
|
-
return { declared, widened, evidence };
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* The field labels that scraped `path` on one side — empty when that side
|
|
305
|
-
* **declared** it, because a declaration is not evidence and reporting the
|
|
306
|
-
* prose restatement of a declared path would read as if the scrape had caused
|
|
307
|
-
* the collision.
|
|
308
|
-
*
|
|
309
|
-
* @param {{ declared: Set<string>, evidence: Map<string, Set<string>> }} side
|
|
310
|
-
* @param {string} path
|
|
311
|
-
* @returns {string[]}
|
|
312
|
-
*/
|
|
313
|
-
function scrapedFields(side, path) {
|
|
314
|
-
if (side.declared.has(path)) return [];
|
|
315
|
-
return [...(side.evidence.get(path) ?? [])];
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Per-path provenance for one colliding path (Story #5265): whether both
|
|
320
|
-
* sides declared it, and — when at least one side did not — the field labels
|
|
321
|
-
* the scrape found it in, unioned across the two sides and sorted.
|
|
322
|
-
*
|
|
323
|
-
* @param {object} fa
|
|
324
|
-
* @param {object} fb
|
|
325
|
-
* @param {string} path
|
|
326
|
-
* @param {boolean} declared
|
|
327
|
-
* @returns {{ path: string, declared: boolean, fields: string[] }}
|
|
328
|
-
*/
|
|
329
|
-
function attributePath(fa, fb, path, declared) {
|
|
330
|
-
const fields = new Set([
|
|
331
|
-
...scrapedFields(fa, path),
|
|
332
|
-
...scrapedFields(fb, path),
|
|
333
|
-
]);
|
|
334
|
-
return { path, declared, fields: [...fields].sort() };
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Record one colliding path, remembering whether **any** occurrence of it was
|
|
339
|
-
* declaration-backed.
|
|
340
|
-
*
|
|
341
|
-
* `declared` is tracked per path rather than per pair because the two hit kinds
|
|
342
|
-
* qualify differently: a shared **concrete** path counts as declared only when
|
|
343
|
-
* both sides declared it, whereas a **glob** names no file to share and counts
|
|
344
|
-
* as declared when its own side declared it. The scraper cannot emit a glob —
|
|
345
|
-
* prose globs are narrative ("everything under `.agents/**`") and never match
|
|
346
|
-
* {@link PROSE_PATH_RE} — so a glob hit is essentially always declared width
|
|
347
|
-
* failing safe, and labelling it `scraped` would make advisory mode read as if
|
|
348
|
-
* the text widening had caused it.
|
|
349
|
-
*
|
|
350
|
-
* @param {Map<string, boolean>} hits
|
|
351
|
-
* @param {string} path
|
|
352
|
-
* @param {boolean} declared
|
|
353
|
-
*/
|
|
354
|
-
function recordHit(hits, path, declared) {
|
|
355
|
-
hits.set(path, (hits.get(path) ?? false) || declared);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
90
|
/**
|
|
359
91
|
* Collect the glob paths on one side. A glob is unknown width, and unknown
|
|
360
92
|
* width is not no width: within a beat it collides with everything, because
|
|
@@ -362,24 +94,23 @@ function recordHit(hits, path, declared) {
|
|
|
362
94
|
* `.agents/scripts/lib/**` alongside one declaring a file underneath it
|
|
363
95
|
* (Story #4539/#4540).
|
|
364
96
|
*
|
|
365
|
-
* @param {
|
|
366
|
-
* @param {
|
|
97
|
+
* @param {Set<string>} hits
|
|
98
|
+
* @param {Set<string>} side
|
|
367
99
|
*/
|
|
368
|
-
function
|
|
369
|
-
for (const path of side
|
|
370
|
-
if (isGlobPath(path))
|
|
100
|
+
function collectGlobs(hits, side) {
|
|
101
|
+
for (const path of side) {
|
|
102
|
+
if (isGlobPath(path)) hits.add(path);
|
|
371
103
|
}
|
|
372
104
|
}
|
|
373
105
|
|
|
374
106
|
/**
|
|
375
|
-
* The colliding paths between two Stories'
|
|
376
|
-
*
|
|
377
|
-
* collide.
|
|
107
|
+
* The colliding paths between two Stories' declared footprints — or `null`
|
|
108
|
+
* when they do not collide.
|
|
378
109
|
*
|
|
379
110
|
* **An empty footprint means "no known overlap"**, so this short-circuits to
|
|
380
111
|
* `null` on one. That is permissive by necessity: a Story with no declared
|
|
381
|
-
* footprint
|
|
382
|
-
*
|
|
112
|
+
* footprint carries no information, and withholding on absence would
|
|
113
|
+
* serialize every run.
|
|
383
114
|
*
|
|
384
115
|
* `concreteOnly` selects between the two guards' deliberately different
|
|
385
116
|
* treatment of unknown width (Story #4960). The beat-local guard counts a glob
|
|
@@ -389,71 +120,30 @@ function recordGlobs(hits, side) {
|
|
|
389
120
|
* run for hours — and `resolve-stories.js` substitutes an UNKNOWN sentinel for
|
|
390
121
|
* any body it cannot parse, so one malformed Story would make a run serial.
|
|
391
122
|
*
|
|
392
|
-
*
|
|
393
|
-
* the
|
|
394
|
-
*
|
|
395
|
-
* field label(s) the scrape read it from otherwise (`title`, `spec`, or
|
|
396
|
-
* `body:<section>`). It is strictly additive — `paths` and `source` are
|
|
397
|
-
* unchanged, so no pair that collided before collides differently now.
|
|
123
|
+
* The second options parameter is accepted for call-site compatibility with
|
|
124
|
+
* the retired evidence-scrape options (`tempRoot`); only `concreteOnly` is
|
|
125
|
+
* read.
|
|
398
126
|
*
|
|
399
127
|
* @param {object} a
|
|
400
128
|
* @param {object} b
|
|
401
129
|
* @param {object} [options]
|
|
402
130
|
* @param {boolean} [options.concreteOnly=false] Skip glob paths on both sides.
|
|
403
|
-
* @
|
|
404
|
-
* @returns {{ paths: string[], source: string, attribution: Array<{ path: string, declared: boolean, fields: string[] }> }|null}
|
|
131
|
+
* @returns {{ paths: string[], source: string }|null}
|
|
405
132
|
*/
|
|
406
|
-
export function detectCollision(
|
|
407
|
-
a
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
)
|
|
411
|
-
const fa = storyFootprints(a, evidence);
|
|
412
|
-
if (fa.widened.size === 0) return null;
|
|
413
|
-
const fb = storyFootprints(b, evidence);
|
|
414
|
-
if (fb.widened.size === 0) return null;
|
|
133
|
+
export function detectCollision(a, b, { concreteOnly = false } = {}) {
|
|
134
|
+
const fa = storyFootprint(a);
|
|
135
|
+
if (fa.size === 0) return null;
|
|
136
|
+
const fb = storyFootprint(b);
|
|
137
|
+
if (fb.size === 0) return null;
|
|
415
138
|
|
|
416
|
-
const hits = new
|
|
417
|
-
for (const path of fa
|
|
418
|
-
if (!isGlobPath(path) && fb.
|
|
419
|
-
recordHit(hits, path, fa.declared.has(path) && fb.declared.has(path));
|
|
420
|
-
}
|
|
139
|
+
const hits = new Set();
|
|
140
|
+
for (const path of fa) {
|
|
141
|
+
if (!isGlobPath(path) && fb.has(path)) hits.add(path);
|
|
421
142
|
}
|
|
422
143
|
if (!concreteOnly) {
|
|
423
|
-
|
|
424
|
-
|
|
144
|
+
collectGlobs(hits, fa);
|
|
145
|
+
collectGlobs(hits, fb);
|
|
425
146
|
}
|
|
426
147
|
if (hits.size === 0) return null;
|
|
427
|
-
|
|
428
|
-
return {
|
|
429
|
-
paths,
|
|
430
|
-
source: [...hits.values()].some(Boolean)
|
|
431
|
-
? OVERLAP_SOURCES.DECLARED
|
|
432
|
-
: OVERLAP_SOURCES.SCRAPED,
|
|
433
|
-
attribution: paths.map((path) =>
|
|
434
|
-
attributePath(fa, fb, path, hits.get(path)),
|
|
435
|
-
),
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Render one collision's scraped-path provenance as a single operator-facing
|
|
441
|
-
* clause, or `''` when every colliding path was declared by both sides.
|
|
442
|
-
*
|
|
443
|
-
* Shared by every report that names a withhold so the tick's envelope note
|
|
444
|
-
* and plan-persist's predicted-serialisation table read identically — the two
|
|
445
|
-
* surfaces describe the same computation and an operator comparing them
|
|
446
|
-
* should not have to translate (Story #5265).
|
|
447
|
-
*
|
|
448
|
-
* @param {Array<{ path: string, declared: boolean, fields: string[] }>} attribution
|
|
449
|
-
* @returns {string}
|
|
450
|
-
*/
|
|
451
|
-
export function renderScrapeAttribution(attribution) {
|
|
452
|
-
const scraped = (Array.isArray(attribution) ? attribution : []).filter(
|
|
453
|
-
(entry) => Array.isArray(entry?.fields) && entry.fields.length > 0,
|
|
454
|
-
);
|
|
455
|
-
if (scraped.length === 0) return '';
|
|
456
|
-
return scraped
|
|
457
|
-
.map((entry) => `${entry.path} ← ${entry.fields.join(', ')}`)
|
|
458
|
-
.join('; ');
|
|
148
|
+
return { paths: [...hits].sort(), source: OVERLAP_SOURCES.DECLARED };
|
|
459
149
|
}
|
|
@@ -296,12 +296,13 @@ function reservesConcretePath(held, candidate, options = {}) {
|
|
|
296
296
|
* the guard *detects* — every would-be withhold is still computed and
|
|
297
297
|
* returned in `footprintWithholds` with `enforced: false` — so turning it on
|
|
298
298
|
* trades serialization for throughput without going blind (Story #5044).
|
|
299
|
-
* @param {string} [args.tempRoot] Resolved `project.paths.tempRoot
|
|
300
|
-
*
|
|
299
|
+
* @param {string} [args.tempRoot] Resolved `project.paths.tempRoot`. Accepted
|
|
300
|
+
* for call-site compatibility; the evidence scrape that read it was retired
|
|
301
|
+
* in Story #5313 and the footprint is the declared `changes[]` alone.
|
|
301
302
|
* @returns {{
|
|
302
303
|
* selected: StoryRecord[],
|
|
303
304
|
* withheldByInFlight: Array<{id: number, blockedBy: number}>,
|
|
304
|
-
* footprintWithholds: Array<{id: number, blockedBy: number, scope: string, source: string, paths: string[],
|
|
305
|
+
* footprintWithholds: Array<{id: number, blockedBy: number, scope: string, source: string, paths: string[], enforced: boolean}>,
|
|
305
306
|
* guardMode: 'enforce'|'advisory'
|
|
306
307
|
* }}
|
|
307
308
|
* `selected` is the dispatch set: a subset of `stories`, ascending by id,
|
|
@@ -309,8 +310,8 @@ function reservesConcretePath(held, candidate, options = {}) {
|
|
|
309
310
|
* names each eligible Story a reservation held back and the in-flight
|
|
310
311
|
* Story that holds it. `footprintWithholds` is the **complete** ledger —
|
|
311
312
|
* beat-local skips as well as cross-beat reservations, each with the
|
|
312
|
-
* colliding paths and its `declared-overlap`
|
|
313
|
-
*
|
|
313
|
+
* colliding paths and its `declared-overlap` source — so no withheld
|
|
314
|
+
* dispatch is unexplained (Story #5044).
|
|
314
315
|
*/
|
|
315
316
|
export function planReadySet({
|
|
316
317
|
stories,
|
|
@@ -23,14 +23,18 @@
|
|
|
23
23
|
* totalMethods: number,
|
|
24
24
|
* } }
|
|
25
25
|
*
|
|
26
|
-
* A truly unrecoverable per-file failure (read error, transpile null
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* aborts the whole scan.
|
|
26
|
+
* A truly unrecoverable per-file failure (read error, transpile null, or a
|
|
27
|
+
* source the kernel cannot parse) surfaces as
|
|
28
|
+
* `{ ok: true, result: { relPath, rows: null, ... } }` so the host loop drops
|
|
29
|
+
* the file and increments its own counter — never aborts the whole scan.
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import { parentPort } from 'node:worker_threads';
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
calculateCrapForSource,
|
|
35
|
+
finalizeMethodRows,
|
|
36
|
+
UNSCORABLE,
|
|
37
|
+
} from '../crap-engine.js';
|
|
34
38
|
import { prepareSourceForScoring } from '../transpile.js';
|
|
35
39
|
import { serveWorkerMessages } from './serve-worker-messages.js';
|
|
36
40
|
|
|
@@ -60,7 +64,7 @@ import { serveWorkerMessages } from './serve-worker-messages.js';
|
|
|
60
64
|
* readFile?: (abs: string) => string,
|
|
61
65
|
* transpile?: (abs: string, source: string, opts?: object) => unknown,
|
|
62
66
|
* prepare?: (abs: string, deps: object) => object,
|
|
63
|
-
* calculateCrap?: (source: string, entry: object|null, mapLine: Function|null) => Array<object
|
|
67
|
+
* calculateCrap?: (source: string, entry: object|null, mapLine: Function|null) => Array<object>|null,
|
|
64
68
|
* }} [deps]
|
|
65
69
|
* @returns {{kind: 'exit'} | {kind: 'reply', message: object}}
|
|
66
70
|
*/
|
|
@@ -87,25 +91,12 @@ export function handleCrapWorkerMessage(msg, _coverage, deps = {}) {
|
|
|
87
91
|
// `item.coverageEntry` may be explicitly `null` when the file has no
|
|
88
92
|
// coverage, or `undefined` when the caller did not supply it (treat as null).
|
|
89
93
|
const entry = item.coverageEntry ?? null;
|
|
90
|
-
if (requireCoverage && entry === null) {
|
|
91
|
-
return {
|
|
92
|
-
kind: 'reply',
|
|
93
|
-
message: {
|
|
94
|
-
ok: true,
|
|
95
|
-
result: {
|
|
96
|
-
relPath,
|
|
97
|
-
skippedFileNoCoverage: true,
|
|
98
|
-
rows: [],
|
|
99
|
-
skippedMethodsNoCoverage: 0,
|
|
100
|
-
hasCoverageEntry: false,
|
|
101
|
-
resolvedMethods: 0,
|
|
102
|
-
totalMethods: 0,
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
94
|
|
|
108
|
-
|
|
95
|
+
// Every non-error outcome answers in the same envelope; each branch below
|
|
96
|
+
// overrides only the fields its own verdict changes. Spelling the seven
|
|
97
|
+
// result fields out per branch is what let the drop shape and the success
|
|
98
|
+
// shape drift apart in the first place.
|
|
99
|
+
const reply = (result) => ({
|
|
109
100
|
kind: 'reply',
|
|
110
101
|
message: {
|
|
111
102
|
ok: true,
|
|
@@ -117,11 +108,17 @@ export function handleCrapWorkerMessage(msg, _coverage, deps = {}) {
|
|
|
117
108
|
hasCoverageEntry: entry !== null,
|
|
118
109
|
resolvedMethods: 0,
|
|
119
110
|
totalMethods: 0,
|
|
120
|
-
...
|
|
111
|
+
...result,
|
|
121
112
|
},
|
|
122
113
|
},
|
|
123
114
|
});
|
|
124
115
|
|
|
116
|
+
if (requireCoverage && entry === null) {
|
|
117
|
+
return reply({ skippedFileNoCoverage: true, rows: [] });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const dropped = (error) => reply(error ? { error } : {});
|
|
121
|
+
|
|
125
122
|
// TS/TSX -> transpile-then-analyze, carrying the source map. The coverage
|
|
126
123
|
// lookup above used the ORIGINAL source path (vitest's coverage-final.json
|
|
127
124
|
// keys on the .ts file, not transpiled output) and the per-method join
|
|
@@ -143,23 +140,17 @@ export function handleCrapWorkerMessage(msg, _coverage, deps = {}) {
|
|
|
143
140
|
err && typeof err.message === 'string' ? err.message : String(err),
|
|
144
141
|
);
|
|
145
142
|
}
|
|
143
|
+
// Story #5311: the kernel answers `UNSCORABLE` — not `[]` — for a source it
|
|
144
|
+
// could not parse. Collapsing the two here is what made this drop path
|
|
145
|
+
// unreachable for the whole parse-failure class: the file reached the host
|
|
146
|
+
// as a successfully-scored file with no methods, and its baseline rows
|
|
147
|
+
// vanished without a counter moving. `rows: null` is the serial path's
|
|
148
|
+
// `parseError` verdict in the shape the host loop already drops on.
|
|
149
|
+
if (methodRows === UNSCORABLE) return dropped(null);
|
|
146
150
|
|
|
147
|
-
|
|
148
|
-
requireCoverage,
|
|
149
|
-
|
|
150
|
-
});
|
|
151
|
-
return {
|
|
152
|
-
kind: 'reply',
|
|
153
|
-
message: {
|
|
154
|
-
ok: true,
|
|
155
|
-
result: {
|
|
156
|
-
relPath,
|
|
157
|
-
skippedFileNoCoverage: false,
|
|
158
|
-
hasCoverageEntry: entry !== null,
|
|
159
|
-
...finalized,
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
};
|
|
151
|
+
return reply(
|
|
152
|
+
finalizeMethodRows(methodRows, { requireCoverage, coverageAvailable }),
|
|
153
|
+
);
|
|
163
154
|
}
|
|
164
155
|
|
|
165
156
|
serveWorkerMessages(parentPort, (msg) => handleCrapWorkerMessage(msg, null));
|
|
@@ -164,23 +164,21 @@ export async function emitPlanContext({
|
|
|
164
164
|
bytes: Buffer.byteLength(json, 'utf8'),
|
|
165
165
|
sourceTickets: (envelope.sourceTickets ?? []).map((t) => t.id),
|
|
166
166
|
duplicates: (envelope.duplicates ?? []).length,
|
|
167
|
-
// Advisory only
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
// offer — advisory, never an automatic reroute. Both ride the digest
|
|
172
|
-
// because with `--out` the digest is the only thing the planner reads.
|
|
167
|
+
// Advisory only: signals, no route. `uiSurface` is the recorded
|
|
168
|
+
// /prototype offer — advisory, never an automatic reroute. It rides the
|
|
169
|
+
// digest because with `--out` the digest is the only thing the planner
|
|
170
|
+
// reads.
|
|
173
171
|
complexitySignals: envelope.complexitySignals
|
|
174
172
|
? {
|
|
175
173
|
artifactCount: envelope.complexitySignals.artifactCount,
|
|
176
|
-
riskHeuristicHits: envelope.complexitySignals.riskHeuristicHits,
|
|
177
174
|
sensitivePathClasses:
|
|
178
175
|
envelope.complexitySignals.sensitivePathClasses,
|
|
179
|
-
deliverLightSuggestion:
|
|
180
|
-
envelope.complexitySignals.deliverLightSuggestion ?? null,
|
|
181
176
|
uiSurface: envelope.complexitySignals.uiSurface ?? null,
|
|
182
177
|
}
|
|
183
178
|
: null,
|
|
179
|
+
// Story #5312: an envelope over the planner-context ceiling is written
|
|
180
|
+
// truncated, and the digest names what was cut.
|
|
181
|
+
truncated: envelope.truncated ?? null,
|
|
184
182
|
amends: envelope.amends ? { id: envelope.amends.id } : null,
|
|
185
183
|
};
|
|
186
184
|
stdout.write(`${JSON.stringify(digest)}\n`);
|