Add plugin-local mission document contracts

This commit is contained in:
Dotta committed 2026-04-22 11:05:47 -05:00
1 parent e1f6a27949
commit cf370c1488
11 files changed
+1660

No files matched your search

@@ -0,0 +1 @@
export * from "./mission/index.js";
@@ -0,0 +1,96 @@
export {
MISSION_FINDING_WAIVER_MARKER_PREFIX,
MISSION_REQUIRED_DOCUMENT_KEYS,
buildMissionFindingWaiverEntry,
parseMissionFeaturesDocument,
parseMissionValidationContractDocument,
parseMissionValidationReportDocument,
type MissionFeature,
type MissionFeaturesDocument,
type MissionFinding,
type MissionMilestone,
type MissionRequiredDocumentKey,
type MissionValidationContract,
type MissionValidationReport,
} from "./mission/index.js";
export { isMissionRequiredDocumentKey } from "./mission/index.js";
import {
isMissionValidationReportKey,
missionFindingWaiverMarker,
parseMissionFindingWaivers as parseMissionFindingWaiversBase,
validationReportRoundFromKey,
type MissionFindingWaiverModel,
} from "./mission/index.js";
export type MissionState =
| "draft"
| "ready"
| "working"
| "validating"
| "fixing"
| "paused"
| "complete";
export type MissionFindingWaiverRecord = {
findingId: string;
rationale: string;
actorLabel: string | null;
createdAt: string | null;
};
export function isValidationReportDocumentKey(key: string) {
return isMissionValidationReportKey(key);
}
export function parseValidationReportRound(key: string) {
return validationReportRoundFromKey(key);
}
export function parseMissionFindingWaivers(decisionLogBody: string | null | undefined): Map<string, MissionFindingWaiverRecord> {
const waivers = new Map<string, MissionFindingWaiverRecord>();
for (const [findingId, waiver] of parseMissionFindingWaiversBase(decisionLogBody)) {
waivers.set(findingId, {
findingId,
rationale: waiver.rationale,
actorLabel: waiver.actorLabel ?? null,
createdAt: toCreatedAt(waiver),
});
}
return waivers;
}
export function buildMissionEventEntry(input: {
markerKey: string;
title: string;
lines: string[];
}) {
const marker = `paperclip:mission-event:${input.markerKey}`;
return {
marker,
body: [
`<!-- ${marker} -->`,
`### ${input.title}`,
"",
...input.lines.map((line) => `- ${line}`),
].join("\n"),
};
}
export function appendDecisionLogEntry(
existingBody: string | null | undefined,
entry: { marker: string; body: string },
) {
const trimmed = existingBody?.trim() ?? "";
const header = trimmed.length > 0 ? trimmed : "# Decision Log";
if (header.includes(entry.marker)) return header;
return [header, entry.body.trim()].filter(Boolean).join("\n\n");
}
function toCreatedAt(waiver: MissionFindingWaiverModel) {
return "recordedAt" in waiver ? waiver.recordedAt ?? null : null;
}
export function missionFindingWaiverCommentMarker(findingId: string) {
return missionFindingWaiverMarker(findingId);
}
@@ -0,0 +1,94 @@
export const MISSION_REQUIRED_DOCUMENT_KEYS = [
"plan",
"mission-brief",
"validation-contract",
"features",
"worker-guidelines",
"services",
"knowledge-base",
"decision-log",
] as const;
export type MissionRequiredDocumentKey = (typeof MISSION_REQUIRED_DOCUMENT_KEYS)[number];
export const MISSION_GENERATED_DOCUMENT_KEY_PREFIXES = [
"validation-report-round-",
"milestone-summary-",
"mission-final-report",
] as const;
export const MISSION_STATES = [
"draft",
"planning",
"ready_for_approval",
"running",
"validating",
"fixing",
"blocked",
"paused",
"completed",
"cancelled",
] as const;
export type MissionState = (typeof MISSION_STATES)[number];
export const MISSION_ROLE_TYPES = [
"orchestrator",
"worker",
"scrutiny_validator",
"user_testing_validator",
"board",
] as const;
export type MissionRoleType = (typeof MISSION_ROLE_TYPES)[number];
export const MISSION_VALIDATION_ASSERTION_STATUSES = [
"unclaimed",
"claimed",
"passing",
"failing",
"blocked",
"waived",
] as const;
export type MissionValidationAssertionStatus = (typeof MISSION_VALIDATION_ASSERTION_STATUSES)[number];
export const MISSION_VALIDATION_TOOLING = [
"unit_test",
"api_call",
"browser",
"screenshot",
"log_inspection",
"code_review",
"cli_command",
"manual_review",
"other",
] as const;
export type MissionValidationTooling = (typeof MISSION_VALIDATION_TOOLING)[number];
export const MISSION_FEATURE_KINDS = ["original", "fix"] as const;
export type MissionFeatureKind = (typeof MISSION_FEATURE_KINDS)[number];
export const MISSION_FEATURE_STATUSES = [
"planned",
"in_progress",
"implemented",
"validating",
"validated",
"blocked",
"cancelled",
] as const;
export type MissionFeatureStatus = (typeof MISSION_FEATURE_STATUSES)[number];
export const MISSION_FINDING_SEVERITIES = ["blocking", "non_blocking", "suggestion"] as const;
export type MissionFindingSeverity = (typeof MISSION_FINDING_SEVERITIES)[number];
export const MISSION_FINDING_STATUSES = ["open", "fix_created", "resolved", "waived"] as const;
export type MissionFindingStatus = (typeof MISSION_FINDING_STATUSES)[number];
export const MISSION_HOST_ISSUE_STATUSES = [
"backlog",
"todo",
"in_progress",
"in_review",
"done",
"blocked",
"cancelled",
] as const;
export type MissionIssueStatus = (typeof MISSION_HOST_ISSUE_STATUSES)[number];
@@ -0,0 +1,148 @@
import { MISSION_REQUIRED_DOCUMENT_KEYS } from "./constants.js";
import { buildMissionFindingWaiverEntry } from "./parsers.js";
import { createDefaultMissionDocumentTemplates } from "./templates.js";
import type {
MissionFeaturesDocument,
MissionTemplateIssueContext,
MissionValidationContract,
MissionValidationReport,
} from "./types.js";
export const missionTemplateIssueFixture: MissionTemplateIssueContext = {
id: "issue-mission-1",
identifier: "PAP-1685",
title: "Port mission document contracts into the missions plugin",
description: "Move the mission document contract into plugin-local modules.",
status: "in_progress",
billingCode: "mission:pap-1685",
};
export const missionValidationContractFixture: MissionValidationContract = {
assertions: [
{
id: "VAL-MISSION-001",
title: "Mission creates issue-backed state",
user_value: "The board can inspect mission work as normal Paperclip work.",
scope: "mission initialization",
setup: "A company with a project and active orchestrator.",
steps: ["Initialize a mission from an existing issue.", "Inspect mission issue documents."],
oracle: "Required documents exist and no unscoped work is created.",
tooling: ["api_call", "manual_review"],
evidence: [
{
kind: "api-response",
description: "JSON response listing created mission documents.",
required: true,
},
],
claimed_by: ["FEAT-MISSION-001"],
status: "claimed",
},
],
};
export const missionValidationContractMarkdownFixture = `
### VAL-MISSION-001: Mission creates issue-backed state
- User value: The board can inspect mission work as normal Paperclip work.
- Scope: mission initialization
- Setup: A company with a project and active orchestrator.
- Steps: Initialize a mission from an existing issue; inspect mission issue documents.
- Oracle: Required documents exist and no unscoped work is created.
- Tooling: api call, manual review
- Evidence: API response listing created mission documents.
- Claimed by: FEAT-MISSION-001
- Status: claimed
`.trim();
export const missionFeaturesFixture: MissionFeaturesDocument = {
milestones: [
{
id: "MILESTONE-MISSION-001",
title: "Foundation",
summary: "Create the mission state contract.",
depends_on: [],
features: [
{
id: "FEAT-MISSION-001",
title: "Create mission documents",
kind: "original",
summary: "Add required document contracts.",
acceptance_criteria: ["Plugin-local modules define every required mission document key."],
claimed_assertion_ids: ["VAL-MISSION-001"],
depends_on: [],
status: "planned",
},
],
},
],
};
export const missionFeaturesMarkdownFixture = `
## MILESTONE-MISSION-001: Foundation
- Summary: Create the mission state contract.
### FEAT-MISSION-001: Create mission documents
- Summary: Add required document contracts.
- Acceptance criteria: Plugin-local modules define every required mission document key.
- Claims: VAL-MISSION-001
- Status: planned
`.trim();
export const missionValidationReportFixture: MissionValidationReport = {
round: 2,
validator_role: "scrutiny_validator",
summary: "One blocking finding found.",
findings: [
{
id: "FINDING-MISSION-001",
severity: "blocking",
assertion_id: "VAL-MISSION-001",
title: "Required document missing",
evidence: ["API response omitted validation-contract.", "Screenshot attached in issue comment."],
repro_steps: ["Initialize mission.", "List issue documents."],
expected: "validation-contract exists.",
actual: "validation-contract is absent.",
suspected_area: "mission initialization",
recommended_fix_scope: "Create the missing document idempotently.",
status: "open",
},
],
};
export const missionValidationReportMarkdownFixture = `
- Round: 2
- Validator role: scrutiny validator
- Summary: One blocking finding found.
### FINDING-MISSION-001: Required document missing
- Severity: blocking
- Assertion: VAL-MISSION-001
- Evidence: API response omitted validation-contract; screenshot attached in issue comment
- Repro steps: Initialize mission; list issue documents
- Expected: validation-contract exists.
- Actual: validation-contract is absent.
- Suspected area: mission initialization
- Recommended fix scope: Create the missing document idempotently.
- Status: open
`.trim();
export const missionFindingWaiverEntryFixture = buildMissionFindingWaiverEntry({
findingId: "FINDING-MISSION-002",
rationale: "Acceptable for MVP.",
actorLabel: "Board",
createdAt: "2026-04-20T12:00:00.000Z",
});
export const missionDecisionLogMarkdownFixture = [
"# Decision Log",
"",
missionFindingWaiverEntryFixture,
].join("\n");
export const missionTemplateFixtures = Object.fromEntries(
createDefaultMissionDocumentTemplates(missionTemplateIssueFixture).map((template) => [template.key, template]),
) as Record<(typeof MISSION_REQUIRED_DOCUMENT_KEYS)[number], ReturnType<typeof createDefaultMissionDocumentTemplates>[number]>;
@@ -0,0 +1,115 @@
export {
MISSION_FEATURE_KINDS,
MISSION_FEATURE_STATUSES,
MISSION_FINDING_SEVERITIES,
MISSION_FINDING_STATUSES,
MISSION_GENERATED_DOCUMENT_KEY_PREFIXES,
MISSION_HOST_ISSUE_STATUSES,
MISSION_REQUIRED_DOCUMENT_KEYS,
MISSION_ROLE_TYPES,
MISSION_STATES,
MISSION_VALIDATION_ASSERTION_STATUSES,
MISSION_VALIDATION_TOOLING,
type MissionFeatureKind,
type MissionFeatureStatus,
type MissionFindingSeverity,
type MissionFindingStatus,
type MissionIssueStatus,
type MissionRequiredDocumentKey,
type MissionRoleType,
type MissionState,
type MissionValidationAssertionStatus,
type MissionValidationTooling,
} from "./constants.js";
export {
deriveIssueBackedMissionState,
getMissionMilestoneSummaryDocumentKey,
getMissionValidationReportDocumentKey,
isMissionDocumentKey,
isMissionRequiredDocumentKey,
MISSION_FINAL_REPORT_DOCUMENT_KEY,
type MissionDocumentKey,
type MissionStateDerivationInput,
} from "./mission.js";
export {
advanceMissionSchema,
decomposeMissionSchema,
missionDecisionLogEntrySchema,
missionDecisionLogSchema,
missionDocumentKeySchema,
missionEvidenceRequirementSchema,
missionFeatureKindSchema,
missionFeatureSchema,
missionFeatureStatusSchema,
missionFeaturesDocumentSchema,
missionFindingSchema,
missionFindingSeveritySchema,
missionFindingStatusSchema,
missionFindingWaiverSchema,
missionMilestoneSchema,
missionRequiredDocumentKeySchema,
missionRoleTypeSchema,
missionStateDerivationInputSchema,
missionStateSchema,
missionValidationAssertionSchema,
missionValidationContractSchema,
missionValidationReportSchema,
waiveMissionFindingSchema,
type AdvanceMission,
type DecomposeMission,
type MissionDecisionLog,
type MissionDecisionLogEntry,
type MissionFeature,
type MissionFeaturesDocument,
type MissionFinding,
type MissionFindingWaiver,
type MissionMilestone,
type MissionStateDerivationInput as MissionStateDerivationInputShape,
type MissionValidationAssertion,
type MissionValidationContract,
type MissionValidationReport,
type WaiveMissionFinding,
} from "./schemas.js";
export {
buildMissionFindingWaiverEntry,
isMissionValidationReportKey,
missionFindingWaiverMarker,
MISSION_FINDING_WAIVER_MARKER_PREFIX,
parseMissionDecisionLogDocument,
parseMissionFeaturesDocument,
parseMissionFindingWaivers,
parseMissionValidationContractDocument,
parseMissionValidationReportDocument,
validationReportRoundFromKey,
} from "./parsers.js";
export {
missionDecisionLogMarkdownFixture,
missionFeaturesFixture,
missionFeaturesMarkdownFixture,
missionFindingWaiverEntryFixture,
missionTemplateFixtures,
missionTemplateIssueFixture,
missionValidationContractFixture,
missionValidationContractMarkdownFixture,
missionValidationReportFixture,
missionValidationReportMarkdownFixture,
} from "./fixtures.js";
export {
createDefaultMissionDocumentTemplates,
defaultMissionDocument,
} from "./templates.js";
export type {
MissionDecisionLog as MissionDecisionLogModel,
MissionDecisionLogEntry as MissionDecisionLogEntryModel,
MissionDocumentTemplate,
MissionEvidenceRequirement,
MissionFeature as MissionFeatureModel,
MissionFeaturesDocument as MissionFeaturesDocumentModel,
MissionFinding as MissionFindingModel,
MissionFindingWaiver as MissionFindingWaiverModel,
MissionMilestone as MissionMilestoneModel,
MissionTemplateIssueContext,
MissionValidationAssertion as MissionValidationAssertionModel,
MissionValidationContract as MissionValidationContractModel,
MissionValidationReport as MissionValidationReportModel,
} from "./types.js";
@@ -0,0 +1,73 @@
import type { MissionIssueStatus, MissionRequiredDocumentKey, MissionState } from "./constants.js";
import { MISSION_REQUIRED_DOCUMENT_KEYS } from "./constants.js";
export const MISSION_FINAL_REPORT_DOCUMENT_KEY = "mission-final-report";
export type MissionDocumentKey =
| MissionRequiredDocumentKey
| typeof MISSION_FINAL_REPORT_DOCUMENT_KEY
| `validation-report-round-${number}`
| `milestone-summary-${string}`;
export function getMissionValidationReportDocumentKey(round: number): `validation-report-round-${number}` {
if (!Number.isInteger(round) || round < 1) {
throw new Error("Mission validation report rounds must be positive integers.");
}
return `validation-report-round-${round}`;
}
export function getMissionMilestoneSummaryDocumentKey(slug: string): `milestone-summary-${string}` {
const normalized = slug.trim().toLowerCase();
if (!/^[a-z0-9][a-z0-9_-]*$/.test(normalized)) {
throw new Error("Mission milestone summary slugs must be document-key-safe.");
}
return `milestone-summary-${normalized}`;
}
export function isMissionRequiredDocumentKey(value: string): value is MissionRequiredDocumentKey {
return (MISSION_REQUIRED_DOCUMENT_KEYS as readonly string[]).includes(value);
}
export function isMissionDocumentKey(value: string): value is MissionDocumentKey {
return (
isMissionRequiredDocumentKey(value) ||
value === MISSION_FINAL_REPORT_DOCUMENT_KEY ||
/^validation-report-round-[1-9][0-9]*$/.test(value) ||
/^milestone-summary-[a-z0-9][a-z0-9_-]*$/.test(value)
);
}
export interface MissionStateDerivationInput {
missionIssueStatus: MissionIssueStatus;
presentDocumentKeys: readonly string[];
approvalRequired?: boolean;
paused?: boolean;
hasActiveFeatureIssues?: boolean;
hasActiveValidationIssues?: boolean;
hasActiveFixIssues?: boolean;
hasBlockingFindings?: boolean;
}
export function deriveIssueBackedMissionState(input: MissionStateDerivationInput): MissionState {
if (input.missionIssueStatus === "cancelled") return "cancelled";
if (input.paused) return "paused";
if (input.missionIssueStatus === "blocked" || input.hasBlockingFindings) return "blocked";
const presentKeys = new Set(input.presentDocumentKeys);
if (input.missionIssueStatus === "done" && presentKeys.has(MISSION_FINAL_REPORT_DOCUMENT_KEY)) {
return "completed";
}
const hasMissionBrief = presentKeys.has("mission-brief");
const hasValidationContract = presentKeys.has("validation-contract");
const hasFeatures = presentKeys.has("features");
if (!hasMissionBrief || !hasValidationContract) return "draft";
if (input.approvalRequired) return "ready_for_approval";
if (!hasFeatures) return "planning";
if (input.hasActiveFixIssues) return "fixing";
if (input.hasActiveValidationIssues) return "validating";
if (input.hasActiveFeatureIssues) return "running";
return "planning";
}
@@ -0,0 +1,375 @@
import {
missionDecisionLogSchema,
missionFeaturesDocumentSchema,
missionValidationContractSchema,
missionValidationReportSchema,
} from "./schemas.js";
import type {
MissionDecisionLog,
MissionFeaturesDocument,
MissionFindingWaiver,
MissionValidationContract,
MissionValidationReport,
} from "./types.js";
const VALIDATION_ID_RE = /\bVAL-[A-Z0-9][A-Z0-9-]*-[0-9]{3,}\b/gi;
const FEATURE_ID_RE = /\bFEAT-[A-Z0-9][A-Z0-9-]*-[0-9]{3,}\b/gi;
const MILESTONE_ID_RE = /\bMILESTONE-[A-Z0-9][A-Z0-9-]*-[0-9]{3,}\b/gi;
const FINDING_ID_RE = /\bFINDING-[A-Z0-9][A-Z0-9-]*-[0-9]{3,}\b/gi;
export const MISSION_FINDING_WAIVER_MARKER_PREFIX = "paperclip:mission-finding-waiver:";
function normalizeId(value: string) {
return value.trim().toUpperCase();
}
function uniqueIds(values: string[]) {
return [...new Set(values.map(normalizeId))];
}
function extractIds(value: string, re: RegExp) {
return uniqueIds(value.match(re) ?? []);
}
function extractJsonPayload(markdown: string): unknown | null {
const trimmed = markdown.trim();
if (!trimmed) return null;
const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(trimmed);
const candidate = fenced?.[1]?.trim() ?? trimmed;
if (!candidate.startsWith("{") && !candidate.startsWith("[")) return null;
try {
return JSON.parse(candidate);
} catch {
return null;
}
}
function parseHeading(line: string) {
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line.trim());
if (!match) return null;
return { text: match[2].trim() };
}
function parseField(line: string): { key: string; value: string } | null {
const match = /^[-*]\s+([A-Za-z][A-Za-z0-9 _/-]{1,48}):\s*(.*?)\s*$/.exec(line.trim());
if (!match) return null;
return {
key: match[1].trim().toLowerCase().replace(/[\s/-]+/g, "_"),
value: match[2].trim(),
};
}
function parseKeyedHeading(text: string, re: RegExp) {
const [rawId] = text.match(re) ?? [];
if (!rawId) return null;
const id = normalizeId(rawId);
const escaped = rawId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const title = text
.replace(new RegExp(`\\b${escaped}\\b`, "i"), "")
.replace(/^[\s:.-]+/, "")
.trim();
return { id, title: title || id };
}
function splitList(value: string) {
return value
.split(/(?:\s*;\s*|\s*,\s*)/)
.map((part) => part.trim())
.filter(Boolean);
}
function normalizeEnumValue(value: string) {
return value.trim().toLowerCase().replace(/[\s-]+/g, "_");
}
export function validationReportRoundFromKey(key: string) {
const match = /^validation-report-round-([1-9][0-9]*)$/.exec(key);
return match ? Number.parseInt(match[1], 10) : null;
}
export function isMissionValidationReportKey(key: string) {
return validationReportRoundFromKey(key) !== null;
}
export function missionFindingWaiverMarker(findingId: string) {
return `${MISSION_FINDING_WAIVER_MARKER_PREFIX}${findingId}`;
}
export function parseMissionValidationContractDocument(markdown: string): MissionValidationContract {
const json = extractJsonPayload(markdown);
if (json) return missionValidationContractSchema.parse(json);
const assertions: MissionValidationContract["assertions"] = [];
let current: MissionValidationContract["assertions"][number] | null = null;
for (const line of markdown.split(/\r?\n/)) {
const heading = parseHeading(line);
if (heading) {
const parsed = parseKeyedHeading(heading.text, VALIDATION_ID_RE);
if (parsed) {
current = {
id: parsed.id,
title: parsed.title,
user_value: "",
scope: "",
setup: "",
steps: [],
oracle: "",
tooling: ["manual_review"],
evidence: [],
claimed_by: [],
status: "unclaimed",
};
assertions.push(current);
continue;
}
}
if (!current) continue;
const field = parseField(line);
if (!field) continue;
if (field.key === "user_value") current.user_value = field.value;
if (field.key === "scope") current.scope = field.value;
if (field.key === "setup") current.setup = field.value;
if (field.key === "steps") current.steps = splitList(field.value);
if (field.key === "oracle") current.oracle = field.value;
if (field.key === "tooling") {
current.tooling = splitList(field.value).map(
(tool) => tool.toLowerCase().replace(/[\s-]+/g, "_"),
) as typeof current.tooling;
}
if (field.key === "evidence") {
current.evidence = splitList(field.value).map((description, index) => ({
kind: index === 0 ? "primary" : `supporting-${index}`,
description,
required: true,
}));
}
if (["claimed_by", "claimed", "claims", "features"].includes(field.key)) {
current.claimed_by = uniqueIds([...current.claimed_by, ...extractIds(field.value, FEATURE_ID_RE)]);
}
if (field.key === "status") current.status = normalizeEnumValue(field.value) as typeof current.status;
}
return missionValidationContractSchema.parse({ assertions });
}
export function parseMissionFeaturesDocument(markdown: string): MissionFeaturesDocument {
const json = extractJsonPayload(markdown);
if (json) return missionFeaturesDocumentSchema.parse(json);
const milestoneById = new Map<string, MissionFeaturesDocument["milestones"][number]>();
let currentMilestone: MissionFeaturesDocument["milestones"][number] | null = null;
let currentFeature: MissionFeaturesDocument["milestones"][number]["features"][number] | null = null;
function ensureMilestone(id: string, title: string) {
const normalizedId = normalizeId(id);
const existing = milestoneById.get(normalizedId);
if (existing) return existing;
const milestone = {
id: normalizedId,
title: title.trim() || normalizedId,
summary: title.trim() || normalizedId,
depends_on: [],
features: [],
};
milestoneById.set(normalizedId, milestone);
return milestone;
}
for (const line of markdown.split(/\r?\n/)) {
const heading = parseHeading(line);
if (heading) {
const featureHeading = parseKeyedHeading(heading.text, FEATURE_ID_RE);
if (featureHeading) {
currentMilestone ??= ensureMilestone("MILESTONE-MISSION-001", "Mission milestone");
currentFeature = {
id: featureHeading.id,
title: featureHeading.title,
kind: "original",
summary: "",
acceptance_criteria: [],
claimed_assertion_ids: [],
depends_on: [],
status: "planned",
};
currentMilestone.features.push(currentFeature);
continue;
}
const milestoneHeading = parseKeyedHeading(heading.text, MILESTONE_ID_RE);
if (milestoneHeading) {
currentMilestone = ensureMilestone(milestoneHeading.id, milestoneHeading.title);
currentFeature = null;
continue;
}
}
const field = parseField(line);
if (!field) continue;
if (currentFeature) {
if (field.key === "kind") currentFeature.kind = normalizeEnumValue(field.value) === "fix" ? "fix" : "original";
if (["summary", "description"].includes(field.key)) currentFeature.summary = field.value;
if (["acceptance_criteria", "success_criteria"].includes(field.key)) {
currentFeature.acceptance_criteria = splitList(field.value);
}
if (["depends_on", "dependencies", "blocked_by"].includes(field.key)) {
currentFeature.depends_on = uniqueIds([
...currentFeature.depends_on,
...extractIds(field.value, FEATURE_ID_RE),
]);
}
if (["claimed_assertion_ids", "claimed_by", "claims", "assertions", "validation"].includes(field.key)) {
currentFeature.claimed_assertion_ids = uniqueIds([
...currentFeature.claimed_assertion_ids,
...extractIds(field.value, VALIDATION_ID_RE),
]);
}
if (field.key === "source_finding_id") currentFeature.source_finding_id = field.value || null;
if (field.key === "status") currentFeature.status = normalizeEnumValue(field.value) as typeof currentFeature.status;
continue;
}
if (currentMilestone) {
if (["summary", "description"].includes(field.key)) currentMilestone.summary = field.value;
if (["depends_on", "dependencies", "blocked_by"].includes(field.key)) {
currentMilestone.depends_on = uniqueIds([
...currentMilestone.depends_on,
...extractIds(field.value, MILESTONE_ID_RE),
]);
}
}
}
return missionFeaturesDocumentSchema.parse({ milestones: [...milestoneById.values()] });
}
export function parseMissionValidationReportDocument(
markdown: string,
options: { round?: number } = {},
): MissionValidationReport {
const json = extractJsonPayload(markdown);
if (json) return missionValidationReportSchema.parse(json);
const report: Partial<MissionValidationReport> = {
round: options.round,
summary: "",
findings: [],
};
let currentFinding: MissionValidationReport["findings"][number] | null = null;
for (const line of markdown.split(/\r?\n/)) {
const heading = parseHeading(line);
if (heading) {
const findingHeading = parseKeyedHeading(heading.text, FINDING_ID_RE);
if (findingHeading) {
currentFinding = {
id: findingHeading.id,
severity: "suggestion",
assertion_id: null,
title: findingHeading.title,
evidence: [],
repro_steps: [],
expected: "",
actual: "",
suspected_area: null,
recommended_fix_scope: null,
status: "open",
};
report.findings!.push(currentFinding);
continue;
}
}
const field = parseField(line);
if (!field) continue;
if (!currentFinding) {
if (field.key === "round") report.round = Number.parseInt(field.value, 10);
if (["validator_role", "role"].includes(field.key)) {
report.validator_role = normalizeEnumValue(field.value) as MissionValidationReport["validator_role"];
}
if (field.key === "summary") report.summary = field.value;
continue;
}
if (field.key === "severity") currentFinding.severity = normalizeEnumValue(field.value) as typeof currentFinding.severity;
if (["assertion", "assertion_id"].includes(field.key)) {
currentFinding.assertion_id = extractIds(field.value, VALIDATION_ID_RE)[0] ?? null;
}
if (field.key === "evidence") currentFinding.evidence = splitList(field.value);
if (["repro_steps", "steps"].includes(field.key)) currentFinding.repro_steps = splitList(field.value);
if (field.key === "expected") currentFinding.expected = field.value;
if (field.key === "actual") currentFinding.actual = field.value;
if (field.key === "suspected_area") currentFinding.suspected_area = field.value || null;
if (field.key === "recommended_fix_scope") currentFinding.recommended_fix_scope = field.value || null;
if (field.key === "status") currentFinding.status = normalizeEnumValue(field.value) as typeof currentFinding.status;
}
return missionValidationReportSchema.parse(report);
}
export function parseMissionDecisionLogDocument(decisionLogBody: string | null | undefined): MissionDecisionLog {
const entries: MissionDecisionLog["entries"] = [];
if (!decisionLogBody) return missionDecisionLogSchema.parse({ entries });
const markerRe = /<!--\s*paperclip:mission-finding-waiver:(FINDING-[A-Z0-9][A-Z0-9-]*-[0-9]{3,})\s*-->/g;
const matches = [...decisionLogBody.matchAll(markerRe)];
for (let index = 0; index < matches.length; index += 1) {
const match = matches[index]!;
const findingId = match[1]!;
const start = match.index! + match[0].length;
const end = matches[index + 1]?.index ?? decisionLogBody.length;
const block = decisionLogBody.slice(start, end);
const rationale =
/^\s*-\s+Rationale:\s*(.+?)\s*$/im.exec(block)?.[1]?.trim() ??
block.trim().split(/\r?\n/).find((line) => line.trim())?.trim() ??
"";
const actorLabel = /^\s*-\s+Decision by:\s*(.+?)\s*$/im.exec(block)?.[1]?.trim() ?? null;
const recordedAt = /^\s*-\s+Recorded at:\s*(.+?)\s*$/im.exec(block)?.[1]?.trim() ?? null;
entries.push({
kind: "finding_waiver",
findingId,
rationale,
actorLabel,
recordedAt,
});
}
return missionDecisionLogSchema.parse({ entries });
}
export function parseMissionFindingWaivers(decisionLogBody: string | null | undefined): Map<string, MissionFindingWaiver> {
const waivers = new Map<string, MissionFindingWaiver>();
for (const entry of parseMissionDecisionLogDocument(decisionLogBody).entries) {
waivers.set(entry.findingId, {
findingId: entry.findingId,
rationale: entry.rationale,
actorLabel: entry.actorLabel ?? null,
recordedAt: entry.recordedAt ?? null,
});
}
return waivers;
}
export function buildMissionFindingWaiverEntry(input: {
findingId: string;
rationale: string;
actorLabel: string;
createdAt: Date | string;
}) {
const createdAt = input.createdAt instanceof Date ? input.createdAt.toISOString() : input.createdAt;
return [
`<!-- ${missionFindingWaiverMarker(input.findingId)} -->`,
`### Waived ${input.findingId}`,
"",
`- Rationale: ${input.rationale}`,
`- Decision by: ${input.actorLabel}`,
`- Recorded at: ${createdAt}`,
].join("\n");
}
@@ -0,0 +1,271 @@
import { z } from "zod";
import {
MISSION_FEATURE_KINDS,
MISSION_FEATURE_STATUSES,
MISSION_FINDING_SEVERITIES,
MISSION_FINDING_STATUSES,
MISSION_HOST_ISSUE_STATUSES,
MISSION_REQUIRED_DOCUMENT_KEYS,
MISSION_ROLE_TYPES,
MISSION_STATES,
MISSION_VALIDATION_ASSERTION_STATUSES,
MISSION_VALIDATION_TOOLING,
} from "./constants.js";
import { isMissionDocumentKey } from "./mission.js";
const validationAssertionIdSchema = z
.string()
.trim()
.regex(/^VAL-[A-Z0-9][A-Z0-9-]*-[0-9]{3,}$/, "Validation assertion IDs must look like VAL-MISSION-001");
const featureIdSchema = z
.string()
.trim()
.regex(/^FEAT-[A-Z0-9][A-Z0-9-]*-[0-9]{3,}$/, "Feature IDs must look like FEAT-MISSION-001");
const milestoneIdSchema = z
.string()
.trim()
.regex(/^MILESTONE-[A-Z0-9][A-Z0-9-]*-[0-9]{3,}$/, "Milestone IDs must look like MILESTONE-MISSION-001");
const findingIdSchema = z
.string()
.trim()
.regex(/^FINDING-[A-Z0-9][A-Z0-9-]*-[0-9]{3,}$/, "Finding IDs must look like FINDING-MISSION-001");
const nonEmptyStringSchema = z.string().trim().min(1);
const nonEmptyStringListSchema = z.array(nonEmptyStringSchema).min(1);
export const missionStateSchema = z.enum(MISSION_STATES);
export const missionRoleTypeSchema = z.enum(MISSION_ROLE_TYPES);
export const missionRequiredDocumentKeySchema = z.enum(MISSION_REQUIRED_DOCUMENT_KEYS);
export const missionDocumentKeySchema = z
.string()
.trim()
.refine(isMissionDocumentKey, "Unknown mission document key");
export const missionEvidenceRequirementSchema = z
.object({
kind: nonEmptyStringSchema,
description: nonEmptyStringSchema,
required: z.boolean().default(true),
})
.strict();
export const missionValidationAssertionSchema = z
.object({
id: validationAssertionIdSchema,
title: nonEmptyStringSchema,
user_value: nonEmptyStringSchema,
scope: nonEmptyStringSchema,
setup: nonEmptyStringSchema,
steps: nonEmptyStringListSchema,
oracle: nonEmptyStringSchema,
tooling: z.array(z.enum(MISSION_VALIDATION_TOOLING)).min(1),
evidence: z.array(missionEvidenceRequirementSchema).min(1),
claimed_by: z.array(featureIdSchema).default([]),
status: z.enum(MISSION_VALIDATION_ASSERTION_STATUSES).default("unclaimed"),
})
.strict();
export const missionValidationContractSchema = z
.object({
assertions: z.array(missionValidationAssertionSchema).min(1),
})
.strict()
.superRefine((value, ctx) => {
const ids = new Set<string>();
value.assertions.forEach((assertion, index) => {
if (ids.has(assertion.id)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Duplicate validation assertion id: ${assertion.id}`,
path: ["assertions", index, "id"],
});
}
ids.add(assertion.id);
});
});
export const missionFeatureKindSchema = z.enum(MISSION_FEATURE_KINDS);
export const missionFeatureStatusSchema = z.enum(MISSION_FEATURE_STATUSES);
export const missionFeatureSchema = z
.object({
id: featureIdSchema,
title: nonEmptyStringSchema,
kind: missionFeatureKindSchema.default("original"),
summary: nonEmptyStringSchema,
acceptance_criteria: nonEmptyStringListSchema,
claimed_assertion_ids: z.array(validationAssertionIdSchema).default([]),
depends_on: z.array(featureIdSchema).default([]),
status: missionFeatureStatusSchema.default("planned"),
source_finding_id: findingIdSchema.nullable().optional(),
})
.strict()
.superRefine((value, ctx) => {
if (value.kind === "original" && value.claimed_assertion_ids.length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Original mission features must claim at least one validation assertion",
path: ["claimed_assertion_ids"],
});
}
if (value.kind === "fix" && !value.source_finding_id) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Fix mission features must reference a source finding",
path: ["source_finding_id"],
});
}
});
export const missionMilestoneSchema = z
.object({
id: milestoneIdSchema,
title: nonEmptyStringSchema,
summary: nonEmptyStringSchema,
depends_on: z.array(milestoneIdSchema).default([]),
features: z.array(missionFeatureSchema).min(1),
})
.strict();
export const missionFeaturesDocumentSchema = z
.object({
milestones: z.array(missionMilestoneSchema).min(1),
})
.strict()
.superRefine((value, ctx) => {
const milestoneIds = new Set<string>();
const featureIds = new Set<string>();
value.milestones.forEach((milestone, milestoneIndex) => {
if (milestoneIds.has(milestone.id)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Duplicate mission milestone id: ${milestone.id}`,
path: ["milestones", milestoneIndex, "id"],
});
}
milestoneIds.add(milestone.id);
milestone.features.forEach((feature, featureIndex) => {
if (featureIds.has(feature.id)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Duplicate mission feature id: ${feature.id}`,
path: ["milestones", milestoneIndex, "features", featureIndex, "id"],
});
}
featureIds.add(feature.id);
});
});
});
export const missionFindingSeveritySchema = z.enum(MISSION_FINDING_SEVERITIES);
export const missionFindingStatusSchema = z.enum(MISSION_FINDING_STATUSES);
export const missionFindingSchema = z
.object({
id: findingIdSchema,
severity: missionFindingSeveritySchema,
assertion_id: validationAssertionIdSchema.nullable().optional(),
title: nonEmptyStringSchema,
evidence: nonEmptyStringListSchema,
repro_steps: nonEmptyStringListSchema,
expected: nonEmptyStringSchema,
actual: nonEmptyStringSchema,
suspected_area: nonEmptyStringSchema.nullable().optional(),
recommended_fix_scope: nonEmptyStringSchema.nullable().optional(),
status: missionFindingStatusSchema.default("open"),
})
.strict()
.superRefine((value, ctx) => {
if (value.severity === "blocking" && !value.assertion_id) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Blocking mission findings must reference a validation assertion",
path: ["assertion_id"],
});
}
if (value.status === "fix_created" && !value.recommended_fix_scope) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Findings with fix_created status must include recommended_fix_scope",
path: ["recommended_fix_scope"],
});
}
});
export const missionValidationReportSchema = z
.object({
round: z.number().int().positive(),
validator_role: z.enum(["scrutiny_validator", "user_testing_validator"]),
summary: nonEmptyStringSchema,
findings: z.array(missionFindingSchema).default([]),
})
.strict();
export const missionFindingWaiverSchema = z
.object({
findingId: findingIdSchema,
rationale: nonEmptyStringSchema.max(5000),
actorLabel: nonEmptyStringSchema.nullable().optional(),
recordedAt: nonEmptyStringSchema.nullable().optional(),
})
.strict();
export const missionDecisionLogEntrySchema = missionFindingWaiverSchema.extend({
kind: z.literal("finding_waiver"),
}).strict();
export const missionDecisionLogSchema = z
.object({
entries: z.array(missionDecisionLogEntrySchema).default([]),
})
.strict();
export const missionStateDerivationInputSchema = z
.object({
missionIssueStatus: z.enum(MISSION_HOST_ISSUE_STATUSES),
presentDocumentKeys: z.array(missionDocumentKeySchema),
approvalRequired: z.boolean().optional(),
paused: z.boolean().optional(),
hasActiveFeatureIssues: z.boolean().optional(),
hasActiveValidationIssues: z.boolean().optional(),
hasActiveFixIssues: z.boolean().optional(),
hasBlockingFindings: z.boolean().optional(),
})
.strict();
export const decomposeMissionSchema = z
.object({
dryRun: z.boolean().optional().default(false),
})
.strict();
export const advanceMissionSchema = z
.object({
budgetLimitCents: z.number().int().positive().optional(),
maxValidationRounds: z.number().int().positive().max(20).optional(),
})
.strict();
export const waiveMissionFindingSchema = z
.object({
rationale: nonEmptyStringSchema.max(5000),
})
.strict();
export type MissionValidationAssertion = z.infer<typeof missionValidationAssertionSchema>;
export type MissionValidationContract = z.infer<typeof missionValidationContractSchema>;
export type MissionFeature = z.infer<typeof missionFeatureSchema>;
export type MissionMilestone = z.infer<typeof missionMilestoneSchema>;
export type MissionFeaturesDocument = z.infer<typeof missionFeaturesDocumentSchema>;
export type MissionFinding = z.infer<typeof missionFindingSchema>;
export type MissionValidationReport = z.infer<typeof missionValidationReportSchema>;
export type MissionFindingWaiver = z.infer<typeof missionFindingWaiverSchema>;
export type MissionDecisionLogEntry = z.infer<typeof missionDecisionLogEntrySchema>;
export type MissionDecisionLog = z.infer<typeof missionDecisionLogSchema>;
export type MissionStateDerivationInput = z.infer<typeof missionStateDerivationInputSchema>;
export type DecomposeMission = z.infer<typeof decomposeMissionSchema>;
export type AdvanceMission = z.infer<typeof advanceMissionSchema>;
export type WaiveMissionFinding = z.infer<typeof waiveMissionFindingSchema>;
@@ -0,0 +1,156 @@
import type { MissionRequiredDocumentKey } from "./constants.js";
import { MISSION_REQUIRED_DOCUMENT_KEYS } from "./constants.js";
import type { MissionDocumentTemplate, MissionTemplateIssueContext } from "./types.js";
function issueReference(issue: Pick<MissionTemplateIssueContext, "identifier" | "id">) {
if (!issue.identifier) return `\`${issue.id}\``;
const prefix = issue.identifier.split("-")[0]?.toUpperCase();
return prefix ? `[${issue.identifier}](/${prefix}/issues/${issue.identifier})` : `\`${issue.id}\``;
}
function missionBillingCode(issue: Pick<MissionTemplateIssueContext, "identifier" | "id" | "billingCode">) {
return issue.billingCode ?? `mission:${issue.identifier ?? issue.id}`;
}
export function defaultMissionDocument(input: {
issue: MissionTemplateIssueContext;
key: MissionRequiredDocumentKey;
}): MissionDocumentTemplate {
const { issue, key } = input;
const ref = issueReference(issue);
const issueTitle = issue.title.trim();
switch (key) {
case "plan":
return {
key,
title: "Plan",
body: [
"# Plan",
"",
`Mission issue: ${ref}`,
"",
"## Objective",
"",
issue.description?.trim() || issueTitle,
"",
"## Milestones",
"",
"- TODO: Decompose the mission into bounded milestones.",
"",
"## Verification",
"",
"- TODO: Link validation assertions from `validation-contract` before implementation starts.",
].join("\n"),
};
case "mission-brief":
return {
key,
title: "Mission Brief",
body: [
"# Mission Brief",
"",
`Mission issue: ${ref}`,
`Current status: \`${issue.status}\``,
`Billing code: \`${missionBillingCode(issue)}\``,
"",
"## Goal",
"",
issueTitle,
"",
"## Scope",
"",
"- TODO: Define the work that is in scope.",
"",
"## Non-Goals",
"",
"- TODO: Define what this mission will not do.",
"",
"## Assumptions",
"",
"- TODO: Record assumptions that need validation.",
].join("\n"),
};
case "validation-contract":
return {
key,
title: "Validation Contract",
body: [
"# Validation Contract",
"",
"Define finite, testable assertions before feature decomposition.",
"",
"```json",
JSON.stringify({ assertions: [] }, null, 2),
"```",
].join("\n"),
};
case "features":
return {
key,
title: "Features",
body: [
"# Features",
"",
"Group implementation features by milestone after the validation contract is written.",
"",
"```json",
JSON.stringify({ milestones: [] }, null, 2),
"```",
].join("\n"),
};
case "worker-guidelines":
return {
key,
title: "Worker Guidelines",
body: [
"# Worker Guidelines",
"",
"- Work only on the assigned child issue.",
"- Preserve normal checkout, ownership, testing, and handoff rules.",
"- Do not decide final correctness; validators judge against the validation contract.",
"- Leave concise evidence in comments, work products, or attachments.",
].join("\n"),
};
case "services":
return {
key,
title: "Services",
body: [
"# Services",
"",
"## Commands",
"",
"- TODO: Document local server, test, and preview commands.",
"",
"## Environment",
"",
"- TODO: Document required accounts, seeded data, secrets, and setup costs.",
].join("\n"),
};
case "knowledge-base":
return {
key,
title: "Knowledge Base",
body: [
"# Knowledge Base",
"",
"- TODO: Add concise discoveries that future workers or validators need.",
].join("\n"),
};
case "decision-log":
return {
key,
title: "Decision Log",
body: [
"# Decision Log",
"",
"- Mission initialized from existing issue state.",
].join("\n"),
};
}
}
export function createDefaultMissionDocumentTemplates(issue: MissionTemplateIssueContext): MissionDocumentTemplate[] {
return MISSION_REQUIRED_DOCUMENT_KEYS.map((key) => defaultMissionDocument({ issue, key }));
}
@@ -0,0 +1,110 @@
import type {
MissionFeatureKind,
MissionFeatureStatus,
MissionFindingSeverity,
MissionFindingStatus,
MissionIssueStatus,
MissionRequiredDocumentKey,
MissionRoleType,
MissionValidationAssertionStatus,
MissionValidationTooling,
} from "./constants.js";
export interface MissionEvidenceRequirement {
kind: string;
description: string;
required: boolean;
}
export interface MissionValidationAssertion {
id: string;
title: string;
user_value: string;
scope: string;
setup: string;
steps: string[];
oracle: string;
tooling: MissionValidationTooling[];
evidence: MissionEvidenceRequirement[];
claimed_by: string[];
status: MissionValidationAssertionStatus;
}
export interface MissionValidationContract {
assertions: MissionValidationAssertion[];
}
export interface MissionFeature {
id: string;
title: string;
kind: MissionFeatureKind;
summary: string;
acceptance_criteria: string[];
claimed_assertion_ids: string[];
depends_on: string[];
status: MissionFeatureStatus;
source_finding_id?: string | null;
}
export interface MissionMilestone {
id: string;
title: string;
summary: string;
depends_on: string[];
features: MissionFeature[];
}
export interface MissionFeaturesDocument {
milestones: MissionMilestone[];
}
export interface MissionFinding {
id: string;
severity: MissionFindingSeverity;
assertion_id?: string | null;
title: string;
evidence: string[];
repro_steps: string[];
expected: string;
actual: string;
suspected_area?: string | null;
recommended_fix_scope?: string | null;
status: MissionFindingStatus;
}
export interface MissionValidationReport {
round: number;
validator_role: Extract<MissionRoleType, "scrutiny_validator" | "user_testing_validator">;
summary: string;
findings: MissionFinding[];
}
export interface MissionFindingWaiver {
findingId: string;
rationale: string;
actorLabel?: string | null;
recordedAt?: string | null;
}
export interface MissionDecisionLogEntry extends MissionFindingWaiver {
kind: "finding_waiver";
}
export interface MissionDecisionLog {
entries: MissionDecisionLogEntry[];
}
export interface MissionTemplateIssueContext {
id: string;
identifier?: string | null;
title: string;
description?: string | null;
status: MissionIssueStatus;
billingCode?: string | null;
}
export interface MissionDocumentTemplate {
key: MissionRequiredDocumentKey;
title: string;
body: string;
}
@@ -0,0 +1,221 @@
import { describe, expect, it } from "vitest";
import {
advanceMissionSchema,
buildMissionFindingWaiverEntry,
createDefaultMissionDocumentTemplates,
decomposeMissionSchema,
deriveIssueBackedMissionState,
getMissionMilestoneSummaryDocumentKey,
getMissionValidationReportDocumentKey,
isMissionDocumentKey,
missionDecisionLogMarkdownFixture,
missionFeaturesDocumentSchema,
missionFeaturesFixture,
missionFeaturesMarkdownFixture,
missionFindingSchema,
missionStateDerivationInputSchema,
missionTemplateIssueFixture,
missionValidationContractFixture,
missionValidationContractMarkdownFixture,
missionValidationContractSchema,
missionValidationReportFixture,
missionValidationReportMarkdownFixture,
missionValidationReportSchema,
parseMissionDecisionLogDocument,
parseMissionFeaturesDocument,
parseMissionFindingWaivers,
parseMissionValidationContractDocument,
parseMissionValidationReportDocument,
} from "../src/mission/index.js";
describe("mission document keys", () => {
it("accepts required and generated mission document keys", () => {
expect(isMissionDocumentKey("validation-contract")).toBe(true);
expect(isMissionDocumentKey("validation-report-round-2")).toBe(true);
expect(isMissionDocumentKey("milestone-summary-foundation")).toBe(true);
expect(isMissionDocumentKey("mission-final-report")).toBe(true);
expect(isMissionDocumentKey("unknown")).toBe(false);
});
it("builds generated document keys from canonical helpers", () => {
expect(getMissionValidationReportDocumentKey(3)).toBe("validation-report-round-3");
expect(getMissionMilestoneSummaryDocumentKey("Foundation_1")).toBe("milestone-summary-foundation_1");
expect(() => getMissionValidationReportDocumentKey(0)).toThrow("positive integers");
expect(() => getMissionMilestoneSummaryDocumentKey("not valid")).toThrow("document-key-safe");
});
});
describe("mission templates", () => {
it("creates the required mission document bundle inside the plugin package", () => {
const templates = createDefaultMissionDocumentTemplates(missionTemplateIssueFixture);
expect(templates).toHaveLength(8);
expect(templates.map((template) => template.key)).toEqual([
"plan",
"mission-brief",
"validation-contract",
"features",
"worker-guidelines",
"services",
"knowledge-base",
"decision-log",
]);
expect(templates.find((template) => template.key === "mission-brief")?.body).toContain("mission:pap-1685");
});
});
describe("mission validation contract schema", () => {
it("accepts testable assertions with evidence requirements", () => {
const parsed = missionValidationContractSchema.parse(missionValidationContractFixture);
expect(parsed.assertions[0]?.id).toBe("VAL-MISSION-001");
});
it("rejects duplicate assertion IDs", () => {
const result = missionValidationContractSchema.safeParse({
assertions: [
missionValidationContractFixture.assertions[0],
missionValidationContractFixture.assertions[0],
],
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues.some((issue) => issue.message.includes("Duplicate validation assertion id"))).toBe(true);
}
});
it("parses validation assertions from structured markdown", () => {
const parsed = parseMissionValidationContractDocument(missionValidationContractMarkdownFixture);
expect(parsed.assertions[0]?.tooling).toEqual(["api_call", "manual_review"]);
});
});
describe("mission features document schema", () => {
it("accepts milestones with assertion-claiming features", () => {
const parsed = missionFeaturesDocumentSchema.parse(missionFeaturesFixture);
expect(parsed.milestones[0]?.features[0]?.claimed_assertion_ids).toEqual(["VAL-MISSION-001"]);
});
it("rejects original features that claim no assertions", () => {
const result = missionFeaturesDocumentSchema.safeParse({
milestones: [
{
...missionFeaturesFixture.milestones[0],
features: [
{
...missionFeaturesFixture.milestones[0]!.features[0]!,
claimed_assertion_ids: [],
},
],
},
],
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues.some((issue) => issue.message.includes("must claim"))).toBe(true);
}
});
it("parses milestone feature lists from structured markdown", () => {
const parsed = parseMissionFeaturesDocument(missionFeaturesMarkdownFixture);
expect(parsed.milestones[0]?.id).toBe("MILESTONE-MISSION-001");
expect(parsed.milestones[0]?.features[0]?.id).toBe("FEAT-MISSION-001");
});
});
describe("mission finding and report schemas", () => {
it("accepts structured validation findings in reports", () => {
const parsed = missionValidationReportSchema.parse(missionValidationReportFixture);
expect(parsed.findings[0]?.severity).toBe("blocking");
});
it("rejects blocking findings without assertion references", () => {
const result = missionFindingSchema.safeParse({
...missionValidationReportFixture.findings[0],
assertion_id: null,
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues.some((issue) => issue.message.includes("must reference"))).toBe(true);
}
});
it("parses validation reports from structured markdown", () => {
const parsed = parseMissionValidationReportDocument(missionValidationReportMarkdownFixture);
expect(parsed.round).toBe(2);
expect(parsed.findings[0]?.evidence).toHaveLength(2);
});
});
describe("mission waivers and decision logs", () => {
it("parses finding waivers from the decision log", () => {
const parsed = parseMissionDecisionLogDocument(missionDecisionLogMarkdownFixture);
expect(parsed.entries[0]).toEqual({
kind: "finding_waiver",
findingId: "FINDING-MISSION-002",
rationale: "Acceptable for MVP.",
actorLabel: "Board",
recordedAt: "2026-04-20T12:00:00.000Z",
});
});
it("builds waiver entries that round-trip through the decision log parser", () => {
const entry = buildMissionFindingWaiverEntry({
findingId: "FINDING-MISSION-003",
rationale: "Known UX tradeoff.",
actorLabel: "CTO",
createdAt: "2026-04-20T13:00:00.000Z",
});
const waivers = parseMissionFindingWaivers(["# Decision Log", "", entry].join("\n"));
expect(waivers.get("FINDING-MISSION-003")).toEqual({
findingId: "FINDING-MISSION-003",
rationale: "Known UX tradeoff.",
actorLabel: "CTO",
recordedAt: "2026-04-20T13:00:00.000Z",
});
});
});
describe("issue-backed mission state and action schemas", () => {
it("derives draft, active, and terminal mission states from issue/document inputs", () => {
expect(
deriveIssueBackedMissionState({
missionIssueStatus: "in_progress",
presentDocumentKeys: ["mission-brief"],
}),
).toBe("draft");
expect(
deriveIssueBackedMissionState({
missionIssueStatus: "in_progress",
presentDocumentKeys: ["mission-brief", "validation-contract", "features"],
hasActiveValidationIssues: true,
}),
).toBe("validating");
expect(
deriveIssueBackedMissionState({
missionIssueStatus: "done",
presentDocumentKeys: ["mission-brief", "validation-contract", "features", "mission-final-report"],
}),
).toBe("completed");
});
it("validates derivation inputs and mission action schemas", () => {
expect(() =>
missionStateDerivationInputSchema.parse({
missionIssueStatus: "in_progress",
presentDocumentKeys: ["invalid-key"],
}),
).toThrow("Unknown mission document key");
expect(decomposeMissionSchema.parse({})).toEqual({ dryRun: false });
expect(advanceMissionSchema.parse({ budgetLimitCents: 100, maxValidationRounds: 2 })).toEqual({
budgetLimitCents: 100,
maxValidationRounds: 2,
});
});
});