@dev-loops/core 1.0.1 → 1.0.2-pre.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/package.json +4 -2
- package/src/claude/hook-decisions.mjs +144 -20
- package/src/config/extension-defaults.yaml +14 -9
- package/src/github/comment-id-guard.mjs +38 -0
- package/src/github/gh.mjs +49 -0
- package/src/loop/commit-msg-guard.mjs +1 -1
- package/src/loop/gate-carry-forward.mjs +45 -21
- package/src/loop/gate-evidence-reconcile.mjs +75 -0
- package/src/loop/gate-fanin.mjs +34 -13
- package/src/loop/handoff-envelope.mjs +2 -2
- package/src/loop/issue-refinement-artifact.mjs +358 -99
- package/src/loop/pr-gate-coordination.mjs +13 -13
- package/src/loop/queue-board-sync.mjs +1 -28
- package/src/loop/review-dispatch-plan.mjs +1 -0
- package/src/loop/spec-authority.mjs +759 -0
- package/src/loop/ui-e2e-scoping.mjs +1 -0
- package/src/loop/worktree-guard.mjs +55 -0
- package/src/projects/list-queue-items.mjs +1 -30
- package/src/projects/move-queue-item.mjs +1 -30
- package/src/security/secret-scan.mjs +13 -1
|
@@ -2,16 +2,35 @@
|
|
|
2
2
|
* Deterministic issue refinement-artifact detection.
|
|
3
3
|
*
|
|
4
4
|
* Implements the bounded refinement check required by the draft gate per
|
|
5
|
-
* issue #532
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
5
|
+
* issue #532, reshaped by #1951 to honor "matrix on the issue, checklist on
|
|
6
|
+
* the PR" WITHOUT duplicate issue-side checklists: a draft PR cannot leave
|
|
7
|
+
* draft unless the linked issue carries the authoritative semantic AC→DoD
|
|
8
|
+
* mapping MATRIX (a real two-column table mapping each acceptance-criterion
|
|
9
|
+
* outcome to its required completion evidence) plus an explicit Non-goals
|
|
10
|
+
* section — or a linked refinement doc that is a complete artifact on its own
|
|
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
|
|
16
|
+
* `validateTrackerBackedPrBodySpec`, never this predicate).
|
|
17
|
+
*
|
|
18
|
+
* Detection validates the structural PRESENCE and SHAPE of the mapping table,
|
|
19
|
+
* not its semantic truthfulness (that stays a reviewer responsibility). An
|
|
20
|
+
* issue whose AC content, DoD content, and Non-goals are present but whose
|
|
21
|
+
* mapping table is absent, empty, malformed, or identifier-only fails closed
|
|
22
|
+
* (#1951 AC2) with the matching finding (`missing_ac_dod_matrix`,
|
|
23
|
+
* `malformed_ac_dod_matrix`, `missing_explicit_non_goals`, or
|
|
12
24
|
* `missing_refinement_artifact`); prose-only issues (Problem / Root Cause /
|
|
13
25
|
* Fix) cause the draft gate to post `verdict=blocked` with the
|
|
14
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.
|
|
15
34
|
*/
|
|
16
35
|
import { existsSync } from "node:fs";
|
|
17
36
|
import path from "node:path";
|
|
@@ -29,6 +48,7 @@ import path from "node:path";
|
|
|
29
48
|
*/
|
|
30
49
|
|
|
31
50
|
export const REFINEMENT_SOURCE = Object.freeze({
|
|
51
|
+
ISSUE_BODY_MATRIX: "issue-body-matrix",
|
|
32
52
|
ISSUE_BODY_AC: "issue-body-ac",
|
|
33
53
|
ISSUE_BODY_DOD: "issue-body-dod",
|
|
34
54
|
LINKED_DOC: "linked-doc",
|
|
@@ -37,13 +57,14 @@ export const REFINEMENT_SOURCE = Object.freeze({
|
|
|
37
57
|
|
|
38
58
|
const REFINEMENT_ARTIFACT_FINDING = "missing_refinement_artifact";
|
|
39
59
|
|
|
40
|
-
// REFINEMENT_ARTIFACT_SOURCES: the
|
|
41
|
-
//
|
|
42
|
-
// doc remains a complete artifact on its own) —
|
|
43
|
-
// COMPLETE artifact, not a menu where any one
|
|
60
|
+
// REFINEMENT_ARTIFACT_SOURCES: the refinement floor vocabulary (#1951). The
|
|
61
|
+
// floor is the authoritative AC→DoD mapping MATRIX plus an explicit Non-goals
|
|
62
|
+
// section (a linked refinement doc remains a complete artifact on its own) —
|
|
63
|
+
// this list is the shape of a COMPLETE artifact, not a menu where any one
|
|
64
|
+
// entry suffices.
|
|
44
65
|
export const REFINEMENT_ARTIFACT_SOURCES = Object.freeze([
|
|
45
|
-
"
|
|
46
|
-
"
|
|
66
|
+
"AC→DoD mapping matrix (a two-column table)",
|
|
67
|
+
"explicit Non-goals section",
|
|
47
68
|
"linked refinement doc",
|
|
48
69
|
]);
|
|
49
70
|
|
|
@@ -57,22 +78,23 @@ export const REFINEMENT_ARTIFACT_SOURCES = Object.freeze([
|
|
|
57
78
|
export const MISSING_EXPLICIT_NON_GOALS_FINDING = "missing_explicit_non_goals";
|
|
58
79
|
|
|
59
80
|
/**
|
|
60
|
-
* #
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
81
|
+
* #1951: finding reported when the issue body carries refinement content (AC
|
|
82
|
+
* content, DoD content, and/or a Non-goals section) but NO authoritative
|
|
83
|
+
* AC→DoD mapping matrix table. Under "matrix on the issue, checklist on the
|
|
84
|
+
* PR" the mapping table is the authoritative issue artifact; interactive
|
|
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.
|
|
66
87
|
*/
|
|
67
|
-
export const
|
|
88
|
+
export const MISSING_AC_DOD_MATRIX_FINDING = "missing_ac_dod_matrix";
|
|
68
89
|
|
|
69
90
|
/**
|
|
70
|
-
* #
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
91
|
+
* #1951: finding reported when the issue body carries an AC→DoD mapping table
|
|
92
|
+
* but it is empty (header/separator only, no data rows) or identifier-only /
|
|
93
|
+
* tautological (cells such as `AC1 → D1` with no concrete criterion or
|
|
94
|
+
* completion-evidence prose). Structural shape validation only — semantic
|
|
95
|
+
* truthfulness of the mapping stays a reviewer responsibility.
|
|
74
96
|
*/
|
|
75
|
-
export const
|
|
97
|
+
export const MALFORMED_AC_DOD_MATRIX_FINDING = "malformed_ac_dod_matrix";
|
|
76
98
|
|
|
77
99
|
/**
|
|
78
100
|
* Canonical list of section headings that satisfy the refinement check.
|
|
@@ -419,6 +441,214 @@ export function extractUncheckedChecklistItems(sectionBody) {
|
|
|
419
441
|
.map((item) => item.text);
|
|
420
442
|
}
|
|
421
443
|
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
// AC→DoD mapping matrix detection (#1951)
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
// The authoritative refined-issue artifact is a semantic AC→DoD mapping table:
|
|
448
|
+
// a GFM pipe table whose rows map each acceptance-criterion outcome to its
|
|
449
|
+
// required completion evidence. This is the "matrix on the issue" half of
|
|
450
|
+
// "matrix on the issue, checklist on the PR". Detection validates the table's
|
|
451
|
+
// PRESENCE and SHAPE only — its semantic truthfulness stays a reviewer duty.
|
|
452
|
+
|
|
453
|
+
// Heading families that name the mapping-matrix section. A qualifying table
|
|
454
|
+
// under one of these headings is treated as the matrix even when its column
|
|
455
|
+
// headers do not name criterion/evidence explicitly.
|
|
456
|
+
const MATRIX_SECTION_PATTERNS = Object.freeze([
|
|
457
|
+
/\bac\b.*\bdod\b.*\b(matrix|mapping|map)\b/i,
|
|
458
|
+
/\b(acceptance|criteri\w*)\b.*\b(matrix|mapping|map)\b/i,
|
|
459
|
+
/\bmapping (matrix|table)\b/i,
|
|
460
|
+
/\bac\s*(?:\/|→|->|to)\s*dod\b/i,
|
|
461
|
+
]);
|
|
462
|
+
|
|
463
|
+
// Header column families: col0 names the criterion side, col1 the evidence
|
|
464
|
+
// side. Used to recognize an unheaded (not under a matrix heading) but clearly
|
|
465
|
+
// criterion→evidence table anywhere in the body. Kept STRONG on purpose (#1951
|
|
466
|
+
// draft_gate correctness review): a generic status table like `| Outcome |
|
|
467
|
+
// Done |` must NOT be mistaken for the refinement matrix — only headers that
|
|
468
|
+
// explicitly name acceptance criteria AND completion evidence / DoD qualify
|
|
469
|
+
// without a matrix heading. A matrix under a weaker header still qualifies via
|
|
470
|
+
// its `## AC / DoD matrix` heading (MATRIX_SECTION_PATTERNS), which is what the
|
|
471
|
+
// loop-grill synthesis and the epic procedure both write.
|
|
472
|
+
const MATRIX_CRITERION_HEADER = /\b(criteri\w*|acceptance|ac)\b/i;
|
|
473
|
+
const MATRIX_EVIDENCE_HEADER = /\b(evidence|dod|definition of done)\b/i;
|
|
474
|
+
|
|
475
|
+
const TABLE_DELIMITER_RE = /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)+\|?\s*$/u;
|
|
476
|
+
|
|
477
|
+
/** Split one GFM table row into trimmed cell strings (drops leading/trailing pipes). */
|
|
478
|
+
function splitTableRow(line) {
|
|
479
|
+
let s = line.trim();
|
|
480
|
+
if (s.startsWith("|")) s = s.slice(1);
|
|
481
|
+
if (s.endsWith("|")) s = s.slice(0, -1);
|
|
482
|
+
// ponytail: no escaped-pipe (`\|`) handling — refined-issue matrix cells are
|
|
483
|
+
// short prose, not pipe-bearing code. Add a split-on-unescaped-pipe pass only
|
|
484
|
+
// if a real matrix cell ever needs a literal `|`.
|
|
485
|
+
return s.split("|").map((c) => c.trim());
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Count real prose words in a matrix cell: runs of >=3 letters that are not the
|
|
490
|
+
* `dod` identifier token. Bare identifiers (`AC1`, `D1`, `DoD`), arrows, and
|
|
491
|
+
* digits contribute nothing, so a tautological/identifier-only cell scores 0.
|
|
492
|
+
*/
|
|
493
|
+
function cellProseWordCount(cell) {
|
|
494
|
+
if (typeof cell !== "string") return 0;
|
|
495
|
+
const stripped = cell.replace(/[*_`]+/gu, " ");
|
|
496
|
+
const runs = stripped.match(/[A-Za-z]{3,}/gu) ?? [];
|
|
497
|
+
return runs.filter((w) => w.toLowerCase() !== "dod").length;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// A matrix data row is semantic when BOTH mapped cells carry at least one real
|
|
501
|
+
// prose word (a letter-run of >=3 chars, excluding the `dod` token). This
|
|
502
|
+
// rejects the identifier-only/tautological rows the contract names — `AC1 | D1`,
|
|
503
|
+
// `AC1 → D1`, `DoD`, and empty cells (all 0 prose words) — WITHOUT false-
|
|
504
|
+
// rejecting a legitimately terse-but-real mapping (e.g. `Feature works |
|
|
505
|
+
// Regression test added`). The threshold is deliberately >=1, not >=2: the goal
|
|
506
|
+
// is to reject bare identifiers, not to mandate a minimum verbosity (#1951
|
|
507
|
+
// draft_gate/Copilot review).
|
|
508
|
+
function rowIsSemantic(criterion, evidence) {
|
|
509
|
+
return cellProseWordCount(criterion) >= 1 && cellProseWordCount(evidence) >= 1;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Parse every GFM pipe table in a Markdown body (skipping fenced code spans via
|
|
514
|
+
* the shared `stepFence`). Returns an array of
|
|
515
|
+
* `{ heading, headerCells, rows }` where `rows` is the list of data rows (each
|
|
516
|
+
* an array of trimmed cell strings). A table is a header line containing `|`,
|
|
517
|
+
* a delimiter row (`|---|---|`), and >=0 data rows.
|
|
518
|
+
*/
|
|
519
|
+
function parseMarkdownTables(body) {
|
|
520
|
+
if (typeof body !== "string" || body.length === 0) return [];
|
|
521
|
+
const lines = body.split(/\r?\n/u);
|
|
522
|
+
const tables = [];
|
|
523
|
+
let fence = null;
|
|
524
|
+
let heading = null;
|
|
525
|
+
let i = 0;
|
|
526
|
+
while (i < lines.length) {
|
|
527
|
+
const step = stepFence(fence, lines[i]);
|
|
528
|
+
fence = step.fence;
|
|
529
|
+
if (step.insideFence) {
|
|
530
|
+
i += 1;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const headingMatch = /^(#{1,6})\s+(.+?)\s*$/u.exec(lines[i]);
|
|
534
|
+
if (headingMatch) {
|
|
535
|
+
heading = normalizeHeadingName(headingMatch[2]);
|
|
536
|
+
i += 1;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const header = lines[i];
|
|
540
|
+
const delim = lines[i + 1];
|
|
541
|
+
if (header.includes("|") && typeof delim === "string" && TABLE_DELIMITER_RE.test(delim)) {
|
|
542
|
+
const headerCells = splitTableRow(header);
|
|
543
|
+
const rows = [];
|
|
544
|
+
let j = i + 2;
|
|
545
|
+
while (j < lines.length) {
|
|
546
|
+
const rowStep = stepFence(fence, lines[j]);
|
|
547
|
+
// A table ends at the first non-fence line without a pipe, or a heading.
|
|
548
|
+
if (rowStep.insideFence) break;
|
|
549
|
+
if (!lines[j].includes("|") || /^#{1,6}\s+/u.test(lines[j])) break;
|
|
550
|
+
rows.push(splitTableRow(lines[j]));
|
|
551
|
+
j += 1;
|
|
552
|
+
}
|
|
553
|
+
tables.push({ heading, headerCells, rows });
|
|
554
|
+
i = j;
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
i += 1;
|
|
558
|
+
}
|
|
559
|
+
return tables;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Detect the authoritative AC→DoD mapping matrix in an issue body.
|
|
564
|
+
*
|
|
565
|
+
* A qualifying table has >=2 columns and EITHER sits under a matrix-named
|
|
566
|
+
* heading ({@link MATRIX_SECTION_PATTERNS}) OR names criterion/evidence-like
|
|
567
|
+
* columns in its header. The matrix is VALID when it carries at least one
|
|
568
|
+
* SEMANTIC data row (both mapped cells carry real prose — see
|
|
569
|
+
* {@link rowIsSemantic}); a header/separator-only table (no data rows) or a
|
|
570
|
+
* table whose rows are all identifier-only/tautological (`AC1 → D1`) is
|
|
571
|
+
* malformed.
|
|
572
|
+
*
|
|
573
|
+
* @param {string} [body]
|
|
574
|
+
* @returns {{ found: boolean, valid: boolean, rowCount: number, rows: { criterion: string, evidence: string }[], reason: string }}
|
|
575
|
+
*/
|
|
576
|
+
export function detectAcDodMatrix(body = "") {
|
|
577
|
+
const tables = parseMarkdownTables(body);
|
|
578
|
+
const candidates = tables.filter((t) => {
|
|
579
|
+
if (!Array.isArray(t.headerCells) || t.headerCells.length < 2) return false;
|
|
580
|
+
const underHeading = typeof t.heading === "string" &&
|
|
581
|
+
MATRIX_SECTION_PATTERNS.some((p) => p.test(t.heading));
|
|
582
|
+
const headerNamesMap =
|
|
583
|
+
MATRIX_CRITERION_HEADER.test(t.headerCells[0] ?? "") &&
|
|
584
|
+
MATRIX_EVIDENCE_HEADER.test(t.headerCells[1] ?? "");
|
|
585
|
+
return underHeading || headerNamesMap;
|
|
586
|
+
});
|
|
587
|
+
if (candidates.length === 0) {
|
|
588
|
+
return { found: false, valid: false, rowCount: 0, rows: [], reason: "No AC→DoD mapping matrix table found." };
|
|
589
|
+
}
|
|
590
|
+
// Prefer the first candidate that has >=1 semantic row; otherwise report the
|
|
591
|
+
// first candidate as malformed.
|
|
592
|
+
for (const table of candidates) {
|
|
593
|
+
const semanticRows = [];
|
|
594
|
+
for (const cells of table.rows) {
|
|
595
|
+
if (cells.length < 2) continue;
|
|
596
|
+
const criterion = cells[0] ?? "";
|
|
597
|
+
const evidence = cells[1] ?? "";
|
|
598
|
+
if (rowIsSemantic(criterion, evidence)) {
|
|
599
|
+
semanticRows.push({ criterion, evidence });
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (semanticRows.length > 0) {
|
|
603
|
+
return {
|
|
604
|
+
found: true,
|
|
605
|
+
valid: true,
|
|
606
|
+
rowCount: semanticRows.length,
|
|
607
|
+
rows: semanticRows,
|
|
608
|
+
reason: `Found an AC→DoD mapping matrix with ${semanticRows.length} semantic row(s).`,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const dataRowCount = candidates[0].rows.length;
|
|
613
|
+
return {
|
|
614
|
+
found: true,
|
|
615
|
+
valid: false,
|
|
616
|
+
rowCount: 0,
|
|
617
|
+
rows: [],
|
|
618
|
+
reason: dataRowCount === 0
|
|
619
|
+
? "AC→DoD mapping matrix table is empty (header/separator only, no data rows)."
|
|
620
|
+
: "AC→DoD mapping matrix table is identifier-only/tautological (no row maps a concrete criterion to concrete completion evidence).",
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Project an issue's AC→DoD mapping matrix into self-contained list-form PR
|
|
626
|
+
* checklists (#1951 AC4): the PR carries list-form Acceptance criteria and
|
|
627
|
+
* Definition of done checkboxes derived from the matrix — never a matrix/table,
|
|
628
|
+
* never checkboxes inside table cells. Accepts a pre-parsed `matrix` (from
|
|
629
|
+
* {@link detectAcDodMatrix}) or a raw `body` to parse. Fails closed on a
|
|
630
|
+
* missing/malformed matrix rather than emitting empty checklists.
|
|
631
|
+
*
|
|
632
|
+
* @param {{ matrix?: ReturnType<typeof detectAcDodMatrix>, body?: string }} input
|
|
633
|
+
* @returns {{ acChecklist: string[], dodChecklist: string[], markdown: string }}
|
|
634
|
+
*/
|
|
635
|
+
export function derivePrChecklistsFromIssueMatrix({ matrix = null, body = "" } = {}) {
|
|
636
|
+
const m = matrix ?? detectAcDodMatrix(body);
|
|
637
|
+
if (!m || !m.found || !m.valid || !Array.isArray(m.rows) || m.rows.length === 0) {
|
|
638
|
+
throw Object.assign(
|
|
639
|
+
new Error(`derivePrChecklistsFromIssueMatrix: ${m?.reason ?? "no valid AC→DoD mapping matrix to project"}`),
|
|
640
|
+
{ code: "MALFORMED_MATRIX_SOURCE" },
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
const dedupe = (items) => [...new Set(items.map((s) => s.trim()).filter((s) => s.length > 0))];
|
|
644
|
+
const acChecklist = dedupe(m.rows.map((r) => r.criterion));
|
|
645
|
+
const dodChecklist = dedupe(m.rows.map((r) => r.evidence));
|
|
646
|
+
const render = (heading, items) =>
|
|
647
|
+
`## ${heading}\n\n${items.map((t) => `- [ ] ${t}`).join("\n")}\n`;
|
|
648
|
+
const markdown = `${render("Acceptance criteria", acChecklist)}\n${render("Definition of done", dodChecklist)}`;
|
|
649
|
+
return { acChecklist, dodChecklist, markdown };
|
|
650
|
+
}
|
|
651
|
+
|
|
422
652
|
/**
|
|
423
653
|
* Detect a linked refinement doc path from the issue body.
|
|
424
654
|
* Looks for explicit `tmp/refinement/<n>-plan.md` style paths and the
|
|
@@ -476,9 +706,12 @@ export function detectLinkedRefinementDoc(body) {
|
|
|
476
706
|
* `validatePrBodySpec` (`PR_BODY_SPEC_NARRATIVE_SECTIONS.non_goals.patterns`),
|
|
477
707
|
* so the two spec surfaces cannot drift on what counts as an explicit
|
|
478
708
|
* Non-goals section. `hasACs` keeps its caller-facing meaning: true only when
|
|
479
|
-
* the FULL check passes
|
|
480
|
-
*
|
|
481
|
-
*
|
|
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.
|
|
482
715
|
*
|
|
483
716
|
* `resolveLinkedDoc` (optional, #1866): a `(path) => boolean` callback used to
|
|
484
717
|
* verify that a linked `tmp/refinement/*.md` doc actually resolves (e.g.
|
|
@@ -508,6 +741,7 @@ export function detectLinkedRefinementDoc(body) {
|
|
|
508
741
|
* dodItems: string[],
|
|
509
742
|
* sections: string[],
|
|
510
743
|
* linkedDoc: { found: boolean, path: string|null, reason: string, resolves?: boolean },
|
|
744
|
+
* matrix: { found: boolean, valid: boolean, rowCount: number, rows: { criterion: string, evidence: string }[], reason: string },
|
|
511
745
|
* reason: string,
|
|
512
746
|
* finding: string|null,
|
|
513
747
|
* }}
|
|
@@ -523,7 +757,8 @@ export function detectIssueRefinementArtifact({ body = "", issueNumber = null, r
|
|
|
523
757
|
dodItems: [],
|
|
524
758
|
sections: [],
|
|
525
759
|
linkedDoc: { found: false, path: null, reason: "empty-body" },
|
|
526
|
-
|
|
760
|
+
matrix: { found: false, valid: false, rowCount: 0, rows: [], reason: "empty-body" },
|
|
761
|
+
reason: "Issue body is empty; no matrix/ACs/DoD/linked-doc can be detected.",
|
|
527
762
|
finding: REFINEMENT_ARTIFACT_FINDING,
|
|
528
763
|
};
|
|
529
764
|
}
|
|
@@ -569,13 +804,15 @@ export function detectIssueRefinementArtifact({ body = "", issueNumber = null, r
|
|
|
569
804
|
findSectionByPatterns(sections, PR_BODY_SPEC_NARRATIVE_SECTIONS.non_goals.patterns),
|
|
570
805
|
);
|
|
571
806
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
807
|
+
// #1951: the authoritative issue artifact is the AC→DoD mapping MATRIX, not
|
|
808
|
+
// duplicate interactive issue-side checklists. Detect its presence + shape.
|
|
809
|
+
const matrix = detectAcDodMatrix(body);
|
|
810
|
+
|
|
811
|
+
// Keep acItems/dodItems populated for downstream consumers (gate context,
|
|
812
|
+
// coordination state) even when the issue carries only the matrix and no
|
|
813
|
+
// interactive checklists: project the matrix rows into AC/DoD items.
|
|
814
|
+
const effectiveAcItems = acItems.length > 0 ? acItems : matrix.rows.map((r) => r.criterion.trim()).filter(Boolean);
|
|
815
|
+
const effectiveDodItems = dodItems.length > 0 ? dodItems : matrix.rows.map((r) => r.evidence.trim()).filter(Boolean);
|
|
579
816
|
|
|
580
817
|
const base = {
|
|
581
818
|
hasNonGoals,
|
|
@@ -584,76 +821,97 @@ export function detectIssueRefinementArtifact({ body = "", issueNumber = null, r
|
|
|
584
821
|
dodItems,
|
|
585
822
|
sections: sectionNames,
|
|
586
823
|
linkedDoc,
|
|
824
|
+
matrix,
|
|
587
825
|
};
|
|
588
826
|
|
|
589
|
-
|
|
827
|
+
// A linked refinement doc remains a complete artifact on its own (the doc
|
|
828
|
+
// carries the matrix). It still requires an explicit Non-goals section.
|
|
829
|
+
if (linkedDocResolves) {
|
|
590
830
|
if (!hasNonGoals) {
|
|
591
831
|
return {
|
|
592
832
|
...base,
|
|
593
833
|
hasACs: false,
|
|
594
|
-
source:
|
|
834
|
+
source: REFINEMENT_SOURCE.LINKED_DOC,
|
|
595
835
|
reason:
|
|
596
|
-
|
|
836
|
+
"Issue body links a refinement doc but has no explicit Non-goals section; " +
|
|
597
837
|
"the tracker-backed refinement contract requires one (rule ARTIFACT-TRACKER-ISSUE-REFINEMENT-FLOOR; " +
|
|
598
838
|
"e.g. run the loop-grill synthesis). Refusing: the refinement check fails closed without an explicit Non-goals section.",
|
|
599
839
|
finding: MISSING_EXPLICIT_NON_GOALS_FINDING,
|
|
600
840
|
};
|
|
601
841
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
842
|
+
return {
|
|
843
|
+
...base,
|
|
844
|
+
hasACs: true,
|
|
845
|
+
source: REFINEMENT_SOURCE.LINKED_DOC,
|
|
846
|
+
acItems: [],
|
|
847
|
+
uncheckedAcItems: [],
|
|
848
|
+
dodItems: [],
|
|
849
|
+
reason: `Issue body links a refinement doc at ${linkedDoc.path}; treating that as the refinement artifact source.`,
|
|
850
|
+
finding: null,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Matrix present but empty/malformed/identifier-only: fail closed (#1951 AC2).
|
|
855
|
+
if (matrix.found && !matrix.valid) {
|
|
856
|
+
return {
|
|
857
|
+
...base,
|
|
858
|
+
hasACs: false,
|
|
859
|
+
source: REFINEMENT_SOURCE.ISSUE_BODY_MATRIX,
|
|
860
|
+
reason:
|
|
861
|
+
`Issue body carries an AC→DoD mapping matrix but it is not a valid semantic mapping (${matrix.reason}); ` +
|
|
862
|
+
"the refinement contract requires a real criterion→completion-evidence mapping " +
|
|
863
|
+
"(rule ARTIFACT-TRACKER-ISSUE-REFINEMENT-FLOOR; e.g. run the loop-grill synthesis). " +
|
|
864
|
+
"Refusing: the refinement check fails closed on a malformed/identifier-only matrix.",
|
|
865
|
+
finding: MALFORMED_AC_DOD_MATRIX_FINDING,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Matrix present and valid: the refinement floor is the matrix + explicit
|
|
870
|
+
// Non-goals. Interactive issue-side AC/DoD checklists are NOT required.
|
|
871
|
+
if (matrix.found && matrix.valid) {
|
|
872
|
+
if (!hasNonGoals) {
|
|
633
873
|
return {
|
|
634
874
|
...base,
|
|
635
875
|
hasACs: false,
|
|
636
|
-
source: REFINEMENT_SOURCE.
|
|
876
|
+
source: REFINEMENT_SOURCE.ISSUE_BODY_MATRIX,
|
|
637
877
|
reason:
|
|
638
|
-
"Issue body carries a
|
|
639
|
-
"the tracker-backed refinement contract requires
|
|
640
|
-
"
|
|
641
|
-
|
|
642
|
-
finding: MISSING_AC_CHECKLIST_FINDING,
|
|
878
|
+
"Issue body carries a valid AC→DoD mapping matrix but no explicit Non-goals section; " +
|
|
879
|
+
"the tracker-backed refinement contract requires one (rule ARTIFACT-TRACKER-ISSUE-REFINEMENT-FLOOR; " +
|
|
880
|
+
"e.g. run the loop-grill synthesis). Refusing: the refinement check fails closed without an explicit Non-goals section.",
|
|
881
|
+
finding: MISSING_EXPLICIT_NON_GOALS_FINDING,
|
|
643
882
|
};
|
|
644
883
|
}
|
|
645
884
|
return {
|
|
646
885
|
...base,
|
|
647
886
|
hasACs: true,
|
|
648
|
-
source: REFINEMENT_SOURCE.
|
|
649
|
-
acItems:
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
reason: `Issue body links a refinement doc at ${linkedDoc.path}; treating that as the refinement artifact source.`,
|
|
887
|
+
source: REFINEMENT_SOURCE.ISSUE_BODY_MATRIX,
|
|
888
|
+
acItems: effectiveAcItems,
|
|
889
|
+
dodItems: effectiveDodItems,
|
|
890
|
+
reason: `Found a valid AC→DoD mapping matrix with ${matrix.rowCount} semantic row(s) and an explicit Non-goals section.`,
|
|
653
891
|
finding: null,
|
|
654
892
|
};
|
|
655
893
|
}
|
|
656
894
|
|
|
895
|
+
// Matrix absent. If the body carries AC/DoD checklist content it is a
|
|
896
|
+
// checklist-bearing issue missing the authoritative matrix (#1951 AC2 /
|
|
897
|
+
// migration): fail closed on the missing matrix so it is re-grilled. A body
|
|
898
|
+
// with no matrix and no AC/DoD content (prose-only, or only a Non-goals
|
|
899
|
+
// section / an unresolved linked-doc mention) stays the pre-existing
|
|
900
|
+
// missing_refinement_artifact.
|
|
901
|
+
if (acItems.length > 0 || dodItems.length > 0) {
|
|
902
|
+
return {
|
|
903
|
+
...base,
|
|
904
|
+
hasACs: false,
|
|
905
|
+
source: REFINEMENT_SOURCE.MISSING,
|
|
906
|
+
reason:
|
|
907
|
+
"Issue body carries Acceptance criteria / Definition of done content but no authoritative AC→DoD mapping matrix table; " +
|
|
908
|
+
"under matrix-on-issue/checklist-on-PR the mapping table is the authoritative issue artifact " +
|
|
909
|
+
"(rule ARTIFACT-TRACKER-ISSUE-REFINEMENT-FLOOR; e.g. run the loop-grill synthesis). " +
|
|
910
|
+
"Refusing: the refinement check fails closed without the mapping matrix.",
|
|
911
|
+
finding: MISSING_AC_DOD_MATRIX_FINDING,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
|
|
657
915
|
return {
|
|
658
916
|
...base,
|
|
659
917
|
hasACs: false,
|
|
@@ -661,7 +919,7 @@ export function detectIssueRefinementArtifact({ body = "", issueNumber = null, r
|
|
|
661
919
|
acItems: [],
|
|
662
920
|
uncheckedAcItems: [],
|
|
663
921
|
dodItems: [],
|
|
664
|
-
reason: "Issue body has no
|
|
922
|
+
reason: "Issue body has no AC→DoD mapping matrix, no Acceptance criteria/DoD content, and no linked refinement doc.",
|
|
665
923
|
finding: REFINEMENT_ARTIFACT_FINDING,
|
|
666
924
|
};
|
|
667
925
|
}
|
|
@@ -1004,9 +1262,9 @@ export function extractPrBodyUncheckedChecklistItems({ body = "" } = {}) {
|
|
|
1004
1262
|
*/
|
|
1005
1263
|
export function decideEnqueueRefinementGate({ artifact, targetIsPickup, auto = false }) {
|
|
1006
1264
|
// `artifact.finding === null` is the explicit "passes the full refinement
|
|
1007
|
-
// check" signal (
|
|
1008
|
-
// section
|
|
1009
|
-
//
|
|
1265
|
+
// check" signal (a valid AC→DoD mapping matrix + an explicit Non-goals
|
|
1266
|
+
// section, or a resolvable linked refinement doc + Non-goals), clearer than
|
|
1267
|
+
// reading `hasACs`, whose name understates what it covers.
|
|
1010
1268
|
if (!targetIsPickup || artifact.finding === null) {
|
|
1011
1269
|
return { action: "enqueue" };
|
|
1012
1270
|
}
|
|
@@ -1019,27 +1277,28 @@ export function decideEnqueueRefinementGate({ artifact, targetIsPickup, auto = f
|
|
|
1019
1277
|
"(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.";
|
|
1020
1278
|
return { action: auto ? "divert" : "block", reason, missing: ["explicit Non-goals section"] };
|
|
1021
1279
|
}
|
|
1022
|
-
// #
|
|
1023
|
-
//
|
|
1024
|
-
|
|
1025
|
-
if (artifact.finding === MISSING_DOD_CHECKLIST_FINDING) {
|
|
1280
|
+
// #1951: matrix present but empty/malformed/identifier-only — name the shape
|
|
1281
|
+
// defect so the fix targets the mapping table, not a missing section.
|
|
1282
|
+
if (artifact.finding === MALFORMED_AC_DOD_MATRIX_FINDING) {
|
|
1026
1283
|
const reason =
|
|
1027
|
-
"Issue carries an
|
|
1028
|
-
"
|
|
1029
|
-
"(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
|
|
1030
|
-
return { action: auto ? "divert" : "block", reason, missing: ["
|
|
1284
|
+
"Issue carries an AC→DoD mapping matrix but it is empty, malformed, or identifier-only/tautological (e.g. `AC1 → D1`). " +
|
|
1285
|
+
"Rewrite the mapping table so each row maps a concrete acceptance-criterion outcome to concrete completion evidence " +
|
|
1286
|
+
"(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
|
+
return { action: auto ? "divert" : "block", reason, missing: ["valid AC→DoD mapping matrix"] };
|
|
1031
1288
|
}
|
|
1032
|
-
|
|
1289
|
+
// #1951: matrix absent (whether or not the body carries duplicate issue-side
|
|
1290
|
+
// checklists) — the mapping table is the authoritative issue artifact.
|
|
1291
|
+
if (artifact.finding === MISSING_AC_DOD_MATRIX_FINDING) {
|
|
1033
1292
|
const reason =
|
|
1034
|
-
"Issue carries
|
|
1035
|
-
"Add
|
|
1036
|
-
"(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 the
|
|
1037
|
-
return { action: auto ? "divert" : "block", reason, missing: ["
|
|
1293
|
+
"Issue carries Acceptance criteria / Definition of done content but no authoritative AC→DoD mapping matrix — under matrix-on-issue/checklist-on-PR the mapping table is the authoritative issue artifact (#1951). " +
|
|
1294
|
+
"Add a semantic AC→DoD mapping table to the issue body (each acceptance-criterion outcome mapped to its required completion evidence), and an explicit Non-goals section if one is not already present " +
|
|
1295
|
+
"(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 the mapping matrix.";
|
|
1296
|
+
return { action: auto ? "divert" : "block", reason, missing: ["AC→DoD mapping matrix"] };
|
|
1038
1297
|
}
|
|
1039
1298
|
const missing = [...REFINEMENT_ARTIFACT_SOURCES];
|
|
1040
1299
|
const reason =
|
|
1041
1300
|
`Issue has no refinement artifact (none of: ${missing.join(", ")}). ` +
|
|
1042
|
-
"Refine the issue to the
|
|
1301
|
+
"Refine the issue to the authoritative AC→DoD mapping matrix (a two-column table mapping each acceptance-criterion outcome to its required completion evidence) plus an explicit Non-goals section — " +
|
|
1043
1302
|
"or link a refinement doc (tmp/refinement/*.md), which is a complete artifact on its own " +
|
|
1044
1303
|
"(e.g. run `/dev-loops:loop-grill <issue> --auto` (or `/loop-grill <issue> --auto` in the dev-loops repo itself), or the refiner) — before it enters the pickup queue.";
|
|
1045
1304
|
return { action: auto ? "divert" : "block", reason, missing };
|