mandrel 2.57.0 → 2.58.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/story-worker.md +12 -11
- package/.agents/scripts/evidence-gate.js +17 -1
- package/.agents/scripts/lib/orchestration/code-review.js +7 -3
- package/.agents/scripts/lib/orchestration/pinned-identifier-lint.js +137 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +11 -3
- package/.agents/scripts/lib/orchestration/plan-persist/acceptance-handle-repair.js +107 -0
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +6 -1
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +14 -9
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +17 -7
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +15 -5
- package/.agents/scripts/lib/orchestration/review-base-ref.js +138 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +37 -5
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +6 -1
- package/.agents/scripts/lib/story-body/story-body.js +36 -2
- package/.agents/scripts/lib/templates/decomposer-prompts.js +53 -4
- package/.agents/scripts/lib/test-run-credit.js +23 -12
- package/.agents/workflows/helpers/deliver-digest.md +22 -15
- package/.agents/workflows/helpers/deliver-story-reference.md +31 -11
- package/.agents/workflows/helpers/deliver-story.md +6 -5
- package/.agents/workflows/helpers/plan-reference.md +35 -6
- package/.agents/workflows/mandrel-plan.md +6 -2
- package/docs/CHANGELOG.md +13 -0
- package/lib/cli/registry.js +98 -2
- package/package.json +1 -1
|
@@ -83,21 +83,22 @@ Do **not** re-read every file in `project.docsContextFiles`. Read the
|
|
|
83
83
|
digest your caller passes, then pull files on demand at the lines it
|
|
84
84
|
names. A null digest path means no docs mandate.
|
|
85
85
|
|
|
86
|
-
## Close gates — one
|
|
86
|
+
## Close gates — one credited run
|
|
87
87
|
|
|
88
88
|
`single-story-close.js` runs the canonical close-validation chain
|
|
89
89
|
(**typecheck, lint, test, format, maintainability, coverage, crap**) and is
|
|
90
90
|
the authoritative gate — do not pre-run it. The **one** exception is the
|
|
91
|
-
full suite: after the self-eval loop's last fix commit, run
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
the
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
91
|
+
full suite: after the self-eval loop's last fix commit, run it once in
|
|
92
|
+
`<workCwd>` through the depositor — `evidence-gate.js --standalone
|
|
93
|
+
--scope-id <storyId> --gate test --worktree <workCwd> -- npm test`. It runs
|
|
94
|
+
whatever `npm test` resolves to and stamps that, so the credit is earned on
|
|
95
|
+
any runner; a later commit voids it, and close captures coverage itself when
|
|
96
|
+
the CRAP gate needs an artifact. If the suite outruns the host's sync Bash
|
|
97
|
+
ceiling, dispatch it in the **background** — its completion notification
|
|
98
|
+
re-invokes you. Never spawn a task to poll or `sleep`-loop against it; a
|
|
99
|
+
waiter with a wrong condition outlives the agent. An exit code is never
|
|
100
|
+
evidence a gate did work — the **output** is: a bare `npm test` deposits
|
|
101
|
+
nothing unless it routes through mandrel's own runner, the only shape that
|
|
101
102
|
says so. Redraft rounds run the scoped projects for the roots you changed
|
|
102
103
|
plus `verify[]`, not the whole suite. Share `lint` / `typecheck` evidence
|
|
103
104
|
with close via `evidence-gate.js`; never stamp coverage / CRAP fresh any
|
|
@@ -269,7 +269,7 @@ runAsCli(import.meta.url, main, {
|
|
|
269
269
|
source: 'evidence-gate',
|
|
270
270
|
usage: {
|
|
271
271
|
invocation:
|
|
272
|
-
'node .agents/scripts/evidence-gate.js --scope-id <id> --gate <name> [--standalone] [--no-evidence] [--cwd <path>] [--worktree <path>]',
|
|
272
|
+
'node .agents/scripts/evidence-gate.js --scope-id <id> --gate <name> [--standalone] [--no-evidence] [--cwd <path>] [--worktree <path>] -- <cmd> [args...]',
|
|
273
273
|
summary:
|
|
274
274
|
'Run one named gate, reusing a prior evidence stamp for the same HEAD instead of re-running it.',
|
|
275
275
|
flags: [
|
|
@@ -282,6 +282,22 @@ runAsCli(import.meta.url, main, {
|
|
|
282
282
|
],
|
|
283
283
|
['--cwd <path>', 'Repository root (default: project root).'],
|
|
284
284
|
['--worktree <path>', 'Worktree the gate runs in.'],
|
|
285
|
+
[
|
|
286
|
+
'-- <cmd> [args...]',
|
|
287
|
+
'Required. The command this gate runs, passed through verbatim (never via a shell).',
|
|
288
|
+
],
|
|
289
|
+
],
|
|
290
|
+
notes: [
|
|
291
|
+
[
|
|
292
|
+
'Everything after the first `--` is the gate. The stamp therefore describes',
|
|
293
|
+
'what actually ran, whatever that is — which is how a project on any test',
|
|
294
|
+
'runner earns the close `test` credit:',
|
|
295
|
+
'',
|
|
296
|
+
' node .agents/scripts/evidence-gate.js --standalone --scope-id 4250 \\',
|
|
297
|
+
' --gate lint --worktree .worktrees/story-4250 -- npm run lint',
|
|
298
|
+
' node .agents/scripts/evidence-gate.js --standalone --scope-id 4250 \\',
|
|
299
|
+
' --gate test --worktree .worktrees/story-4250 -- npm test',
|
|
300
|
+
].join('\n'),
|
|
285
301
|
],
|
|
286
302
|
},
|
|
287
303
|
});
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
import { hasSurvivingCritical } from '../audit-suite/findings.js';
|
|
43
43
|
import { resolveConfig } from '../config-resolver.js';
|
|
44
44
|
import { computeChangeSet } from './change-set.js';
|
|
45
|
+
import { remoteBaseRef } from './review-base-ref.js';
|
|
45
46
|
import { deriveChangeLevel, resolveDepth } from './review-depth.js';
|
|
46
47
|
import {
|
|
47
48
|
collectProviderDegradations,
|
|
@@ -70,11 +71,14 @@ import { upsertStructuredComment } from './ticketing.js';
|
|
|
70
71
|
*/
|
|
71
72
|
|
|
72
73
|
/**
|
|
73
|
-
* Resolve the
|
|
74
|
-
*
|
|
74
|
+
* Resolve the base ref used when a caller omits `baseRef`. Remote-qualified,
|
|
75
|
+
* never the bare branch name — a caller that does not name a base must not
|
|
76
|
+
* silently inherit the local ref's drift (Story #5325). An unfetched remote
|
|
77
|
+
* then yields an unenumerable diff, which every downstream consumer already
|
|
78
|
+
* fails safe on, instead of a confidently-wrong wide one.
|
|
75
79
|
*/
|
|
76
80
|
function resolveConfigBase(config) {
|
|
77
|
-
return config?.project?.baseBranch ?? 'main';
|
|
81
|
+
return remoteBaseRef(config?.project?.baseBranch ?? 'main');
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
/** Positive-integer override, else the supplied default. */
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pinned-identifier-lint.js — the `pinned-identifier` advisory lint over a
|
|
3
|
+
* draft Story's `acceptance[]` (Story #5323).
|
|
4
|
+
*
|
|
5
|
+
* `acceptance[]` is the Story's **binding** contract and `changes[]` only an
|
|
6
|
+
* advisory sketch the deliverer may revise, so an acceptance item naming an
|
|
7
|
+
* internal symbol pins something the executor is free to rename out from
|
|
8
|
+
* under it. The story-author prompt has always said so; nothing surfaced a
|
|
9
|
+
* violation, which left the rule enforced only by the authoring model
|
|
10
|
+
* remembering it.
|
|
11
|
+
*
|
|
12
|
+
* The classifier lives in its own module because its vocabulary is its own:
|
|
13
|
+
* four token grammars and a call-suffix strip that the sibling prose lint in
|
|
14
|
+
* `plan-text-hygiene.js` shares nothing with.
|
|
15
|
+
*
|
|
16
|
+
* Advisory by contract: findings are deterministic text for the persist
|
|
17
|
+
* dry-run's warning list. They never gate persist and spawn nothing.
|
|
18
|
+
*
|
|
19
|
+
* Pure, synchronous, no I/O.
|
|
20
|
+
*
|
|
21
|
+
* @module lib/orchestration/pinned-identifier-lint
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* An inline code span carrying one of these is naming something other than a
|
|
26
|
+
* source identifier, and is never a pinned identifier:
|
|
27
|
+
*
|
|
28
|
+
* - `/` — a file path or a glob (`src/app.js`, `tests/x/*.spec.ts`);
|
|
29
|
+
* - `.` — a dotted path, a filename, or a config key
|
|
30
|
+
* (`delivery.routing.closeAndLand`, `story-body.js`);
|
|
31
|
+
* - `:` — a label (`agent::ready`, `type::story`);
|
|
32
|
+
* - `-` — a kebab token: a `data-testid` value, a slug, a package name, or
|
|
33
|
+
* a CLI flag (`--dry-run`);
|
|
34
|
+
* - whitespace — an argv shape, so a command (`npm run lint`);
|
|
35
|
+
* - `[` / `]` — a field reference (`acceptance[]`).
|
|
36
|
+
*/
|
|
37
|
+
const NON_IDENTIFIER_MARKERS = /[/.:\-\s[\]]/;
|
|
38
|
+
|
|
39
|
+
/** A bare source identifier, with an optional call suffix stripped. */
|
|
40
|
+
const BARE_IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* An UPPER_SNAKE token. Exempt by contract: an environment variable and an
|
|
44
|
+
* internal constant are the same shape, and warning on every `DATABASE_URL`
|
|
45
|
+
* to catch the occasional pinned constant trades a real class of false
|
|
46
|
+
* positives for a marginal gain.
|
|
47
|
+
*/
|
|
48
|
+
const UPPER_SNAKE_RE = /^[A-Z0-9_$]+$/;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A case transition (`staffCan`, `MyCalendarBoard`, `TimeGrid`) — the
|
|
52
|
+
* signature that separates a source identifier from a prose word a criterion
|
|
53
|
+
* legitimately quotes (`landed`, `pending`, `main`).
|
|
54
|
+
*/
|
|
55
|
+
const CASE_TRANSITION_RE = /[a-z][A-Z]/;
|
|
56
|
+
|
|
57
|
+
/** The remedy, either side of the identifiers a finding names. */
|
|
58
|
+
const MESSAGE_HEAD = 'Acceptance item pins the internal identifier(s) ';
|
|
59
|
+
const MESSAGE_TAIL =
|
|
60
|
+
'; `changes[]` is an advisory sketch the deliverer may reshape, so assert ' +
|
|
61
|
+
'the observable behaviour instead of the symbol that implements it.';
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Collect the inline code spans of one acceptance item.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} item
|
|
67
|
+
* @returns {string[]}
|
|
68
|
+
*/
|
|
69
|
+
function codeSpans(item) {
|
|
70
|
+
return [...String(item ?? '').matchAll(/`([^`\n]+)`/g)].map((m) => m[1]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Decide whether one code span pins a source identifier: a bare token
|
|
75
|
+
* carrying a case transition, with no separator that would make it a path, a
|
|
76
|
+
* label, a kebab token, a flag or a command, and not an UPPER_SNAKE name.
|
|
77
|
+
*
|
|
78
|
+
* Deliberately conservative in one direction only: a false positive costs a
|
|
79
|
+
* warning line the author dismisses, while a false negative on a path, a
|
|
80
|
+
* testid or a command would train the author to ignore the lint.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} span
|
|
83
|
+
* @returns {boolean}
|
|
84
|
+
*/
|
|
85
|
+
function isPinnedIdentifier(span) {
|
|
86
|
+
const token = span.trim().replace(/\(\s*\)$/, '');
|
|
87
|
+
return (
|
|
88
|
+
!NON_IDENTIFIER_MARKERS.test(token) &&
|
|
89
|
+
BARE_IDENTIFIER_RE.test(token) &&
|
|
90
|
+
!UPPER_SNAKE_RE.test(token) &&
|
|
91
|
+
CASE_TRANSITION_RE.test(token)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The authored `acceptance[]` of one draft Story.
|
|
97
|
+
*
|
|
98
|
+
* It is authored at the ticket's top level — the machine contract the
|
|
99
|
+
* validators read — and synced into the body only at assembly, so the top
|
|
100
|
+
* level wins; the parsed body covers a draft that carries it inline.
|
|
101
|
+
*
|
|
102
|
+
* @param {object} story The raw draft ticket.
|
|
103
|
+
* @param {object} body Its parsed body.
|
|
104
|
+
* @returns {string[]}
|
|
105
|
+
*/
|
|
106
|
+
function resolveAcceptance(story, body) {
|
|
107
|
+
if (Array.isArray(story?.acceptance)) return story.acceptance;
|
|
108
|
+
return Array.isArray(body?.acceptance) ? body.acceptance : [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Evaluate the lint over one draft Story — one finding per offending
|
|
113
|
+
* acceptance item, naming every identifier it pinned.
|
|
114
|
+
*
|
|
115
|
+
* @param {object} story The raw draft ticket.
|
|
116
|
+
* @param {object} body Its parsed body.
|
|
117
|
+
* @param {string} slug
|
|
118
|
+
* @param {(text: string) => string} excerpt Evidence truncator, shared with
|
|
119
|
+
* the sibling lints so every finding excerpts alike.
|
|
120
|
+
* @returns {Array<{ kind: 'pinned-identifier', slug: string, evidence: string, message: string }>}
|
|
121
|
+
*/
|
|
122
|
+
export function findPinnedIdentifiers(story, body, slug, excerpt) {
|
|
123
|
+
const acceptance = resolveAcceptance(story, body);
|
|
124
|
+
const findings = [];
|
|
125
|
+
for (const item of acceptance) {
|
|
126
|
+
const pinned = codeSpans(item).filter(isPinnedIdentifier);
|
|
127
|
+
if (pinned.length === 0) continue;
|
|
128
|
+
const named = pinned.map((name) => `\`${name}\``).join(', ');
|
|
129
|
+
findings.push({
|
|
130
|
+
kind: 'pinned-identifier',
|
|
131
|
+
slug,
|
|
132
|
+
evidence: excerpt(String(item ?? '')),
|
|
133
|
+
message: `${MESSAGE_HEAD}${named}${MESSAGE_TAIL}`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return findings;
|
|
137
|
+
}
|
|
@@ -29,6 +29,7 @@ import { parse as parseStoryBody } from '../story-body/story-body.js';
|
|
|
29
29
|
import {
|
|
30
30
|
renderStoryAuthorCore,
|
|
31
31
|
renderStorySplitRules,
|
|
32
|
+
ticketsModePromptField,
|
|
32
33
|
} from '../templates/decomposer-prompts.js';
|
|
33
34
|
import {
|
|
34
35
|
renderAcceptanceSpecSystemPrompt,
|
|
@@ -640,14 +641,21 @@ function withAdvisorySignals(complexitySignals, { config, cwd } = {}) {
|
|
|
640
641
|
* rules a planner reads only when the default-single split policy clears
|
|
641
642
|
* carried separately as `storySplitRules` (Story #5312).
|
|
642
643
|
*
|
|
643
|
-
*
|
|
644
|
+
* `storyTicketsRules` is the one mode-conditional field (Story #5323): it
|
|
645
|
+
* only means anything when the seed is an existing ticket, and an envelope
|
|
646
|
+
* that carries it in every mode teaches the author to look for a source
|
|
647
|
+
* ticket that a `--seed` run does not have.
|
|
648
|
+
*
|
|
649
|
+
* @param {{ mode?: string }} [args]
|
|
650
|
+
* @returns {{ spec: string, acceptance: string, story: string, storySplitRules: string, storyTicketsRules?: string }}
|
|
644
651
|
*/
|
|
645
|
-
export function buildSystemPrompts() {
|
|
652
|
+
export function buildSystemPrompts({ mode } = {}) {
|
|
646
653
|
return {
|
|
647
654
|
spec: renderTechSpecSystemPrompt(),
|
|
648
655
|
acceptance: renderAcceptanceSpecSystemPrompt(),
|
|
649
656
|
story: renderStoryAuthorCore(),
|
|
650
657
|
storySplitRules: renderStorySplitRules(),
|
|
658
|
+
...ticketsModePromptField(mode),
|
|
651
659
|
};
|
|
652
660
|
}
|
|
653
661
|
|
|
@@ -1007,7 +1015,7 @@ async function buildTicketsModeEnvelope({
|
|
|
1007
1015
|
memoryPoolAdvisory: authoring.memoryPoolAdvisory,
|
|
1008
1016
|
priorFeedback: authoring.priorFeedback,
|
|
1009
1017
|
ticketSchema: TICKET_SCHEMA_DESCRIPTOR,
|
|
1010
|
-
systemPrompts: buildSystemPrompts(),
|
|
1018
|
+
systemPrompts: buildSystemPrompts({ mode: 'tickets' }),
|
|
1011
1019
|
planState: null,
|
|
1012
1020
|
planProfile:
|
|
1013
1021
|
ticketIds.length === 1 ? 'story-default' : 'story-from-tickets',
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* acceptance-handle-repair.js — repair-before-judging for the `AC-<n>:`
|
|
3
|
+
* presentation handle on authored acceptance items (Story #5323).
|
|
4
|
+
*
|
|
5
|
+
* The handle belongs to the body renderer, which numbers each checkbox from
|
|
6
|
+
* its position in `acceptance[]` (`story-body.js`). An author planning from
|
|
7
|
+
* an existing ticket reads that rendered body as a template and carries the
|
|
8
|
+
* handle forward, so the persisted checkbox reads `- [ ] AC-1: AC-1: …`; a
|
|
9
|
+
* lettered handle copied out of a hand-edited source (`AC-14a:`) misnumbers
|
|
10
|
+
* the rest of the list against its own text.
|
|
11
|
+
*
|
|
12
|
+
* The correction is mechanical and total — strip the handle the renderer
|
|
13
|
+
* will re-apply — so this module applies it and **reports** it, exactly as
|
|
14
|
+
* `changes-repair.js` does for the `{ path, assumption }` formality. Charging
|
|
15
|
+
* the author a re-draft round to paste back text the strip already produced
|
|
16
|
+
* buys nothing.
|
|
17
|
+
*
|
|
18
|
+
* It lives beside the validator rather than inside it for the same reason
|
|
19
|
+
* `changes-repair.js` does: the validator's job is to judge, and mixing a
|
|
20
|
+
* mutating repair pass into a module of pure collectors muddies both.
|
|
21
|
+
* `persist-helpers.js#validateTickets` calls this first, then the validators.
|
|
22
|
+
*
|
|
23
|
+
* @module lib/orchestration/plan-persist/acceptance-handle-repair
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { stripAcceptanceHandle } from '../../story-body/story-body.js';
|
|
27
|
+
import { renderChangeRepair } from './changes-repair.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Strip the handle off one surface's `acceptance[]`, recording each distinct
|
|
31
|
+
* strip on `repairs`.
|
|
32
|
+
*
|
|
33
|
+
* @param {object} surface An object that may carry an `acceptance` array.
|
|
34
|
+
* @param {string} slug
|
|
35
|
+
* @param {Set<string>} reported Items already reported for this ticket.
|
|
36
|
+
* @param {object[]} repairs Accumulator, mutated.
|
|
37
|
+
* @returns {void}
|
|
38
|
+
*/
|
|
39
|
+
function repairSurface(surface, slug, reported, repairs) {
|
|
40
|
+
if (!Array.isArray(surface.acceptance)) return;
|
|
41
|
+
surface.acceptance = surface.acceptance.map((item) => {
|
|
42
|
+
const { text, stripped } = stripAcceptanceHandle(item);
|
|
43
|
+
if (!stripped) return item;
|
|
44
|
+
const from = String(item ?? '');
|
|
45
|
+
if (!reported.has(from)) {
|
|
46
|
+
reported.add(from);
|
|
47
|
+
repairs.push({ kind: 'acceptance-handle', slug, from, to: text });
|
|
48
|
+
}
|
|
49
|
+
return text;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Strip the presentation `AC-<n>:` handle off every authored `acceptance[]`
|
|
55
|
+
* item, on both surfaces that can carry one: the ticket's top-level array
|
|
56
|
+
* (the machine contract) and a structured body's.
|
|
57
|
+
*
|
|
58
|
+
* A serialized **string** body needs no pass of its own — `parse()` strips
|
|
59
|
+
* the handle with the same grammar this does, so the two surfaces converge
|
|
60
|
+
* on one list whichever carried the handle, and the contract sync (which
|
|
61
|
+
* fails closed on a disagreement) sees them agree.
|
|
62
|
+
*
|
|
63
|
+
* Mutates `tickets` in place — the persist pipeline threads this same array
|
|
64
|
+
* on to assembly. Total: a non-array argument and non-Story tickets are
|
|
65
|
+
* no-ops.
|
|
66
|
+
*
|
|
67
|
+
* @param {object[]} tickets
|
|
68
|
+
* @returns {Array<{ kind: 'acceptance-handle', slug: string, from: string, to: string }>}
|
|
69
|
+
*/
|
|
70
|
+
export function normalizeAcceptanceHandles(tickets) {
|
|
71
|
+
const repairs = [];
|
|
72
|
+
for (const ticket of Array.isArray(tickets) ? tickets : []) {
|
|
73
|
+
if (!ticket || typeof ticket !== 'object' || ticket.type !== 'story') {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const slug = ticket.slug ?? ticket.title ?? '<unknown>';
|
|
77
|
+
// A ticket normally carries the same list on both surfaces, so report
|
|
78
|
+
// each distinct item once — the operator reads one correction, not two.
|
|
79
|
+
const reported = new Set();
|
|
80
|
+
repairSurface(ticket, slug, reported, repairs);
|
|
81
|
+
const body = ticket.body;
|
|
82
|
+
if (body && typeof body === 'object') {
|
|
83
|
+
repairSurface(body, slug, reported, repairs);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return repairs;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Render one entry of the dry-run's mixed repair list.
|
|
91
|
+
*
|
|
92
|
+
* The list carries two kinds — a `changes[]` entry repaired by probing base,
|
|
93
|
+
* and an `acceptance[]` item whose handle was normalised off — and both are
|
|
94
|
+
* mechanical corrections the run applied on the author's behalf, so both
|
|
95
|
+
* belong on the one list the operator reads. This module owns the dispatch
|
|
96
|
+
* because it owns the newer kind: it renders its own line and delegates
|
|
97
|
+
* every other kind to `changes-repair.js`, so neither producer has to know
|
|
98
|
+
* the other's shape.
|
|
99
|
+
*
|
|
100
|
+
* @param {{ kind?: string, slug: string, from: string, to?: string }} repair
|
|
101
|
+
* @returns {string}
|
|
102
|
+
*/
|
|
103
|
+
export function renderRepair(repair) {
|
|
104
|
+
if (repair.kind !== 'acceptance-handle') return renderChangeRepair(repair);
|
|
105
|
+
const { slug, from, to } = repair;
|
|
106
|
+
return `Story "${slug}": acceptance[] item "${from}" carried an AC-<n> handle — normalised to "${to}"; the body renderer numbers the checkboxes.`;
|
|
107
|
+
}
|
|
@@ -261,7 +261,12 @@ function repairTicket(ticket, existsAtBase) {
|
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
/**
|
|
264
|
-
* Render one repair as the dry-run line the operator reads.
|
|
264
|
+
* Render one `changes[]` repair as the dry-run line the operator reads.
|
|
265
|
+
*
|
|
266
|
+
* The dry-run's repair list is mixed — an `acceptance[]` handle strip is
|
|
267
|
+
* reported on it too — but the dispatch across kinds lives in
|
|
268
|
+
* [`acceptance-handle-repair.js`](acceptance-handle-repair.js)`#renderRepair`,
|
|
269
|
+
* which delegates here for this kind. Each producer owns its own line.
|
|
265
270
|
*
|
|
266
271
|
* @param {{ slug: string, from: string, path: string, assumption: string, reason: string }} repair
|
|
267
272
|
* @returns {string}
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* checkout on CI has no local `main`), else nothing — a shallow checkout
|
|
11
11
|
* with no base at all skips the probes instead of reading every path as
|
|
12
12
|
* absent.
|
|
13
|
-
* - `validateTickets(tickets, config, opts)` —
|
|
14
|
-
* `
|
|
15
|
-
*
|
|
13
|
+
* - `validateTickets(tickets, config, opts)` — normalises the authored
|
|
14
|
+
* `acceptance[]` handles and repairs the mechanical `changes[]`
|
|
15
|
+
* formalities against the base branch, then runs the cross-link,
|
|
16
|
+
* freshness, and task-body validators in one pass.
|
|
16
17
|
*
|
|
17
18
|
* Story #5312 deleted the fan-out probe that lived here: the delete
|
|
18
19
|
* blast-radius count never refused a real plan, and the `git grep` it paid
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
import { gitSpawn } from '../../git-utils.js';
|
|
25
26
|
import { validateTaskBodies } from '../task-body-validator.js';
|
|
26
27
|
import { validateAndNormalizeTickets } from '../ticket-validator.js';
|
|
28
|
+
import { normalizeAcceptanceHandles } from './acceptance-handle-repair.js';
|
|
27
29
|
import { repairChangeEntries } from './changes-repair.js';
|
|
28
30
|
|
|
29
31
|
/**
|
|
@@ -167,13 +169,16 @@ function defineHidden(validated, extras) {
|
|
|
167
169
|
export function validateTickets(tickets, config, opts = {}) {
|
|
168
170
|
const baseBranch = resolveBaseBranchRef(config);
|
|
169
171
|
const baseBranchRef = resolveProbeRef({ baseBranch, cwd: opts.cwd });
|
|
170
|
-
const repairs =
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
const repairs = [
|
|
173
|
+
...normalizeAcceptanceHandles(tickets),
|
|
174
|
+
...repairChangeEntries(tickets, {
|
|
175
|
+
existsAtBase: makeExistsAtBase({
|
|
176
|
+
baseBranchRef,
|
|
177
|
+
cwd: opts.cwd,
|
|
178
|
+
gitRunner: opts.gitRunner,
|
|
179
|
+
}),
|
|
175
180
|
}),
|
|
176
|
-
|
|
181
|
+
];
|
|
177
182
|
const validated = validateAndNormalizeTickets(tickets, {
|
|
178
183
|
baseBranchRef: baseBranchRef ?? undefined,
|
|
179
184
|
gitRunner: opts.gitRunner,
|
|
@@ -62,8 +62,8 @@ import {
|
|
|
62
62
|
conflictFindingKey,
|
|
63
63
|
} from '../ticket-validator-conflicts.js';
|
|
64
64
|
import { upsertStructuredComment } from '../ticketing.js';
|
|
65
|
+
import { renderRepair } from './acceptance-handle-repair.js';
|
|
65
66
|
import { recordAuditFilings, withAuditLabels } from './audit-provenance.js';
|
|
66
|
-
import { renderChangeRepair } from './changes-repair.js';
|
|
67
67
|
import {
|
|
68
68
|
resolveContainerEpic,
|
|
69
69
|
resolveCrossPlanLinks,
|
|
@@ -146,7 +146,7 @@ function enforceTicketValidation(validated) {
|
|
|
146
146
|
);
|
|
147
147
|
}
|
|
148
148
|
const warnings = [
|
|
149
|
-
...(validated.repairs ?? []).map(
|
|
149
|
+
...(validated.repairs ?? []).map(renderRepair),
|
|
150
150
|
...(validated.warnings ?? []),
|
|
151
151
|
];
|
|
152
152
|
return {
|
|
@@ -170,18 +170,28 @@ function freshnessCounts(probeRef, warnings) {
|
|
|
170
170
|
return { stale: warnings.length, ambiguous: 0 };
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
/** How each text-hygiene finding kind names itself on the warning list. */
|
|
174
|
+
const TEXT_HYGIENE_LABELS = {
|
|
175
|
+
'open-question': 'open question in body',
|
|
176
|
+
'pinned-identifier': 'pinned identifier in acceptance[]',
|
|
177
|
+
};
|
|
178
|
+
|
|
173
179
|
/**
|
|
174
|
-
* The
|
|
180
|
+
* The advisory text-hygiene lints over the draft (Story #5312, #5323) — an
|
|
175
181
|
* operator-directed question persisted into a Story a non-interactive
|
|
176
|
-
* sub-agent executes
|
|
182
|
+
* sub-agent executes, and an acceptance item pinning an internal symbol the
|
|
183
|
+
* advisory `changes[]` may reshape. Warnings the dry-run lists, never
|
|
184
|
+
* refusals.
|
|
177
185
|
*
|
|
178
186
|
* @param {object[]} rawStories
|
|
179
187
|
* @returns {string[]}
|
|
180
188
|
*/
|
|
181
|
-
function
|
|
189
|
+
function collectTextHygieneWarnings(rawStories) {
|
|
182
190
|
return evaluateTextHygiene({ draftStories: rawStories }).findings.map(
|
|
183
191
|
(finding) =>
|
|
184
|
-
`Story "${finding.slug}":
|
|
192
|
+
`Story "${finding.slug}": ${
|
|
193
|
+
TEXT_HYGIENE_LABELS[finding.kind] ?? finding.kind
|
|
194
|
+
} — "${finding.evidence}". ${finding.message}`,
|
|
185
195
|
);
|
|
186
196
|
}
|
|
187
197
|
|
|
@@ -595,7 +605,7 @@ export async function runPlanPersist({
|
|
|
595
605
|
enforceTicketValidation(validated);
|
|
596
606
|
const warnings = [
|
|
597
607
|
...validationWarnings,
|
|
598
|
-
...
|
|
608
|
+
...collectTextHygieneWarnings(rawStories),
|
|
599
609
|
];
|
|
600
610
|
logWarnings(warnings);
|
|
601
611
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* plan-text-hygiene.js — the `open-question`
|
|
3
|
-
* (Story #4599; narrowed to one lint by Story #5312)
|
|
2
|
+
* plan-text-hygiene.js — the advisory draft-Story lints: `open-question`
|
|
3
|
+
* over body prose (Story #4599; narrowed to one lint by Story #5312) and
|
|
4
|
+
* `pinned-identifier` over `acceptance[]` (Story #5323).
|
|
4
5
|
*
|
|
5
6
|
* A Story is executed by a non-interactive sub-agent, so an operator-directed
|
|
6
7
|
* open question persisted into its body ("Flag if…", "TBD", "confirm with the
|
|
@@ -12,6 +13,11 @@
|
|
|
12
13
|
* lints with the critic gate that surfaced them: both scored prose shape the
|
|
13
14
|
* authoring model already judges, and neither ever changed a persisted body.
|
|
14
15
|
*
|
|
16
|
+
* `pinned-identifier` is the one lint that scores the **binding** half of the
|
|
17
|
+
* ticket; its classifier lives in
|
|
18
|
+
* [`pinned-identifier-lint.js`](pinned-identifier-lint.js), which shares no
|
|
19
|
+
* vocabulary with the prose heuristic here.
|
|
20
|
+
*
|
|
15
21
|
* Advisory by contract: findings are deterministic text for the dry-run's
|
|
16
22
|
* warning list. They never gate persist and spawn nothing.
|
|
17
23
|
*
|
|
@@ -24,6 +30,7 @@
|
|
|
24
30
|
*/
|
|
25
31
|
|
|
26
32
|
import { parse } from '../story-body/story-body.js';
|
|
33
|
+
import { findPinnedIdentifiers } from './pinned-identifier-lint.js';
|
|
27
34
|
|
|
28
35
|
/** Truncation length for the `evidence` excerpt on each finding. */
|
|
29
36
|
const EVIDENCE_MAX_CHARS = 160;
|
|
@@ -41,7 +48,7 @@ const OPEN_QUESTION_MARKERS = [
|
|
|
41
48
|
|
|
42
49
|
/**
|
|
43
50
|
* @typedef {Object} TextHygieneFinding
|
|
44
|
-
* @property {'open-question'} kind
|
|
51
|
+
* @property {'open-question'|'pinned-identifier'} kind
|
|
45
52
|
* @property {string} slug - The draft Story's slug ('' when absent).
|
|
46
53
|
* @property {string} evidence - Excerpt of the offending text.
|
|
47
54
|
* @property {string} message - Human-readable, re-author-actionable text.
|
|
@@ -123,7 +130,7 @@ function findOpenQuestions(prose, slug) {
|
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
/**
|
|
126
|
-
* Evaluate the
|
|
133
|
+
* Evaluate the advisory lints over a draft Story array.
|
|
127
134
|
*
|
|
128
135
|
* @param {{ draftStories?: Array<object>|null }} args - The draft
|
|
129
136
|
* `stories.json` array (raw Story objects with top-level `slug` /
|
|
@@ -146,7 +153,10 @@ export function evaluateTextHygiene({ draftStories = null } = {}) {
|
|
|
146
153
|
}
|
|
147
154
|
const goal = typeof body.goal === 'string' ? body.goal : '';
|
|
148
155
|
const spec = typeof body.spec === 'string' ? body.spec : '';
|
|
149
|
-
findings.push(
|
|
156
|
+
findings.push(
|
|
157
|
+
...findOpenQuestions([goal, spec].join('\n'), slug),
|
|
158
|
+
...findPinnedIdentifiers(story, body, slug, excerpt),
|
|
159
|
+
);
|
|
150
160
|
}
|
|
151
161
|
return { findings };
|
|
152
162
|
}
|