@mstar-harness/engine 3.10.2 → 3.11.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/dist/audit.d.ts CHANGED
@@ -167,11 +167,47 @@ export type AuditFinding = {
167
167
  effort: AuditEffort;
168
168
  risk: AuditRisk;
169
169
  confidence: AuditConfidence;
170
- evidence: readonly string[];
170
+ /** Free-text string (legacy form, rendered byte-for-byte) or a structured
171
+ * location. Authored JSON should prefer the structured form. */
172
+ evidence: readonly (string | AuditEvidence)[];
171
173
  priority: AuditPriority;
172
174
  fixSketch?: string;
173
175
  verification?: string;
174
176
  dependsOn?: string;
177
+ /** Source-derived root-cause identity (audit-finding-contract.md §4).
178
+ * Optional; never invented by the engine. */
179
+ fingerprint?: string;
180
+ /** Entry-point → propagation → sink path (§5). Supplements evidence. */
181
+ trace?: readonly AuditTraceStep[];
182
+ /** Author-supplied severity assessment (§6). Never inferred from
183
+ * priority/risk/confidence. */
184
+ severity?: AuditSeverity;
185
+ };
186
+ /** One structured evidence location. `file` is a safe repository-relative
187
+ * POSIX path; `line` is omitted when unknown (never invented). */
188
+ export type AuditEvidence = {
189
+ file: string;
190
+ line?: number;
191
+ description: string;
192
+ };
193
+ /** Trace-step kind (§5): where data enters, how it travels, where it lands. */
194
+ export type AuditTraceKind = "entrypoint" | "propagation" | "sink";
195
+ /** One trace step. `line` is a positive safe integer; `scope` names the
196
+ * enclosing route/function/module. */
197
+ export type AuditTraceStep = {
198
+ kind: AuditTraceKind;
199
+ file: string;
200
+ line: number;
201
+ scope: string;
202
+ description: string;
203
+ };
204
+ /** Severity rank ordinal labels (§6). Harness diagnostic `Severity` is a
205
+ * DIFFERENT enum (`nit`, no `informational`) — never conflate them. */
206
+ export type AuditSeverityRank = "informational" | "low" | "medium" | "high" | "critical";
207
+ export type AuditSeverity = {
208
+ likelihood: AuditSeverityRank;
209
+ impact: AuditSeverityRank;
210
+ overall: AuditSeverityRank;
175
211
  };
176
212
  /** Options for `scaffoldAuditPlan`. `plannedAt` defaults to the
177
213
  * `repoShortSha` + `date`; `date` defaults to today (YYYY-MM-DD). */
@@ -205,6 +241,21 @@ export type ScaffoldAuditPlanResult = {
205
241
  files: string[];
206
242
  nextNumber: number;
207
243
  };
