@mstar-harness/engine 3.10.3 → 3.11.1
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 +52 -1
- package/dist/audit.js +203 -8
- package/dist/coordination-write.d.ts +1 -1
- package/dist/coordination.d.ts +14 -8
- package/dist/engine.js +391 -25
- package/dist/index.d.ts +4 -4
- package/dist/workflow.d.ts +75 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
@@ -1944,6 +1944,176 @@ function slugify(title) {
|
|
|
1944
1944
|
}
|
|
1945
1945
|
var escapeCell = (value) => value.replace(/\|/g, "\\|");
|
|
1946
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
|
+
}
|
|
1947
2117
|
function renderPlanFile(finding, plannedAt) {
|
|
1948
2118
|
const sections = [
|
|
1949
2119
|
`# ${finding.title}`,
|
|
@@ -1952,15 +2122,22 @@ function renderPlanFile(finding, plannedAt) {
|
|
|
1952
2122
|
`- **Priority**: ${finding.priority}`,
|
|
1953
2123
|
`- **Effort**: ${finding.effort}`,
|
|
1954
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}`] : [],
|
|
1955
2128
|
`- **Depends on**: ${finding.dependsOn ?? "none"}`,
|
|
1956
2129
|
`- **Category**: ${finding.category}`,
|
|
2130
|
+
...finding.evidence.length > 0 ? [`- **Evidence**: ${collapseEvidenceWs(evidenceText(finding.evidence[0]))}`] : [],
|
|
1957
2131
|
`- **Planned at**: commit \`${plannedAt.commit}\`, ${plannedAt.date}`,
|
|
1958
2132
|
"",
|
|
1959
2133
|
"## Impact",
|
|
1960
2134
|
finding.impact
|
|
1961
2135
|
];
|
|
1962
2136
|
if (finding.evidence.length > 0) {
|
|
1963
|
-
sections.push("", "## Evidence", ...finding.evidence.map((
|
|
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")} |`));
|
|
1964
2141
|
}
|
|
1965
2142
|
if (finding.fixSketch !== undefined) {
|
|
1966
2143
|
sections.push("", "## Fix sketch", finding.fixSketch);
|
|
@@ -1980,7 +2157,8 @@ function redactFinding(finding) {
|
|
|
1980
2157
|
...finding,
|
|
1981
2158
|
title: redactText(finding.title),
|
|
1982
2159
|
impact: redactText(finding.impact),
|
|
1983
|
-
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) })) } : {},
|
|
1984
2162
|
...finding.fixSketch !== undefined ? { fixSketch: redactText(finding.fixSketch) } : {},
|
|
1985
2163
|
...finding.verification !== undefined ? { verification: redactText(finding.verification) } : {}
|
|
1986
2164
|
};
|
|
@@ -2002,7 +2180,10 @@ function extractSecurityDispositionSections(text) {
|
|
|
2002
2180
|
}
|
|
2003
2181
|
function renderIndex(params) {
|
|
2004
2182
|
const { date, repoName, repoShortSha, rows, rejected, needsVerification, hardeningChecked } = params;
|
|
2005
|
-
const
|
|
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(`
|
|
2006
2187
|
`);
|
|
2007
2188
|
const directionRows = rows.filter((r) => r.category === "direction").map((r) => `- ${escapeCell(r.title)} — ${escapeCell(truncate(r.impact, 120))}`).join(`
|
|
2008
2189
|
`);
|
|
@@ -2015,8 +2196,8 @@ function renderIndex(params) {
|
|
|
2015
2196
|
"",
|
|
2016
2197
|
"## Findings",
|
|
2017
2198
|
"",
|
|
2018
|
-
"| # | Finding | Category | Impact | Effort | Risk | Confidence | Evidence |",
|
|
2019
|
-
"
|
|
2199
|
+
"| # | Finding | Category | Impact | Effort | Risk | Confidence | Evidence" + (showFingerprint ? " | Fingerprint" : "") + (showSeverity ? " | Likelihood | Severity impact | Severity" : "") + " |",
|
|
2200
|
+
"|---|---------|----------|--------|--------|------|------------|----------" + (showFingerprint ? "|------------" : "") + (showSeverity ? "|------------|-----------------|----------" : "") + "|",
|
|
2020
2201
|
findingsRows
|
|
2021
2202
|
];
|
|
2022
2203
|
if (directionRows !== "") {
|
|
@@ -2040,6 +2221,11 @@ function renderIndex(params) {
|
|
|
2040
2221
|
function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
2041
2222
|
const date = options.date ?? new Date().toISOString().slice(0, 10);
|
|
2042
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
|
+
}
|
|
2043
2229
|
mkdirSync5(outDir, { recursive: true });
|
|
2044
2230
|
const existingReadme = join9(outDir, "README.md");
|
|
2045
2231
|
const carried = existsSync7(existingReadme) ? extractSecurityDispositionSections(readFileSync7(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
|
|
@@ -2074,10 +2260,14 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
2074
2260
|
impact: "see plan file",
|
|
2075
2261
|
effort: fields.get("Effort") ?? "—",
|
|
2076
2262
|
risk: fields.get("Risk") ?? "—",
|
|
2077
|
-
confidence: "—",
|
|
2263
|
+
confidence: fields.get("Confidence") ?? "—",
|
|
2078
2264
|
evidence: fields.get("Evidence") ?? "—",
|
|
2079
2265
|
priority: fields.get("Priority") ?? "—",
|
|
2080
|
-
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")
|
|
2081
2271
|
};
|
|
2082
2272
|
});
|
|
2083
2273
|
const byNum = new Map(rows.map((r) => [r.num, r]));
|
|
@@ -2092,9 +2282,13 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
2092
2282
|
row.effort = finding.effort;
|
|
2093
2283
|
row.risk = finding.risk;
|
|
2094
2284
|
row.confidence = finding.confidence;
|
|
2095
|
-
row.evidence = finding.evidence[0]
|
|
2285
|
+
row.evidence = finding.evidence.length > 0 ? collapseEvidenceWs(evidenceText(finding.evidence[0])) : "";
|
|
2096
2286
|
row.priority = finding.priority;
|
|
2097
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;
|
|
2098
2292
|
}
|
|
2099
2293
|
});
|
|
2100
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;
|
|
@@ -2281,5 +2475,6 @@ export {
|
|
|
2281
2475
|
scaffoldAuditPlan,
|
|
2282
2476
|
scanSecrets,
|
|
2283
2477
|
supplyChainChecks,
|
|
2478
|
+
validateAuditFindingGates,
|
|
2284
2479
|
validateAuditStatusBlocks
|
|
2285
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", "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"];
|
|
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.invalid-session-id", "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
|
package/dist/coordination.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export type ResolvedPlanScope = {
|
|
|
38
38
|
};
|
|
39
39
|
/**
|
|
40
40
|
* Session envelope persisted at
|
|
41
|
-
* `{WORKFLOW_DIR}/<workflow-id>/sessions/<session-id>.json` (mode `0600`).
|
|
41
|
+
* `{WORKFLOW_DIR}/<workflow-id>/sessions/<role>-<session-id>.json` (mode `0600`).
|
|
42
42
|
* The envelope is the durable proof of who holds the session: the snapshot
|
|
43
43
|
* stores its canonical path and every later call must present the same file.
|
|
44
44
|
*/
|
|
@@ -51,20 +51,24 @@ export type CoordinationSession = {
|
|
|
51
51
|
harness_root: string;
|
|
52
52
|
};
|
|
53
53
|
/**
|
|
54
|
-
* Bind addressing (spec §B). A fresh bind
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
54
|
+
* Bind addressing (spec §B). A fresh bind generates the session UUID and
|
|
55
|
+
* creates `<workflow-dir>/<workflow-id>/sessions/<role>-<session-id>.json`
|
|
56
|
+
* itself, unless the caller supplies `sessionId` — then the engine adopts that
|
|
57
|
+
* value as the identity after validating it as a single safe path component (it
|
|
58
|
+
* names the envelope's file, prefixed by the role). An existing session is
|
|
59
|
+
* reached only through its explicit `resumePath`, which resumes read-only,
|
|
60
|
+
* never writes and never re-identifies.
|
|
59
61
|
*/
|
|
60
62
|
export type BindPlanSessionInput = {
|
|
61
63
|
scope: PlanScopeInput;
|
|
62
64
|
cwd: string;
|
|
65
|
+
sessionId?: string;
|
|
63
66
|
} | {
|
|
64
67
|
coordinator: true;
|
|
65
68
|
workflowId: string;
|
|
66
69
|
harnessDir?: string;
|
|
67
70
|
cwd: string;
|
|
71
|
+
sessionId?: string;
|
|
68
72
|
} | {
|
|
69
73
|
resumePath: string;
|
|
70
74
|
cwd: string;
|
|
@@ -263,8 +267,10 @@ export declare function readCoordinatedArtifact(harnessRoot: string, ref: Artifa
|
|
|
263
267
|
*/
|
|
264
268
|
/**
|
|
265
269
|
* Bind a session (spec §B, §C2). Fresh addressing never supplies a session
|
|
266
|
-
* path: the engine generates the UUID
|
|
267
|
-
*
|
|
270
|
+
* path: the engine generates the UUID — or adopts the caller-supplied
|
|
271
|
+
* `sessionId`, refused unless it is a single safe path component — and creates
|
|
272
|
+
* the envelope. `resumePath` names an existing envelope and resumes read-only,
|
|
273
|
+
* so it takes no identity input at all.
|
|
268
274
|
*/
|
|
269
275
|
export declare function bindPlanSession(input: BindPlanSessionInput): Promise<CoordinationResult>;
|
|
270
276
|
/**
|
package/dist/engine.js
CHANGED
|
@@ -2084,6 +2084,158 @@ async function registerPlanWorkflow(workflowId, options) {
|
|
|
2084
2084
|
return { workflowId, snapshotPath, recovered: false };
|
|
2085
2085
|
});
|
|
2086
2086
|
}
|
|
2087
|
+
function iterationWorkflowRegistrationIdentity(snapshot) {
|
|
2088
|
+
const coordinator = snapshot.coordination?.coordinator;
|
|
2089
|
+
return stableJson({
|
|
2090
|
+
id: snapshot.id,
|
|
2091
|
+
type: snapshot.type,
|
|
2092
|
+
status: snapshot.status,
|
|
2093
|
+
compass_ref: snapshot.compass_ref ?? null,
|
|
2094
|
+
branch: snapshot.branch ?? null,
|
|
2095
|
+
project: snapshot.project ?? null,
|
|
2096
|
+
plans: snapshot.plans,
|
|
2097
|
+
coordinator: coordinator ? { session_id: coordinator.session_id, session_file: coordinator.session_file } : null
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
async function registerIterationWorkflow(workflowId, options) {
|
|
2101
|
+
const refuse = (detail) => new Error(`registerIterationWorkflow: ${detail}`);
|
|
2102
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
2103
|
+
throw refuse("options must be an object (harnessDir, compassRef, branch, rows)");
|
|
2104
|
+
}
|
|
2105
|
+
if (typeof options.harnessDir !== "string" || options.harnessDir.trim() === "") {
|
|
2106
|
+
throw refuse("options.harnessDir is required (must contain status.json + workflows/)");
|
|
2107
|
+
}
|
|
2108
|
+
assertSafePathComponent(workflowId, "workflow id");
|
|
2109
|
+
if (typeof options.compassRef !== "string" || options.compassRef.trim() === "") {
|
|
2110
|
+
throw refuse("options.compassRef must be a non-empty string");
|
|
2111
|
+
}
|
|
2112
|
+
if (typeof options.branch !== "object" || options.branch === null || Array.isArray(options.branch)) {
|
|
2113
|
+
throw refuse("options.branch must be an object (base, integration, target)");
|
|
2114
|
+
}
|
|
2115
|
+
for (const anchor of ["base", "integration", "target"]) {
|
|
2116
|
+
const value = options.branch[anchor];
|
|
2117
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
2118
|
+
throw refuse(`options.branch.${anchor} must be a non-empty string`);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
if (options.project !== undefined && (typeof options.project !== "string" || options.project.trim() === "")) {
|
|
2122
|
+
throw refuse("options.project must be a non-empty string when given");
|
|
2123
|
+
}
|
|
2124
|
+
const rows = options.rows;
|
|
2125
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
2126
|
+
throw refuse("options.rows must be a non-empty array of { id, title, file }");
|
|
2127
|
+
}
|
|
2128
|
+
const seenRowIds = new Set;
|
|
2129
|
+
for (const row of rows) {
|
|
2130
|
+
if (typeof row !== "object" || row === null || Array.isArray(row)) {
|
|
2131
|
+
throw refuse("each row must be an object ({ id, title, file })");
|
|
2132
|
+
}
|
|
2133
|
+
for (const field of ["id", "title", "file"]) {
|
|
2134
|
+
if (typeof row[field] !== "string" || row[field].trim() === "") {
|
|
2135
|
+
throw refuse(`each row.${field} must be a non-empty string`);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
if ("status" in row) {
|
|
2139
|
+
throw refuse(`row ${JSON.stringify(row.id)} supplies a status — rows are always written status "Todo"; ` + "a state transition is requested through the lifecycle seams, never at registration");
|
|
2140
|
+
}
|
|
2141
|
+
if (seenRowIds.has(row.id)) {
|
|
2142
|
+
throw refuse(`duplicate row id ${JSON.stringify(row.id)} — plan row ids are unique per workflow`);
|
|
2143
|
+
}
|
|
2144
|
+
seenRowIds.add(row.id);
|
|
2145
|
+
}
|
|
2146
|
+
const startedAt = options.startedAt ?? new Date().toISOString();
|
|
2147
|
+
if (!isCloseTimestamp(startedAt)) {
|
|
2148
|
+
throw refuse("options.startedAt must be a valid YYYY-MM-DD date or RFC3339 timestamp");
|
|
2149
|
+
}
|
|
2150
|
+
const harnessDir = resolve6(options.harnessDir);
|
|
2151
|
+
const statusPath = join6(harnessDir, "status.json");
|
|
2152
|
+
const workflowDir = join6(harnessDir, "workflows", workflowId);
|
|
2153
|
+
const snapshotPath = join6(workflowDir, WORKFLOW_SNAPSHOT_FILE);
|
|
2154
|
+
const store = getArtifactStore();
|
|
2155
|
+
const snapshot = {
|
|
2156
|
+
schema_version: 1,
|
|
2157
|
+
id: workflowId,
|
|
2158
|
+
type: "iteration",
|
|
2159
|
+
status: "running",
|
|
2160
|
+
started_at: startedAt,
|
|
2161
|
+
updated_at: startedAt.slice(0, 10),
|
|
2162
|
+
compass_ref: options.compassRef,
|
|
2163
|
+
branch: { base: options.branch.base, integration: options.branch.integration, target: options.branch.target },
|
|
2164
|
+
plans: rows.map((r) => ({
|
|
2165
|
+
id: r.id,
|
|
2166
|
+
title: r.title,
|
|
2167
|
+
file: r.file,
|
|
2168
|
+
status: "Todo",
|
|
2169
|
+
metadata: {
|
|
2170
|
+
iteration_refs: [options.compassRef],
|
|
2171
|
+
spec_integration_branch: options.branch.integration,
|
|
2172
|
+
merge_target: options.branch.integration
|
|
2173
|
+
}
|
|
2174
|
+
}))
|
|
2175
|
+
};
|
|
2176
|
+
if (options.project !== undefined)
|
|
2177
|
+
snapshot.project = options.project;
|
|
2178
|
+
const entry = {
|
|
2179
|
+
id: workflowId,
|
|
2180
|
+
type: "iteration",
|
|
2181
|
+
started_at: snapshot.started_at,
|
|
2182
|
+
dir: `workflows/${workflowId}`
|
|
2183
|
+
};
|
|
2184
|
+
const entryGate = validateWorkflowEntry(entry);
|
|
2185
|
+
if (!entryGate.ok) {
|
|
2186
|
+
throw new Error(`refusing to register invalid workflow entry: ${entryGate.violations.map((v) => v.message).join("; ")}`);
|
|
2187
|
+
}
|
|
2188
|
+
return withStatusWriteLock(statusPath, async () => {
|
|
2189
|
+
const rootDoc = readJson(statusPath);
|
|
2190
|
+
const workflows = Array.isArray(rootDoc.workflows) ? rootDoc.workflows : [];
|
|
2191
|
+
if (workflows.some((candidate) => isPlainObject2(candidate) && candidate.id === workflowId)) {
|
|
2192
|
+
throw new Error(`refusing to register workflow ${JSON.stringify(workflowId)}: it is already registered ` + `(root entry in ${statusPath}) — registration is create-only; ` + "remove that workflow before registering again");
|
|
2193
|
+
}
|
|
2194
|
+
if (existsSync4(snapshotPath)) {
|
|
2195
|
+
const existing = readWorkflowSnapshot(workflowDir);
|
|
2196
|
+
if (existing.snapshot.id !== workflowId || iterationWorkflowRegistrationIdentity(existing.snapshot) !== iterationWorkflowRegistrationIdentity(snapshot)) {
|
|
2197
|
+
throw new Error(`refusing to register workflow ${JSON.stringify(workflowId)}: snapshot ${snapshotPath} already exists ` + "with a different registration identity — remove that workflow or register under a different id");
|
|
2198
|
+
}
|
|
2199
|
+
const recoveryEntry = {
|
|
2200
|
+
id: workflowId,
|
|
2201
|
+
type: "iteration",
|
|
2202
|
+
started_at: existing.snapshot.started_at,
|
|
2203
|
+
dir: `workflows/${workflowId}`
|
|
2204
|
+
};
|
|
2205
|
+
const recoveryGate = validateWorkflowEntry(recoveryEntry);
|
|
2206
|
+
if (!recoveryGate.ok) {
|
|
2207
|
+
throw new Error(`refusing to register invalid workflow entry: ${recoveryGate.violations.map((v) => v.message).join("; ")}`);
|
|
2208
|
+
}
|
|
2209
|
+
await registerWorkflowEntryLocked(statusPath, recoveryEntry);
|
|
2210
|
+
return { workflowId, snapshotPath, recovered: true };
|
|
2211
|
+
}
|
|
2212
|
+
let createdVersion;
|
|
2213
|
+
try {
|
|
2214
|
+
await writeWorkflowSnapshot(snapshot, workflowDir, { createOnly: true });
|
|
2215
|
+
createdVersion = readArtifactBytes(snapshotPath)?.version;
|
|
2216
|
+
await registerWorkflowEntryLocked(statusPath, entry);
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
if (createdVersion !== undefined) {
|
|
2219
|
+
await withStatusWriteLock(snapshotPath, async () => {
|
|
2220
|
+
const current = readArtifactBytes(snapshotPath);
|
|
2221
|
+
if (current === undefined || current.version !== createdVersion)
|
|
2222
|
+
return;
|
|
2223
|
+
const remove = store.delete?.bind(store);
|
|
2224
|
+
if (remove !== undefined) {
|
|
2225
|
+
await withProtectedWrite(snapshotPath, "delete", () => remove({ kind: "snapshot", key: workflowId }));
|
|
2226
|
+
}
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
try {
|
|
2230
|
+
if (readdirSync2(workflowDir).length === 0) {
|
|
2231
|
+
rmdirSync2(workflowDir);
|
|
2232
|
+
}
|
|
2233
|
+
} catch {}
|
|
2234
|
+
throw error;
|
|
2235
|
+
}
|
|
2236
|
+
return { workflowId, snapshotPath, recovered: false };
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2087
2239
|
|
|
2088
2240
|
// src/status.ts
|
|
2089
2241
|
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -7627,6 +7779,176 @@ function slugify(title) {
|
|
|
7627
7779
|
}
|
|
7628
7780
|
var escapeCell = (value) => value.replace(/\|/g, "\\|");
|
|
7629
7781
|
var truncate = (value, max) => value.length > max ? `${value.slice(0, max)}…` : value;
|
|
7782
|
+
var collapseEvidenceWs = (value) => value.replace(/\s*[\r\n]\s*/g, " ");
|
|
7783
|
+
var evidenceText = (item) => typeof item === "string" ? item : `${item.file}${item.line !== undefined ? `:${item.line}` : ""} — ${item.description}`;
|
|
7784
|
+
var hasEnrichedMetadata = (finding) => finding.fingerprint !== undefined || finding.trace !== undefined || finding.severity !== undefined || finding.evidence.some((item) => typeof item !== "string");
|
|
7785
|
+
var AUDIT_FINGERPRINT_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@+-]*(?![\s\S])/;
|
|
7786
|
+
var AUDIT_SEVERITY_ORDER = { informational: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
7787
|
+
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;
|
|
7788
|
+
var LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
7789
|
+
var isPlainObject9 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7790
|
+
function isVisibleText(value) {
|
|
7791
|
+
if (LONE_SURROGATE_RE.test(value))
|
|
7792
|
+
return false;
|
|
7793
|
+
for (const ch of value) {
|
|
7794
|
+
if (!/\p{White_Space}/u.test(ch) && !DEFAULT_IGNORABLE_RE.test(ch))
|
|
7795
|
+
return true;
|
|
7796
|
+
}
|
|
7797
|
+
return false;
|
|
7798
|
+
}
|
|
7799
|
+
function safeAuditPath(value) {
|
|
7800
|
+
if (typeof value !== "string")
|
|
7801
|
+
return false;
|
|
7802
|
+
if (value === "")
|
|
7803
|
+
return false;
|
|
7804
|
+
if (value.includes("\\"))
|
|
7805
|
+
return false;
|
|
7806
|
+
if (/[\u0000-\u001F\u007F-\u009F]/.test(value))
|
|
7807
|
+
return false;
|
|
7808
|
+
if (LONE_SURROGATE_RE.test(value))
|
|
7809
|
+
return false;
|
|
7810
|
+
if (value.startsWith("/"))
|
|
7811
|
+
return false;
|
|
7812
|
+
if (/^[A-Za-z]:/.test(value))
|
|
7813
|
+
return false;
|
|
7814
|
+
for (const segment of value.split("/")) {
|
|
7815
|
+
if (segment === "" || segment === "." || segment === "..")
|
|
7816
|
+
return false;
|
|
7817
|
+
if (segment.endsWith(".") || segment.endsWith(" "))
|
|
7818
|
+
return false;
|
|
7819
|
+
}
|
|
7820
|
+
return true;
|
|
7821
|
+
}
|
|
7822
|
+
function validateAuditFindingGates(findings) {
|
|
7823
|
+
const violations = [];
|
|
7824
|
+
const seen = new Map;
|
|
7825
|
+
let previousFingerprint;
|
|
7826
|
+
if (!Array.isArray(findings)) {
|
|
7827
|
+
violations.push(violation10("high", "audit.finding.shape", "findings — audit.finding.shape"));
|
|
7828
|
+
return { ok: false, violations };
|
|
7829
|
+
}
|
|
7830
|
+
findings.forEach((finding, index) => {
|
|
7831
|
+
const at = (field) => `findings[${index}].${field}`;
|
|
7832
|
+
const push = (code, field) => {
|
|
7833
|
+
violations.push(violation10("high", code, `${at(field)} — ${code}`));
|
|
7834
|
+
};
|
|
7835
|
+
if (!isPlainObject9(finding)) {
|
|
7836
|
+
violations.push(violation10("high", "audit.finding.shape", `findings[${index}] — audit.finding.shape`));
|
|
7837
|
+
return;
|
|
7838
|
+
}
|
|
7839
|
+
if (finding.fingerprint !== undefined) {
|
|
7840
|
+
const fingerprint = finding.fingerprint;
|
|
7841
|
+
if (typeof fingerprint !== "string" || !AUDIT_FINGERPRINT_RE.test(fingerprint))
|
|
7842
|
+
push("audit.finding.fingerprint.grammar", "fingerprint");
|
|
7843
|
+
else if (redactSecrets(fingerprint).text !== fingerprint)
|
|
7844
|
+
push("audit.finding.fingerprint.secret", "fingerprint");
|
|
7845
|
+
else {
|
|
7846
|
+
const firstAt = seen.get(fingerprint);
|
|
7847
|
+
if (firstAt !== undefined)
|
|
7848
|
+
push("audit.finding.fingerprint.duplicate", "fingerprint");
|
|
7849
|
+
else
|
|
7850
|
+
seen.set(fingerprint, index);
|
|
7851
|
+
if (previousFingerprint !== undefined && fingerprint < previousFingerprint) {
|
|
7852
|
+
push("audit.finding.fingerprint.order", "fingerprint");
|
|
7853
|
+
}
|
|
7854
|
+
previousFingerprint = fingerprint;
|
|
7855
|
+
}
|
|
7856
|
+
}
|
|
7857
|
+
if (finding.severity !== undefined) {
|
|
7858
|
+
if (!isPlainObject9(finding.severity)) {
|
|
7859
|
+
push("audit.finding.severity.shape", "severity");
|
|
7860
|
+
} else {
|
|
7861
|
+
const { likelihood, impact, overall } = finding.severity;
|
|
7862
|
+
const rankOf = (r) => AUDIT_SEVERITY_ORDER[r];
|
|
7863
|
+
for (const [field, value] of [
|
|
7864
|
+
["likelihood", likelihood],
|
|
7865
|
+
["impact", impact],
|
|
7866
|
+
["overall", overall]
|
|
7867
|
+
]) {
|
|
7868
|
+
if (rankOf(value) === undefined)
|
|
7869
|
+
push("audit.finding.severity.rank", `severity.${field}`);
|
|
7870
|
+
}
|
|
7871
|
+
const impactRank = rankOf(impact);
|
|
7872
|
+
const overallRank = rankOf(overall);
|
|
7873
|
+
if (impactRank !== undefined && overallRank !== undefined && overallRank > impactRank) {
|
|
7874
|
+
push("audit.finding.severity.overall-exceeds-impact", "severity.overall");
|
|
7875
|
+
}
|
|
7876
|
+
}
|
|
7877
|
+
}
|
|
7878
|
+
const textViolation = (field, value) => {
|
|
7879
|
+
if (value === undefined)
|
|
7880
|
+
return;
|
|
7881
|
+
if (typeof value !== "string")
|
|
7882
|
+
push("audit.finding.text.type", field);
|
|
7883
|
+
else if (LONE_SURROGATE_RE.test(value))
|
|
7884
|
+
push("audit.finding.text.surrogate", field);
|
|
7885
|
+
else if (!isVisibleText(value))
|
|
7886
|
+
push("audit.finding.text.invisible", field);
|
|
7887
|
+
};
|
|
7888
|
+
if (finding.trace !== undefined) {
|
|
7889
|
+
if (!Array.isArray(finding.trace))
|
|
7890
|
+
push("audit.finding.trace.shape", "trace");
|
|
7891
|
+
else if (finding.trace.length === 0)
|
|
7892
|
+
push("audit.finding.trace.empty", "trace");
|
|
7893
|
+
else {
|
|
7894
|
+
const kinds = finding.trace.map((s) => isPlainObject9(s) ? s.kind : undefined);
|
|
7895
|
+
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");
|
|
7896
|
+
if (!topologyOk)
|
|
7897
|
+
push("audit.finding.trace.topology", "trace");
|
|
7898
|
+
finding.trace.forEach((step, stepIndex) => {
|
|
7899
|
+
const stepAt = `trace[${stepIndex}]`;
|
|
7900
|
+
if (!isPlainObject9(step)) {
|
|
7901
|
+
push("audit.finding.trace.shape", stepAt);
|
|
7902
|
+
return;
|
|
7903
|
+
}
|
|
7904
|
+
if (typeof step.line !== "number" || !Number.isSafeInteger(step.line) || step.line <= 0) {
|
|
7905
|
+
push("audit.finding.trace.line", `${stepAt}.line`);
|
|
7906
|
+
}
|
|
7907
|
+
const stepFile = step.file;
|
|
7908
|
+
if (typeof stepFile !== "string" || !safeAuditPath(stepFile))
|
|
7909
|
+
push("audit.finding.path.unsafe", `${stepAt}.file`);
|
|
7910
|
+
else if (redactSecrets(stepFile).text !== stepFile)
|
|
7911
|
+
push("audit.finding.path.secret", `${stepAt}.file`);
|
|
7912
|
+
if (step.scope === undefined)
|
|
7913
|
+
push("audit.finding.trace.shape", `${stepAt}.scope`);
|
|
7914
|
+
else
|
|
7915
|
+
textViolation(`${stepAt}.scope`, step.scope);
|
|
7916
|
+
if (step.description === undefined)
|
|
7917
|
+
push("audit.finding.trace.shape", `${stepAt}.description`);
|
|
7918
|
+
else
|
|
7919
|
+
textViolation(`${stepAt}.description`, step.description);
|
|
7920
|
+
});
|
|
7921
|
+
}
|
|
7922
|
+
}
|
|
7923
|
+
if (!Array.isArray(finding.evidence))
|
|
7924
|
+
push("audit.finding.evidence.shape", "evidence");
|
|
7925
|
+
else
|
|
7926
|
+
finding.evidence.forEach((item, itemIndex) => {
|
|
7927
|
+
if (typeof item === "string") {
|
|
7928
|
+
textViolation(`evidence[${itemIndex}]`, item);
|
|
7929
|
+
return;
|
|
7930
|
+
}
|
|
7931
|
+
if (!isPlainObject9(item)) {
|
|
7932
|
+
push("audit.finding.evidence.shape", `evidence[${itemIndex}]`);
|
|
7933
|
+
return;
|
|
7934
|
+
}
|
|
7935
|
+
const itemFile = item.file;
|
|
7936
|
+
if (typeof itemFile !== "string" || !safeAuditPath(itemFile))
|
|
7937
|
+
push("audit.finding.path.unsafe", `evidence[${itemIndex}].file`);
|
|
7938
|
+
else if (redactSecrets(itemFile).text !== itemFile)
|
|
7939
|
+
push("audit.finding.path.secret", `evidence[${itemIndex}].file`);
|
|
7940
|
+
if (item.line !== undefined && (typeof item.line !== "number" || !Number.isSafeInteger(item.line) || item.line <= 0)) {
|
|
7941
|
+
push("audit.finding.evidence.line", `evidence[${itemIndex}].line`);
|
|
7942
|
+
}
|
|
7943
|
+
textViolation(`evidence[${itemIndex}].description`, item.description);
|
|
7944
|
+
});
|
|
7945
|
+
textViolation("title", finding.title);
|
|
7946
|
+
textViolation("impact", finding.impact);
|
|
7947
|
+
textViolation("fixSketch", finding.fixSketch);
|
|
7948
|
+
textViolation("verification", finding.verification);
|
|
7949
|
+
});
|
|
7950
|
+
return { ok: violations.length === 0, violations };
|
|
7951
|
+
}
|
|
7630
7952
|
function renderPlanFile(finding, plannedAt) {
|
|
7631
7953
|
const sections = [
|
|
7632
7954
|
`# ${finding.title}`,
|
|
@@ -7635,15 +7957,22 @@ function renderPlanFile(finding, plannedAt) {
|
|
|
7635
7957
|
`- **Priority**: ${finding.priority}`,
|
|
7636
7958
|
`- **Effort**: ${finding.effort}`,
|
|
7637
7959
|
`- **Risk**: ${finding.risk}`,
|
|
7960
|
+
...finding.confidence !== "MED" || hasEnrichedMetadata(finding) ? [`- **Confidence**: ${finding.confidence}`] : [],
|
|
7961
|
+
...finding.fingerprint !== undefined ? [`- **Fingerprint**: ${finding.fingerprint}`] : [],
|
|
7962
|
+
...finding.severity !== undefined ? [`- **Likelihood**: ${finding.severity.likelihood}`, `- **Severity impact**: ${finding.severity.impact}`, `- **Severity**: ${finding.severity.overall}`] : [],
|
|
7638
7963
|
`- **Depends on**: ${finding.dependsOn ?? "none"}`,
|
|
7639
7964
|
`- **Category**: ${finding.category}`,
|
|
7965
|
+
...finding.evidence.length > 0 ? [`- **Evidence**: ${collapseEvidenceWs(evidenceText(finding.evidence[0]))}`] : [],
|
|
7640
7966
|
`- **Planned at**: commit \`${plannedAt.commit}\`, ${plannedAt.date}`,
|
|
7641
7967
|
"",
|
|
7642
7968
|
"## Impact",
|
|
7643
7969
|
finding.impact
|
|
7644
7970
|
];
|
|
7645
7971
|
if (finding.evidence.length > 0) {
|
|
7646
|
-
sections.push("", "## Evidence", ...finding.evidence.map((
|
|
7972
|
+
sections.push("", "## Evidence", ...finding.evidence.map((item) => `- ${evidenceText(item)}`));
|
|
7973
|
+
}
|
|
7974
|
+
if (finding.trace !== undefined) {
|
|
7975
|
+
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")} |`));
|
|
7647
7976
|
}
|
|
7648
7977
|
if (finding.fixSketch !== undefined) {
|
|
7649
7978
|
sections.push("", "## Fix sketch", finding.fixSketch);
|
|
@@ -7663,7 +7992,8 @@ function redactFinding(finding) {
|
|
|
7663
7992
|
...finding,
|
|
7664
7993
|
title: redactText(finding.title),
|
|
7665
7994
|
impact: redactText(finding.impact),
|
|
7666
|
-
evidence: finding.evidence.map(redactText),
|
|
7995
|
+
evidence: finding.evidence.map((item) => typeof item === "string" ? redactText(item) : { ...item, description: redactText(item.description) }),
|
|
7996
|
+
...finding.trace !== undefined ? { trace: finding.trace.map((step) => ({ ...step, scope: redactText(step.scope), description: redactText(step.description) })) } : {},
|
|
7667
7997
|
...finding.fixSketch !== undefined ? { fixSketch: redactText(finding.fixSketch) } : {},
|
|
7668
7998
|
...finding.verification !== undefined ? { verification: redactText(finding.verification) } : {}
|
|
7669
7999
|
};
|
|
@@ -7685,7 +8015,10 @@ function extractSecurityDispositionSections(text) {
|
|
|
7685
8015
|
}
|
|
7686
8016
|
function renderIndex(params) {
|
|
7687
8017
|
const { date, repoName, repoShortSha, rows, rejected, needsVerification, hardeningChecked } = params;
|
|
7688
|
-
const
|
|
8018
|
+
const showFingerprint = rows.some((r) => r.fingerprint !== undefined);
|
|
8019
|
+
const showSeverity = rows.some((r) => r.likelihood !== undefined || r.severityImpact !== undefined || r.severity !== undefined);
|
|
8020
|
+
const cell = (value) => escapeCell(value ?? "—");
|
|
8021
|
+
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(`
|
|
7689
8022
|
`);
|
|
7690
8023
|
const directionRows = rows.filter((r) => r.category === "direction").map((r) => `- ${escapeCell(r.title)} — ${escapeCell(truncate(r.impact, 120))}`).join(`
|
|
7691
8024
|
`);
|
|
@@ -7698,8 +8031,8 @@ function renderIndex(params) {
|
|
|
7698
8031
|
"",
|
|
7699
8032
|
"## Findings",
|
|
7700
8033
|
"",
|
|
7701
|
-
"| # | Finding | Category | Impact | Effort | Risk | Confidence | Evidence |",
|
|
7702
|
-
"
|
|
8034
|
+
"| # | Finding | Category | Impact | Effort | Risk | Confidence | Evidence" + (showFingerprint ? " | Fingerprint" : "") + (showSeverity ? " | Likelihood | Severity impact | Severity" : "") + " |",
|
|
8035
|
+
"|---|---------|----------|--------|--------|------|------------|----------" + (showFingerprint ? "|------------" : "") + (showSeverity ? "|------------|-----------------|----------" : "") + "|",
|
|
7703
8036
|
findingsRows
|
|
7704
8037
|
];
|
|
7705
8038
|
if (directionRows !== "") {
|
|
@@ -7723,6 +8056,11 @@ function renderIndex(params) {
|
|
|
7723
8056
|
function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
7724
8057
|
const date = options.date ?? new Date().toISOString().slice(0, 10);
|
|
7725
8058
|
const plannedAt = options.plannedAt ?? { commit: options.repoShortSha ?? "unknown", date };
|
|
8059
|
+
const gate2 = validateAuditFindingGates(findings);
|
|
8060
|
+
if (!gate2.ok) {
|
|
8061
|
+
const first = gate2.violations[0];
|
|
8062
|
+
throw new TypeError(`invalid audit findings — ${first.code}: ${first.message}`);
|
|
8063
|
+
}
|
|
7726
8064
|
mkdirSync8(outDir, { recursive: true });
|
|
7727
8065
|
const existingReadme = join16(outDir, "README.md");
|
|
7728
8066
|
const carried = existsSync12(existingReadme) ? extractSecurityDispositionSections(readFileSync11(existingReadme, "utf8")) : { needsVerification: [], hardeningChecked: [] };
|
|
@@ -7757,10 +8095,14 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
7757
8095
|
impact: "see plan file",
|
|
7758
8096
|
effort: fields.get("Effort") ?? "—",
|
|
7759
8097
|
risk: fields.get("Risk") ?? "—",
|
|
7760
|
-
confidence: "—",
|
|
8098
|
+
confidence: fields.get("Confidence") ?? "—",
|
|
7761
8099
|
evidence: fields.get("Evidence") ?? "—",
|
|
7762
8100
|
priority: fields.get("Priority") ?? "—",
|
|
7763
|
-
dependsOn: fields.get("Depends on") ?? "—"
|
|
8101
|
+
dependsOn: fields.get("Depends on") ?? "—",
|
|
8102
|
+
fingerprint: fields.get("Fingerprint"),
|
|
8103
|
+
likelihood: fields.get("Likelihood"),
|
|
8104
|
+
severityImpact: fields.get("Severity impact"),
|
|
8105
|
+
severity: fields.get("Severity")
|
|
7764
8106
|
};
|
|
7765
8107
|
});
|
|
7766
8108
|
const byNum = new Map(rows.map((r) => [r.num, r]));
|
|
@@ -7775,9 +8117,13 @@ function scaffoldAuditPlan(outDir, findings, options = {}) {
|
|
|
7775
8117
|
row.effort = finding.effort;
|
|
7776
8118
|
row.risk = finding.risk;
|
|
7777
8119
|
row.confidence = finding.confidence;
|
|
7778
|
-
row.evidence = finding.evidence[0]
|
|
8120
|
+
row.evidence = finding.evidence.length > 0 ? collapseEvidenceWs(evidenceText(finding.evidence[0])) : "";
|
|
7779
8121
|
row.priority = finding.priority;
|
|
7780
8122
|
row.dependsOn = finding.dependsOn ?? "none";
|
|
8123
|
+
row.fingerprint = finding.fingerprint;
|
|
8124
|
+
row.likelihood = finding.severity?.likelihood;
|
|
8125
|
+
row.severityImpact = finding.severity?.impact;
|
|
8126
|
+
row.severity = finding.severity?.overall;
|
|
7781
8127
|
}
|
|
7782
8128
|
});
|
|
7783
8129
|
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;
|
|
@@ -9013,7 +9359,7 @@ function computePrTally(input) {
|
|
|
9013
9359
|
var REVIEW_SCHEMA_ID = "mstar.review/v1";
|
|
9014
9360
|
var INSPECTOR_VERDICTS = ["comment", "request_changes", "approve"];
|
|
9015
9361
|
var INSPECTOR_SEVERITIES = ["critical", "warning", "suggestion", "info"];
|
|
9016
|
-
function
|
|
9362
|
+
function isPlainObject10(value) {
|
|
9017
9363
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9018
9364
|
}
|
|
9019
9365
|
var TALLY_COUNT_KEYS = ["mustFix", "shouldFix", "nit", "unverified"];
|
|
@@ -9024,7 +9370,7 @@ function checkProvidedTallyShape(tally, violations) {
|
|
|
9024
9370
|
if (typeof tally.scorePct !== "number" || !Number.isInteger(tally.scorePct) || tally.scorePct < 0 || tally.scorePct > 100) {
|
|
9025
9371
|
violations.push(violation15("high", "review.tally-malformed", `tally.scorePct must be an integer in [0, 100] - got ${String(tally.scorePct)}`));
|
|
9026
9372
|
}
|
|
9027
|
-
if (!
|
|
9373
|
+
if (!isPlainObject10(tally.tally)) {
|
|
9028
9374
|
violations.push(violation15("high", "review.tally-malformed", "tally.tally must be an object carrying the four class counts"));
|
|
9029
9375
|
} else {
|
|
9030
9376
|
for (const key of TALLY_COUNT_KEYS) {
|
|
@@ -9040,7 +9386,7 @@ function checkProvidedTallyShape(tally, violations) {
|
|
|
9040
9386
|
}
|
|
9041
9387
|
function validateMstarReviewV1(doc) {
|
|
9042
9388
|
const violations = [];
|
|
9043
|
-
if (!
|
|
9389
|
+
if (!isPlainObject10(doc)) {
|
|
9044
9390
|
return {
|
|
9045
9391
|
ok: false,
|
|
9046
9392
|
violations: [violation15("high", "review.not-object", "review document must be a JSON object")]
|
|
@@ -9067,7 +9413,7 @@ function validateMstarReviewV1(doc) {
|
|
|
9067
9413
|
violations.push(violation15("high", "review.findings-not-array", "findings must be an array"));
|
|
9068
9414
|
} else {
|
|
9069
9415
|
doc.findings.forEach((finding, index) => {
|
|
9070
|
-
if (!
|
|
9416
|
+
if (!isPlainObject10(finding)) {
|
|
9071
9417
|
violations.push(violation15("high", "review.invalid-finding", `findings[${index}] must be an object`));
|
|
9072
9418
|
return;
|
|
9073
9419
|
}
|
|
@@ -9107,7 +9453,7 @@ function validateMstarReviewV1(doc) {
|
|
|
9107
9453
|
});
|
|
9108
9454
|
}
|
|
9109
9455
|
if (doc.tally !== undefined) {
|
|
9110
|
-
if (!
|
|
9456
|
+
if (!isPlainObject10(doc.tally)) {
|
|
9111
9457
|
violations.push(violation15("high", "review.invalid-tally", "tally must be a PrTallyResult object"));
|
|
9112
9458
|
} else {
|
|
9113
9459
|
checkProvidedTallyShape(doc.tally, violations);
|
|
@@ -9117,7 +9463,7 @@ function validateMstarReviewV1(doc) {
|
|
|
9117
9463
|
}
|
|
9118
9464
|
}
|
|
9119
9465
|
if (doc.target !== undefined) {
|
|
9120
|
-
if (!
|
|
9466
|
+
if (!isPlainObject10(doc.target)) {
|
|
9121
9467
|
violations.push(violation15("high", "review.invalid-target", "target must be an object"));
|
|
9122
9468
|
} else {
|
|
9123
9469
|
if (doc.target.owner !== undefined && typeof doc.target.owner !== "string") {
|
|
@@ -10030,8 +10376,8 @@ function reportedBranchesOf(row) {
|
|
|
10030
10376
|
function snapshotPathOf(harnessRoot, workflowId) {
|
|
10031
10377
|
return join20(resolveWorkflowDir(harnessRoot, { harnessDir: harnessRoot }), workflowId, SNAPSHOT_FILE2);
|
|
10032
10378
|
}
|
|
10033
|
-
function sessionFilePath(harnessRoot, workflowId, sessionId) {
|
|
10034
|
-
return join20(resolveWorkflowDir(harnessRoot, { harnessDir: harnessRoot }), workflowId, SESSION_DIR, `${sessionId}.json`);
|
|
10379
|
+
function sessionFilePath(harnessRoot, workflowId, role, sessionId) {
|
|
10380
|
+
return join20(resolveWorkflowDir(harnessRoot, { harnessDir: harnessRoot }), workflowId, SESSION_DIR, `${role}-${sessionId}.json`);
|
|
10035
10381
|
}
|
|
10036
10382
|
function localStore(harnessRoot) {
|
|
10037
10383
|
let store;
|
|
@@ -10192,6 +10538,22 @@ function safePlanId(planId, where) {
|
|
|
10192
10538
|
}
|
|
10193
10539
|
return planId;
|
|
10194
10540
|
}
|
|
10541
|
+
var SESSION_ID_MAX_LENGTH = 128;
|
|
10542
|
+
function safeSessionId(value) {
|
|
10543
|
+
if (value === undefined)
|
|
10544
|
+
return;
|
|
10545
|
+
if (typeof value !== "string")
|
|
10546
|
+
throw invalidInput("sessionId must be a string");
|
|
10547
|
+
if (value.length > SESSION_ID_MAX_LENGTH) {
|
|
10548
|
+
throw new CoordinationError("coordination.invalid-session-id", `session id is longer than ${SESSION_ID_MAX_LENGTH} characters: ${JSON.stringify(value)}`, { session_id: value, max_length: SESSION_ID_MAX_LENGTH });
|
|
10549
|
+
}
|
|
10550
|
+
try {
|
|
10551
|
+
assertSafePathComponent(value, "session id");
|
|
10552
|
+
} catch (error) {
|
|
10553
|
+
throw new CoordinationError("coordination.invalid-session-id", `session id ${JSON.stringify(value)} is not a safe path component: ${errorMessage(error)}`, { session_id: value });
|
|
10554
|
+
}
|
|
10555
|
+
return value;
|
|
10556
|
+
}
|
|
10195
10557
|
function readSnapshot(dir) {
|
|
10196
10558
|
const snapshotPath = join20(dir, SNAPSHOT_FILE2);
|
|
10197
10559
|
if (!existsSync15(snapshotPath)) {
|
|
@@ -10375,7 +10737,7 @@ function readSessionEnvelope(sessionPath) {
|
|
|
10375
10737
|
return session;
|
|
10376
10738
|
}
|
|
10377
10739
|
function createSessionEnvelope(session) {
|
|
10378
|
-
const path = sessionFilePath(session.harness_root, session.workflow_id, session.session_id);
|
|
10740
|
+
const path = sessionFilePath(session.harness_root, session.workflow_id, session.role, session.session_id);
|
|
10379
10741
|
mkdirSync9(dirname12(path), { recursive: true });
|
|
10380
10742
|
try {
|
|
10381
10743
|
writeFileSync7(path, `${JSON.stringify(session, null, 2)}
|
|
@@ -10680,7 +11042,7 @@ function assertCoordinatorResidency(cwd, snapshot) {
|
|
|
10680
11042
|
}
|
|
10681
11043
|
return main;
|
|
10682
11044
|
}
|
|
10683
|
-
async function bindCoordinatorSession(cwd, workflowId, harnessDir) {
|
|
11045
|
+
async function bindCoordinatorSession(cwd, workflowId, harnessDir, sessionId) {
|
|
10684
11046
|
const harnessRoot = requireProcessRoot(cwd, harnessDir);
|
|
10685
11047
|
safePlanId(workflowId, "workflowId");
|
|
10686
11048
|
const snapshotPath = snapshotPathOf(harnessRoot, workflowId);
|
|
@@ -10689,7 +11051,7 @@ async function bindCoordinatorSession(cwd, workflowId, harnessDir) {
|
|
|
10689
11051
|
const session = {
|
|
10690
11052
|
schema_version: 1,
|
|
10691
11053
|
role: "coordinator",
|
|
10692
|
-
session_id: randomUUID2(),
|
|
11054
|
+
session_id: sessionId ?? randomUUID2(),
|
|
10693
11055
|
workflow_id: workflowId,
|
|
10694
11056
|
harness_root: harnessRoot
|
|
10695
11057
|
};
|
|
@@ -10729,7 +11091,7 @@ function requireProcessRoot(cwd, harnessDir) {
|
|
|
10729
11091
|
}
|
|
10730
11092
|
return root;
|
|
10731
11093
|
}
|
|
10732
|
-
async function bindPlanSessionForPlan(scope) {
|
|
11094
|
+
async function bindPlanSessionForPlan(scope, sessionId) {
|
|
10733
11095
|
const { harnessRoot, workflowId, planId } = scope;
|
|
10734
11096
|
const snapshotPath = snapshotPathOf(harnessRoot, workflowId);
|
|
10735
11097
|
assertSnapshotPath(harnessRoot, workflowId, snapshotPath);
|
|
@@ -10752,7 +11114,7 @@ async function bindPlanSessionForPlan(scope) {
|
|
|
10752
11114
|
const session = {
|
|
10753
11115
|
schema_version: 1,
|
|
10754
11116
|
role: "plan-pm",
|
|
10755
|
-
session_id: randomUUID2(),
|
|
11117
|
+
session_id: sessionId ?? randomUUID2(),
|
|
10756
11118
|
workflow_id: workflowId,
|
|
10757
11119
|
plan_id: planId,
|
|
10758
11120
|
harness_root: harnessRoot
|
|
@@ -10819,19 +11181,21 @@ async function bindPlanSession(input) {
|
|
|
10819
11181
|
return resumeBoundSession(input.resumePath);
|
|
10820
11182
|
}
|
|
10821
11183
|
if ("coordinator" in input) {
|
|
10822
|
-
assertExactKeys(input, ["coordinator", "workflowId", "harnessDir", "cwd"], "bind coordinator input");
|
|
11184
|
+
assertExactKeys(input, ["coordinator", "workflowId", "harnessDir", "cwd", "sessionId"], "bind coordinator input");
|
|
10823
11185
|
if (input.coordinator !== true)
|
|
10824
11186
|
throw invalidInput("`coordinator` is only meaningful as true");
|
|
10825
11187
|
if (!isNonEmptyString(input.workflowId))
|
|
10826
11188
|
throw invalidInput("workflowId is required");
|
|
10827
11189
|
requireCwd(input.cwd);
|
|
10828
|
-
|
|
11190
|
+
const sessionId2 = safeSessionId(input.sessionId);
|
|
11191
|
+
return bindCoordinatorSession(input.cwd, input.workflowId, input.harnessDir, sessionId2);
|
|
10829
11192
|
}
|
|
10830
|
-
assertExactKeys(input, ["scope", "cwd"], "bind plan input");
|
|
11193
|
+
assertExactKeys(input, ["scope", "cwd", "sessionId"], "bind plan input");
|
|
10831
11194
|
if (!isPlainObject2(input.scope))
|
|
10832
11195
|
throw invalidInput("a plan bind requires a scope");
|
|
10833
11196
|
requireCwd(input.cwd);
|
|
10834
|
-
|
|
11197
|
+
const sessionId = safeSessionId(input.sessionId);
|
|
11198
|
+
return bindPlanSessionForPlan(await resolvePlanScope(input.scope, input.cwd), sessionId);
|
|
10835
11199
|
}
|
|
10836
11200
|
function requireCwd(cwd) {
|
|
10837
11201
|
if (!isNonEmptyString(cwd) || !isAbsolute12(cwd))
|
|
@@ -13430,6 +13794,7 @@ export {
|
|
|
13430
13794
|
readWorkflowSnapshot,
|
|
13431
13795
|
recordWorkflowDelivery,
|
|
13432
13796
|
referenceExists,
|
|
13797
|
+
registerIterationWorkflow,
|
|
13433
13798
|
registerPlanWorkflow,
|
|
13434
13799
|
registerWorkflow,
|
|
13435
13800
|
releaseLease,
|
|
@@ -13474,6 +13839,7 @@ export {
|
|
|
13474
13839
|
techDebtRollup,
|
|
13475
13840
|
unregisterWorkflow,
|
|
13476
13841
|
validateAssignmentFields,
|
|
13842
|
+
validateAuditFindingGates,
|
|
13477
13843
|
validateAuditStatusBlocks,
|
|
13478
13844
|
validateCompassFrontmatter,
|
|
13479
13845
|
validateDesignTokenFrontmatter,
|
package/dist/index.d.ts
CHANGED
|
@@ -45,8 +45,8 @@ export type { PlanRow, ResidualEntry, StatusDoc, StatusV2Doc, WorkflowEntry, } f
|
|
|
45
45
|
export { normalizeSeverity, registerWorkflow, resolveCompassEnforcement, resolveMstarcEnforcement, resolveRepoEnforcement, unregisterWorkflow, validatePlanRow, validateResidual, validateStatus, validateStatusV2, validateWorkflowEntry, } from "./status.js";
|
|
46
46
|
export type { ClaimLeaseFields, ExecutionLease, ExecutionLeaseLocations, IntegrationMergeLease, LeaseTransition, LeaseVerifyResult, } from "./lease.js";
|
|
47
47
|
export { canSteal, claimLease, planExecutionLeaseLocations, releaseLease, sameHolderResume, validateExecutionLease, validateIntegrationMergeLease, verifyPlanExecutionLease, withStatusWriteLock, } from "./lease.js";
|
|
48
|
-
export type { CloseWorkflowOptions, DeclareWorkflowDeliveryKindOptions, DeliveryRegistrationEvidence, RecordWorkflowDeliveryOptions, RecordWorkflowDeliveryResult, RegisterPlanWorkflowOptions, RegisterPlanWorkflowResult, WorkflowBranchAnchors, WorkflowCompoundOutcome, WorkflowDeliveryEvidence, WorkflowDeliveryKind, WorkflowExecutionPolicy, WorkflowLifecycleStatus, WorkflowLifecycleType, WorkflowSnapshot, WorkflowSnapshotRead, } from "./workflow.js";
|
|
49
|
-
export { assertDeliveryRegistrationCoherence, closeWorkflow, consultDeliveryEvidence, declareWorkflowDeliveryKind, isTerminalSnapshot, LEGACY_WORKTREE_PATH_CODE, recordWorkflowDelivery, registerPlanWorkflow, WORKFLOW_COMPOUND_OUTCOMES, WORKFLOW_DELIVERY_KINDS, WORKFLOW_LIFECYCLE_STATUSES, WORKFLOW_LIFECYCLE_TYPES, WORKFLOW_SNAPSHOT_FILE, WORKFLOW_TERMINAL_STATUSES, readWorkflowSnapshot, validateWorkflowSnapshot, writeWorkflowSnapshot, } from "./workflow.js";
|
|
48
|
+
export type { CloseWorkflowOptions, DeclareWorkflowDeliveryKindOptions, DeliveryRegistrationEvidence, RecordWorkflowDeliveryOptions, RecordWorkflowDeliveryResult, RegisterIterationWorkflowOptions, RegisterIterationWorkflowResult, RegisterPlanWorkflowOptions, RegisterPlanWorkflowResult, WorkflowBranchAnchors, WorkflowCompoundOutcome, WorkflowDeliveryEvidence, WorkflowDeliveryKind, WorkflowExecutionPolicy, WorkflowLifecycleStatus, WorkflowLifecycleType, WorkflowSnapshot, WorkflowSnapshotRead, } from "./workflow.js";
|
|
49
|
+
export { assertDeliveryRegistrationCoherence, closeWorkflow, consultDeliveryEvidence, declareWorkflowDeliveryKind, isTerminalSnapshot, LEGACY_WORKTREE_PATH_CODE, recordWorkflowDelivery, registerIterationWorkflow, registerPlanWorkflow, WORKFLOW_COMPOUND_OUTCOMES, WORKFLOW_DELIVERY_KINDS, WORKFLOW_LIFECYCLE_STATUSES, WORKFLOW_LIFECYCLE_TYPES, WORKFLOW_SNAPSHOT_FILE, WORKFLOW_TERMINAL_STATUSES, readWorkflowSnapshot, validateWorkflowSnapshot, writeWorkflowSnapshot, } from "./workflow.js";
|
|
50
50
|
export type { CleanupDecision, CleanupFacts, CleanupTarget, CleanupTargetKind, } from "./cleanup.js";
|
|
51
51
|
export { planWorktreeCleanup } from "./cleanup.js";
|
|
52
52
|
export type { EvidenceArtifactFact, EvidenceAssessment, EvidenceCaptureRequest, EvidenceCoverage, EvidenceEnvironmentKey, EvidenceExpectation, EvidenceInputEntry, EvidenceInputSnapshot, EvidenceInputSpec, EvidenceLimits, EvidenceLog, EvidenceOutcome, EvidenceToolFingerprint, SddEvidenceRecord, } from "./evidence.js";
|
|
@@ -67,8 +67,8 @@ export type { MigrateNotesFile, MigrateOptions, MigratePlan, MigrateRegister, Mi
|
|
|
67
67
|
export { ARCHIVED_STATUS_V1_FILE, MIGRATE_STATUS_FILE, NOTES_LEDGER_FILE, applyMigratePlan, migrateHarnessTree, } from "./migrate.js";
|
|
68
68
|
export type { CompletenessItem, CompletenessLevel, CompletenessPlaceholder, CompletenessResult, DesignFrontmatter, } from "./design-md.js";
|
|
69
69
|
export { assertLightDarkParity, completenessLevel, parseDesignFrontmatter, validateDesignTokenFrontmatter, } from "./design-md.js";
|
|
70
|
-
export type { AuditCategory, AuditConfidence, AuditEffort, AuditFinding, AuditPriority, AuditRisk, PromoteAuditPlansOptions, RedactResult, ScaffoldAuditPlanOptions, ScaffoldAuditPlanResult, ScannedSecret, SecretFinding, SupplyChainFinding, SupplyChainFindingKind, SupplyChainResult, } from "./audit.js";
|
|
71
|
-
export { AUDIT_CATEGORIES, AUDIT_CONFIDENCES, AUDIT_EFFORTS, AUDIT_PRIORITIES, AUDIT_RISKS, promoteAuditPlans, scaffoldAuditPlan, scanSecrets, supplyChainChecks, validateAuditStatusBlocks, } from "./audit.js";
|
|
70
|
+
export type { AuditCategory, AuditConfidence, AuditEffort, AuditEvidence, AuditFinding, AuditPriority, AuditRisk, AuditSeverity, AuditSeverityRank, AuditTraceKind, AuditTraceStep, PromoteAuditPlansOptions, RedactResult, ScaffoldAuditPlanOptions, ScaffoldAuditPlanResult, ScannedSecret, SecretFinding, SupplyChainFinding, SupplyChainFindingKind, SupplyChainResult, } from "./audit.js";
|
|
71
|
+
export { AUDIT_CATEGORIES, AUDIT_CONFIDENCES, AUDIT_EFFORTS, AUDIT_PRIORITIES, AUDIT_RISKS, promoteAuditPlans, scaffoldAuditPlan, scanSecrets, supplyChainChecks, validateAuditFindingGates, validateAuditStatusBlocks, } from "./audit.js";
|
|
72
72
|
export { KNOWLEDGE_BUG_PROBLEM_TYPES, KNOWLEDGE_CATEGORY_MAP, KNOWLEDGE_KNOWLEDGE_PROBLEM_TYPES, KNOWLEDGE_PROBLEM_TYPES, KNOWLEDGE_REQUIRED_FIELDS, KNOWLEDGE_RESOLUTION_TYPES, KNOWLEDGE_SEVERITIES, assertIndexRows, compoundRefreshScope, referenceExists, scopeGuard, validateSchemaYaml, } from "./compound.js";
|
|
73
73
|
export type { ReferenceCheckResult } from "./compound.js";
|
|
74
74
|
export type { EphemeralCitation, PlanQualityFinding, PlanQualityResult, ProvenanceCitation, SimplifyMarker, TemporaryMarker, TemporaryMarkerResult, } from "./lint.js";
|
package/dist/workflow.d.ts
CHANGED
|
@@ -522,3 +522,78 @@ export type RegisterPlanWorkflowResult = {
|
|
|
522
522
|
* root could differ — the routed writers fail loud on a path mismatch.
|
|
523
523
|
*/
|
|
524
524
|
export declare function registerPlanWorkflow(workflowId: string, options: RegisterPlanWorkflowOptions): Promise<RegisterPlanWorkflowResult>;
|
|
525
|
+
/** Options for `registerIterationWorkflow`. `harnessDir` is required — the
|
|
526
|
+
* snapshot and `status.json` live under the harness root. */
|
|
527
|
+
export type RegisterIterationWorkflowOptions = {
|
|
528
|
+
/** Absolute harness dir that contains `status.json` + `workflows/`. Required. */
|
|
529
|
+
harnessDir: string;
|
|
530
|
+
/** Harness-relative compass ref recorded on the snapshot, e.g. `iterations/<id>/delivery-compass.md`. Required. */
|
|
531
|
+
compassRef: string;
|
|
532
|
+
/** The three iteration branch anchors. All three required. */
|
|
533
|
+
branch: {
|
|
534
|
+
base: string;
|
|
535
|
+
integration: string;
|
|
536
|
+
target: string;
|
|
537
|
+
};
|
|
538
|
+
/** Initial plan rows. At least one. Every row is written `status: "Todo"`. */
|
|
539
|
+
rows: Array<{
|
|
540
|
+
id: string;
|
|
541
|
+
title: string;
|
|
542
|
+
file: string;
|
|
543
|
+
}>;
|
|
544
|
+
/** Project register id recorded on the snapshot. */
|
|
545
|
+
project?: string;
|
|
546
|
+
/** Registration timestamp (YYYY-MM-DD or RFC3339). Default: now. */
|
|
547
|
+
startedAt?: string;
|
|
548
|
+
};
|
|
549
|
+
export type RegisterIterationWorkflowResult = {
|
|
550
|
+
workflowId: string;
|
|
551
|
+
snapshotPath: string;
|
|
552
|
+
/**
|
|
553
|
+
* True on orphan recovery: existing snapshot bytes/timestamps are preserved
|
|
554
|
+
* and only the missing root entry is written.
|
|
555
|
+
*/
|
|
556
|
+
recovered: boolean;
|
|
557
|
+
};
|
|
558
|
+
/**
|
|
559
|
+
* Register an iteration workflow (seam S1 sibling of `registerPlanWorkflow`).
|
|
560
|
+
* Builds the create-only `type: iteration` snapshot (workflow id, compass
|
|
561
|
+
* ref, the three branch anchors, Todo rows whose `metadata` —
|
|
562
|
+
* `iteration_refs`, `spec_integration_branch`, `merge_target` — is derived
|
|
563
|
+
* from the already-required compass/integration inputs; registration never
|
|
564
|
+
* authorizes implementation) and the root `workflows[]` entry, then writes
|
|
565
|
+
* BOTH under one atomic section of the root `withStatusWriteLock` — the same
|
|
566
|
+
* create-only `writeWorkflowSnapshot(..., { createOnly: true })` →
|
|
567
|
+
* `registerWorkflowEntryLocked` primitive sequence the plan producer uses —
|
|
568
|
+
* with rollback that removes ONLY the exact snapshot version this call
|
|
569
|
+
* created (plus the now-empty workflow dir; a non-empty directory is never
|
|
570
|
+
* force-removed).
|
|
571
|
+
*
|
|
572
|
+
* Refusals (fail-loud, before any write): non-empty `workflowId`
|
|
573
|
+
* (`assertSafePathComponent`), `harnessDir`, `compassRef`, all three branch
|
|
574
|
+
* anchors, `project` when given, and each row's `id`/`title`/`file`;
|
|
575
|
+
* non-empty row array; duplicate row ids; a SUPPLIED row `status` (a
|
|
576
|
+
* requested state transition is never treated as successful registration);
|
|
577
|
+
* `startedAt` must be a valid YYYY-MM-DD / RFC3339 timestamp. Rows are
|
|
578
|
+
* constructed from the declared fields — untrusted row objects are never
|
|
579
|
+
* spread.
|
|
580
|
+
*
|
|
581
|
+
* Already-registered refusals: inside the root lock, an existing entry for
|
|
582
|
+
* the requested id refuses BEFORE either branch — including a stale entry
|
|
583
|
+
* whose snapshot is missing. This producer is not the low-level upsert and
|
|
584
|
+
* does not repair/repoint an existing identity. Malformed/v1 roots refuse
|
|
585
|
+
* without replacing their bytes.
|
|
586
|
+
*
|
|
587
|
+
* Crash/retry recovery: a snapshot without its root entry (an orphan)
|
|
588
|
+
* re-registers only when `iterationWorkflowRegistrationIdentity` matches —
|
|
589
|
+
* the existing snapshot's bytes and timestamps are preserved verbatim and
|
|
590
|
+
* the root entry is written with the orphan's `started_at` (`recovered:
|
|
591
|
+
* true`). A differing identity (including a mismatched snapshot id or an
|
|
592
|
+
* existing coordinator binding) refuses; a failed recovery root write never
|
|
593
|
+
* deletes the orphan.
|
|
594
|
+
*
|
|
595
|
+
* The caller must pin the artifact store to the harness root first
|
|
596
|
+
* (`setArtifactStore(createFsStore(harnessDir))`) when the active store's
|
|
597
|
+
* root could differ — the routed writers fail loud on a path mismatch.
|
|
598
|
+
*/
|
|
599
|
+
export declare function registerIterationWorkflow(workflowId: string, options: RegisterIterationWorkflowOptions): Promise<RegisterIterationWorkflowResult>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mstar-harness/engine",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.11.1",
|
|
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": {
|