// lib/executive-update.ts — parser for the cited ms-executive-update Machine contract. // // The contract bytes are pinned and verified by Mosaic Core before this parser // is reachable for a version-4 role. This module accepts no alternate format, // heading, whitespace form, or identifier spelling. import { GOAL_REPORT_CONTRACT_BLOB, GOAL_REPORT_CONTRACT_PATH, GOAL_REPORT_CONTRACT_SECTION, GOAL_REPORT_CONTRACT_SHA256, GOAL_REPORT_FORMAT, type GoalItemResolutionV1, type GoalPolicyPublication, type GoalTrackedItem, validateGoalItemResolution, } from "../../mosaic-core/lib/goal-policy.ts"; export { GOAL_REPORT_FORMAT, GOAL_REPORT_CONTRACT_PATH, GOAL_REPORT_CONTRACT_SECTION, GOAL_REPORT_CONTRACT_BLOB, GOAL_REPORT_CONTRACT_SHA256, }; export type ExecutiveUpdateSection = GoalTrackedItem["section"]; export type ExecutiveUpdateParseResult = | Readonly<{ ok: true; kind: "update" | "no-change"; items: readonly GoalTrackedItem[] }> | Readonly<{ ok: false; reason: string }>; const SECTIONS: readonly ExecutiveUpdateSection[] = ["Just Completed", "Next Step", "Blocked"]; const CONTROL_RE = /[\u0000-\u0009\u000b-\u001f\u007f-\u009f]/; const TASK_RE = /^T[0-9]+$/; const PR_RE = /^#[0-9]+$/; const ROW_RE = /^[A-Z]{1,4}\.?[0-9]+(?:\.[0-9]+)*$/; const MARKER_RE = /^[A-Z0-9]+(?:-[A-Z0-9]+)*-Q[0-9]+$/; const SHA_RE = /^[0-9a-f]{40}$/; const PATH_RE = /^`[^`\s]+`$/; function invalid(reason: string): ExecutiveUpdateParseResult { return Object.freeze({ ok: false, reason }); } function valid(items: GoalTrackedItem[], kind: "update" | "no-change" = "update"): ExecutiveUpdateParseResult { return Object.freeze({ ok: true, kind, items: Object.freeze(items) }); } function isContractItemToken(token: string): boolean { if (TASK_RE.test(token) || PR_RE.test(token) || ROW_RE.test(token) || MARKER_RE.test(token) || SHA_RE.test(token)) return true; if (!PATH_RE.test(token)) return false; const path = token.slice(1, -1); return !path.startsWith("/") && !path.startsWith("./") && !path.startsWith("../") && !/(?:^|\/)\.\.(?:\/|$)/.test(path); } function hasValidBlockedSuffix(text: string): boolean { return text.endsWith("\u2014 nothing from you") || /\u2014 Decision needed: \(1\) .+ \(2\) .+; recommend [1-9][0-9]*$/.test(text); } /** The contract's distinct single-line update form. */ function parseNoChange(raw: string): ExecutiveUpdateParseResult | undefined { const match = /^No change since ([^;\n]+); still waiting on (.+)\.$/.exec(raw); if (!match) return undefined; const [, since, waiting] = match; if (!isContractItemToken(since) || !isContractItemToken(waiting)) return invalid("No change: invalid item identifier"); // The trusted resolver's public input is deliberately the existing closed // GoalTrackedItem shape. Both references are waiting-state identifiers; the // no-change kind below supplies the stricter unchanged-state rule. return valid([ Object.freeze({ token: since, section: "Next Step" }), Object.freeze({ token: waiting, section: "Next Step" }), ], "no-change"); } function parseSection(section: ExecutiveUpdateSection, body: string): ExecutiveUpdateParseResult | GoalTrackedItem[] { if (body === "* none\n") return []; if (!body.endsWith("\n")) return invalid(`${section}: final bullet must end with LF`); const lines = body.slice(0, -1).split("\n"); if (lines.length < 1 || lines.length > 5) return invalid(`${section}: requires one to five bullets or * none`); const items: GoalTrackedItem[] = []; for (const line of lines) { if (line === "" || /[ \t]$/.test(line)) return invalid(`${section}: blank or trailing-whitespace line`); const match = /^\* ([^:\n]+): ([^\n]+)$/.exec(line); if (!match) return invalid(`${section}: invalid bullet grammar`); const [, token, text] = match; if (!isContractItemToken(token)) return invalid(`${section}: invalid item identifier`); if (text.startsWith("-")) return invalid(`${section}: bullet text may not start with -`); if (section === "Blocked" && !hasValidBlockedSuffix(text)) return invalid("Blocked: required terminal disposition missing"); items.push(Object.freeze({ token, section })); } return items; } /** Parse the exact three-section Machine contract before any Goal state mutation. */ export function parseExecutiveUpdate(raw: unknown): ExecutiveUpdateParseResult { if (typeof raw !== "string") return invalid("payload must be a string"); if (raw.length === 0) return invalid("payload is empty"); if (raw !== raw.normalize("NFC")) return invalid("payload is not NFC-normalized"); if (CONTROL_RE.test(raw)) return invalid("payload contains control characters or non-LF line endings"); if (/(^|\n)[^\n]*[ \t](?=\n|$)/.test(raw)) return invalid("payload contains trailing whitespace"); const noChange = parseNoChange(raw); if (noChange) return noChange; const first = "Just Completed:\n\n"; const second = "\nNext Step:\n\n"; const third = "\nBlocked:\n\n"; if (!raw.startsWith(first)) return invalid("first heading must be Just Completed"); const nextIndex = raw.indexOf(second, first.length); if (nextIndex < 0 || raw.indexOf(second, nextIndex + 1) !== -1) return invalid("Next Step heading must appear exactly once after Just Completed"); const blockedIndex = raw.indexOf(third, nextIndex + second.length); if (blockedIndex < 0 || raw.indexOf(third, blockedIndex + 1) !== -1) return invalid("Blocked heading must appear exactly once after Next Step"); if (raw.includes("\nJust Completed:\n", first.length) || raw.includes("\nNext Step:\n", 0) && raw.indexOf(second) !== nextIndex) return invalid("heading duplication or order violation"); const bodies = [ raw.slice(first.length, nextIndex), raw.slice(nextIndex + second.length, blockedIndex), raw.slice(blockedIndex + third.length), ]; const parsed: GoalTrackedItem[] = []; for (let index = 0; index < SECTIONS.length; index++) { const sectionItems = parseSection(SECTIONS[index], bodies[index]); if (!Array.isArray(sectionItems)) return sectionItems; parsed.push(...sectionItems); } return valid(parsed); } export type AttestedGoalReportResult = | Readonly<{ ok: true; items: readonly GoalTrackedItem[] }> | Readonly<{ ok: false; reason: string }>; function resolutionError(item: GoalTrackedItem, result: GoalItemResolutionV1): AttestedGoalReportResult | undefined { if (result.outcome !== "resolved") return Object.freeze({ ok: false, reason: `resolver-${result.outcome}` }); if (item.section === "Just Completed" && !result.changedSincePreviousAcceptedReport) { return Object.freeze({ ok: false, reason: "unchanged-completion" }); } if (item.section === "Just Completed" && result.completionEvidenceId === null) { return Object.freeze({ ok: false, reason: "completion-evidence-missing" }); } return undefined; } export type AttestedGoalReportStatus = "satisfied" | "blocked" | "in_progress"; /** Resolve each parser-produced item through the attested, read-only resolver. */ export async function validateAttestedGoalReport( raw: unknown, publication: GoalPolicyPublication, status: AttestedGoalReportStatus, ): Promise { const parsed = parseExecutiveUpdate(raw); if (!parsed.ok) return parsed; if (parsed.kind === "no-change" && status !== "in_progress") { return Object.freeze({ ok: false, reason: "no-change-status" }); } for (const item of parsed.items) { let resolution: unknown; try { resolution = await publication.resolver(item); } catch { return Object.freeze({ ok: false, reason: "resolver-unavailable" }); } if (!validateGoalItemResolution(resolution)) { return Object.freeze({ ok: false, reason: "resolver-malformed" }); } if (parsed.kind === "no-change") { if (resolution.outcome !== "resolved") return Object.freeze({ ok: false, reason: `resolver-${resolution.outcome}` }); if (resolution.changedSincePreviousAcceptedReport) return Object.freeze({ ok: false, reason: "no-change-state-changed" }); continue; } const error = resolutionError(item, resolution); if (error) return error; } return Object.freeze({ ok: true, items: parsed.items }); } /** Stable code for Core's payload-free denial journal. */ export function goalPolicyDenialCode(reason: string): string { return ["resolver-zero", "resolver-multiple", "resolver-unavailable", "resolver-stale", "resolver-malformed", "unchanged-completion", "completion-evidence-missing", "no-change-status", "no-change-state-changed"].includes(reason) ? reason : "parser-rejected"; }