244
+ /**
245
+ * Deterministic finding gates (audit-finding-contract.md §§2–6): fingerprint
246
+ * grammar / exact uniqueness / ordinal ordering of the supplied subsequence,
247
+ * `severity.overall ≤ severity.impact`, nonempty trace topology, positive
248
+ * safe-integer trace lines, safe typed paths, visible well-formed text, and
249
+ * opaque-field credential rejection (a fingerprint or typed location that
250
+ * `redactSecrets` would alter is REJECTED, never redacted into a different
251
+ * identity). Contextual judgement (reportability, semantic exclusion,
252
+ * coverage) stays in skill prose (§2).
253
+ *
254
+ * Violation codes are stable `audit.finding.<family>.<rule>`; messages carry
255
+ * `findings[index].field` paths only — never raw submitted values. Pure:
256
+ * the input is never mutated and nothing is sorted or repaired.
257
+ */
258
+ export declare function validateAuditFindingGates(findings: readonly AuditFinding[]): GateResult;
208
259
  /**
209
260
  * Scaffold an audit plan directory (`{PLAN_DIR}/audit-<date>/` layout,
210
261
  * mstar-audit SKILL.md § Plan output (all variants)): numbered `NNN-<slug>.md` plan files from
package/dist/audit.js CHANGED
@@ -279,7 +279,7 @@ function validatePlanProgress(value, what = "coordination.progress") {
279
279
  }
280
280
  return violations;
281
281
  }
282
- function validatePlanHandoff(value, what = "coordination.handoff") {
282
+ function validatePlanHandoff(value, what = "coordination.handoff", route = "integration") {
283
283
  if (!isPlainObject2(value))
284
284
  return [invalid("coordination.row.handoff-shape", `${what} must be an object`)];
285
285
  const allowed = [
@@ -419,7 +419,25 @@ function validatePlanHandoff(value, what = "coordination.handoff") {
419
419
  }
420
420
  }
421
421
  if ((value.state === "integrating" || value.state === "merged" || value.state === "completed") && value.integration === undefined) {
422
- violations.push(invalid("coordination.row.handoff-field", `${what}.state ${String(value.state)} requires integration`));
422
+ if (route === "standalone-development" && value.state === "completed") {
423
+ if (value.completed_at === undefined) {
424
+ violations.push(invalid("coordination.row.handoff-field", `${what}.state completed requires completed_at for a standalone handoff`));
425
+ }
426
+ if (!isNonEmptyString(value.accepted_at)) {
427
+ violations.push(invalid("coordination.row.handoff-field", `${what}.accepted_at is required for a standalone completed handoff`));
428
+ }
429
+ if (!isNonEmptyString(value.accepted_by)) {
430
+ violations.push(invalid("coordination.row.handoff-field", `${what}.accepted_by is required for a standalone completed handoff`));
431
+ }
432
+ if (value.qc === undefined) {
433
+ violations.push(invalid("coordination.row.handoff-field", `${what}.qc is required for a standalone completed handoff`));
434
+ }
435
+ if (value.qa === undefined) {
436
+ violations.push(invalid("coordination.row.handoff-field", `${what}.qa is required for a standalone completed handoff`));
437
+ }
438
+ } else {
439
+ violations.push(invalid("coordination.row.handoff-field", `${what}.state ${String(value.state)} requires integration`));
440
+ }
423
441
  }
424
442
  return violations;
425
443
  }
@@ -455,7 +473,7 @@ function validatePreparedCoordination(value, what = "coordination.prepared") {
455
473
  }
456
474
  return violations;
457
475
  }
458
- function validateRowCoordination(value, what = "coordination") {
476
+ function validateRowCoordination(value, what = "coordination", route = "integration") {
459
477
  if (!isPlainObject2(value))
460
478
  return [invalid("coordination.row.shape", `${what} must be an object`)];
461
479
  const allowed = ["revision", "prepared", "session", "progress", "handoff"];
@@ -474,7 +492,7 @@ function validateRowCoordination(value, what = "coordination") {
474
492
  if (value.progress !== undefined)
475
493
  violations.push(...validatePlanProgress(value.progress, `${what}.progress`));
476
494
  if (value.handoff !== undefined)
477
- violations.push(...validatePlanHandoff(value.handoff, `${what}.handoff`));
495
+ violations.push(...validatePlanHandoff(value.handoff, `${what}.handoff`, route));
478
496
  if (value.handoff !== undefined && value.session === undefined) {
479
497
  violations.push(invalid("coordination.row.handoff-field", `${what}.handoff requires a bound plan session`));
480
498
  }
@@ -797,6 +815,45 @@ var WORKFLOW_TERMINAL_STATUSES = ["completed", "failed", "stopped"];
797
815
  var WORKFLOW_LIFECYCLE_TYPES = ["plan", "iteration"];
798
816
  var WORKFLOW_DELIVERY_KINDS = ["development", "verification/report-only"];
799
817
  var WORKFLOW_COMPOUND_OUTCOMES = ["created", "updated", "skipped"];
818
+ function isStandaloneDevelopmentWorkflow(snapshot) {
819
+ return snapshot.type === "plan" && snapshot.delivery_kind === "development" && Array.isArray(snapshot.plans) && snapshot.plans.length === 1;
820
+ }
821
+ function rowValidationRoute(snapshot, row) {
822
+ if (isStandaloneDevelopmentWorkflow(snapshot) && snapshot.plans[0]?.id === row.id) {
823
+ return "standalone-development";
824
+ }
825
+ return "integration";
826
+ }
827
+ function validateStandaloneCompletedCoherence(snapshot, row) {
828
+ const violations = [];
829
+ if (!isStandaloneDevelopmentWorkflow(snapshot) || row.id !== snapshot.plans[0]?.id)
830
+ return violations;
831
+ const coordination = row.coordination;
832
+ if (!isPlainObject2(coordination) || !isPlainObject2(coordination.handoff))
833
+ return violations;
834
+ const handoff = coordination.handoff;
835
+ if (handoff.state !== "completed" || handoff.integration !== undefined)
836
+ return violations;
837
+ if (row.status !== "Done") {
838
+ violations.push(violation2("high", "coordination.row.handoff-field", `standalone completed handoff requires row ${String(row.id)} to be Done`));
839
+ }
840
+ if (row.execution_lease !== undefined) {
841
+ violations.push(violation2("high", "coordination.row.handoff-field", `standalone completed handoff requires no execution lease on row ${String(row.id)}`));
842
+ }
843
+ if (snapshot.integration_merge_lease !== undefined) {
844
+ violations.push(violation2("high", "coordination.row.handoff-field", "standalone completed handoff requires no integration_merge_lease on the snapshot"));
845
+ }
846
+ const source = snapshot.branch?.source;
847
+ const target = snapshot.branch?.target;
848
+ if (!isNonEmptyString(source) || !isNonEmptyString(target)) {
849
+ violations.push(violation2("high", "coordination.row.handoff-field", "standalone completed handoff requires nonblank branch.source and branch.target"));
850
+ } else {
851
+ if (handoff.source_branch !== source) {
852
+ violations.push(violation2("high", "coordination.row.handoff-field", `standalone completed handoff source_branch ${String(handoff.source_branch)} must equal branch.source ${source}`));
853
+ }
854
+ }
855
+ return violations;
856
+ }
800
857
  function stableJson(value) {
801
858
  if (Array.isArray(value))
802
859
  return `[${value.map(stableJson).join(",")}]`;
@@ -916,13 +973,17 @@ function validateWorkflowSnapshot(doc) {
916
973
  } else if (!Array.isArray(doc.plans)) {
917
974
  violations.push(violation2("high", "workflow.snapshot.invalid-plans", "plans must be an array of legacy plan rows"));
918
975
  } else {
976
+ const snapshotDoc = doc;
919
977
  for (const row of doc.plans) {
920
978
  violations.push(...validatePlanRow(row).violations);
921
979
  if (isPlainObject2(row) && row.execution_lease !== undefined) {
922
980
  violations.push(...validateExecutionLease(row.execution_lease).violations);
923
981
  }
924
982
  if (isPlainObject2(row) && row.coordination !== undefined) {
925
- violations.push(...validateRowCoordination(row.coordination, `plans[${String(row.id)}].coordination`));
983
+ const planRow = row;
984
+ const route = rowValidationRoute(snapshotDoc, planRow);
985
+ violations.push(...validateRowCoordination(row.coordination, `plans[${String(row.id)}].coordination`, route));
986
+ violations.push(...validateStandaloneCompletedCoherence(snapshotDoc, planRow));
926
987
  }
927
988
  }
928
989
  }
@@ -1883,6 +1944,176 @@ function slugify(title) {
1883
1944
  }
1884
1945
  var escapeCell = (value) => value.replace(/\|/g, "\\|");
1885
1946
  var truncate = (value, max) => value.length > max ? `${value.slice(0, max)}…` : value;
1947
+ var collapseEvidenceWs = (value) => value.replace(/\s*[\r\n]\s*/g, " ");
1948
+ var evidenceText = (item) => typeof item === "string" ? item : `${item.file}${item.line !== undefined ? `:${item.line}` : ""} — ${item.description}`;
1949
+ var hasEnrichedMetadata = (finding) => finding.fingerprint !== undefined || finding.trace !== undefined || finding.severity !== undefined || finding.evidence.some((item) => typeof item !== "string");
1950
+ var AUDIT_FINGERPRINT_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@+-]*(?![\s\S])/;
1951
+ var AUDIT_SEVERITY_ORDER = { informational: 0, low: 1, medium: 2, high: 3, critical: 4 };
1952
+ var DEFAULT_IGNORABLE_RE = /[\u00AD\u034F\u061C\u115F\u1160\u17B4\u17B5\u180B-\u180F\u200B-\u200F\u202A-\u202E\u2060-\u206F\u3164\uFE00-\uFE0F\uFEFF\uFFA0\uFFF0-\uFFF8\u{1BCA0}-\u{1BCA3}\u{1D173}-\u{1D17A}\u{E0000}-\u{E0FFF}]/u;
1953
+ var LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
1954
+ var isPlainObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1955
+ function isVisibleText(value) {
1956
+ if (LONE_SURROGATE_RE.test(value))
1957
+ return false;
1958
+ for (const ch of value) {
1959
+ if (!/\p{White_Space}/u.test(ch) && !DEFAULT_IGNORABLE_RE.test(ch))
1960
+ return true;
1961
+ }
1962
+ return false;
1963
+ }
1964
+ function safeAuditPath(value) {
1965
+ if (typeof value !== "string")
1966
+ return false;
1967
+ if (value === "")
1968
+ return false;
1969
+ if (value.includes("\\"))
1970
+ return false;
1971
+ if (/[\u0000-\u001F\u007F-\u009F]/.test(value))
1972
+ return false;
1973
+ if (LONE_SURROGATE_RE.test(value))
1974
+ return false;
1975
+ if (value.startsWith("/"))
1976
+ return false;
1977
+ if (/^[A-Za-z]:/.test(value))
1978
+ return false;
1979
+ for (const segment of value.split("/")) {
1980
+ if (segment === "" || segment === "." || segment === "..")
1981
+ return false;
1982
+ if (segment.endsWith(".") || segment.endsWith(" "))
1983
+ return false;
1984
+ }
1985
+ return true;
1986
+ }
1987
+ function validateAuditFindingGates(findings) {
1988
+ const violations = [];
1989
+ const seen = new Map;
1990
+ let previousFingerprint;
1991
+ if (!Array.isArray(findings)) {
1992
+ violations.push(violation4("high", "audit.finding.shape", "findings — audit.finding.shape"));
1993
+ return { ok: false, violations };
1994
+ }
1995
+ findings.forEach((finding, index) => {
1996
+ const at = (field) => `findings[${index}].${field}`;
1997
+ const push = (code, field) => {
1998
+ violations.push(violation4("high", code, `${at(field)} — ${code}`));
1999
+ };
2000
+ if (!isPlainObject3(finding)) {
2001
+ violations.push(violation4("high", "audit.finding.shape", `findings[${index}] — audit.finding.shape`));
2002
+ return;
2003
+ }
2004
+ if (finding.fingerprint !== undefined) {
2005
+ const fingerprint = finding.fingerprint;
2006
+ if (typeof fingerprint !== "string" || !AUDIT_FINGERPRINT_RE.test(fingerprint))
2007
+ push("audit.finding.fingerprint.grammar", "fingerprint");
2008
+ else if (redactSecrets(fingerprint).text !== fingerprint)
2009
+ push("audit.finding.fingerprint.secret", "fingerprint");
2010
+ else {
2011
+ const firstAt = seen.get(fingerprint);
2012
+ if (firstAt !== undefined)
2013
+ push("audit.finding.fingerprint.duplicate", "fingerprint");
2014
+ else
2015
+ seen.set(fingerprint, index);
2016
+ if (previousFingerprint !== undefined && fingerprint < previousFingerprint) {
2017
+ push("audit.finding.fingerprint.order", "fingerprint");
2018
+ }
2019
+ previousFingerprint = fingerprint;
2020
+ }
2021
+ }
2022
+ if (finding.severity !== undefined) {
2023
+ if (!isPlainObject3(finding.severity)) {
2024
+ push("audit.finding.severity.shape", "severity");
2025
+ } else {
2026
+ const { likelihood, impact, overall } = finding.severity;
2027
+ const rankOf = (r) => AUDIT_SEVERITY_ORDER[r];
2028
+ for (const [field, value] of [
2029
+ ["likelihood", likelihood],
2030
+ ["impact", impact],
2031
+ ["overall", overall]
2032
+ ]) {
2033
+ if (rankOf(value) === undefined)
2034
+ push("audit.finding.severity.rank", `severity.${field}`);
2035
+ }
2036
+ const impactRank = rankOf(impact);
2037
+ const overallRank = rankOf(overall);
2038
+ if (impactRank !== undefined && overallRank !== undefined && overallRank > impactRank) {
2039
+ push("audit.finding.severity.overall-exceeds-impact", "severity.overall");
2040
+ }
2041
+ }
2042
+ }
2043
+ const textViolation = (field, value) => {
2044
+ if (value === undefined)
2045
+ return;
2046
+ if (typeof value !== "string")
2047
+ push("audit.finding.text.type", field);
2048
+ else if (LONE_SURROGATE_RE.test(value))
2049
+ push("audit.finding.text.surrogate", field);
2050
+ else if (!isVisibleText(value))
2051
+ push("audit.finding.text.invisible", field);
2052
+ };
2053
+ if (finding.trace !== undefined) {
2054
+ if (!Array.isArray(finding.trace))
2055
+ push("audit.finding.trace.shape", "trace");
2056
+ else if (finding.trace.length === 0)
2057
+ push("audit.finding.trace.empty", "trace");
2058
+ else {
2059
+ const kinds = finding.trace.map((s) => isPlainObject3(s) ? s.kind : undefined);
2060
+ const topologyOk = finding.trace.length === 1 ? kinds[0] === "entrypoint" || kinds[0] === "sink" : kinds[0] === "entrypoint" && kinds[kinds.length - 1] === "sink" && kinds.slice(1, -1).every((k) => k === "propagation");
2061
+ if (!topologyOk)
2062
+ push("audit.finding.trace.topology", "trace");
2063
+ finding.trace.forEach((step, stepIndex) => {
2064
+ const stepAt = `trace[${stepIndex}]`;
2065
+ if (!isPlainObject3(step)) {
2066
+ push("audit.finding.trace.shape", stepAt);
2067
+ return;
2068
+ }
2069
+ if (typeof step.line !== "number" || !Number.isSafeInteger(step.line) || step.line <= 0) {
2070
+ push("audit.finding.trace.line", `${stepAt}.line`);
2071
+ }
2072
+ const stepFile = step.file;
2073
+ if (typeof stepFile !== "string" || !safeAuditPath(stepFile))
2074
+ push("audit.finding.path.unsafe", `${stepAt}.file`);
2075
+ else if (redactSecrets(stepFile).text !== stepFile)
2076
+ push("audit.finding.path.secret", `${stepAt}.file`);
2077
+ if (step.scope === undefined)
2078
+ push("audit.finding.trace.shape", `${stepAt}.scope`);
2079
+ else
2080
+ textViolation(`${stepAt}.scope`, step.scope);
2081
+ if (step.description === undefined)
2082
+ push("audit.finding.trace.shape", `${stepAt}.description`);
2083
+ else
2084
+ textViolation(`${stepAt}.description`, step.description);
2085
+ });
2086
+ }
2087
+ }
2088
+ if (!Array.isArray(finding.evidence))
2089
+ push("audit.finding.evidence.shape", "evidence");
2090
+ else
2091
+ finding.evidence.forEach((item, itemIndex) => {
2092
+ if (typeof item === "string") {
2093
+ textViolation(`evidence[${itemIndex}]`, item);
2094
+ return;
2095
+ }
2096
+ if (!isPlainObject3(item)) {
2097
+ push("audit.finding.evidence.shape", `evidence[${itemIndex}]`);
2098
+ return;
2099
+ }
2100
+ const itemFile = item.file;
2101
+ if (typeof itemFile !== "string" || !safeAuditPath(itemFile))
2102
+ push("audit.finding.path.unsafe", `evidence[${itemIndex}].file`);
2103
+ else if (redactSecrets(itemFile).text !== itemFile)
2104
+ push("audit.finding.path.secret", `evidence[${itemIndex}].file`);
2105
+ if (item.line !== undefined && (typeof item.line !== "number" || !Number.isSafeInteger(item.line) || item.line <= 0)) {
2106
+ push("audit.finding.evidence.line", `evidence[${itemIndex}].line`);
2107
+ }
2108
+ textViolation(`evidence[${itemIndex}].description`, item.description);
2109
+ });
2110
+ textViolation("title", finding.title);
2111
+ textViolation("impact", finding.impact);
2112
+ textViolation("fixSketch", finding.fixSketch);
2113
+ textViolation("verification", finding.verification);
2114
+ });
2115
+ return { ok: violations.length === 0, violations };
2116
+ }
1886
2117
  function renderPlanFile(finding, plannedAt) {
1887
2118
  const sections = [
1888
2119
  `# ${finding.title}`,
@@ -1891,15 +2122,22 @@ function renderPlanFile(finding, plannedAt) {
1891
2122
  `- **Priority**: ${finding.priority}`,
1892
2123
  `- **Effort**: ${finding.effort}`,
1893
2124
  `- **Risk**: ${finding.risk}`,
2125
+ ...finding.confidence !== "MED" || hasEnrichedMetadata(finding) ? [`- **Confidence**: ${finding.confidence}`] : [],
2126
+ ...finding.fingerprint !== undefined ? [`- **Fingerprint**: ${finding.fingerprint}`] : [],
2127
+ ...finding.severity !== undefined ? [`- **Likelihood**: ${finding.severity.likelihood}`, `- **Severity impact**: ${finding.severity.impact}`, `- **Severity**: ${finding.severity.overall}`] : [],
1894
2128
  `- **Depends on**: ${finding.dependsOn ?? "none"}`,
1895
2129
  `- **Category**: ${finding.category}`,
2130
+ ...finding.evidence.length > 0 ? [`- **Evidence**: ${collapseEvidenceWs(evidenceText(finding.evidence[0]))}`] : [],
1896
2131
  `- **Planned at**: commit \`${plannedAt.commit}\`, ${plannedAt.date}`,
1897
2132
  "",
1898
2133
  "## Impact",
1899
2134
  finding.impact
1900
2135
  ];
1901
2136
  if (finding.evidence.length > 0) {
1902
- sections.push("", "## Evidence", ...finding.evidence.map((e) => `- ${e}`));
2137
+ sections.push("", "## Evidence", ...finding.evidence.map((item) => `- ${evidenceText(item)}`));
2138
+ }
2139
+ if (finding.trace !== undefined) {
2140
+ sections.push("", "## Trace", "", "| Kind | Location | Scope / Description |", "|------|----------|---------------------|", ...finding.trace.map((step) => `| ${escapeCell(step.kind)} | ${escapeCell(`${step.file}:${step.line}`)} | ${escapeCell(`${step.scope} — ${step.description}`).replace(/\r\n|\r|\n/g, "\\n")} |`));
1903
2141
  }
1904
2142
  if (finding.fixSketch !== undefined) {
1905
2143
  sections.push("", "## Fix sketch", finding.fixSketch);
@@ -1919,7 +2157,8 @@ function redactFinding(finding) {
1919
2157
  ...finding,
1920
2158
  title: redactText(finding.title),
1921
2159
  impact: redactText(finding.impact),
1922
- evidence: finding.evidence.map(redactText),
2160
+ evidence: finding.evidence.map((item) => typeof item === "string" ? redactText(item) : { ...item, description: redactText(item.description) }),
2161
+ ...finding.trace !== undefined ? { trace: finding.trace.map((step) => ({ ...step, scope: redactText(step.scope), description: redactText(step.description) })) } : {},
1923
2162
  ...finding.fixSketch !== undefined ? { fixSketch: redactText(finding.fixSketch) } : {},
1924
2163
  ...finding.verification !== undefined ? { verification: redactText(finding.verification) } : {}
1925
2164
  };
@@ -1941,7 +2180,10 @@ function extractSecurityDispositionSections(text) {
1941
2180
  }
1942
2181
  function renderIndex(params) {
1943
2182
  const { date, repoName, repoShortSha, rows, rejected, needsVerification, hardeningChecked } = params;
1944
- const findingsRows = rows.map((r) => `| ${r.num} | ${escapeCell(r.title)} | ${r.category} | ${escapeCell(truncate(r.impact, 80))} | ${r.effort} | ${r.risk} | ${r.confidence} | ${escapeCell(truncate(r.evidence, 80))} |`).join(`
2183
+ const showFingerprint = rows.some((r) => r.fingerprint !== undefined);
2184
+ const showSeverity = rows.some((r) => r.likelihood !== undefined || r.severityImpact !== undefined || r.severity !== undefined);
2185
+ const cell = (value) => escapeCell(value ?? "—");
2186
+ const findingsRows = rows.map((r) => `| ${r.num} | ${escapeCell(r.title)} | ${r.category} | ${escapeCell(truncate(r.impact, 80))} | ${r.effort} | ${r.risk} | ${r.confidence} | ${escapeCell(truncate(r.evidence, 80))}` + (showFingerprint ? ` | ${cell(r.fingerprint)}` : "") + (showSeverity ? ` | ${cell(r.likelihood)} | ${cell(r.severityImpact)} | ${cell(r.severity)}` : "") + " |").join(`
1945
2187
  `);
1946
2188
  const directionRows = rows.filter((r) => r.category === "direction").map((r) => `- ${escapeCell(r.title)} — ${escapeCell(truncate(r.impact, 120))}`).join(`
1947
2189
  `);
@@ -1954,8 +2196,8 @@ function renderIndex(params) {
1954
2196
  "",
1955
2197
  "## Findings",
1956
2198
  "",
1957
- "| # | Finding | Category | Impact | Effort | Risk | Confidence | Evidence |",
1958
- "|---|---------|----------|--------|--------|------|------------|----------|",
2199
+ "| # | Finding | Category | Impact | Effort | Risk | Confidence | Evidence" + (showFingerprint ? " | Fingerprint" : "") + (showSeverity ? " | Likelihood | Severity impact | Severity" : "") + " |",
2200
+ "|---|---------|----------|--------|--------|------|------------|----------" + (showFingerprint ? "|------------" : "") + (showSeverity ? "|------------|-----------------|----------" : "") + "|",
1959
2201
  findingsRows
1960
2202
  ];
1961
2203
  if (directionRows !== "") {
@@ -1979,6 +2221,11 @@ function renderIndex(params) {
1979
2221
  function scaffoldAuditPlan(outDir, findings, options = {}) {
1980
2222
  const date = options.date ?? new Date().toISOString().slice(0, 10);
1981
2223
  const plannedAt = options.plannedAt ?? { commit: options.repoShortSha ?? "unknown", date };
2224
+ const gate = validateAuditFindingGates(findings);
2225
+ if (!gate.ok) {
2226
+ const first = gate.violations[0];
2227
+ throw new TypeError(`invalid audit findings — ${first.code}: ${first.message}`);
2228
+ }
1982
2229
  mkdirSync5(outDir, { recursive: true });
1983
2230
  const existingReadme = join9(outDir, "README.md");
1984
2231
  const carried = existsSync7(existingReadme) ? extractSecurityDispositionSections(readFileSync7(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
@@ -2013,10 +2260,14 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
2013
2260
  impact: "see plan file",
2014
2261
  effort: fields.get("Effort") ?? "—",
2015
2262
  risk: fields.get("Risk") ?? "—",
2016
- confidence: "—",
2263
+ confidence: fields.get("Confidence") ?? "—",
2017
2264
  evidence: fields.get("Evidence") ?? "—",
2018
2265
  priority: fields.get("Priority") ?? "—",
2019
- dependsOn: fields.get("Depends on") ?? "—"
2266
+ dependsOn: fields.get("Depends on") ?? "—",
2267
+ fingerprint: fields.get("Fingerprint"),
2268
+ likelihood: fields.get("Likelihood"),
2269
+ severityImpact: fields.get("Severity impact"),
2270
+ severity: fields.get("Severity")
2020
2271
  };
2021
2272
  });
2022
2273
  const byNum = new Map(rows.map((r) => [r.num, r]));
@@ -2031,9 +2282,13 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
2031
2282
  row.effort = finding.effort;
2032
2283
  row.risk = finding.risk;
2033
2284
  row.confidence = finding.confidence;
2034
- row.evidence = finding.evidence[0] ?? "";
2285
+ row.evidence = finding.evidence.length > 0 ? collapseEvidenceWs(evidenceText(finding.evidence[0])) : "";
2035
2286
  row.priority = finding.priority;
2036
2287
  row.dependsOn = finding.dependsOn ?? "none";
2288
+ row.fingerprint = finding.fingerprint;
2289
+ row.likelihood = finding.severity?.likelihood;
2290
+ row.severityImpact = finding.severity?.impact;
2291
+ row.severity = finding.severity?.overall;
2037
2292
  }
2038
2293
  });
2039
2294
  const needsVerificationLines = options.needsVerification !== undefined ? options.needsVerification.map((nv) => `- ${escapeCell(redactText(nv.lead))}: ${escapeCell(redactText(nv.how))}${nv.evidence ? ` (${escapeCell(redactText(nv.evidence))})` : ""}`) : carried.needsVerification;
@@ -2220,5 +2475,6 @@ export {
2220
2475
  scaffoldAuditPlan,
2221
2476
  scanSecrets,
2222
2477
  supplyChainChecks,
2478
+ validateAuditFindingGates,
2223
2479
  validateAuditStatusBlocks
2224
2480
  };
@@ -1,6 +1,6 @@
1
1
  import type { ValidationResult } from "./core.js";
2
2
  /** Stable refusal codes of the scoped coordination surface (spec §C4). */
3
- export declare const COORDINATION_ERROR_CODES: readonly ["coordination.harness-not-found", "coordination.workflow-not-found", "coordination.plan-not-found", "coordination.scope-mismatch", "coordination.path-mismatch", "coordination.assignment-invalid", "coordination.assignment-stale", "coordination.not-prepared", "coordination.duplicate-holder", "coordination.session-mismatch", "coordination.session-not-found", "coordination.session-role", "coordination.version-conflict", "coordination.expected-version-required", "coordination.invalid-transition", "coordination.invalid-input", "coordination.forbidden-field", "coordination.not-in-git", "coordination.git-unavailable", "coordination.git-proof", "coordination.evidence-stale", "coordination.integration-unresolved", "coordination.integration-diverged", "coordination.local-store-required", "coordination.direct-write-refused", "coordination.scoped-writer-required", "coordination.unknown-operation", "coordination.store", "coordination.prepare-amendment.stale", "coordination.prepare-amendment.invalid-patch", "coordination.prepare-amendment.not-prepare", "coordination.prepare-amendment.execution-started", "coordination.prepare-amendment.duplicate-plan", "coordination.prepare-amendment.invalid-plan", "coordination.prepare-amendment.compass-mismatch", "coordination.prepare-amendment.invalid-worktree"];
3
+ export declare const COORDINATION_ERROR_CODES: readonly ["coordination.harness-not-found", "coordination.workflow-not-found", "coordination.plan-not-found", "coordination.scope-mismatch", "coordination.path-mismatch", "coordination.assignment-invalid", "coordination.assignment-stale", "coordination.not-prepared", "coordination.duplicate-holder", "coordination.session-mismatch", "coordination.session-not-found", "coordination.session-role", "coordination.version-conflict", "coordination.expected-version-required", "coordination.invalid-transition", "coordination.invalid-input", "coordination.forbidden-field", "coordination.not-in-git", "coordination.git-unavailable", "coordination.git-proof", "coordination.evidence-stale", "coordination.integration-unresolved", "coordination.integration-diverged", "coordination.local-store-required", "coordination.direct-write-refused", "coordination.scoped-writer-required", "coordination.unknown-operation", "coordination.store", "coordination.prepare-amendment.stale", "coordination.prepare-amendment.invalid-patch", "coordination.prepare-amendment.not-prepare", "coordination.prepare-amendment.execution-started", "coordination.prepare-amendment.duplicate-plan", "coordination.prepare-amendment.invalid-plan", "coordination.prepare-amendment.compass-mismatch", "coordination.prepare-amendment.invalid-worktree", "coordination.delivery-source-repair.unsupported-workflow", "coordination.delivery-source-repair.terminal", "coordination.delivery-source-repair.no-accepted-handoff", "coordination.delivery-source-repair.already-aligned", "coordination.delivery-source-repair.not-legacy-shape", "coordination.delivery-source-repair.pr-conflict"];
4
4
  export type CoordinationErrorCode = (typeof COORDINATION_ERROR_CODES)[number];
5
5
  /**
6
6
  * Stable exception of the coordination surface: `code` is the consumer
@@ -156,12 +156,14 @@ export declare const HANDOFF_STATES: readonly HandoffState[];
156
156
  export declare const PLAN_PROGRESS_STATUSES: readonly PlanProgressStatus[];
157
157
  /** Validate a stored `PlanProgress` (`status`, `summary`, `evidence_paths`, `track_branches`). */
158
158
  export declare function validatePlanProgress(value: unknown, what?: string): ValidationResult[];
159
+ /** Route-aware stored handoff validation (spec A5). Default remains strict integration. */
160
+ export type RowValidationRoute = "integration" | "standalone-development";
159
161
  /** Validate a stored `PlanHandoff`, including its state/field coherence. */
160
- export declare function validatePlanHandoff(value: unknown, what?: string): ValidationResult[];
162
+ export declare function validatePlanHandoff(value: unknown, what?: string, route?: RowValidationRoute): ValidationResult[];
161
163
  /** Validate a stored `PreparedCoordination`. */
162
164
  export declare function validatePreparedCoordination(value: unknown, what?: string): ValidationResult[];
163
165
  /** Validate one plan row's `coordination` object (spec §C2). */
164
- export declare function validateRowCoordination(value: unknown, what?: string): ValidationResult[];
166
+ export declare function validateRowCoordination(value: unknown, what?: string, route?: RowValidationRoute): ValidationResult[];
165
167
  /** Validate a snapshot's top `coordination` block (spec §C2). */
166
168
  export declare function validateSnapshotCoordination(value: unknown, what?: string): ValidationResult[];
167
169
  /** Hash-pinned evidence reference for an absolute path, read from disk. */
@@ -197,6 +197,9 @@ export type PlanCoordinationOperation = {
197
197
  } | {
198
198
  kind: "complete";
199
199
  handoffId: string;
200
+ } | {
201
+ kind: "repair-delivery-source";
202
+ handoffId: string;
200
203
  } | {
201
204
  kind: "reconcile";
202
205
  handoffId: string;
@@ -272,6 +275,8 @@ export declare function bindPlanSession(input: BindPlanSessionInput): Promise<Co
272
275
  * an unknown key anywhere is rejected before any state is touched.
273
276
  */
274
277
  export declare function mutatePlanCoordination(request: CoordinationRequest): Promise<CoordinationResult>;
278
+ /** Test-only hook to observe the precheck→mutate gap in standalone complete. */
279
+ export declare function setCompleteStandaloneMutateGapForTest(callback: (() => void) | undefined): void;
275
280
  /**
276
281
  * Replace a coordinated artifact with an exact-version precondition (spec §B,
277
282
  * §C4 line 156). Snapshot replacement goes through the canonical snapshot