@@ -0,0 +1,185 @@
|
||||
// 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<AttestedGoalReportResult> {
|
||||
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";
|
||||
}
|
||||
Reference in New Issue
Block a user