mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 16:27:18 +02:00
Add plugin-local mission document contracts
This commit is contained in:
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user