mandrel 2.18.0 → 2.20.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/docs/SDLC.md +1 -1
- package/.agents/docs/agentrc-reference.json +0 -21
- package/.agents/docs/configuration.md +11 -14
- package/.agents/docs/execution-reference.md +8 -5
- package/.agents/schemas/agentrc.schema.json +0 -31
- package/.agents/scripts/check-doc-links.js +141 -9
- package/.agents/scripts/check-test-temp-hygiene.js +153 -14
- package/.agents/scripts/lib/baselines/env-overrides.js +40 -48
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +3 -4
- package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -2
- package/.agents/scripts/lib/config/explain.js +0 -8
- package/.agents/scripts/lib/config/temp-paths.js +12 -1
- package/.agents/scripts/lib/config-settings-schema.js +8 -24
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +51 -77
- package/.agents/scripts/lib/orchestration/check-baselines/phases/parse-args.js +20 -12
- package/.agents/scripts/lib/orchestration/file-assumptions.js +4 -2
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +2 -1
- package/.agents/scripts/lib/orchestration/plan-context.js +13 -14
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +12 -66
- package/.agents/scripts/lib/test-env.js +15 -3
- package/.agents/scripts/lib/test-temp.js +311 -0
- package/.agents/workflows/audit-performance.md +2 -2
- package/.agents/workflows/helpers/diagnose.md +1 -1
- package/.agents/workflows/helpers/plan-reference.md +19 -4
- package/.agents/workflows/helpers/signals.md +2 -2
- package/.agents/workflows/mandrel-update.md +4 -4
- package/.agents/workflows/plan.md +7 -6
- package/docs/CHANGELOG.md +16 -0
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +113 -0
- package/package.json +1 -1
- package/.agents/scripts/lib/codebase-snapshot.js +0 -513
- package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +0 -147
- package/.agents/scripts/lib/orchestration/spec-freshness.js +0 -129
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* planning/spec-authoring-grounding.js — F10 Spec code-grounding.
|
|
3
|
-
*
|
|
4
|
-
* Story #4139 (Epic #4131). `/plan` Phase 7 authors the Tech Spec
|
|
5
|
-
* from the Epic body, the scraped project docs, and the
|
|
6
|
-
* `codebaseSnapshot` structural view of the consumer repo. Two failure
|
|
7
|
-
* modes made that grounding silently partial:
|
|
8
|
-
*
|
|
9
|
-
* 1. The skinny-tier snapshot caps its file list at `MAX_FILES_SKINNY`
|
|
10
|
-
* and sets `truncated: true` — but the only operator-visible signal
|
|
11
|
-
* was a stderr `Logger.warn` (Story #3959). The spec author consumes
|
|
12
|
-
* the JSON envelope, not stderr, so it never learned the snapshot
|
|
13
|
-
* was partial. A run that dropped "377 of 627 files" looked complete
|
|
14
|
-
* to the author.
|
|
15
|
-
*
|
|
16
|
-
* 2. When the Epic body cites a code path that is **absent** from the
|
|
17
|
-
* snapshot's file set, nothing surfaced the gap *during* authoring.
|
|
18
|
-
* The post-author spec-freshness gate (Story #2635) catches stale
|
|
19
|
-
* citations only after the Tech Spec is written — one phase too late
|
|
20
|
-
* to ground the author's choices.
|
|
21
|
-
*
|
|
22
|
-
* `buildAuthoringGrounding` derives a small, bounded `grounding` block that
|
|
23
|
-
* is attached to the `codebaseSnapshot` envelope so the author (and the
|
|
24
|
-
* operator inspecting the run) see both signals before the spec is written:
|
|
25
|
-
*
|
|
26
|
-
* - `truncation` — non-null when the snapshot dropped files. Carries the
|
|
27
|
-
* dropped count, the matched/shown totals, and the two remedies. This
|
|
28
|
-
* is the structured, in-envelope form of the Story #3959 stderr warning.
|
|
29
|
-
* - `citedButAbsent` — path-shaped references pulled from the authoring
|
|
30
|
-
* prose (the Epic body) that are **not** present in the snapshot's file
|
|
31
|
-
* set and are not phrased as net-new. Bounded to `MAX_CITED_ABSENT`
|
|
32
|
-
* entries so a pathological Epic body cannot blow the envelope budget.
|
|
33
|
-
*
|
|
34
|
-
* The grounding is targeted (the prose the author is grounding *from* and
|
|
35
|
-
* the files the snapshot already carries), not a whole-repo dump — the
|
|
36
|
-
* snapshot file set is the only source consulted, so this adds no new
|
|
37
|
-
* filesystem or git probes.
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Hard cap on the `citedButAbsent` list so an Epic body that mentions a
|
|
42
|
-
* very large number of paths cannot inflate the authoring envelope. The
|
|
43
|
-
* cap is generous relative to a realistic Epic citation count; when it is
|
|
44
|
-
* hit, `citedButAbsentTruncated: true` flags the elision so the signal is
|
|
45
|
-
* not silently dropped (the very failure mode this Story fixes).
|
|
46
|
-
*/
|
|
47
|
-
export const MAX_CITED_ABSENT = 40;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Build the operator-visible truncation signal from a snapshot envelope.
|
|
51
|
-
* Returns `null` when the snapshot is absent or was not truncated.
|
|
52
|
-
*
|
|
53
|
-
* @param {object|null} snapshot - The `codebaseSnapshot` envelope.
|
|
54
|
-
* @returns {{ dropped: number, matched: number, shown: number, tier: string, remedies: string[] } | null}
|
|
55
|
-
*/
|
|
56
|
-
export function buildTruncationSignal(snapshot) {
|
|
57
|
-
if (!snapshot || snapshot.truncated !== true) return null;
|
|
58
|
-
const matched = Number.isInteger(snapshot.fileCount) ? snapshot.fileCount : 0;
|
|
59
|
-
const shown = Array.isArray(snapshot.files) ? snapshot.files.length : 0;
|
|
60
|
-
const dropped = Math.max(0, matched - shown);
|
|
61
|
-
return {
|
|
62
|
-
dropped,
|
|
63
|
-
matched,
|
|
64
|
-
shown,
|
|
65
|
-
tier: typeof snapshot.tier === 'string' ? snapshot.tier : 'skinny',
|
|
66
|
-
remedies: [
|
|
67
|
-
'Set planning.codebaseSnapshot.tier: "medium" in .agentrc.json to restore full grounding.',
|
|
68
|
-
'Narrow planning.codebaseSnapshot.include in .agentrc.json so the cited surfaces survive the cap.',
|
|
69
|
-
],
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Surface path-shaped references from the authoring prose that are absent
|
|
75
|
-
* from the snapshot's file set. Reuses the spec-freshness path extractor so
|
|
76
|
-
* the citation shapes recognised here match the post-author freshness gate
|
|
77
|
-
* exactly — a path the author cites in prose is detected the same way before
|
|
78
|
-
* and after authoring.
|
|
79
|
-
*
|
|
80
|
-
* A reference is reported only when it is **not** present in `snapshotFiles`
|
|
81
|
-
* **and** the surrounding prose does not phrase it as net-new (the same
|
|
82
|
-
* cue heuristic the freshness gate uses to demote intentional new-file
|
|
83
|
-
* mentions). Results are deduped by path, sorted, and bounded to
|
|
84
|
-
* `MAX_CITED_ABSENT`.
|
|
85
|
-
*
|
|
86
|
-
* @param {string} prose - The authoring prose (typically the Epic body).
|
|
87
|
-
* @param {string[]} snapshotFiles - The snapshot's `files` array.
|
|
88
|
-
* @param {object} deps
|
|
89
|
-
* @param {Function} deps.collectReferences - (body) => Array<{ path, index, matchLength }>.
|
|
90
|
-
* @param {Function} deps.hasNewFileCue - (body, index, matchLength) => boolean.
|
|
91
|
-
* @returns {{ paths: string[], truncated: boolean }}
|
|
92
|
-
*/
|
|
93
|
-
export function findCitedButAbsent(prose, snapshotFiles, deps) {
|
|
94
|
-
const { collectReferences, hasNewFileCue } = deps;
|
|
95
|
-
if (typeof prose !== 'string' || prose.length === 0) {
|
|
96
|
-
return { paths: [], truncated: false };
|
|
97
|
-
}
|
|
98
|
-
const present = new Set(
|
|
99
|
-
(Array.isArray(snapshotFiles) ? snapshotFiles : []).map((f) =>
|
|
100
|
-
String(f).replace(/\\/g, '/'),
|
|
101
|
-
),
|
|
102
|
-
);
|
|
103
|
-
const absent = new Set();
|
|
104
|
-
for (const { path, index, matchLength } of collectReferences(prose)) {
|
|
105
|
-
const normalised = path.replace(/\\/g, '/');
|
|
106
|
-
if (present.has(normalised)) continue;
|
|
107
|
-
if (hasNewFileCue(prose, index, matchLength)) continue;
|
|
108
|
-
absent.add(normalised);
|
|
109
|
-
}
|
|
110
|
-
const sorted = [...absent].sort();
|
|
111
|
-
return {
|
|
112
|
-
paths: sorted.slice(0, MAX_CITED_ABSENT),
|
|
113
|
-
truncated: sorted.length > MAX_CITED_ABSENT,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Build the full `grounding` block attached to the `codebaseSnapshot`
|
|
119
|
-
* envelope. Pure with respect to its inputs (no filesystem or git probes):
|
|
120
|
-
* the snapshot file set is the sole grounding source, keeping the context
|
|
121
|
-
* bounded for cost.
|
|
122
|
-
*
|
|
123
|
-
* @param {object} opts
|
|
124
|
-
* @param {object|null} opts.snapshot - The `codebaseSnapshot` envelope.
|
|
125
|
-
* @param {string} opts.prose - The authoring prose (Epic body) to scan.
|
|
126
|
-
* @param {Function} opts.collectReferences - spec-freshness path extractor.
|
|
127
|
-
* @param {Function} opts.hasNewFileCue - spec-freshness net-new cue check.
|
|
128
|
-
* @returns {{ truncation: object|null, citedButAbsent: string[], citedButAbsentTruncated: boolean }}
|
|
129
|
-
*/
|
|
130
|
-
export function buildAuthoringGrounding({
|
|
131
|
-
snapshot,
|
|
132
|
-
prose,
|
|
133
|
-
collectReferences,
|
|
134
|
-
hasNewFileCue,
|
|
135
|
-
}) {
|
|
136
|
-
const truncation = buildTruncationSignal(snapshot);
|
|
137
|
-
const { paths, truncated } = findCitedButAbsent(
|
|
138
|
-
prose,
|
|
139
|
-
snapshot?.files ?? [],
|
|
140
|
-
{ collectReferences, hasNewFileCue },
|
|
141
|
-
);
|
|
142
|
-
return {
|
|
143
|
-
truncation,
|
|
144
|
-
citedButAbsent: paths,
|
|
145
|
-
citedButAbsentTruncated: truncated,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* spec-freshness.js — path-reference helpers for plan authoring.
|
|
3
|
-
*
|
|
4
|
-
* Stage 5 retired the epic-era `validateSpecFreshness` / structured-comment
|
|
5
|
-
* reporter (no production callers remained after the planning collapse).
|
|
6
|
-
* Survivors: path-cue extraction used by `planning/authoring-context.js`
|
|
7
|
-
* and the plan-authoring grounding tests.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Path-shape regexes. Three forms the Architect persona emits today:
|
|
12
|
-
*
|
|
13
|
-
* 1. Backticked reference → `` `src/auth.ts` ``
|
|
14
|
-
* 2. Code-block file header → `// src/auth.ts` or `# lib/foo.py`
|
|
15
|
-
* 3. Inline prose mention → bare `src/auth.ts` between word boundaries
|
|
16
|
-
*
|
|
17
|
-
* All three pull the same captured-path group. We anchor the path on a
|
|
18
|
-
* known repo root (`.agents`, `src`, `lib`, `app`, `tests`, `packages`,
|
|
19
|
-
* `scripts`, `docs`) so we don't false-positive on `library`, `testimonial`,
|
|
20
|
-
* versioned semver fragments, etc.
|
|
21
|
-
*/
|
|
22
|
-
const PATH_ROOTS = [
|
|
23
|
-
'\\.agents',
|
|
24
|
-
'src',
|
|
25
|
-
'lib',
|
|
26
|
-
'app',
|
|
27
|
-
'tests',
|
|
28
|
-
'packages',
|
|
29
|
-
'scripts',
|
|
30
|
-
'docs',
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
const PATH_BODY = '[\\w./-]+\\.[a-zA-Z0-9]{1,8}';
|
|
34
|
-
|
|
35
|
-
const BACKTICK_PATH_RE = new RegExp(
|
|
36
|
-
`\`((?:${PATH_ROOTS.join('|')})/${PATH_BODY})\``,
|
|
37
|
-
'g',
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
const COMMENT_HEADER_PATH_RE = new RegExp(
|
|
41
|
-
`(?:^|\\n)\\s*(?://|#)\\s*((?:${PATH_ROOTS.join('|')})/${PATH_BODY})\\b`,
|
|
42
|
-
'g',
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const BARE_PATH_RE = new RegExp(
|
|
46
|
-
`(?:^|[\\s([<>])((?:${PATH_ROOTS.join('|')})/${PATH_BODY})(?=[\\s)\\].,;:]|$)`,
|
|
47
|
-
'g',
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Words in surrounding prose that signal a reference is intentionally
|
|
52
|
-
* net-new — the planner is *proposing* the path, not asserting it exists.
|
|
53
|
-
* When any of these appear within `AMBIGUITY_WINDOW` characters of the
|
|
54
|
-
* match, we treat the citation as intentional rather than drift.
|
|
55
|
-
*/
|
|
56
|
-
const NEW_FILE_CUES = [
|
|
57
|
-
'introduce',
|
|
58
|
-
'introduces',
|
|
59
|
-
'introducing',
|
|
60
|
-
'add',
|
|
61
|
-
'adds',
|
|
62
|
-
'adding',
|
|
63
|
-
'create',
|
|
64
|
-
'creates',
|
|
65
|
-
'creating',
|
|
66
|
-
'new file',
|
|
67
|
-
'new module',
|
|
68
|
-
'new helper',
|
|
69
|
-
'to be created',
|
|
70
|
-
'will be created',
|
|
71
|
-
'net-new',
|
|
72
|
-
'scaffold',
|
|
73
|
-
'scaffolds',
|
|
74
|
-
'scaffolding',
|
|
75
|
-
];
|
|
76
|
-
|
|
77
|
-
const AMBIGUITY_WINDOW = 80;
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Check whether the prose surrounding `index` carries one of the
|
|
81
|
-
* net-new cue phrases. Looks both before and after the match within
|
|
82
|
-
* `AMBIGUITY_WINDOW` characters because authors phrase the cue either
|
|
83
|
-
* way: "introduce src/x.ts" *or* "src/x.ts (new helper)".
|
|
84
|
-
*
|
|
85
|
-
* Case-insensitive substring match — deliberately not regex/word-boundary
|
|
86
|
-
* so future cue variants don't silently slip the gate.
|
|
87
|
-
*
|
|
88
|
-
* @param {string} body
|
|
89
|
-
* @param {number} index
|
|
90
|
-
* @param {number} matchLength
|
|
91
|
-
* @returns {boolean}
|
|
92
|
-
*/
|
|
93
|
-
export function hasNewFileCue(body, index, matchLength) {
|
|
94
|
-
const start = Math.max(0, index - AMBIGUITY_WINDOW);
|
|
95
|
-
const end = Math.min(body.length, index + matchLength + AMBIGUITY_WINDOW);
|
|
96
|
-
const window = body.slice(start, end).toLowerCase();
|
|
97
|
-
for (const cue of NEW_FILE_CUES) {
|
|
98
|
-
if (window.includes(cue)) return true;
|
|
99
|
-
}
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Collect every `(path, index, matchLength)` triple from `body` using the
|
|
105
|
-
* three path-shape regexes. The same path can surface at multiple indices —
|
|
106
|
-
* each callsite is preserved so the ambiguity check runs against the cue at
|
|
107
|
-
* *that* index, not a different one.
|
|
108
|
-
*
|
|
109
|
-
* @param {string} body
|
|
110
|
-
* @returns {Array<{ path: string, index: number, matchLength: number }>}
|
|
111
|
-
*/
|
|
112
|
-
export function collectReferences(body) {
|
|
113
|
-
const refs = [];
|
|
114
|
-
for (const re of [BACKTICK_PATH_RE, COMMENT_HEADER_PATH_RE, BARE_PATH_RE]) {
|
|
115
|
-
re.lastIndex = 0;
|
|
116
|
-
let match = re.exec(body);
|
|
117
|
-
while (match !== null) {
|
|
118
|
-
const captured = match[1];
|
|
119
|
-
const captureIndex = match.index + match[0].indexOf(captured);
|
|
120
|
-
refs.push({
|
|
121
|
-
path: captured,
|
|
122
|
-
index: captureIndex,
|
|
123
|
-
matchLength: captured.length,
|
|
124
|
-
});
|
|
125
|
-
match = re.exec(body);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return refs;
|
|
129
|
-
}
|