@mstar-harness/engine 3.9.0 → 3.9.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/dist/dispatch.d.ts +46 -9
- package/dist/engine.js +240 -2
- package/dist/index.d.ts +6 -1
- package/dist/project.d.ts +3 -1
- package/dist/prreview.d.ts +12 -0
- package/dist/qcreview.d.ts +50 -0
- package/package.json +1 -1
package/dist/dispatch.d.ts
CHANGED
|
@@ -6,6 +6,19 @@ export type AssignmentFields = {
|
|
|
6
6
|
taskCategory?: string;
|
|
7
7
|
workingBranch?: string;
|
|
8
8
|
branchPolicy?: string;
|
|
9
|
+
/**
|
|
10
|
+
* `Budget (review / QC seats)` round cap. Required (present, non-empty,
|
|
11
|
+
* not `N/A`) on review-seat and audit rounds — see
|
|
12
|
+
* {@link REVIEW_SEAT_ROLES}. The value is prose (file opens / wall clock);
|
|
13
|
+
* it is never parsed numerically.
|
|
14
|
+
*/
|
|
15
|
+
budget?: string;
|
|
16
|
+
/**
|
|
17
|
+
* `Return shape (review / QC seats)` — what the seat returns and how it
|
|
18
|
+
* stops. Same presence-only rule as {@link AssignmentFields.budget}, one
|
|
19
|
+
* violation code per field so the PM sees exactly which label is missing.
|
|
20
|
+
*/
|
|
21
|
+
returnShape?: string;
|
|
9
22
|
};
|
|
10
23
|
export type ValidateAssignmentFieldsOptions = {
|
|
11
24
|
/**
|
|
@@ -39,7 +52,15 @@ export type ExecutionModeToNResult = GateResult & {
|
|
|
39
52
|
* are accepted so the engine parser is the SINGLE grammar for Assignment
|
|
40
53
|
* header fields (the Slice-2 opencode presence parser tolerated bullets;
|
|
41
54
|
* its acceptance is folded into this parser, not forked —).
|
|
42
|
-
|
|
55
|
+
*
|
|
56
|
+
* The function scopes its OWN input: it reads the {@link
|
|
57
|
+
* assignmentHeaderRegion} of whatever text it is handed, so any caller (host
|
|
58
|
+
* gate, CLI, plugin) may pass the whole dispatch/prompt — a field label
|
|
59
|
+
* quoted in the task body can never be read as a header field. An
|
|
60
|
+
* already-sliced header may be passed as well: a slice holds no body marker,
|
|
61
|
+
* so re-slicing is a no-op (idempotent). Every Assignment field/branch read
|
|
62
|
+
* therefore shares ONE header rule by construction, rather than by each call
|
|
63
|
+
* site remembering to pre-slice. */
|
|
43
64
|
export declare function parseAssignmentFields(assignmentText: string): AssignmentFields;
|
|
44
65
|
/** Where the `Enforcement` flag was declared (roadmap §8.5 C4/D2). */
|
|
45
66
|
export type EnforcementSource = "assignment" | "compass" | "mstarc" | "none";
|
|
@@ -51,9 +72,11 @@ export type EnforcementFlag = {
|
|
|
51
72
|
/**
|
|
52
73
|
* Slice an Assignment's header region — the text before the first body
|
|
53
74
|
* marker (see {@link ASSIGNMENT_BODY_START_RE}). Returns the full text when
|
|
54
|
-
* no marker is present.
|
|
55
|
-
*
|
|
56
|
-
*
|
|
75
|
+
* no marker is present. This is the SINGLE header-scope rule: the Assignment
|
|
76
|
+
* field parsers ({@link parseAssignmentFields} plus the branch forms built
|
|
77
|
+
* on it) apply it to their own input, and the Assignment enforcement flag is
|
|
78
|
+
* parsed against THIS region, so an example line `**Enforcement**: hard`
|
|
79
|
+
* quoted in the task body cannot harden the dispatch. */
|
|
57
80
|
export declare function assignmentHeaderRegion(assignmentText: string): string;
|
|
58
81
|
/**
|
|
59
82
|
* Parse the `Enforcement: hard` flag (roadmap §8.5 C4 + decision D2 — v2
|
|
@@ -110,8 +133,9 @@ export type AssignmentBranchForms = {
|
|
|
110
133
|
* Parse an Assignment's branch forms via the engine's single parser
|
|
111
134
|
* (`parseAssignmentFields` + {@link parseWorkingBranchValue}). Consumed by
|
|
112
135
|
* the CLI `dispatch validate` gate-branch derivation and the opencode hook;
|
|
113
|
-
* also the internal grammar behind `validateAssignmentFields`.
|
|
114
|
-
|
|
136
|
+
* also the internal grammar behind `validateAssignmentFields`. Input may be
|
|
137
|
+
* the whole dispatch — the header scope is inherited from
|
|
138
|
+
* {@link parseAssignmentFields}. */
|
|
115
139
|
export declare function parseAssignmentBranchForms(assignmentText: string): AssignmentBranchForms;
|
|
116
140
|
/**
|
|
117
141
|
* Parse the Assignment's `Branch policy: direct on <branch> — <reason>`
|
|
@@ -119,7 +143,8 @@ export declare function parseAssignmentBranchForms(assignmentText: string): Assi
|
|
|
119
143
|
* form (branch + non-empty reason; separator set [—–]|--|-); undefined when
|
|
120
144
|
* absent or malformed — the default-branch gate recognizes explicit
|
|
121
145
|
* direct-on exceptions only. Single engine grammar shared by CLI + plugin
|
|
122
|
-
|
|
146
|
+
* (the header scope is inherited from {@link parseAssignmentBranchForms}, so
|
|
147
|
+
* a whole dispatch may be passed). */
|
|
123
148
|
export declare function parseBranchPolicyDirectOnBranch(assignmentText: string): string | undefined;
|
|
124
149
|
/**
|
|
125
150
|
* True when the Assignment's `Execute as` role is a read-only orientation
|
|
@@ -134,11 +159,23 @@ export declare function isReadOnlyAssignmentRole(roleId: string): boolean;
|
|
|
134
159
|
*
|
|
135
160
|
* Required: `Execute as` / `Delegation` / `Task category` present with
|
|
136
161
|
* non-empty values (paste-only shells are caught here — every field missing).
|
|
137
|
-
*
|
|
162
|
+
* Review-seat and audit rounds must additionally declare both round-bounding
|
|
163
|
+
* fields, `Budget (review / QC seats)` and `Return shape (review / QC
|
|
164
|
+
* seats)` (one violation code per missing label). Writable assignments must
|
|
165
|
+
* carry EXACTLY ONE branch form; `create <new>`
|
|
138
166
|
* from <base>` without `<base>` (incl. the dangling `create <new> from`
|
|
139
167
|
* / `create from <base>` typos) and `Branch policy` without branch/reason
|
|
140
168
|
* are flagged. The three core-field violations carry the legacy
|
|
141
|
-
* `assignment.presence.*` codes as aliases.
|
|
169
|
+
* `assignment.presence.*` codes as aliases.
|
|
170
|
+
*
|
|
171
|
+
* Every read is scoped to the Assignment HEADER region — the scope is
|
|
172
|
+
* applied by the field parsers themselves ({@link parseAssignmentFields} /
|
|
173
|
+
* {@link parseAssignmentBranchForms}), never the task body — the same rule
|
|
174
|
+
* {@link parseEnforcementFlag} follows at the compose gate. Dispatch text
|
|
175
|
+
* routinely quotes the field labels (template blocks, `## Task` bodies, the
|
|
176
|
+
* `**Task**:` line's own prose), and a quoted `**Budget (review / QC
|
|
177
|
+
* seats)**: …` line must not satisfy a gate the Assignment header left
|
|
178
|
+
* open. */
|
|
142
179
|
export declare function validateAssignmentFields(assignmentText: string, opts?: ValidateAssignmentFieldsOptions): GateResult;
|
|
143
180
|
/**
|
|
144
181
|
* Flag writable work on a default protected branch (`main`/`master` per
|
package/dist/engine.js
CHANGED
|
@@ -600,12 +600,18 @@ var REQUIRED_FIELDS = [
|
|
|
600
600
|
{ key: "delegation", label: "Delegation", code: "delegation" },
|
|
601
601
|
{ key: "taskCategory", label: "Task category", code: "task-category" }
|
|
602
602
|
];
|
|
603
|
+
var REVIEW_SEAT_ROLES = ["qc-specialist", "qc-specialist-2", "qc-specialist-3", "code-reviewer", "qa-engineer"];
|
|
604
|
+
var BUDGET_LABELS = ["Budget (review / QC seats)", "Budget"];
|
|
605
|
+
var RETURN_SHAPE_LABELS = ["Return shape (review / QC seats)", "Return shape"];
|
|
603
606
|
function violation2(severity, code, message, fix) {
|
|
604
607
|
return { ok: false, severity, code, message, fix };
|
|
605
608
|
}
|
|
609
|
+
function describeAbsence(value) {
|
|
610
|
+
return value === undefined ? "the field is absent" : value === "" ? "the field is empty" : value.trim().toLowerCase() === "n/a" ? 'the value is "N/A"' : undefined;
|
|
611
|
+
}
|
|
606
612
|
function parseAssignmentFields(assignmentText) {
|
|
607
613
|
const fields = {};
|
|
608
|
-
for (const line of assignmentText.split(/\r?\n/)) {
|
|
614
|
+
for (const line of assignmentHeaderRegion(assignmentText).split(/\r?\n/)) {
|
|
609
615
|
const match = line.match(/^[ \t]*(?:[-*][ \t]+)?\*\*\s*([^*:]+?)\s*\*\*\s*:\s*(.*)$/) ?? line.match(/^[ \t]*(?:[-*][ \t]+)?([A-Za-z][A-Za-z -]*?)\s*:\s*(.*)$/);
|
|
610
616
|
if (!match)
|
|
611
617
|
continue;
|
|
@@ -620,6 +626,10 @@ function parseAssignmentFields(assignmentText) {
|
|
|
620
626
|
fields.workingBranch = value;
|
|
621
627
|
else if (label === "Branch policy")
|
|
622
628
|
fields.branchPolicy = value;
|
|
629
|
+
else if (BUDGET_LABELS.includes(label))
|
|
630
|
+
fields.budget = value;
|
|
631
|
+
else if (RETURN_SHAPE_LABELS.includes(label))
|
|
632
|
+
fields.returnShape = value;
|
|
623
633
|
}
|
|
624
634
|
return fields;
|
|
625
635
|
}
|
|
@@ -707,6 +717,33 @@ function validateAssignmentFields(assignmentText, opts = {}) {
|
|
|
707
717
|
for (const { key, label, code } of REQUIRED_FIELDS) {
|
|
708
718
|
requireField(violations, fields[key], label, code);
|
|
709
719
|
}
|
|
720
|
+
const role = (fields.executeAs ?? "").trim().replace(/^@/, "").toLowerCase().split(/\s+/)[0] ?? "";
|
|
721
|
+
const reviewSeat = REVIEW_SEAT_ROLES.includes(role);
|
|
722
|
+
const category = (fields.taskCategory ?? "").replace(/^[*_`"'\s]+/, "").toLowerCase();
|
|
723
|
+
const auditRound = /^audit(?![A-Za-z0-9])/.test(category);
|
|
724
|
+
if (reviewSeat || auditRound) {
|
|
725
|
+
const subject = reviewSeat ? `review seat "${role}"` : `audit round (Task category: ${category})`;
|
|
726
|
+
const boundingFields = [
|
|
727
|
+
{
|
|
728
|
+
code: "assignment.field.budget-missing",
|
|
729
|
+
label: "Budget",
|
|
730
|
+
fix: `add "**Budget (review / QC seats)**: <cap> — may only tighten the default in mstar-harness-core § 定向执行与验证边界, never loosen it"`,
|
|
731
|
+
value: fields.budget
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
code: "assignment.field.return-shape-missing",
|
|
735
|
+
label: "Return shape",
|
|
736
|
+
fix: `add "**Return shape (review / QC seats)**: <what the seat returns and how it stops — verdict + findings shape; a clean round returns findings: [] explicitly>"`,
|
|
737
|
+
value: fields.returnShape
|
|
738
|
+
}
|
|
739
|
+
];
|
|
740
|
+
for (const field of boundingFields) {
|
|
741
|
+
const missing = describeAbsence(field.value);
|
|
742
|
+
if (missing === undefined)
|
|
743
|
+
continue;
|
|
744
|
+
violations.push(violation2("high", field.code, `${subject} must declare a round ${field.label} — ${missing}`, field.fix));
|
|
745
|
+
}
|
|
746
|
+
}
|
|
710
747
|
if (writable) {
|
|
711
748
|
const workingPresent = fields.workingBranch !== undefined && fields.workingBranch !== "";
|
|
712
749
|
const policyPresent = fields.branchPolicy !== undefined && fields.branchPolicy !== "";
|
|
@@ -2039,7 +2076,9 @@ function findingsCleanupGate(register, planId, opts) {
|
|
|
2039
2076
|
const id = typeof entry.id === "string" ? entry.id : "<unnamed>";
|
|
2040
2077
|
const label = `R#${id}`;
|
|
2041
2078
|
if (mode === "zero-residual") {
|
|
2042
|
-
if (entry.severity === "
|
|
2079
|
+
if (normalizeSeverity(entry.severity) === "critical") {
|
|
2080
|
+
violations.push(violation6("high", "findings.zero-residual-critical", `${label}: unresolved critical blocks approval under zero-residual — fix now or close via explicit risk acceptance`));
|
|
2081
|
+
} else if (entry.severity === "nit") {
|
|
2043
2082
|
violations.push(violation6("medium", "findings.zero-residual-nit", `${label}: style-only nits must be fixed in-session or dropped — never left open under zero-residual`));
|
|
2044
2083
|
} else if (entry.decision === "risk-accepted" || entry.lifecycle === "waived") {
|
|
2045
2084
|
violations.push(violation6("medium", "findings.zero-residual-risk-accepted", `${label}: waived/risk-accepted findings must be closed/archived, not left open under zero-residual`));
|
|
@@ -8447,6 +8486,203 @@ function resolvePrReviewTier(input) {
|
|
|
8447
8486
|
return "quick";
|
|
8448
8487
|
return "default";
|
|
8449
8488
|
}
|
|
8489
|
+
// src/qcreview.ts
|
|
8490
|
+
var QC_VERDICTS = ["Approve", "Request Changes", "Needs Discussion", "Unconfirmed"];
|
|
8491
|
+
var REPORT_FIELDS = ["report_kind", "reviewer", "reviewer_index", "plan_id", "verdict", "generated_at"];
|
|
8492
|
+
var REPORT_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
8493
|
+
var REPORT_SEVERITIES = ["Critical", "Warning", "Suggestion", "Unconfirmed"];
|
|
8494
|
+
var BODY_VERDICT_RE = /^[ \t]*(?:[-*+][ \t]+)?(?:#{1,6}[ \t]+)?[*_\s]*Verdict[*_\s]*:[ \t]*(.*)$/i;
|
|
8495
|
+
var VERDICT_DECORATION_RE = /^[*_`"'\s~]+/;
|
|
8496
|
+
var TRUNCATED_COVERAGE_RE = /^[ \t]*(?:[-*+][ \t]+)?(?:\*\*)?Truncated coverage(?:\*\*)?[ \t]*:/;
|
|
8497
|
+
var EMPTY_SECTION_RE = /^(?:\(none\)|none(?![A-Za-z])|\uff08?\u65e0\uff09?)/i;
|
|
8498
|
+
var LIST_ITEM_RE = /^(?:[-*][ \t]+|\d+[.)][ \t]+)(.*)$/;
|
|
8499
|
+
function sectionRange(lines, heading) {
|
|
8500
|
+
const start = lines.findIndex((line) => {
|
|
8501
|
+
const m = /^(#{1,6})[ \t]+(.*?)[ \t]*$/.exec(line);
|
|
8502
|
+
return m !== null && m[1].length === 2 && m[2] === heading;
|
|
8503
|
+
});
|
|
8504
|
+
if (start < 0)
|
|
8505
|
+
return;
|
|
8506
|
+
let end = lines.length;
|
|
8507
|
+
for (let i = start + 1;i < lines.length; i++) {
|
|
8508
|
+
if (/^##[ \t]/.test(lines[i])) {
|
|
8509
|
+
end = i;
|
|
8510
|
+
break;
|
|
8511
|
+
}
|
|
8512
|
+
}
|
|
8513
|
+
return [start + 1, end];
|
|
8514
|
+
}
|
|
8515
|
+
function bodyLines(lines) {
|
|
8516
|
+
if (lines[0]?.trim() !== "---")
|
|
8517
|
+
return lines;
|
|
8518
|
+
for (let i = 1;i < lines.length; i++) {
|
|
8519
|
+
if (lines[i].trim() === "---")
|
|
8520
|
+
return lines.slice(i + 1);
|
|
8521
|
+
}
|
|
8522
|
+
return lines;
|
|
8523
|
+
}
|
|
8524
|
+
function linesUnclosedFence(text) {
|
|
8525
|
+
const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
|
|
8526
|
+
if (lines.length === 0 || lines[0].trim() !== "---")
|
|
8527
|
+
return false;
|
|
8528
|
+
return !lines.slice(1).some((line) => line.trim() === "---");
|
|
8529
|
+
}
|
|
8530
|
+
function tableCells(line) {
|
|
8531
|
+
const trimmed = line.trim();
|
|
8532
|
+
if (!trimmed.startsWith("|") || !trimmed.endsWith("|"))
|
|
8533
|
+
return;
|
|
8534
|
+
const cells = trimmed.slice(1, -1).split("|").map((cell) => cell.trim());
|
|
8535
|
+
return cells.length >= 2 ? cells : undefined;
|
|
8536
|
+
}
|
|
8537
|
+
function namesSeverity(text, severity) {
|
|
8538
|
+
const index = text.indexOf(severity);
|
|
8539
|
+
if (index < 0)
|
|
8540
|
+
return false;
|
|
8541
|
+
return !/[A-Za-z]/.test(text.charAt(index - 1)) && !/[A-Za-z]/.test(text.charAt(index + severity.length));
|
|
8542
|
+
}
|
|
8543
|
+
function summaryCounts(lines) {
|
|
8544
|
+
const counts = {};
|
|
8545
|
+
const range = sectionRange(lines, "Summary");
|
|
8546
|
+
if (range === undefined)
|
|
8547
|
+
return counts;
|
|
8548
|
+
for (let i = range[0];i < range[1]; i++) {
|
|
8549
|
+
const cells = tableCells(lines[i]);
|
|
8550
|
+
if (cells === undefined)
|
|
8551
|
+
continue;
|
|
8552
|
+
const values = cells.slice(1).map((cell) => /(\d+)/.exec(cell)?.[1]);
|
|
8553
|
+
const tail = values.filter((digits) => digits !== undefined).pop();
|
|
8554
|
+
if (tail === undefined)
|
|
8555
|
+
continue;
|
|
8556
|
+
for (const severity of REPORT_SEVERITIES) {
|
|
8557
|
+
if (!namesSeverity(cells[0], severity))
|
|
8558
|
+
continue;
|
|
8559
|
+
counts[severity] = Number(tail);
|
|
8560
|
+
}
|
|
8561
|
+
}
|
|
8562
|
+
return counts;
|
|
8563
|
+
}
|
|
8564
|
+
function findingsCounts(lines) {
|
|
8565
|
+
const counts = {};
|
|
8566
|
+
const range = sectionRange(lines, "Findings");
|
|
8567
|
+
if (range === undefined)
|
|
8568
|
+
return counts;
|
|
8569
|
+
let current;
|
|
8570
|
+
for (let i = range[0];i < range[1]; i++) {
|
|
8571
|
+
const line = lines[i];
|
|
8572
|
+
const heading = /^###[ \t]+(.*?)[ \t]*$/.exec(line);
|
|
8573
|
+
if (heading !== null) {
|
|
8574
|
+
current = REPORT_SEVERITIES.find((severity) => namesSeverity(heading[1], severity));
|
|
8575
|
+
if (current !== undefined && counts[current] === undefined)
|
|
8576
|
+
counts[current] = 0;
|
|
8577
|
+
continue;
|
|
8578
|
+
}
|
|
8579
|
+
if (current === undefined)
|
|
8580
|
+
continue;
|
|
8581
|
+
const item = LIST_ITEM_RE.exec(line);
|
|
8582
|
+
if (item === null || EMPTY_SECTION_RE.test(item[1].trim()))
|
|
8583
|
+
continue;
|
|
8584
|
+
counts[current] = (counts[current] ?? 0) + 1;
|
|
8585
|
+
}
|
|
8586
|
+
return counts;
|
|
8587
|
+
}
|
|
8588
|
+
function bodyVerdictPhrase(lines) {
|
|
8589
|
+
let phrase;
|
|
8590
|
+
for (const line of bodyLines(lines)) {
|
|
8591
|
+
const m = BODY_VERDICT_RE.exec(line);
|
|
8592
|
+
if (m === null)
|
|
8593
|
+
continue;
|
|
8594
|
+
const text = m[1].trim().replace(VERDICT_DECORATION_RE, "");
|
|
8595
|
+
phrase = text.split(/\s+/)[0] ?? "";
|
|
8596
|
+
for (const verdict of [...QC_VERDICTS].sort((a, b) => b.length - a.length)) {
|
|
8597
|
+
if (!text.startsWith(verdict))
|
|
8598
|
+
continue;
|
|
8599
|
+
const next = text.charAt(verdict.length);
|
|
8600
|
+
if (next === "" || !/[A-Za-z0-9]/.test(next)) {
|
|
8601
|
+
phrase = verdict;
|
|
8602
|
+
break;
|
|
8603
|
+
}
|
|
8604
|
+
}
|
|
8605
|
+
}
|
|
8606
|
+
return phrase;
|
|
8607
|
+
}
|
|
8608
|
+
function violation16(severity, code, message, fix) {
|
|
8609
|
+
return { ok: false, severity, code, message, fix };
|
|
8610
|
+
}
|
|
8611
|
+
function validateQcReport(text) {
|
|
8612
|
+
const violations = [];
|
|
8613
|
+
if (lines_missing_fence(text)) {
|
|
8614
|
+
return {
|
|
8615
|
+
ok: false,
|
|
8616
|
+
violations: [
|
|
8617
|
+
violation16("high", "qcreview.report.missing-frontmatter", "no `---` fenced frontmatter found - a QC seat report must open with the machine-readable frontmatter block (report-template.md § Frontmatter)", "open the report with `---` and the fields `report_kind: qc` / `reviewer` / `reviewer_index` / `plan_id` / `verdict` / `generated_at`")
|
|
8618
|
+
]
|
|
8619
|
+
};
|
|
8620
|
+
}
|
|
8621
|
+
const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
|
|
8622
|
+
if (linesUnclosedFence(text)) {
|
|
8623
|
+
return {
|
|
8624
|
+
ok: false,
|
|
8625
|
+
violations: [
|
|
8626
|
+
violation16("high", "qcreview.report.unclosed-frontmatter", "the frontmatter `---` fence is never closed - every line after it is read as frontmatter, so the report body is not part of the document", "add a closing `---` line after the last frontmatter key (report-template.md § Frontmatter)")
|
|
8627
|
+
]
|
|
8628
|
+
};
|
|
8629
|
+
}
|
|
8630
|
+
const { doc } = parseReportFrontmatter(text);
|
|
8631
|
+
for (const field of REPORT_FIELDS) {
|
|
8632
|
+
const value = doc[field];
|
|
8633
|
+
if (value === undefined || value.trim() === "") {
|
|
8634
|
+
violations.push(violation16("medium", `qcreview.report.missing-${field}`, `missing required frontmatter field: ${field}`, `add "${field}: <value>" to the frontmatter block (report-template.md § Frontmatter)`));
|
|
8635
|
+
}
|
|
8636
|
+
}
|
|
8637
|
+
const reportKind = doc.report_kind?.trim() ?? "";
|
|
8638
|
+
if (reportKind !== "" && reportKind !== "qc") {
|
|
8639
|
+
violations.push(violation16("medium", "qcreview.report.invalid-report-kind", `report_kind "${reportKind}" is not "qc"`, "use `report_kind: qc` for a QC seat report (a PM consolidated report is not a seat report)"));
|
|
8640
|
+
}
|
|
8641
|
+
const frontmatterVerdict = doc.verdict?.trim() ?? "";
|
|
8642
|
+
const frontmatterVerdictIsValid = QC_VERDICTS.includes(frontmatterVerdict);
|
|
8643
|
+
if (frontmatterVerdict !== "" && !frontmatterVerdictIsValid) {
|
|
8644
|
+
violations.push(violation16("medium", "qcreview.report.invalid-verdict", `frontmatter verdict "${frontmatterVerdict}" is not one of ${JSON.stringify(QC_VERDICTS)}`, `use one of: ${QC_VERDICTS.join(" | ")} - the vocabulary is verbatim (report-template.md § Report body template)`));
|
|
8645
|
+
}
|
|
8646
|
+
const generatedAt = doc.generated_at?.trim() ?? "";
|
|
8647
|
+
if (generatedAt !== "" && !REPORT_DATE_RE.test(generatedAt)) {
|
|
8648
|
+
violations.push(violation16("medium", "qcreview.report.invalid-generated-at", `generated_at "${generatedAt}" must be a calendar date (YYYY-MM-DD)`, 'write `generated_at: "YYYY-MM-DD"` (report-template.md § Frontmatter)'));
|
|
8649
|
+
}
|
|
8650
|
+
const bodyPhrase = bodyVerdictPhrase(lines);
|
|
8651
|
+
const bodyPhraseIsValid = bodyPhrase !== undefined && QC_VERDICTS.includes(bodyPhrase);
|
|
8652
|
+
if (bodyPhrase === undefined) {
|
|
8653
|
+
violations.push(violation16("high", "qcreview.report.missing-body-verdict", "the report body carries no verdict line (`**Verdict**: <verdict>` or `## Verdict: <verdict>`)", `add a body verdict line using one of: ${QC_VERDICTS.join(" | ")}`));
|
|
8654
|
+
} else if (!bodyPhraseIsValid) {
|
|
8655
|
+
violations.push(violation16("high", "qcreview.report.verdict-mismatch", bodyPhrase === "" ? `the body verdict line is empty - expected one of ${JSON.stringify(QC_VERDICTS)}` : `body verdict "${bodyPhrase}" is not one of ${JSON.stringify(QC_VERDICTS)}`, `start the verdict line with one of: ${QC_VERDICTS.join(" | ")}`));
|
|
8656
|
+
} else if (frontmatterVerdictIsValid && bodyPhrase !== frontmatterVerdict) {
|
|
8657
|
+
violations.push(violation16("high", "qcreview.report.verdict-mismatch", `body verdict "${bodyPhrase}" does not match the frontmatter verdict "${frontmatterVerdict}"`, "make the frontmatter verdict and the body verdict line agree on the same final verdict"));
|
|
8658
|
+
}
|
|
8659
|
+
const summary = summaryCounts(lines);
|
|
8660
|
+
const findings = findingsCounts(lines);
|
|
8661
|
+
for (const severity of REPORT_SEVERITIES) {
|
|
8662
|
+
const claimed = summary[severity];
|
|
8663
|
+
const counted = findings[severity];
|
|
8664
|
+
if (claimed === undefined || counted === undefined || claimed === counted)
|
|
8665
|
+
continue;
|
|
8666
|
+
violations.push(violation16("high", "qcreview.report.summary-count-mismatch", `## Summary ${severity} count ${claimed} does not match the ${counted} top-level entr${counted === 1 ? "y" : "ies"} under ## Findings (§ ${severity})`, `recount § ${severity} and update the ## Summary row - one top-level entry per finding, detail lines indented (report-template.md § Findings)`));
|
|
8667
|
+
}
|
|
8668
|
+
const effectiveVerdict = frontmatterVerdictIsValid ? frontmatterVerdict : bodyPhraseIsValid ? bodyPhrase : undefined;
|
|
8669
|
+
if (effectiveVerdict !== undefined) {
|
|
8670
|
+
const critical = summary.Critical;
|
|
8671
|
+
const warning = summary.Warning;
|
|
8672
|
+
const blocking = (critical ?? 0) + (warning ?? 0);
|
|
8673
|
+
if (effectiveVerdict === "Approve" && blocking > 0) {
|
|
8674
|
+
violations.push(violation16("high", "qcreview.report.verdict-contradicts-counts", `verdict "Approve" with ${blocking} Critical/Warning finding(s) in ## Summary (Critical ${critical ?? 0}, Warning ${warning ?? 0})`, 'do not Approve with open Critical/Warning findings - close them or use "Request Changes" / "Needs Discussion"'));
|
|
8675
|
+
}
|
|
8676
|
+
const unconfirmed = summary.Unconfirmed;
|
|
8677
|
+
if (unconfirmed !== undefined && unconfirmed > 0 && effectiveVerdict !== "Unconfirmed") {
|
|
8678
|
+
violations.push(violation16("high", "qcreview.report.verdict-contradicts-counts", `verdict "${effectiveVerdict}" with ${unconfirmed} Unconfirmed finding(s) in ## Summary - a failed evidence channel transmits as "Unconfirmed" (mstar-review-qc § 席位预算与截断)`, 'set the verdict to "Unconfirmed" or re-establish the evidence channel before converging'));
|
|
8679
|
+
}
|
|
8680
|
+
if (effectiveVerdict === "Unconfirmed" && lines.some((line) => TRUNCATED_COVERAGE_RE.test(line))) {
|
|
8681
|
+
violations.push(violation16("high", "qcreview.report.truncation-verdict", 'the report declares `Truncated coverage:` but carries the verdict "Unconfirmed" - a budget/scope cut is not a failed evidence channel', 'keep the verdict earned for the reviewed scope and reserve "Unconfirmed" for a failed evidence channel'));
|
|
8682
|
+
}
|
|
8683
|
+
}
|
|
8684
|
+
return { ok: violations.length === 0, violations };
|
|
8685
|
+
}
|
|
8450
8686
|
export {
|
|
8451
8687
|
ARCHIVED_STATUS_V1_FILE,
|
|
8452
8688
|
AUDIT_CATEGORIES,
|
|
@@ -8481,6 +8717,7 @@ export {
|
|
|
8481
8717
|
PR_REVIEW_TIER_BUDGETS,
|
|
8482
8718
|
PR_VERDICTS,
|
|
8483
8719
|
QC_REVIEWER_PARAMS,
|
|
8720
|
+
QC_VERDICTS,
|
|
8484
8721
|
REVIEW_EMOJI,
|
|
8485
8722
|
ROADMAP_STATUSES,
|
|
8486
8723
|
ROLE_MAPPING,
|
|
@@ -8633,6 +8870,7 @@ export {
|
|
|
8633
8870
|
validatePlanRow,
|
|
8634
8871
|
validatePrReviewReport,
|
|
8635
8872
|
validateProjectRegister,
|
|
8873
|
+
validateQcReport,
|
|
8636
8874
|
validateResidual,
|
|
8637
8875
|
validateRoadmap,
|
|
8638
8876
|
validateRoleMapping,
|
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
* compound-refresh scope. `roles` validates the role reference mapping +
|
|
18
18
|
* parameter tables and the load-order contract, `prreview` implements the
|
|
19
19
|
* PR-review tally/score/verdict arithmetic and the merge-class/verdict
|
|
20
|
-
* constants (mstar-audit pr-review.md § Tally and derived score), `
|
|
20
|
+
* constants (mstar-audit pr-review.md § Tally and derived score), `qcreview`
|
|
21
|
+
* is the QC seat-report contract (frontmatter fields + verbatim verdict
|
|
22
|
+
* vocabulary + body-verdict agreement + Summary/Findings count parity +
|
|
23
|
+
* truncation/verdict coherence, `mstar-review-qc` SKILL.md § 席位预算与截断), `host`
|
|
21
24
|
* detects the active
|
|
22
25
|
* host from tool shapes, resolves skill roots and defines the type-only
|
|
23
26
|
* `HostAdapter` contract, `gates` is the host-neutral coordination-write
|
|
@@ -77,6 +80,8 @@ export type { FiveQuestionMode, FiveQuestionSection, SkillLintKind, SkillLintPro
|
|
|
77
80
|
export { classifySkillLint, FIVE_QUESTION_SECTIONS, RUNTIME_HEADING_ALIASES, lintFiveQuestion, lintFrontmatter, resolveAssetPath, stripFrontmatter, } from "./skill-authoring.js";
|
|
78
81
|
export type { MergeClass, MstarReviewFinding, MstarReviewV1, PrReportTarget, PrReviewSeatPromptOptions, PrReviewSizing, PrReviewTier, PrTierKeyword, PrSizeBand, PrTallyInput, PrTallyResult, PrVerdict, ResolvePrReviewTierInput, ReviewChangesetMode, ReviewInlineComment, ReviewPostPlan, ValidateFindingDocOptions, } from "./prreview.js";
|
|
79
82
|
export { MERGE_CLASSES, PR_REVIEW_TIER_BUDGETS, PR_VERDICTS, REVIEW_EMOJI, computePrTally, pickReviewBranchName, planReviewPost, preflightChangeset, prReviewReportPath, prReviewSeatPrompt, prReviewSizing, resolvePrReviewTier, synthesizeReview, validateFindingDoc, validateMstarReviewV1, validatePrReviewReport, } from "./prreview.js";
|
|
83
|
+
export type { QcVerdict } from "./qcreview.js";
|
|
84
|
+
export { QC_VERDICTS, validateQcReport } from "./qcreview.js";
|
|
80
85
|
export type { ArtifactDoc, ArtifactKind, ArtifactRef, ArtifactStore } from "./store.js";
|
|
81
86
|
export { assertFsStorePath, createFsStore, getArtifactStore, loadStoreModule, resolveArtifactPath, setArtifactStore } from "./store.js";
|
|
82
87
|
export { collectActiveLifecycleBranches, scanActiveLifecycleBranches, type ActiveLifecycleScan } from "./lifecycle-branches.js";
|
package/dist/project.d.ts
CHANGED
|
@@ -170,7 +170,9 @@ export declare function closeProjectRegisterEntry(opts: CloseProjectRegisterEntr
|
|
|
170
170
|
* snapshot's plan row). Every OPEN entry of the plan is checked.
|
|
171
171
|
* `zero-residual`: only true blocker-defers (`decision: defer` + non-empty
|
|
172
172
|
* `target`) may stay open — fixable findings, `nit`s, and waived/
|
|
173
|
-
* risk-accepted entries are violations
|
|
173
|
+
* risk-accepted entries are violations, and an unresolved Critical is a
|
|
174
|
+
* violation for EVERY decision (it must be fixed or closed by explicit
|
|
175
|
+
* risk acceptance, not carried as a defer). `allow-residual` (default): open
|
|
174
176
|
* residuals are fine unless an unresolved Critical remains. Mode resolution:
|
|
175
177
|
* explicit `opts.mode` → `allow-residual` (the v1
|
|
176
178
|
* `plans[].metadata.findings_cleanup` mirror is deleted — no dual-track).
|
package/dist/prreview.d.ts
CHANGED
|
@@ -166,6 +166,16 @@ export declare function prReviewReportPath(opts: {
|
|
|
166
166
|
stage?: 1 | 2;
|
|
167
167
|
slug?: string;
|
|
168
168
|
}): string;
|
|
169
|
+
/**
|
|
170
|
+
* Parse the narrow frontmatter subset reports actually carry: scalar
|
|
171
|
+
* `key: value` lines inside a leading `---` fence (inline `# comment`
|
|
172
|
+
* tails stripped, surrounding quotes trimmed). Returns null when the fence
|
|
173
|
+
* is missing. Unreadable lines surface as violations, never throws.
|
|
174
|
+
*/
|
|
175
|
+
export declare function parseReportFrontmatter(text: string): {
|
|
176
|
+
doc: Record<string, string>;
|
|
177
|
+
unreadable: number;
|
|
178
|
+
};
|
|
169
179
|
/**
|
|
170
180
|
* Validate a saved local PR-review report against the machine-readable
|
|
171
181
|
* contract (pr-review.md § Local report archive Frontmatter + § Output
|
|
@@ -191,6 +201,8 @@ export declare function prReviewReportPath(opts: {
|
|
|
191
201
|
* (legacy reports without elapsed still pass).
|
|
192
202
|
*/
|
|
193
203
|
export declare function validatePrReviewReport(text: string): GateResult;
|
|
204
|
+
/** True when `text` opens with a `---` fenced frontmatter block. */
|
|
205
|
+
export declare function lines_missing_fence(text: string): boolean;
|
|
194
206
|
/** One inline review comment: `path` + `line` in the three-dot diff, RIGHT
|
|
195
207
|
* side only (§ Comment posting step 2 — comments[] entry shape). */
|
|
196
208
|
export type ReviewInlineComment = {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* qcreview — QC 席位报告契约(review-seat return side)。
|
|
3
|
+
*
|
|
4
|
+
* spec: `mstar-review-qc` SKILL.md § 席位预算与截断(该节 callout 声明的
|
|
5
|
+
* `qc validate-report`)、`report-template.md` § Frontmatter / § Report body
|
|
6
|
+
* template / § Findings(verdict 词表与报告形状逐字来源)、
|
|
7
|
+
* `qc-specialist-shared.md` § Budget and stopping / § Targeted re-review
|
|
8
|
+
* (就地 revalidation 刷新当前状态,报告只有一份计数)。
|
|
9
|
+
*
|
|
10
|
+
* 机器可判定的十一条规则(violation code 前缀一律 `qcreview.report.`):
|
|
11
|
+
* 1. `missing-frontmatter` — 报告必须以 `---` 围栏 frontmatter 开头。
|
|
12
|
+
* 2. `unclosed-frontmatter` — 起始围栏必须有闭合 `---` 行。缺闭合行时其后的
|
|
13
|
+
* 全部内容都被读作 frontmatter(`parseReportFrontmatter` 直到文末收集
|
|
14
|
+
* `key: value`),正文与 frontmatter 的边界不存在,后续规则无从判定。
|
|
15
|
+
* 3. `missing-<field>` — `report_kind` / `reviewer` / `reviewer_index` /
|
|
16
|
+
* `plan_id` / `verdict` / `generated_at` 缺失或空值。
|
|
17
|
+
* 4. `invalid-report-kind` — `report_kind` 必须是 `qc`。
|
|
18
|
+
* 5. `invalid-verdict` — frontmatter `verdict` 必须逐字属于 `QC_VERDICTS`。
|
|
19
|
+
* 6. `invalid-generated-at` — `generated_at` 必须是日历日期 `YYYY-MM-DD`。
|
|
20
|
+
* 7. `missing-body-verdict` — 正文必须有 verdict 行。
|
|
21
|
+
* 8. `verdict-mismatch` — 正文 verdict 取词必须属于 `QC_VERDICTS` 且与
|
|
22
|
+
* frontmatter verdict 一致;允许尾随散文(真实报告写
|
|
23
|
+
* `**Verdict**: Approve. The diff keeps ...`,历史上还出现
|
|
24
|
+
* `## Verdict: **Approve with residuals**`)。
|
|
25
|
+
* 9. `summary-count-mismatch` — `## Summary` 计数必须等于 `## Findings`
|
|
26
|
+
* 对应 severity 分区的顶层条目数(`None.` = 0)。`## Summary` 是报告
|
|
27
|
+
* 唯一的当前计数:就地 revalidation 刷新它与 `## Findings`,
|
|
28
|
+
* `## Revalidation` 只记过程与逐 finding 处置,不另立计数。
|
|
29
|
+
* 10. `verdict-contradicts-counts` — Critical / Warning 计数 > 0 不允许
|
|
30
|
+
* `Approve`;Unconfirmed 计数 > 0 要求 verdict 为 `Unconfirmed`。
|
|
31
|
+
* 11. `truncation-verdict` — 已声明 `Truncated coverage:` 行时 verdict 不得为
|
|
32
|
+
* `Unconfirmed`(截断是范围收缩,不是证据通道失败)。
|
|
33
|
+
*
|
|
34
|
+
* 不可判定即静默:形状未文档化(分区或计数行缺失、计数单元格非数字、
|
|
35
|
+
* frontmatter verdict 本身非法)时不出 violation,绝不猜测;每条 violation
|
|
36
|
+
* 都带可执行的 `fix`。
|
|
37
|
+
*/
|
|
38
|
+
import type { GateResult } from "./core.js";
|
|
39
|
+
/**
|
|
40
|
+
* Verdict 词表 —— 逐字取自 `report-template.md` § Report body template。
|
|
41
|
+
* `Unconfirmed` 是证据通道失败态(不是"未审"),预算截断不得用它。
|
|
42
|
+
*/
|
|
43
|
+
export declare const QC_VERDICTS: readonly ["Approve", "Request Changes", "Needs Discussion", "Unconfirmed"];
|
|
44
|
+
/** 席位报告 verdict 取值(`QC_VERDICTS` 成员)。 */
|
|
45
|
+
export type QcVerdict = (typeof QC_VERDICTS)[number];
|
|
46
|
+
/**
|
|
47
|
+
* Validate a QC 席位报告 against the rules in the module header.
|
|
48
|
+
* Structural checks only — 报告形状与 frontmatter 一致性,不重审审查内容。
|
|
49
|
+
*/
|
|
50
|
+
export declare function validateQcReport(text: string): GateResult;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mstar-harness/engine",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.2",
|
|
4
4
|
"description": "Morning Star Harness Workflow Engine — deterministic workflow enforcement library (path, status, lease, dispatch, sdd, iteration, lint gates).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|