@dev-loops/core 1.0.2-pre.0 → 1.0.2
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/package.json +3 -1
- package/src/analysis/diff-analyzer.mjs +85 -137
- package/src/claude/asset-generation.mjs +7 -7
- package/src/claude/hook-decisions.mjs +29 -36
- package/src/config/config.mjs +410 -787
- package/src/config/extension-defaults.yaml +17 -0
- package/src/github/closing-ref-guard.mjs +80 -0
- package/src/github/comment-id-guard.mjs +2 -2
- package/src/github/copilot-helpers.mjs +90 -158
- package/src/loop/bash-command-classify.mjs +34 -49
- package/src/loop/conductor-routing.mjs +15 -23
- package/src/loop/copilot-loop-state.mjs +46 -94
- package/src/loop/gate-carry-forward.mjs +2 -2
- package/src/loop/gate-fanin.mjs +252 -442
- package/src/loop/handoff-envelope.mjs +132 -25
- package/src/loop/issue-refinement-artifact.mjs +188 -263
- package/src/loop/lifecycle-state.mjs +10 -21
- package/src/loop/pr-gate-coordination.mjs +37 -37
- package/src/loop/queue-board-sync.mjs +16 -55
- package/src/loop/retrospective-checkpoint.mjs +7 -8
- package/src/loop/review-dispatch-plan.mjs +60 -122
- package/src/loop/review-lineage.mjs +19 -44
- package/src/loop/spec-authority.mjs +39 -69
- package/src/loop/steering.mjs +16 -68
- package/src/loop/worktree-guard.mjs +80 -13
- package/src/projects/list-queue-items.mjs +16 -146
- package/src/projects/move-queue-item.mjs +15 -141
- package/src/projects/projects-access.mjs +202 -0
|
@@ -1,36 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deterministic issue refinement-artifact detection.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* (the doc carries the matrix). Interactive issue-side Acceptance criteria /
|
|
12
|
-
* Definition of done CHECKLISTS are NO LONGER required merely to satisfy
|
|
13
|
-
* detection (#1951 AC1): the matrix is the authoritative issue artifact, and
|
|
14
|
-
* the PR carries the derived self-contained list-form AC/DoD checklists
|
|
15
|
-
* (`derivePrChecklistsFromIssueMatrix`; the PR body is validated by
|
|
4
|
+
* The authoritative refined-issue artifact is the semantic AC→DoD mapping
|
|
5
|
+
* MATRIX (a two-column table mapping each acceptance-criterion outcome to its
|
|
6
|
+
* required completion evidence) plus an explicit Non-goals section — or a
|
|
7
|
+
* linked refinement doc that is a complete artifact on its own (the doc carries
|
|
8
|
+
* the matrix). Interactive issue-side AC/DoD checklists are NOT a substitute
|
|
9
|
+
* for the matrix; the PR carries the derived self-contained list-form
|
|
10
|
+
* checklists (`derivePrChecklistsFromIssueMatrix`; the PR body is validated by
|
|
16
11
|
* `validateTrackerBackedPrBodySpec`, never this predicate).
|
|
17
12
|
*
|
|
18
13
|
* Detection validates the structural PRESENCE and SHAPE of the mapping table,
|
|
19
|
-
* not its semantic truthfulness (
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* Fix) cause the draft gate to post `verdict=blocked` with the
|
|
26
|
-
* `missing_refinement_artifact` finding.
|
|
27
|
-
*
|
|
28
|
-
* Migration (#1951 AC7/D7): existing checklist-bearing issues stay readable —
|
|
29
|
-
* the parser still extracts their AC/DoD checklist content — but a body that
|
|
30
|
-
* carries only checklists and no mapping matrix now fails closed with
|
|
31
|
-
* `missing_ac_dod_matrix` and is re-grilled (loop-grill synthesizes the
|
|
32
|
-
* matrix) rather than being silently grandfathered. No compatibility alias is
|
|
33
|
-
* retained.
|
|
14
|
+
* not its semantic truthfulness (a reviewer responsibility). A matrix that is
|
|
15
|
+
* absent, empty, malformed, or identifier-only fails closed with the matching
|
|
16
|
+
* finding (`missing_ac_dod_matrix`, `malformed_ac_dod_matrix`,
|
|
17
|
+
* `missing_explicit_non_goals`, or `missing_refinement_artifact`). A body
|
|
18
|
+
* carrying only checklists and no matrix fails closed and is re-grilled; no
|
|
19
|
+
* compatibility alias is retained.
|
|
34
20
|
*/
|
|
35
21
|
import { existsSync } from "node:fs";
|
|
36
22
|
import path from "node:path";
|
|
@@ -57,11 +43,9 @@ export const REFINEMENT_SOURCE = Object.freeze({
|
|
|
57
43
|
|
|
58
44
|
const REFINEMENT_ARTIFACT_FINDING = "missing_refinement_artifact";
|
|
59
45
|
|
|
60
|
-
// REFINEMENT_ARTIFACT_SOURCES: the
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// this list is the shape of a COMPLETE artifact, not a menu where any one
|
|
64
|
-
// entry suffices.
|
|
46
|
+
// REFINEMENT_ARTIFACT_SOURCES: the shape of a COMPLETE refinement artifact,
|
|
47
|
+
// not a menu where any one entry suffices. A linked refinement doc
|
|
48
|
+
// remains a complete artifact on its own.
|
|
65
49
|
export const REFINEMENT_ARTIFACT_SOURCES = Object.freeze([
|
|
66
50
|
"AC→DoD mapping matrix (a two-column table)",
|
|
67
51
|
"explicit Non-goals section",
|
|
@@ -69,30 +53,25 @@ export const REFINEMENT_ARTIFACT_SOURCES = Object.freeze([
|
|
|
69
53
|
]);
|
|
70
54
|
|
|
71
55
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* (`PR_BODY_SPEC_NARRATIVE_SECTIONS.non_goals.code`) so both spec surfaces
|
|
76
|
-
* name the missing invariant identically.
|
|
56
|
+
* Finding: refinement artifact present but no explicit Non-goals section.
|
|
57
|
+
* Mirrors the PR-path code (`PR_BODY_SPEC_NARRATIVE_SECTIONS.non_goals.code`)
|
|
58
|
+
* so both spec surfaces name the missing invariant identically.
|
|
77
59
|
*/
|
|
78
60
|
export const MISSING_EXPLICIT_NON_GOALS_FINDING = "missing_explicit_non_goals";
|
|
79
61
|
|
|
80
62
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* issue-side AC/DoD checklists are not a substitute for it. Fails closed so
|
|
86
|
-
* the issue is re-grilled to add the matrix.
|
|
63
|
+
* Finding: refinement content present but NO authoritative AC→DoD mapping
|
|
64
|
+
* matrix table. The mapping table is the authoritative issue artifact;
|
|
65
|
+
* issue-side checklists are not a substitute. Fails closed so the issue is
|
|
66
|
+
* re-grilled to add the matrix.
|
|
87
67
|
*/
|
|
88
68
|
export const MISSING_AC_DOD_MATRIX_FINDING = "missing_ac_dod_matrix";
|
|
89
69
|
|
|
90
70
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* truthfulness of the mapping stays a reviewer responsibility.
|
|
71
|
+
* Finding: an AC→DoD mapping table is present but empty (header/separator only)
|
|
72
|
+
* or identifier-only/tautological (cells such as `AC1 → D1` with no concrete
|
|
73
|
+
* criterion or evidence prose). Structural shape validation only — semantic
|
|
74
|
+
* truthfulness stays a reviewer responsibility.
|
|
96
75
|
*/
|
|
97
76
|
export const MALFORMED_AC_DOD_MATRIX_FINDING = "malformed_ac_dod_matrix";
|
|
98
77
|
|
|
@@ -104,47 +83,35 @@ export const MALFORMED_AC_DOD_MATRIX_FINDING = "malformed_ac_dod_matrix";
|
|
|
104
83
|
* - one DoD-style section (DoD or Definition of Done)
|
|
105
84
|
*/
|
|
106
85
|
const ACCEPTANCE_SECTION_PATTERNS = Object.freeze([
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
// (
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
// spelled-out phrase). The anchor stays distinct from the alias family below
|
|
113
|
-
// (`/^ac\b/`), so the precedence contract is unchanged: a spelled-out
|
|
114
|
-
// canonical heading always outranks an abbreviation-shaped alias heading.
|
|
86
|
+
// Index 0 is the exact-canonical ANCHOR (`^acceptance criteria\b`); the rest
|
|
87
|
+
// are aliases. A decorated-variant canonical heading (`## Acceptance criteria
|
|
88
|
+
// (v2)`) still lands in the exact bucket rather than matching no pattern. The
|
|
89
|
+
// anchor stays distinct from the `/^ac\b/` alias, so a spelled-out canonical
|
|
90
|
+
// heading always outranks an abbreviation-shaped alias heading.
|
|
115
91
|
/^acceptance criteria\b.*$/i,
|
|
116
92
|
/^ac\b.*$/i,
|
|
117
93
|
]);
|
|
118
94
|
|
|
119
95
|
const DOD_SECTION_PATTERNS = Object.freeze([
|
|
120
|
-
// Same anchor-family widening as the AC family
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
// disarms the PR-side DoD read and false-blocks the issue side).
|
|
96
|
+
// Same anchor-family widening as the AC family. A decorated-variant alias
|
|
97
|
+
// heading (`## DoD (v2)`) must land in the alias bucket, not in no bucket: a
|
|
98
|
+
// `$`-anchored alias silently disarms the PR-side DoD read and false-blocks
|
|
99
|
+
// the issue side.
|
|
125
100
|
/^definition of done\b.*$/i,
|
|
126
101
|
/^done\b.*$/i,
|
|
127
102
|
/^dod\b.*$/i,
|
|
128
103
|
]);
|
|
129
104
|
|
|
130
105
|
/**
|
|
131
|
-
* Normalize a heading name before section-pattern matching
|
|
132
|
-
*
|
|
133
|
-
* headings
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* still matches its alias family. Strips: surrounding emphasis runs of any
|
|
141
|
-
* of `*`/`_` (bold `**`/`__` and single-char italic `*`/`_` alike, #1877
|
|
142
|
-
* round-7), surrounding backtick runs, trailing `:` and surrounding
|
|
143
|
-
* whitespace.
|
|
144
|
-
* Deliberately NOT touched: interior text (a real `AC (v2) - final` name keeps
|
|
145
|
-
* its interior), leading `#` (ATX markers never reach `match[2]`), and any
|
|
146
|
-
* decoration a section pattern itself could rely on (none does — every family
|
|
147
|
-
* anchors at the name's start).
|
|
106
|
+
* Normalize a heading name before section-pattern matching. Strip
|
|
107
|
+
* harmless decoration once, at the parse boundary, so decorated canonical
|
|
108
|
+
* headings (`## **Acceptance criteria**`, `## Acceptance criteria:`) still
|
|
109
|
+
* match their pattern family instead of silently disarming the AC/DoD reads.
|
|
110
|
+
* Exact-vs-alias precedence stays intact: a normalized `Acceptance criteria`
|
|
111
|
+
* still matches the exact pattern, a decorated alias still matches its alias
|
|
112
|
+
* family. Strips surrounding emphasis/backtick runs (bold and single-char
|
|
113
|
+
* italic), trailing `:`, closing ATX `#`, and surrounding whitespace.
|
|
114
|
+
* NOT touched: interior text, and leading `#` (never reaches `match[2]`).
|
|
148
115
|
*/
|
|
149
116
|
function normalizeHeadingName(name) {
|
|
150
117
|
if (typeof name !== "string") return name;
|
|
@@ -152,30 +119,23 @@ function normalizeHeadingName(name) {
|
|
|
152
119
|
// trailing decoration first: closing `##` ATX-style, colons, whitespace
|
|
153
120
|
.replace(/\s*:*\s*$/u, "")
|
|
154
121
|
.replace(/\s*#+\s*$/u, "")
|
|
155
|
-
// surrounding emphasis/backtick runs (any length, must pair;
|
|
156
|
-
//
|
|
157
|
-
// `**`/`__`, so `## *Acceptance criteria*` and `## _Definition of done_`
|
|
158
|
-
// normalize exactly like their bold forms)
|
|
122
|
+
// surrounding emphasis/backtick runs (any length, must pair; a run may be
|
|
123
|
+
// single-char italic `*`/`_` as well as bold `**`/`__`)
|
|
159
124
|
.replace(/^[*_`]+/u, "")
|
|
160
125
|
.replace(/[*_`]+$/u, "")
|
|
161
126
|
.trim();
|
|
162
127
|
}
|
|
163
128
|
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
// each pattern family into [exact, aliases] by convention: pattern index 0
|
|
169
|
-
// is the exact canonical match, the rest are aliases.
|
|
129
|
+
// Exact canonical headings (pattern index 0 in each family) must outrank loose
|
|
130
|
+
// aliases (`/^ac\b/`, `/^dod\b/`) so a matrix-shaped heading (`## AC/DoD
|
|
131
|
+
// matrix`) can never hijack the canonical section read. Index 0 is the exact
|
|
132
|
+
// canonical match, the rest are aliases.
|
|
170
133
|
const EXACT_PATTERN_INDEX = 0;
|
|
171
134
|
|
|
172
135
|
/**
|
|
173
|
-
* Resolve the
|
|
174
|
-
* precedence
|
|
175
|
-
*
|
|
176
|
-
* (e.g. `## AC/DoD matrix` before `## Acceptance criteria`). When no exact
|
|
177
|
-
* match exists, the first alias match is returned (alias-only bodies keep
|
|
178
|
-
* working). Returns null when no section matches at all.
|
|
136
|
+
* Resolve the section matching a heading-pattern family with exact-first
|
|
137
|
+
* precedence: the first EXACT canonical match (index 0) wins over any
|
|
138
|
+
* earlier alias-only match. Falls back to the first alias match, else null.
|
|
179
139
|
*/
|
|
180
140
|
function findSectionByPatterns(sections, patterns) {
|
|
181
141
|
const exact = patterns[EXACT_PATTERN_INDEX];
|
|
@@ -195,10 +155,9 @@ function findSectionByPatterns(sections, patterns) {
|
|
|
195
155
|
}
|
|
196
156
|
|
|
197
157
|
/**
|
|
198
|
-
* Collect ALL sections matching a heading-pattern family, exact-first ordered
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* and union consumers (#1877 PR-body unchecked-box extraction) cannot drift.
|
|
158
|
+
* Collect ALL sections matching a heading-pattern family, exact-first ordered.
|
|
159
|
+
* Shares `findSectionByPatterns`'s precedence semantics so single-section and
|
|
160
|
+
* union consumers (PR-body unchecked-box extraction) cannot drift.
|
|
202
161
|
*/
|
|
203
162
|
function findAllSectionsByPatterns(sections, patterns) {
|
|
204
163
|
const exact = patterns[EXACT_PATTERN_INDEX];
|
|
@@ -220,28 +179,19 @@ function findAllSectionsByPatterns(sections, patterns) {
|
|
|
220
179
|
}
|
|
221
180
|
|
|
222
181
|
/**
|
|
223
|
-
* Flatten a section
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* to the next same-or-shallower heading. `parseMarkdownSections` terminates a
|
|
228
|
-
* section's `bodyLines` at ANY heading, which is correct for heading
|
|
229
|
-
* matching but hides unchecked boxes from consumers that must see ALL of a
|
|
230
|
-
* canonical section's boxes.
|
|
182
|
+
* Flatten a section into a body string that extends past `###` sub-headings
|
|
183
|
+
* by joining the section and every following DEEPER-level section up to the
|
|
184
|
+
* next same-or-shallower heading, so nested checklist items stay visible to
|
|
185
|
+
* consumers that must see ALL of a canonical section's boxes.
|
|
231
186
|
*/
|
|
232
187
|
function flattenSectionDeep(sections, startIndex) {
|
|
233
188
|
const start = sections[startIndex];
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
// (`###
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
// Only already-classified bodyLines are joined — a heading can never match
|
|
241
|
-
// any line-level grammar, and real boxes under sub-headings stay visible
|
|
242
|
-
// because their bodyLines still join normally. (Keeping a marker line is
|
|
243
|
-
// unnecessary: parseChecklistItems never needed the heading boundary to
|
|
244
|
-
// track fence state — body lines carry their own fences.)
|
|
189
|
+
// Anti-spoof: the raw sub-heading NAME is NEVER re-injected into the text the
|
|
190
|
+
// checklist parser re-parses. A fence-opening name (`### ``` `) would corrupt
|
|
191
|
+
// fence state and eat real boxes (fail-open); a checkbox-shaped name
|
|
192
|
+
// (`### - [ ] fake`) would count as a phantom unchecked item (fail-closed).
|
|
193
|
+
// Only already-classified bodyLines are joined; real boxes under sub-headings
|
|
194
|
+
// stay visible because their bodyLines still join normally.
|
|
245
195
|
const parts = [start.bodyLines.join("\n")];
|
|
246
196
|
for (let i = startIndex + 1; i < sections.length; i += 1) {
|
|
247
197
|
if (sections[i].level <= start.level) break;
|
|
@@ -260,7 +210,7 @@ function flattenSectionDeep(sections, startIndex) {
|
|
|
260
210
|
* CommonMark: an N-marker fence (``` or ~~~) closes only on a line of >= N
|
|
261
211
|
* markers of the SAME char with no info string. This is the single source of
|
|
262
212
|
* truth shared by parseMarkdownSections (headings) and extractChecklistItems
|
|
263
|
-
* (checkboxes) so the two anti-spoof layers cannot drift
|
|
213
|
+
* (checkboxes) so the two anti-spoof layers cannot drift.
|
|
264
214
|
*/
|
|
265
215
|
function stepFence(fence, line) {
|
|
266
216
|
const openMatch = /^\s*(`{3,}|~{3,})/u.exec(line);
|
|
@@ -287,7 +237,7 @@ function stepFence(fence, line) {
|
|
|
287
237
|
*
|
|
288
238
|
* Headings inside a fenced code span (``` or ~~~) are NOT treated as headings —
|
|
289
239
|
* otherwise a body could spoof the refinement/spec gate with real-looking
|
|
290
|
-
* headings that carry no real spec (gate integrity
|
|
240
|
+
* headings that carry no real spec (gate integrity).
|
|
291
241
|
*/
|
|
292
242
|
export function parseMarkdownSections(body) {
|
|
293
243
|
if (typeof body !== "string" || body.length === 0) {
|
|
@@ -313,11 +263,9 @@ export function parseMarkdownSections(body) {
|
|
|
313
263
|
}
|
|
314
264
|
current = {
|
|
315
265
|
level: match[1].length,
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
// longer re-injects it), so normalization is the only consumer of the
|
|
320
|
-
// capture — the raw form is not retained (no consumer reads it).
|
|
266
|
+
// Normalize the captured name so decorated canonical headings
|
|
267
|
+
// (`## **Acceptance criteria**`) match the section patterns. The raw
|
|
268
|
+
// form is never re-parsed as body text, so it is not retained.
|
|
321
269
|
name: normalizeHeadingName(match[2]),
|
|
322
270
|
bodyLines: [],
|
|
323
271
|
};
|
|
@@ -337,27 +285,21 @@ export function parseMarkdownSections(body) {
|
|
|
337
285
|
|
|
338
286
|
|
|
339
287
|
/**
|
|
340
|
-
* Parse bullet/checkbox items from a section body into item states.
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
* not counted, so a section of only unfilled placeholders reports as unrefined.
|
|
351
|
-
* Code-fenced lines are skipped (same fence logic as parseMarkdownSections,
|
|
352
|
-
* issue #1025) so a body cannot spoof the AC/DoD gate with code-fenced
|
|
353
|
-
* checkboxes.
|
|
288
|
+
* Parse bullet/checkbox items from a section body into item states. A checkbox
|
|
289
|
+
* item — any GFM/CommonMark task-list marker (`-`/`*`/`+` bullets, ordered
|
|
290
|
+
* `N.`/`N)`, blockquote-nested `> - [ ]`; parity with
|
|
291
|
+
* tick-verified-checkboxes.mjs) — becomes `{ text, checked }`, where `checked`
|
|
292
|
+
* is true only for a ticked `[x]`/`[X]` marker read from the captured marker
|
|
293
|
+
* group, never a whole-line re-test. A top-level plain bullet (`- text`, dash
|
|
294
|
+
* at column 0) becomes `{ text, checked: null }` — it has no checkbox. Empty
|
|
295
|
+
* placeholders (`- [ ]` with no text) are skipped. Code-fenced lines are
|
|
296
|
+
* skipped (same fence logic as parseMarkdownSections) so a body cannot spoof
|
|
297
|
+
* the AC/DoD gate with code-fenced checkboxes.
|
|
354
298
|
*
|
|
355
|
-
* Shared by `extractChecklistItems`
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
* `detectIssueRefinementArtifact`), so counting plain bullets is scoped to
|
|
360
|
-
* those sections and never affects prose sections.
|
|
299
|
+
* Shared by `extractChecklistItems` and `extractUncheckedChecklistItems` so the
|
|
300
|
+
* two never drift on what counts as an item or on the checkbox-state read.
|
|
301
|
+
* Only called on an already-recognized AC/DoD section, so counting
|
|
302
|
+
* plain bullets never affects prose sections.
|
|
361
303
|
*/
|
|
362
304
|
function parseChecklistItems(sectionBody) {
|
|
363
305
|
if (typeof sectionBody !== "string" || sectionBody.length === 0) {
|
|
@@ -374,20 +316,15 @@ function parseChecklistItems(sectionBody) {
|
|
|
374
316
|
if (step.insideFence) {
|
|
375
317
|
continue;
|
|
376
318
|
}
|
|
377
|
-
// Checklist item: GFM/CommonMark task-list markers (
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
// CAPTURED marker group of this single match — never a second whole-line
|
|
387
|
-
// re-test. An unanchored `/\[(?:[xX])\]/u.test(line)` reads an UNCHECKED box
|
|
388
|
-
// whose label text merely mentions `[x]` (e.g. `- [ ] verify [x] flags`) as
|
|
389
|
-
// checked, silently disarming the deterministic block — the exact
|
|
390
|
-
// fail-open class the marker-anchored pre-#1877 read could not produce.
|
|
319
|
+
// Checklist item: GFM/CommonMark task-list markers (`-`/`*`/`+` bullets,
|
|
320
|
+
// ordered `N.`/`N)`, blockquote-nested `> - [ ]`) — grammar parity with
|
|
321
|
+
// tick-verified-checkboxes.mjs so every form surfaced as unchecked is
|
|
322
|
+
// flippable by the tick tool. Push only when the line carries text, so
|
|
323
|
+
// empty placeholders are skipped. The tick state comes from the CAPTURED
|
|
324
|
+
// marker group, never a second whole-line re-test: an unanchored
|
|
325
|
+
// `/\[[xX]\]/.test(line)` would read an unchecked box whose label merely
|
|
326
|
+
// mentions `[x]` (`- [ ] verify [x] flags`) as checked, a fail-open the
|
|
327
|
+
// marker-anchored read cannot produce.
|
|
391
328
|
const checkboxMatch =
|
|
392
329
|
/^\s*(?:>|\s)*(?:[-*+]|\d+[.)])\s+\[([ xX])\](?:\s+(.+?))?\s*$/u.exec(line);
|
|
393
330
|
if (checkboxMatch) {
|
|
@@ -421,7 +358,7 @@ function parseChecklistItems(sectionBody) {
|
|
|
421
358
|
* are skipped. Returns the trimmed item text for each matching line; the
|
|
422
359
|
* checkbox state is not preserved (use `extractUncheckedChecklistItems` for
|
|
423
360
|
* that). Thin wrapper over `parseChecklistItems` so the text-only contract
|
|
424
|
-
* stays byte-identical to its
|
|
361
|
+
* stays byte-identical to its original shape.
|
|
425
362
|
*/
|
|
426
363
|
export function extractChecklistItems(sectionBody) {
|
|
427
364
|
return parseChecklistItems(sectionBody).map((item) => item.text);
|
|
@@ -431,7 +368,7 @@ export function extractChecklistItems(sectionBody) {
|
|
|
431
368
|
* Extract the text of UNCHECKED checkbox items (`- [ ]`) from a section body.
|
|
432
369
|
* A ticked box (`- [x]`/`- [X]`) and a plain bullet (no checkbox) are both
|
|
433
370
|
* excluded — only an actual unticked checkbox is an "unticked AC item"
|
|
434
|
-
* (
|
|
371
|
+
* (ACCEPT-CRITERIA-VERIFY-AND-REFLECT). Empty placeholders are skipped.
|
|
435
372
|
* Thin wrapper over `parseChecklistItems` so the unticked read never drifts
|
|
436
373
|
* from `extractChecklistItems` on what counts as a checklist item.
|
|
437
374
|
*/
|
|
@@ -442,7 +379,7 @@ export function extractUncheckedChecklistItems(sectionBody) {
|
|
|
442
379
|
}
|
|
443
380
|
|
|
444
381
|
// ---------------------------------------------------------------------------
|
|
445
|
-
// AC→DoD mapping matrix detection
|
|
382
|
+
// AC→DoD mapping matrix detection
|
|
446
383
|
// ---------------------------------------------------------------------------
|
|
447
384
|
// The authoritative refined-issue artifact is a semantic AC→DoD mapping table:
|
|
448
385
|
// a GFM pipe table whose rows map each acceptance-criterion outcome to its
|
|
@@ -461,14 +398,12 @@ const MATRIX_SECTION_PATTERNS = Object.freeze([
|
|
|
461
398
|
]);
|
|
462
399
|
|
|
463
400
|
// Header column families: col0 names the criterion side, col1 the evidence
|
|
464
|
-
// side.
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
// explicitly name acceptance criteria AND completion evidence / DoD qualify
|
|
401
|
+
// side. Recognizes an unheaded but clearly criterion→evidence table anywhere in
|
|
402
|
+
// the body. Kept STRONG on purpose: a generic status table like
|
|
403
|
+
// `| Outcome | Done |` must NOT be mistaken for the refinement matrix — only
|
|
404
|
+
// headers naming acceptance criteria AND completion evidence / DoD qualify
|
|
469
405
|
// without a matrix heading. A matrix under a weaker header still qualifies via
|
|
470
|
-
// its `## AC / DoD matrix` heading (MATRIX_SECTION_PATTERNS)
|
|
471
|
-
// loop-grill synthesis and the epic procedure both write.
|
|
406
|
+
// its `## AC / DoD matrix` heading (MATRIX_SECTION_PATTERNS).
|
|
472
407
|
const MATRIX_CRITERION_HEADER = /\b(criteri\w*|acceptance|ac)\b/i;
|
|
473
408
|
const MATRIX_EVIDENCE_HEADER = /\b(evidence|dod|definition of done)\b/i;
|
|
474
409
|
|
|
@@ -498,17 +433,33 @@ function cellProseWordCount(cell) {
|
|
|
498
433
|
}
|
|
499
434
|
|
|
500
435
|
// A matrix data row is semantic when BOTH mapped cells carry at least one real
|
|
501
|
-
// prose word
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
// is to reject bare identifiers, not to mandate a minimum verbosity (#1951
|
|
507
|
-
// draft_gate/Copilot review).
|
|
436
|
+
// prose word. This rejects identifier-only/tautological rows (`AC1 | D1`,
|
|
437
|
+
// `AC1 → D1`, `DoD`, empty cells) WITHOUT false-rejecting a terse-but-real
|
|
438
|
+
// mapping (`Feature works | Regression test added`). The threshold is
|
|
439
|
+
// deliberately >=1, not >=2: reject bare identifiers, do not mandate a minimum
|
|
440
|
+
// verbosity.
|
|
508
441
|
function rowIsSemantic(criterion, evidence) {
|
|
509
442
|
return cellProseWordCount(criterion) >= 1 && cellProseWordCount(evidence) >= 1;
|
|
510
443
|
}
|
|
511
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Resolve which columns hold the criterion and evidence by HEADER NAME, so a
|
|
447
|
+
* leading index column (`#`, `No`, `Idx`, empty, …) shifts the mapped columns
|
|
448
|
+
* off positions 0/1 without breaking detection. Returns `{ criterionCol,
|
|
449
|
+
* evidenceCol }` when both header families are named in distinct columns.
|
|
450
|
+
* Falls back to positions 0/1 for a matrix-heading table whose columns are not
|
|
451
|
+
* explicitly named (the pre-index-aware behavior); returns null otherwise.
|
|
452
|
+
*/
|
|
453
|
+
function resolveMatrixColumns(headerCells, underHeading) {
|
|
454
|
+
const criterionCol = headerCells.findIndex((h) => MATRIX_CRITERION_HEADER.test(h ?? ""));
|
|
455
|
+
const evidenceCol = headerCells.findIndex((h) => MATRIX_EVIDENCE_HEADER.test(h ?? ""));
|
|
456
|
+
if (criterionCol >= 0 && evidenceCol >= 0 && criterionCol !== evidenceCol) {
|
|
457
|
+
return { criterionCol, evidenceCol };
|
|
458
|
+
}
|
|
459
|
+
if (underHeading) return { criterionCol: 0, evidenceCol: 1 };
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
|
|
512
463
|
/**
|
|
513
464
|
* Parse every GFM pipe table in a Markdown body (skipping fenced code spans via
|
|
514
465
|
* the shared `stepFence`). Returns an array of
|
|
@@ -575,26 +526,27 @@ function parseMarkdownTables(body) {
|
|
|
575
526
|
*/
|
|
576
527
|
export function detectAcDodMatrix(body = "") {
|
|
577
528
|
const tables = parseMarkdownTables(body);
|
|
578
|
-
const candidates =
|
|
579
|
-
|
|
529
|
+
const candidates = [];
|
|
530
|
+
for (const t of tables) {
|
|
531
|
+
if (!Array.isArray(t.headerCells) || t.headerCells.length < 2) continue;
|
|
580
532
|
const underHeading = typeof t.heading === "string" &&
|
|
581
533
|
MATRIX_SECTION_PATTERNS.some((p) => p.test(t.heading));
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
return underHeading || headerNamesMap;
|
|
586
|
-
});
|
|
534
|
+
const cols = resolveMatrixColumns(t.headerCells, underHeading);
|
|
535
|
+
if (cols) candidates.push({ ...t, ...cols });
|
|
536
|
+
}
|
|
587
537
|
if (candidates.length === 0) {
|
|
588
538
|
return { found: false, valid: false, rowCount: 0, rows: [], reason: "No AC→DoD mapping matrix table found." };
|
|
589
539
|
}
|
|
590
540
|
// Prefer the first candidate that has >=1 semantic row; otherwise report the
|
|
591
541
|
// first candidate as malformed.
|
|
592
542
|
for (const table of candidates) {
|
|
543
|
+
const { criterionCol, evidenceCol } = table;
|
|
544
|
+
const minCells = Math.max(criterionCol, evidenceCol) + 1;
|
|
593
545
|
const semanticRows = [];
|
|
594
546
|
for (const cells of table.rows) {
|
|
595
|
-
if (cells.length <
|
|
596
|
-
const criterion = cells[
|
|
597
|
-
const evidence = cells[
|
|
547
|
+
if (cells.length < minCells) continue;
|
|
548
|
+
const criterion = cells[criterionCol] ?? "";
|
|
549
|
+
const evidence = cells[evidenceCol] ?? "";
|
|
598
550
|
if (rowIsSemantic(criterion, evidence)) {
|
|
599
551
|
semanticRows.push({ criterion, evidence });
|
|
600
552
|
}
|
|
@@ -623,7 +575,7 @@ export function detectAcDodMatrix(body = "") {
|
|
|
623
575
|
|
|
624
576
|
/**
|
|
625
577
|
* Project an issue's AC→DoD mapping matrix into self-contained list-form PR
|
|
626
|
-
* checklists
|
|
578
|
+
* checklists: the PR carries list-form Acceptance criteria and
|
|
627
579
|
* Definition of done checkboxes derived from the matrix — never a matrix/table,
|
|
628
580
|
* never checkboxes inside table cells. Accepts a pre-parsed `matrix` (from
|
|
629
581
|
* {@link detectAcDodMatrix}) or a raw `body` to parse. Fails closed on a
|
|
@@ -697,36 +649,21 @@ export function detectLinkedRefinementDoc(body) {
|
|
|
697
649
|
/**
|
|
698
650
|
* Detect the refinement artifact on a parsed issue body.
|
|
699
651
|
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
* `
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
* the FULL check passes (#1951: a valid AC→DoD mapping matrix plus an explicit
|
|
710
|
-
* Non-goals section, or a resolvable linked refinement doc plus Non-goals), so
|
|
711
|
-
* every `.hasACs` consumer (enqueue gate, draft gate, parked-items discovery,
|
|
712
|
-
* gate context) fails closed with no call-site change. `acItems`/`dodItems`
|
|
713
|
-
* stay populated for downstream consumers: from the issue's own checklist
|
|
714
|
-
* sections when present, otherwise projected from the matrix rows.
|
|
715
|
-
*
|
|
716
|
-
* `resolveLinkedDoc` (optional, #1866): a `(path) => boolean` callback used to
|
|
717
|
-
* verify that a linked `tmp/refinement/*.md` doc actually resolves (e.g.
|
|
718
|
-
* `existsSync`). Enforcement-point callers (enqueue gate, draft-gate
|
|
719
|
-
* linked-issue path) supply it; a linked doc found in the body then satisfies
|
|
720
|
-
* the artifact check only when the callback returns true. When the callback is
|
|
721
|
-
* not supplied the predicate stays pure/no-I/O and behavior is unchanged, and
|
|
722
|
-
* the `linkedDoc` result carries no `resolves` field. When supplied and the
|
|
723
|
-
* doc does not resolve, `linkedDoc.resolves === false` and the linked doc does
|
|
724
|
-
* not satisfy the artifact check (other artifact sources still count).
|
|
652
|
+
* The floor is a valid AC→DoD mapping matrix (or a resolvable linked refinement
|
|
653
|
+
* doc) AND an explicit, non-empty Non-goals section. A missing Non-goals
|
|
654
|
+
* section fails closed with the distinct finding
|
|
655
|
+
* `MISSING_EXPLICIT_NON_GOALS_FINDING`; its matcher is shared with
|
|
656
|
+
* `validatePrBodySpec` so the two spec surfaces cannot drift. `hasACs` is true
|
|
657
|
+
* only when the FULL check passes, so every `.hasACs` consumer fails closed with
|
|
658
|
+
* no call-site change. `acItems`/`dodItems` stay populated for downstream
|
|
659
|
+
* consumers: from the issue's own checklist sections when present, otherwise
|
|
660
|
+
* projected from the matrix rows.
|
|
725
661
|
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
662
|
+
* `resolveLinkedDoc` (optional): a `(path) => boolean` callback verifying that a
|
|
663
|
+
* linked `tmp/refinement/*.md` doc actually resolves (e.g. `existsSync`). When
|
|
664
|
+
* not supplied the predicate stays pure/no-I/O and `linkedDoc` carries no
|
|
665
|
+
* `resolves` field; when supplied and the doc does not resolve, the linked doc
|
|
666
|
+
* does not satisfy the check (other artifact sources still count).
|
|
730
667
|
*
|
|
731
668
|
* @param {object} input
|
|
732
669
|
* @param {string} [input.body] Raw issue body Markdown.
|
|
@@ -769,7 +706,7 @@ export function detectIssueRefinementArtifact({ body = "", issueNumber = null, r
|
|
|
769
706
|
const acceptanceSection = findSectionByPatterns(sections, ACCEPTANCE_SECTION_PATTERNS);
|
|
770
707
|
const dodSection = findSectionByPatterns(sections, DOD_SECTION_PATTERNS);
|
|
771
708
|
|
|
772
|
-
// CONSUMER-CONTRACT BOUNDARY (
|
|
709
|
+
// CONSUMER-CONTRACT BOUNDARY (intentional asymmetry): the issue-side
|
|
773
710
|
// reads above are strict — ONE exact-first section, NO deep flattening —
|
|
774
711
|
// while extractPrBodyUncheckedChecklistItems (PR side) unions ALL matching
|
|
775
712
|
// sections and deep-flattens past ### sub-headings. The issue side is a
|
|
@@ -784,7 +721,7 @@ export function detectIssueRefinementArtifact({ body = "", issueNumber = null, r
|
|
|
784
721
|
const acItems = acceptanceSection ? extractChecklistItems(acceptanceSection.bodyLines.join("\n")) : [];
|
|
785
722
|
// Unticked AC checkboxes (`- [ ]`) of the spec-of-record — the
|
|
786
723
|
// ACCEPT-CRITERIA-VERIFY-AND-REFLECT precondition a clean pre_approval_gate
|
|
787
|
-
// must refuse on
|
|
724
|
+
// must refuse on. Only actual unticked checkboxes count; a ticked
|
|
788
725
|
// box and a plain bullet (no checkbox) are both excluded.
|
|
789
726
|
const uncheckedAcItems = acceptanceSection ? extractUncheckedChecklistItems(acceptanceSection.bodyLines.join("\n")) : [];
|
|
790
727
|
const dodItems = dodSection ? extractChecklistItems(dodSection.bodyLines.join("\n")) : [];
|
|
@@ -796,7 +733,7 @@ export function detectIssueRefinementArtifact({ body = "", issueNumber = null, r
|
|
|
796
733
|
linkedDoc = { ...linkedDoc, resolves: linkedDocResolves };
|
|
797
734
|
}
|
|
798
735
|
|
|
799
|
-
//
|
|
736
|
+
// explicit Non-goals section required on a refined tracker-backed
|
|
800
737
|
// issue body — same matcher the PR-body spec path uses, so the two cannot
|
|
801
738
|
// drift. A heading-only or fenced-only section does not count
|
|
802
739
|
// (sectionHasBody anti-spoof).
|
|
@@ -804,7 +741,7 @@ export function detectIssueRefinementArtifact({ body = "", issueNumber = null, r
|
|
|
804
741
|
findSectionByPatterns(sections, PR_BODY_SPEC_NARRATIVE_SECTIONS.non_goals.patterns),
|
|
805
742
|
);
|
|
806
743
|
|
|
807
|
-
//
|
|
744
|
+
// the authoritative issue artifact is the AC→DoD mapping MATRIX, not
|
|
808
745
|
// duplicate interactive issue-side checklists. Detect its presence + shape.
|
|
809
746
|
const matrix = detectAcDodMatrix(body);
|
|
810
747
|
|
|
@@ -851,7 +788,7 @@ export function detectIssueRefinementArtifact({ body = "", issueNumber = null, r
|
|
|
851
788
|
};
|
|
852
789
|
}
|
|
853
790
|
|
|
854
|
-
// Matrix present but empty/malformed/identifier-only: fail closed
|
|
791
|
+
// Matrix present but empty/malformed/identifier-only: fail closed.
|
|
855
792
|
if (matrix.found && !matrix.valid) {
|
|
856
793
|
return {
|
|
857
794
|
...base,
|
|
@@ -893,8 +830,8 @@ export function detectIssueRefinementArtifact({ body = "", issueNumber = null, r
|
|
|
893
830
|
}
|
|
894
831
|
|
|
895
832
|
// Matrix absent. If the body carries AC/DoD checklist content it is a
|
|
896
|
-
// checklist-bearing issue missing the authoritative matrix
|
|
897
|
-
// migration
|
|
833
|
+
// checklist-bearing issue missing the authoritative matrix during
|
|
834
|
+
// migration: fail closed on the missing matrix so it is re-grilled. A body
|
|
898
835
|
// with no matrix and no AC/DoD content (prose-only, or only a Non-goals
|
|
899
836
|
// section / an unresolved linked-doc mention) stays the pre-existing
|
|
900
837
|
// missing_refinement_artifact.
|
|
@@ -925,7 +862,7 @@ export function detectIssueRefinementArtifact({ body = "", issueNumber = null, r
|
|
|
925
862
|
}
|
|
926
863
|
|
|
927
864
|
/**
|
|
928
|
-
* PR-body-as-spec invariant sections (
|
|
865
|
+
* PR-body-as-spec invariant sections (lightweight path).
|
|
929
866
|
*
|
|
930
867
|
* When a lightweight session uses the PR description itself as the
|
|
931
868
|
* spec-of-record (no committed phase/plan doc), the PR body must still carry
|
|
@@ -960,14 +897,13 @@ export const PR_BODY_SPEC_NARRATIVE_SECTIONS = Object.freeze({
|
|
|
960
897
|
/**
|
|
961
898
|
* GitHub's accepted closing-keyword issue references (close/closes/closed,
|
|
962
899
|
* fix/fixes/fixed, resolve/resolves/resolved), case-insensitive, followed by
|
|
963
|
-
* `#N` or the cross-repo `owner/repo#N` form.
|
|
964
|
-
* lightweight
|
|
965
|
-
* lightweight PRs merged without this and none auto-closed their issue).
|
|
900
|
+
* `#N` or the cross-repo `owner/repo#N` form. Required linkage on the PR body:
|
|
901
|
+
* five lightweight PRs merged without this and none auto-closed their issue.
|
|
966
902
|
*/
|
|
967
903
|
const CLOSING_ISSUE_REFERENCE_PATTERN =
|
|
968
904
|
/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:[\w.-]+\/[\w.-]+)?#(\d+)/giu;
|
|
969
905
|
|
|
970
|
-
function extractClosingIssueNumbers(body) {
|
|
906
|
+
export function extractClosingIssueNumbers(body) {
|
|
971
907
|
// Same fence-skip as sectionHasBody: a `Closes #N` line quoted inside a
|
|
972
908
|
// ```fenced``` example (e.g. a PR-template sample) must not spoof the gate.
|
|
973
909
|
let fence = null;
|
|
@@ -1000,7 +936,7 @@ function extractClosingIssueNumbers(body) {
|
|
|
1000
936
|
function sectionHasBody(section) {
|
|
1001
937
|
// A real body needs >=1 non-whitespace line OUTSIDE any fenced code span —
|
|
1002
938
|
// a section whose only content is a ```fenced``` block is treated as empty so
|
|
1003
|
-
// it cannot spoof the narrative-invariant gate (
|
|
939
|
+
// it cannot spoof the narrative-invariant gate (same stepFence as
|
|
1004
940
|
// parseMarkdownSections + extractChecklistItems).
|
|
1005
941
|
if (!section) return false;
|
|
1006
942
|
let fence = null;
|
|
@@ -1017,40 +953,29 @@ function sectionHasBody(section) {
|
|
|
1017
953
|
* Validate that a PR body carries every invariant required to serve as the
|
|
1018
954
|
* lightweight spec-of-record: Objective/why, in-scope, explicit non-goals,
|
|
1019
955
|
* testable Acceptance criteria (>=1 checklist item), Definition of done
|
|
1020
|
-
* (>=1 checklist item), Open questions/risks, and — unless
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
* (parseMarkdownSections / AC + DoD patterns / extractChecklistItems) so
|
|
1025
|
-
* there is no parallel validator. Fails closed: every missing invariant is
|
|
1026
|
-
* reported under its distinct `missing_*` code. Pure; no side effects.
|
|
956
|
+
* (>=1 checklist item), Open questions/risks, and — unless issue-less mode is
|
|
957
|
+
* requested — a GitHub closing-keyword issue reference. Reuses the generic
|
|
958
|
+
* markdown logic so there is no parallel validator. Fails closed: every missing
|
|
959
|
+
* invariant is reported under its distinct `missing_*` code. Pure; no I/O.
|
|
1027
960
|
*
|
|
1028
|
-
* Issue-less mode (`issueLess: true
|
|
1029
|
-
*
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
* so callers can tell "no issue expected" apart from "issue expected but
|
|
1035
|
-
* absent". `expectedIssue` and `issueLess` are mutually exclusive; callers
|
|
1036
|
-
* pick exactly one mode (tracker-backed, with or without a specific
|
|
1037
|
-
* expected issue) or issue-less — never both.
|
|
961
|
+
* Issue-less mode (`issueLess: true`): the closing-issue linkage flips from
|
|
962
|
+
* REQUIRED to FORBIDDEN (the PR is the sole artifact), failing closed under
|
|
963
|
+
* `unexpected_closing_issue_reference` — distinct from
|
|
964
|
+
* `missing_closing_issue_reference` (tracker-backed, the default).
|
|
965
|
+
* `expectedIssue` and `issueLess` are mutually exclusive; callers pick exactly
|
|
966
|
+
* one mode.
|
|
1038
967
|
*
|
|
1039
|
-
* `requireOpenQuestions` (default `true
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
1043
|
-
* contract") does not name one. Pass `false` (see
|
|
1044
|
-
* `validateTrackerBackedPrBodySpec` below) to skip the `missing_open_questions`
|
|
1045
|
-
* check without touching any other invariant — the lightweight caller's
|
|
1046
|
-
* default stays byte-identical.
|
|
968
|
+
* `requireOpenQuestions` (default `true`): the tracker-backed PR-description
|
|
969
|
+
* contract does not name an Open questions/risks section; pass `false` (see
|
|
970
|
+
* `validateTrackerBackedPrBodySpec`) to skip that check without touching any
|
|
971
|
+
* other invariant.
|
|
1047
972
|
*
|
|
1048
973
|
* @param {{ body?: string, expectedIssue?: number, issueLess?: boolean, requireOpenQuestions?: boolean }} input
|
|
1049
974
|
* @returns {{ checker: "validate-pr-body-spec", ok: boolean, errors: { code: string, message: string }[], sections: string[], acItems: string[], dodItems: string[], closesIssues: number[] }}
|
|
1050
975
|
*/
|
|
1051
976
|
|
|
1052
977
|
// ---------------------------------------------------------------------------
|
|
1053
|
-
// Grill sub-loop body predicates (GRILL-SUBLOOP
|
|
978
|
+
// Grill sub-loop body predicates (GRILL-SUBLOOP-*)
|
|
1054
979
|
// ---------------------------------------------------------------------------
|
|
1055
980
|
// The loop-grill skill writes its raw Q&A transcript and synthesis to an
|
|
1056
981
|
// ephemeral tmp artifact and keeps only the canonical synthesized sections
|
|
@@ -1170,8 +1095,8 @@ export function validatePrBodySpec({ body = "", expectedIssue = null, issueLess
|
|
|
1170
1095
|
|
|
1171
1096
|
/**
|
|
1172
1097
|
* Validate a TRACKER-BACKED PR's own body against the PR-description contract
|
|
1173
|
-
* (skills/docs/copilot-loop-operations.md "PR description contract"
|
|
1174
|
-
*
|
|
1098
|
+
* (skills/docs/copilot-loop-operations.md "PR description contract"):
|
|
1099
|
+
* Acceptance criteria + Definition of done checklists, an explicit
|
|
1175
1100
|
* Non-goals section, and a `Closes #N`/`Fixes #N` reference — regardless of
|
|
1176
1101
|
* whether the linked issue itself already carries a refinement artifact. A
|
|
1177
1102
|
* linked issue with real ACs is necessary but not sufficient: the PR body is
|
|
@@ -1194,7 +1119,7 @@ export function validateTrackerBackedPrBodySpec({ body = "", closingIssues = []
|
|
|
1194
1119
|
}
|
|
1195
1120
|
|
|
1196
1121
|
/**
|
|
1197
|
-
*
|
|
1122
|
+
* Extract the UNCHECKED AC/DoD checkbox items from a PR body's own
|
|
1198
1123
|
* Acceptance criteria / Definition of done checklists — the derived,
|
|
1199
1124
|
* self-contained checklist that mirrors the linked issue's AC/DoD/Non-goals
|
|
1200
1125
|
* matrix. Any unchecked `- [ ]` in those sections means an acceptance
|
|
@@ -1212,7 +1137,7 @@ export function validateTrackerBackedPrBodySpec({ body = "", closingIssues = []
|
|
|
1212
1137
|
* (same `parseMarkdownSections` + `extractUncheckedChecklistItems` seams as
|
|
1213
1138
|
* `detectIssueRefinementArtifact` / `validatePrBodySpec`) so no parallel
|
|
1214
1139
|
* parser can drift. Sections absent from the body contribute no items — the
|
|
1215
|
-
* draft-exit `validateTrackerBackedPrBodySpec` check
|
|
1140
|
+
* draft-exit `validateTrackerBackedPrBodySpec` check already owns
|
|
1216
1141
|
* requiring the sections to EXIST.
|
|
1217
1142
|
*
|
|
1218
1143
|
* @param {{ body?: string }} input
|
|
@@ -1225,7 +1150,7 @@ export function extractPrBodyUncheckedChecklistItems({ body = "" } = {}) {
|
|
|
1225
1150
|
const sections = parseMarkdownSections(body);
|
|
1226
1151
|
// Union the unchecked boxes across ALL sections matching each pattern
|
|
1227
1152
|
// family (exact-first ordered), flattening each section past its deeper
|
|
1228
|
-
// sub-headings
|
|
1153
|
+
// sub-headings: a body nesting ACs under `###` subsections, or
|
|
1229
1154
|
// repeating an AC/DoD heading, must not hide unchecked boxes from the
|
|
1230
1155
|
// deterministic completeness block. Deduped by text (same box re-read in a
|
|
1231
1156
|
// duplicate section is the same box).
|
|
@@ -1268,7 +1193,7 @@ export function decideEnqueueRefinementGate({ artifact, targetIsPickup, auto = f
|
|
|
1268
1193
|
if (!targetIsPickup || artifact.finding === null) {
|
|
1269
1194
|
return { action: "enqueue" };
|
|
1270
1195
|
}
|
|
1271
|
-
//
|
|
1196
|
+
// artifact present but the contract-mandated Non-goals section is
|
|
1272
1197
|
// absent/empty — a distinct failure with its own guidance.
|
|
1273
1198
|
if (artifact.finding === MISSING_EXPLICIT_NON_GOALS_FINDING) {
|
|
1274
1199
|
const reason =
|
|
@@ -1277,7 +1202,7 @@ export function decideEnqueueRefinementGate({ artifact, targetIsPickup, auto = f
|
|
|
1277
1202
|
"(rule ARTIFACT-TRACKER-ISSUE-REFINEMENT-FLOOR; e.g. run `/dev-loops:loop-grill <issue> --auto` (or `/loop-grill <issue> --auto` in the dev-loops repo itself)) — refusing to enqueue without an explicit Non-goals section.";
|
|
1278
1203
|
return { action: auto ? "divert" : "block", reason, missing: ["explicit Non-goals section"] };
|
|
1279
1204
|
}
|
|
1280
|
-
//
|
|
1205
|
+
// matrix present but empty/malformed/identifier-only — name the shape
|
|
1281
1206
|
// defect so the fix targets the mapping table, not a missing section.
|
|
1282
1207
|
if (artifact.finding === MALFORMED_AC_DOD_MATRIX_FINDING) {
|
|
1283
1208
|
const reason =
|
|
@@ -1286,7 +1211,7 @@ export function decideEnqueueRefinementGate({ artifact, targetIsPickup, auto = f
|
|
|
1286
1211
|
"(rule ARTIFACT-TRACKER-ISSUE-REFINEMENT-FLOOR; e.g. run `/dev-loops:loop-grill <issue> --auto` (or `/loop-grill <issue> --auto` in the dev-loops repo itself)) — refusing to enqueue on a malformed matrix.";
|
|
1287
1212
|
return { action: auto ? "divert" : "block", reason, missing: ["valid AC→DoD mapping matrix"] };
|
|
1288
1213
|
}
|
|
1289
|
-
//
|
|
1214
|
+
// matrix absent (whether or not the body carries duplicate issue-side
|
|
1290
1215
|
// checklists) — the mapping table is the authoritative issue artifact.
|
|
1291
1216
|
if (artifact.finding === MISSING_AC_DOD_MATRIX_FINDING) {
|
|
1292
1217
|
const reason =
|
|
@@ -1333,7 +1258,7 @@ export async function runPickupRefinementGate({ issueNumber, repo, env, runChild
|
|
|
1333
1258
|
throw new Error("Invalid JSON input");
|
|
1334
1259
|
}
|
|
1335
1260
|
const body = typeof bodyPayload?.body === "string" ? bodyPayload.body : "";
|
|
1336
|
-
//
|
|
1261
|
+
// a linked refinement doc satisfies the gate only when it actually
|
|
1337
1262
|
// resolves. Paths follow the `tmp/refinement/*.md` convention and are
|
|
1338
1263
|
// anchored to the caller's repo root (`repoRoot` option, falling back to
|
|
1339
1264
|
// process.cwd()) — never the ambient cwd of whichever subdirectory the
|