@mstar-harness/engine 3.10.3 → 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 +52 -1
- package/dist/audit.js +203 -8
- package/dist/engine.js +362 -14
- 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
|
};
|
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") {
|
|
@@ -13430,6 +13776,7 @@ export {
|
|
|
13430
13776
|
readWorkflowSnapshot,
|
|
13431
13777
|
recordWorkflowDelivery,
|
|
13432
13778
|
referenceExists,
|
|
13779
|
+
registerIterationWorkflow,
|
|
13433
13780
|
registerPlanWorkflow,
|
|
13434
13781
|
registerWorkflow,
|
|
13435
13782
|
releaseLease,
|
|
@@ -13474,6 +13821,7 @@ export {
|
|
|
13474
13821
|
techDebtRollup,
|
|
13475
13822
|
unregisterWorkflow,
|
|
13476
13823
|
validateAssignmentFields,
|
|
13824
|
+
validateAuditFindingGates,
|
|
13477
13825
|
validateAuditStatusBlocks,
|
|
13478
13826
|
validateCompassFrontmatter,
|
|
13479
13827
|
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.0",
|
|
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": {
|