Merge OPR.0.4.0.35 rig scope stage and verified enforcement

This commit is contained in:
mvschwarz committed 2026-06-20 16:06:19 -07:00
commit 51554eeec0
5 files changed
+835 -6

No files matched your search

+323 -6
View File
@@ -14,11 +14,14 @@ import {
CLOSE_REASONS,
MISSION_TEMPLATE_KINDS,
SLICE_TEMPLATE_KINDS,
STAGE_VALUES,
ScopeCliError,
type CloseReason,
type MissionTemplateKind,
type SliceInfo,
type SliceTemplateKind,
type SliceState,
type Stage,
} from "../lib/scope/types.js";
import {
DEFAULT_PROJECT_PREFIX,
@@ -28,6 +31,7 @@ import {
sliceIdFromMission,
} from "../lib/scope/dot-id.js";
import {
ensureMissionId,
ensureMissionIdPersisted,
findMission,
findSlice,
@@ -57,6 +61,7 @@ import {
PROGRESS_STATUSES,
setProgressRow,
} from "../lib/scope/progress-edit.js";
import { deriveScopeTrust } from "../lib/scope/trust.js";
// ---------------------------------------------------------------------
// Shared helpers
@@ -122,6 +127,17 @@ function getOpts(cmd: Command): RootOpts {
return {};
}
/** FR-5: a one-line human render of the derived stage. Shows the declared
* stage, and (when a weak `verified` downgrades it) the effective stage +
* why. Derived at read time; nothing is written. */
function formatTrustLine(trust: ReturnType<typeof deriveScopeTrust>): string {
const declared = trust.declaredStage || "—";
if (trust.downgraded) {
return ` stage: ${declared} (effective: ${trust.effectiveStage} — ${trust.verified.status})\n`;
}
return ` stage: ${declared}\n`;
}
// ---------------------------------------------------------------------
// rig scope slice ls
// ---------------------------------------------------------------------
@@ -192,6 +208,8 @@ function buildSliceShowCommand(): Command {
const readme = slice.readmePath ? fs.readFileSync(slice.readmePath, "utf8") : null;
const children = fs.readdirSync(slice.absPath, { withFileTypes: true })
.map((e) => ({ name: e.name, kind: e.isDirectory() ? "dir" : "file" as const }));
// FR-5: derive read-time trust from (stage x verified) — NEVER stored.
const trust = deriveScopeTrust(slice.frontmatter);
const payload = {
ok: true,
slice: {
@@ -201,6 +219,7 @@ function buildSliceShowCommand(): Command {
status: slice.status,
path: slice.absPath,
frontmatter: slice.frontmatter,
trust,
readme,
children,
},
@@ -211,6 +230,7 @@ function buildSliceShowCommand(): Command {
out.write(`Slice: ${slice.missionName}/${slice.name}\n`);
out.write(` id: ${slice.id ?? "—"}\n`);
out.write(` status: ${slice.status ?? "—"}\n`);
out.write(formatTrustLine(trust));
out.write(` path: ${slice.absPath}\n`);
out.write(` children: ${children.length}\n`);
if (readme) {
@@ -541,6 +561,8 @@ function buildMissionShowCommand(): Command {
const slices = listSlices(mission, "all").map((s) => ({
name: s.name, id: s.id, status: s.status, nn: s.nn,
}));
// FR-5: derive read-time trust from (stage x verified) — NEVER stored.
const trust = deriveScopeTrust(mission.frontmatter);
const payload = {
ok: true,
mission: {
@@ -550,6 +572,7 @@ function buildMissionShowCommand(): Command {
activeSliceCount: mission.activeSliceCount,
closedSliceCount: mission.closedSliceCount,
frontmatter: mission.frontmatter,
trust,
readme,
slices,
},
@@ -559,6 +582,7 @@ function buildMissionShowCommand(): Command {
} else {
out.write(`Mission: ${mission.name}\n`);
out.write(` id: ${mission.id ?? "—"}\n`);
out.write(formatTrustLine(trust));
out.write(` active slices: ${mission.activeSliceCount}\n`);
out.write(` closed slices: ${mission.closedSliceCount}\n`);
out.write(` path: ${mission.absPath}\n`);
@@ -1056,9 +1080,276 @@ function backfillScopeProgress(scopeDir: string, level: "mission" | "slice"): Ba
return { scope: level, name, created: true, reason: "backfilled", path: progressPath };
}
// ---------------------------------------------------------------------
// OPR.0.4.1.6 — stage + verified verbs (deterministic §2 maturity edits)
// ---------------------------------------------------------------------
/** Validate a stage against the §2 enum, rejecting invented values. */
function validateStage(raw: string): Stage {
if (!STAGE_VALUES.includes(raw as Stage)) {
throw new ScopeCliError({
fact: `Invalid stage "${raw}".`,
consequence: "Stage not changed.",
action: `Use one of: ${STAGE_VALUES.join(" | ")}.`,
});
}
return raw as Stage;
}
/** Surgically set `stage` (+ `superseded-by` when superseded) on a scope
* README, enforcing the §2 superseded-needs-successor rule. */
function applyStage(readmePath: string, stage: Stage, successor: unknown): void {
const updates: Record<string, unknown> = { stage };
if (stage === "superseded") {
const id = typeof successor === "string" ? successor.trim() : "";
if (!id) {
throw new ScopeCliError({
fact: "stage 'superseded' requires a successor.",
consequence: "Stage not changed (a superseded scope must name what replaces it, per scope-and-versioning §2).",
action: "Re-run with --successor <id>, e.g. --successor OPR.0.4.1.7.",
});
}
updates["superseded-by"] = id;
}
updateFrontmatter(readmePath, updates);
}
/** `retired` is an exit, not a rung — warn (do not block). */
function warnRetired(stage: Stage): void {
if (stage === "retired") {
process.stderr.write("Warning: stage 'retired' means do-not-use (an exit, not a maturity rung).\n");
}
}
/** Validate a --against provenance: mandatory, non-empty, non-whitespace
* (the §2 "no bare timestamp" rule). Returns the trimmed source. */
function validateAgainst(raw: unknown): string {
const source = typeof raw === "string" ? raw.trim() : "";
if (!source) {
throw new ScopeCliError({
fact: "--against provenance is empty or missing.",
consequence: "verified not stamped — scope-and-versioning §2 forbids a bare timestamp without a named source.",
action: 'Provide what it was verified against, e.g. --against "runtime (npm+tag+origin)".',
});
}
return source;
}
function buildSliceStageCommand(): Command {
return new Command("stage")
.description(`Set a slice's epistemic stage (${STAGE_VALUES.join(" | ")}); superseded needs --successor`)
.argument("<slice-path>", "Slice path (absolute, relative, or NN-slug)")
.argument("<new-stage>", `New stage: ${STAGE_VALUES.join(" | ")}`)
.option("--successor <id>", "Successor scope id — REQUIRED when new-stage is superseded")
.option("--mission <name>", "Hint mission when slice-path is just NN-slug")
.option("--json", "Machine-readable output")
.action(async (slicePath: string, newStage: string, opts, command) => {
const out = makeStdout();
const json = Boolean(opts.json);
try {
const stage = validateStage(newStage);
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
const slice = findSlice(missionsRoot, slicePath, opts.mission ?? null);
if (!slice.readmePath) {
throw new ScopeCliError({
fact: `Slice ${slice.name} has no README.md.`,
consequence: "Stage is a frontmatter field on the README; nothing to write.",
action: "Create the slice README (rig scope slice create) before setting its stage.",
});
}
applyStage(slice.readmePath, stage, opts.successor);
warnRetired(stage);
const supersededBy = stage === "superseded" ? String(opts.successor).trim() : undefined;
emit(out, { ok: true, scope: { tier: "slice", mission: slice.missionName, name: slice.name, id: slice.id, stage, ...(supersededBy ? { supersededBy } : {}) } }, json, [
`Set ${slice.missionName}/${slice.name} stage: ${stage}`,
...(supersededBy ? [` superseded-by: ${supersededBy}`] : []),
]);
} catch (err) {
fail(err, json, out);
}
});
}
function buildMissionStageCommand(): Command {
return new Command("stage")
.description(`Set a mission's epistemic stage (${STAGE_VALUES.join(" | ")}); superseded needs --successor`)
.argument("<mission>", "Mission name")
.argument("<new-stage>", `New stage: ${STAGE_VALUES.join(" | ")}`)
.option("--successor <id>", "Successor scope id — REQUIRED when new-stage is superseded")
.option("--json", "Machine-readable output")
.action(async (missionName: string, newStage: string, opts, command) => {
const out = makeStdout();
const json = Boolean(opts.json);
try {
const stage = validateStage(newStage);
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
const mission = findMission(missionsRoot, missionName);
if (!mission.readmePath) {
throw new ScopeCliError({
fact: `Mission ${mission.name} has no README.md.`,
consequence: "Stage is a frontmatter field on the README; nothing to write.",
action: "Create the mission README before setting its stage.",
});
}
applyStage(mission.readmePath, stage, opts.successor);
warnRetired(stage);
const supersededBy = stage === "superseded" ? String(opts.successor).trim() : undefined;
emit(out, { ok: true, scope: { tier: "mission", name: mission.name, id: mission.id, stage, ...(supersededBy ? { supersededBy } : {}) } }, json, [
`Set ${mission.name} stage: ${stage}`,
...(supersededBy ? [` superseded-by: ${supersededBy}`] : []),
]);
} catch (err) {
fail(err, json, out);
}
});
}
function buildSliceVerifiedCommand(): Command {
return new Command("verified")
.description("Stamp a slice's verified line: <today> against <source> (provenance mandatory; overwrites the prior line)")
.argument("<slice-path>", "Slice path (absolute, relative, or NN-slug)")
.option("--against <source>", "What it was verified against — MANDATORY (no bare timestamps)")
.option("--mission <name>", "Hint mission when slice-path is just NN-slug")
.option("--json", "Machine-readable output")
.action(async (slicePath: string, opts, command) => {
const out = makeStdout();
const json = Boolean(opts.json);
try {
const source = validateAgainst(opts.against);
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
const slice = findSlice(missionsRoot, slicePath, opts.mission ?? null);
if (!slice.readmePath) {
throw new ScopeCliError({
fact: `Slice ${slice.name} has no README.md.`,
consequence: "verified is a frontmatter field on the README; nothing to write.",
action: "Create the slice README before stamping verified.",
});
}
const verified = `${todayDateISO()} against ${source}`;
updateFrontmatter(slice.readmePath, { verified });
emit(out, { ok: true, scope: { tier: "slice", mission: slice.missionName, name: slice.name, id: slice.id, verified } }, json, [
`Stamped ${slice.missionName}/${slice.name} verified: ${verified}`,
]);
} catch (err) {
fail(err, json, out);
}
});
}
function buildMissionVerifiedCommand(): Command {
return new Command("verified")
.description("Stamp a mission's verified line: <today> against <source> (provenance mandatory; overwrites the prior line)")
.argument("<mission>", "Mission name")
.option("--against <source>", "What it was verified against — MANDATORY (no bare timestamps)")
.option("--json", "Machine-readable output")
.action(async (missionName: string, opts, command) => {
const out = makeStdout();
const json = Boolean(opts.json);
try {
const source = validateAgainst(opts.against);
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
const mission = findMission(missionsRoot, missionName);
if (!mission.readmePath) {
throw new ScopeCliError({
fact: `Mission ${mission.name} has no README.md.`,
consequence: "verified is a frontmatter field on the README; nothing to write.",
action: "Create the mission README before stamping verified.",
});
}
const verified = `${todayDateISO()} against ${source}`;
updateFrontmatter(mission.readmePath, { verified });
emit(out, { ok: true, scope: { tier: "mission", name: mission.name, id: mission.id, verified } }, json, [
`Stamped ${mission.name} verified: ${verified}`,
]);
} catch (err) {
fail(err, json, out);
}
});
}
// OPR.0.4.1.6 FR-4 — frontmatter-conformance backfill (extends `repair`).
// `repair` historically backfilled a missing PROGRESS.md only; per the
// convention's "consolidate, do not invent" it now ALSO conforms the mandatory
// scope-and-versioning §1/§2 frontmatter (id / stage / verified) in the SAME
// idempotent verb, rather than adding a parallel `reconcile`.
interface FrontmatterConformResult {
/** Minted+written id, or null if already present / unmintable. */
idAdded: string | null;
/** Added stage, or null if already present. */
stageAdded: string | null;
/** Added verified placeholder, or null if already present. */
verifiedAdded: string | null;
changed: boolean;
}
/** Map a legacy `status:` to a §4 migration stage. Default `wip` when absent
* or unmapped (the safe floor). */
function mapLegacyStatusToStage(status: unknown): string {
const s = typeof status === "string" ? status.toLowerCase().trim() : "";
if (s === "placeholder") return "wip";
if (s === "draft" || s === "draft-for-comms") return "wip";
if (s === "active" || s === "in-flight") return "established";
if (s.startsWith("shipped") || s.startsWith("closed")) return "established";
if (s === "ready-for-mission" || s === "ready-for-orch-dispatch") return "provisional";
return "wip";
}
/** Idempotently conform a scope README's mandatory frontmatter. Only ADDS
* absent fields — never clobbers an existing id/stage/verified. `mintId` is
* called only when `id` is absent (it may persist a parent id per §1 lazy
* adoption). */
function conformReadmeFrontmatter(readmePath: string, mintId: () => string | null): FrontmatterConformResult {
const fm = readFrontmatter(readmePath);
const updates: Record<string, unknown> = {};
let idAdded: string | null = null;
const hasId = typeof fm.id === "string" && fm.id.length > 0;
if (!hasId) {
const minted = mintId();
if (minted) { idAdded = minted; updates.id = minted; }
}
let stageAdded: string | null = null;
if (!fm.stage) {
stageAdded = mapLegacyStatusToStage(fm.status);
updates.stage = stageAdded;
}
let verifiedAdded: string | null = null;
if (!fm.verified) {
verifiedAdded = `${todayDateISO()} against backfill (rig scope repair)`;
updates.verified = verifiedAdded;
}
const changed = Object.keys(updates).length > 0;
if (changed) updateFrontmatter(readmePath, updates);
return { idAdded, stageAdded, verifiedAdded, changed };
}
/** Mint a slice's id from its (persisted) parent mission id + NN — the §1
* lazy parent-ID adoption site. Null when the folder has no NN. */
function mintSliceIdClosure(slice: SliceInfo, missionsRoot: string): () => string | null {
return () => {
if (slice.nn == null) return null;
const mission = findMission(missionsRoot, slice.missionName);
const missionId = ensureMissionIdPersisted(mission, missionsRoot);
return sliceIdFromMission(missionId, slice.nn);
};
}
function conformLines(scope: string, r: FrontmatterConformResult): string[] {
if (!r.changed) return [` frontmatter: conformant (no change)`];
const parts: string[] = [];
if (r.idAdded) parts.push(`id=${r.idAdded}`);
if (r.stageAdded) parts.push(`stage=${r.stageAdded}`);
if (r.verifiedAdded) parts.push(`verified=${r.verifiedAdded}`);
return [` frontmatter conformed: ${parts.join(", ")}`];
}
function buildSliceRepairCommand(): Command {
return new Command("repair")
.description("Backfill a missing PROGRESS.md for a slice (idempotent; skips readme-only and existing rails)")
.description("Backfill a slice's missing PROGRESS.md + conform mandatory frontmatter (id/stage/verified); idempotent")
.argument("<slice-path>", "Slice path (absolute, relative, or NN-slug)")
.option("--mission <name>", "Hint mission when slice-path is just NN-slug")
.option("--json", "Machine-readable output")
@@ -1069,9 +1360,13 @@ function buildSliceRepairCommand(): Command {
const missionsRoot = resolveMissionsRoot({ override: getOpts(command).workspace });
const slice = findSlice(missionsRoot, slicePath, opts.mission ?? null);
const result = backfillScopeProgress(slice.absPath, "slice");
emit(out, { ok: true, result }, json, [
const frontmatter = slice.readmePath
? conformReadmeFrontmatter(slice.readmePath, mintSliceIdClosure(slice, missionsRoot))
: { idAdded: null, stageAdded: null, verifiedAdded: null, changed: false };
emit(out, { ok: true, result, frontmatter }, json, [
`${result.created ? "Backfilled" : "Skipped"} ${slice.name}: ${result.reason}`,
...(result.path ? [` path: ${result.path}`] : []),
...conformLines("slice", frontmatter),
]);
} catch (err) {
fail(err, json, out);
@@ -1081,7 +1376,7 @@ function buildSliceRepairCommand(): Command {
function buildMissionRepairCommand(): Command {
return new Command("repair")
.description("Backfill missing PROGRESS.md for a mission and its slices (idempotent; skips readme-only)")
.description("Backfill missing PROGRESS.md + conform mandatory frontmatter (id/stage/verified) for a mission and its slices; idempotent")
.argument("<mission>", "Mission name")
.option("--json", "Machine-readable output")
.action(async (missionName: string, opts, command) => {
@@ -1099,10 +1394,28 @@ function buildMissionRepairCommand(): Command {
results.push(backfillScopeProgress(path.join(slicesDir, entry.name), "slice"));
}
}
// FR-4: conform mandatory frontmatter — mission first (mints+persists
// the mission id), then each slice (child ids derive from the now-
// persisted parent id).
const conformed: Array<{ scope: "mission" | "slice"; name: string; frontmatter: FrontmatterConformResult }> = [];
if (mission.readmePath) {
const fm = conformReadmeFrontmatter(mission.readmePath, () => ensureMissionId(mission, missionsRoot));
conformed.push({ scope: "mission", name: mission.name, frontmatter: fm });
}
const freshMission = findMission(missionsRoot, mission.name);
for (const slice of listSlices(freshMission, "all")) {
if (!slice.readmePath) continue;
const fm = conformReadmeFrontmatter(slice.readmePath, mintSliceIdClosure(slice, missionsRoot));
conformed.push({ scope: "slice", name: slice.name, frontmatter: fm });
}
const created = results.filter((r) => r.created);
emit(out, { ok: true, mission: mission.name, created, results }, json, [
`Repaired ${mission.name}: ${created.length} PROGRESS.md backfilled`,
...created.map((r) => ` + ${r.scope}/${r.name}`),
const fmChanged = conformed.filter((c) => c.frontmatter.changed);
emit(out, { ok: true, mission: mission.name, created, results, conformed }, json, [
`Repaired ${mission.name}: ${created.length} PROGRESS.md backfilled, ${fmChanged.length} frontmatter conformed`,
...created.map((r) => ` + PROGRESS ${r.scope}/${r.name}`),
...fmChanged.map((c) => ` ~ frontmatter ${c.scope}/${c.name}`),
]);
} catch (err) {
fail(err, json, out);
@@ -1128,6 +1441,8 @@ export function scopeCommand(): Command {
slice.addCommand(buildSliceMoveCommand());
slice.addCommand(buildSliceProgressCommand());
slice.addCommand(buildSliceRepairCommand());
slice.addCommand(buildSliceStageCommand());
slice.addCommand(buildSliceVerifiedCommand());
cmd.addCommand(slice);
const mission = new Command("mission").description("Mission-tier commands");
@@ -1136,6 +1451,8 @@ export function scopeCommand(): Command {
mission.addCommand(buildMissionCreateCommand());
mission.addCommand(buildMissionProgressCommand());
mission.addCommand(buildMissionRepairCommand());
mission.addCommand(buildMissionStageCommand());
mission.addCommand(buildMissionVerifiedCommand());
cmd.addCommand(mission);
cmd.addCommand(buildAuditCommand());
+112
View File
@@ -0,0 +1,112 @@
// OPR.0.4.1.6 — read-time-trust derivation for `rig scope` (FR-5).
//
// The scope-and-versioning convention §2 keystone: an `established`/`canonical`
// artifact whose `verified` is stale, absent, bare, or a not-yet-verified
// placeholder is treated as `provisional`. Trust is DERIVED at read time from
// (stage x verified) and NEVER stored (a stored composite re-rots).
//
// This is the static-artifact sibling of the daemon's skill-audit. The 90-day
// freshness window + the bare/stale/verified taxonomy MIRROR
// `packages/daemon/src/domain/skill-audit.ts:34` (`FRESHNESS_WINDOW_DAYS = 90`)
// + its `checkStaleness` (:118-126). The daemon module is not importable from
// the CLI (separate package; the daemon is a spawned app, not a library), so we
// mirror the constant + taxonomy here rather than cross-import. The convention
// itself names no age window — 90 days is the load-bearing pin shared with
// skill-audit.
/** Mirror of skill-audit.ts:34 — keep in sync if that window changes. */
export const FRESHNESS_WINDOW_DAYS = 90;
export type VerifiedTrust =
| "verified" // <date> against <real source>, within the window
| "stale_verified" // <date> against <real source>, older than the window
| "bare_verified" // present but no parseable `<date> against <source>`
| "missing_verified" // absent / empty
| "unverified_provenance"; // a tool-scaffolded placeholder source, not a real verification
export interface VerifiedTrustResult {
status: VerifiedTrust;
date?: string;
source?: string;
}
const VERIFIED_RE = /^(\d{4}-\d{2}-\d{2})\s+against\s+(.+)$/i;
// Tool-written placeholder provenances. None is a real verification against a
// named source, so an established/canonical carrying one cannot coast as fresh
// (the exact "looks fresh while lying" trap §2 closes). `rig scope verified`
// replaces these with a real source.
const PLACEHOLDER_PROVENANCE_RE =
/\bscaffold \(rig scope create\)|\bbackfill \(rig scope repair\)|\(unverified\)/i;
function isValidISODate(dateStr: string): boolean {
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return false;
const d = new Date(dateStr);
if (isNaN(d.getTime())) return false;
// Reject overflowed components (e.g. 2026-13-99 → Date rolls over).
return d.toISOString().slice(0, 10) === dateStr;
}
/** Classify a `verified:` frontmatter value. Mirrors skill-audit checkStaleness. */
export function deriveVerifiedTrust(verified: unknown, now: Date = new Date()): VerifiedTrustResult {
if (typeof verified !== "string" || verified.trim().length === 0) {
return { status: "missing_verified" };
}
const m = VERIFIED_RE.exec(verified.trim());
if (!m) {
return { status: "bare_verified" };
}
const dateStr = m[1]!;
const source = m[2]!.trim();
if (!isValidISODate(dateStr)) {
return { status: "bare_verified", date: dateStr };
}
if (PLACEHOLDER_PROVENANCE_RE.test(source)) {
return { status: "unverified_provenance", date: dateStr, source };
}
const daysDiff = (now.getTime() - new Date(dateStr).getTime()) / (1000 * 60 * 60 * 24);
if (daysDiff > FRESHNESS_WINDOW_DAYS) {
return { status: "stale_verified", date: dateStr, source };
}
return { status: "verified", date: dateStr, source };
}
export interface EffectiveStageResult {
/** The stage as written in frontmatter (echoed, may be empty). */
declaredStage: string;
/** The stage to actually trust right now. */
effectiveStage: string;
/** True when the declared stage was downgraded by a weak `verified`. */
downgraded: boolean;
/** One-line why, or null when not downgraded. */
reason: string | null;
}
/** Derive the effective (trust-adjusted) stage. Only `established`/`canonical`
* can be downgraded — they are the stages that claim "rely on me", so a weak
* `verified` knocks them back to `provisional`. wip/provisional/exits are
* echoed unchanged. */
export function deriveEffectiveStage(stage: unknown, verifiedStatus: VerifiedTrust): EffectiveStageResult {
const declaredStage = typeof stage === "string" ? stage : "";
const claimsReliable = declaredStage === "established" || declaredStage === "canonical";
const trusted = verifiedStatus === "verified";
if (claimsReliable && !trusted) {
return {
declaredStage,
effectiveStage: "provisional",
downgraded: true,
reason: `${declaredStage} with ${verifiedStatus} verified -> treated as provisional (scope-and-versioning §2)`,
};
}
return { declaredStage, effectiveStage: declaredStage, downgraded: false, reason: null };
}
/** Convenience: derive full read-time trust from a parsed frontmatter object. */
export function deriveScopeTrust(
frontmatter: Record<string, unknown>,
now: Date = new Date(),
): { verified: VerifiedTrustResult } & EffectiveStageResult {
const verified = deriveVerifiedTrust(frontmatter.verified, now);
const effective = deriveEffectiveStage(frontmatter.stage, verified.status);
return { verified, ...effective };
}
+20
View File
@@ -95,3 +95,23 @@ export const CLOSE_REASONS: ReadonlyArray<CloseReason> = [
"superseded",
"stale",
];
/** OPR.0.4.1.6 — the scope-and-versioning §2 epistemic-maturity `stage` enum
* (4 rungs + 2 exits). The ONLY valid stage values; invented values
* (shape/shaped/draft) are rejected. `superseded` must name its successor;
* `retired` means do-not-use. */
export type Stage =
| "wip"
| "provisional"
| "established"
| "canonical"
| "superseded"
| "retired";
export const STAGE_VALUES: ReadonlyArray<Stage> = [
"wip",
"provisional",
"established",
"canonical",
"superseded",
"retired",
];
+292
View File
@@ -735,3 +735,295 @@ describe("rig scope audit edge cases (guard BLOCKING fixes)", () => {
expect(sliceEntry.findings.some((f: { kind: string }) => f.kind === "id_convention_violation" || f.kind === "slice_naming_convention")).toBe(true);
});
});
// ---------------------------------------------------------------------
// OPR.0.4.1.6 — FR-1 stage verb (deterministic maturity)
// ---------------------------------------------------------------------
describe("rig scope <tier> stage (OPR.0.4.1.6 FR-1)", () => {
let substrate: { root: string; missionsRoot: string };
beforeEach(() => { substrate = seedSubstrate(); });
afterEach(() => { fs.rmSync(substrate.root, { recursive: true, force: true }); });
const sliceReadme = () => path.join(substrate.missionsRoot, "release-0.3.2", "slices", "01-existing", "README.md");
it("AC-1: sets stage surgically, preserving id + other fields", async () => {
const r = await run(["slice", "stage", "01-existing", "provisional", "--mission", "release-0.3.2", "--json"], substrate.missionsRoot);
expect(r.exitCode).toBe(0);
const fm = readFrontmatter(sliceReadme());
expect(fm.stage).toBe("provisional");
expect(fm.id).toBe("OPR.0.3.2.1"); // untouched
expect(fm.status).toBe("active"); // untouched
});
it("AC-1: rejects an invalid stage value and names the valid enum", async () => {
const r = await run(["slice", "stage", "01-existing", "shape", "--mission", "release-0.3.2"], substrate.missionsRoot);
expect(r.exitCode).toBe(1);
expect(r.stderr).toMatch(/wip.*provisional.*established.*canonical.*superseded.*retired/s);
expect(readFrontmatter(sliceReadme()).stage).toBeUndefined(); // not written
});
it("AC-1: superseded WITHOUT --successor is rejected (names the rule)", async () => {
const r = await run(["slice", "stage", "01-existing", "superseded", "--mission", "release-0.3.2"], substrate.missionsRoot);
expect(r.exitCode).toBe(1);
expect(r.stderr).toMatch(/successor/i);
expect(readFrontmatter(sliceReadme()).stage).toBeUndefined();
});
it("AC-1: superseded WITH --successor sets stage + records the successor", async () => {
const r = await run(["slice", "stage", "01-existing", "superseded", "--successor", "OPR.0.3.2.7", "--mission", "release-0.3.2", "--json"], substrate.missionsRoot);
expect(r.exitCode).toBe(0);
const fm = readFrontmatter(sliceReadme());
expect(fm.stage).toBe("superseded");
expect(fm["superseded-by"]).toBe("OPR.0.3.2.7");
});
it("retired sets the stage and warns (exit 0)", async () => {
const r = await run(["slice", "stage", "01-existing", "retired", "--mission", "release-0.3.2", "--json"], substrate.missionsRoot);
expect(r.exitCode).toBe(0);
expect(readFrontmatter(sliceReadme()).stage).toBe("retired");
});
it("mission stage sets the mission README stage", async () => {
const r = await run(["mission", "stage", "release-0.3.2", "established", "--json"], substrate.missionsRoot);
expect(r.exitCode).toBe(0);
const fm = readFrontmatter(path.join(substrate.missionsRoot, "release-0.3.2", "README.md"));
expect(fm.stage).toBe("established");
expect(fm.id).toBe("OPR.0.3.2");
});
});
// ---------------------------------------------------------------------
// OPR.0.4.1.6 — FR-2 verified verb (provenance-stamped freshness)
// ---------------------------------------------------------------------
describe("rig scope <tier> verified (OPR.0.4.1.6 FR-2)", () => {
let substrate: { root: string; missionsRoot: string };
beforeEach(() => { substrate = seedSubstrate(); });
afterEach(() => { fs.rmSync(substrate.root, { recursive: true, force: true }); });
const sliceReadme = () => path.join(substrate.missionsRoot, "release-0.3.2", "slices", "01-existing", "README.md");
const today = () => new Date().toISOString().slice(0, 10);
it("AC-2: stamps verified: <today> against <source>, preserving id", async () => {
const r = await run(["slice", "verified", "01-existing", "--against", "runtime (npm+tag)", "--mission", "release-0.3.2", "--json"], substrate.missionsRoot);
expect(r.exitCode).toBe(0);
const fm = readFrontmatter(sliceReadme());
expect(fm.verified).toBe(`${today()} against runtime (npm+tag)`);
expect(fm.id).toBe("OPR.0.3.2.1");
});
it("AC-2: rejects empty/whitespace --against (no bare timestamps)", async () => {
const r = await run(["slice", "verified", "01-existing", "--against", " ", "--mission", "release-0.3.2"], substrate.missionsRoot);
expect(r.exitCode).toBe(1);
expect(r.stderr).toMatch(/against|provenance|source/i);
expect(readFrontmatter(sliceReadme()).verified).toBeUndefined();
});
it("AC-2: rejects a missing --against entirely", async () => {
const r = await run(["slice", "verified", "01-existing", "--mission", "release-0.3.2"], substrate.missionsRoot);
expect(r.exitCode).toBe(1);
});
it("AC-2: overwrites a prior verified line in place", async () => {
await run(["slice", "verified", "01-existing", "--against", "first source", "--mission", "release-0.3.2", "--json"], substrate.missionsRoot);
await run(["slice", "verified", "01-existing", "--against", "second source", "--mission", "release-0.3.2", "--json"], substrate.missionsRoot);
const fm = readFrontmatter(sliceReadme());
expect(fm.verified).toBe(`${today()} against second source`);
});
it("mission verified stamps the mission README", async () => {
const r = await run(["mission", "verified", "release-0.3.2", "--against", "founder review", "--json"], substrate.missionsRoot);
expect(r.exitCode).toBe(0);
expect(readFrontmatter(path.join(substrate.missionsRoot, "release-0.3.2", "README.md")).verified).toBe(`${today()} against founder review`);
});
});
// ---------------------------------------------------------------------
// OPR.0.4.1.6 — FR-4 repair frontmatter-conformance backfill
// ---------------------------------------------------------------------
describe("rig scope repair frontmatter conformance (OPR.0.4.1.6 FR-4)", () => {
let substrate: { root: string; missionsRoot: string };
beforeEach(() => { substrate = seedSubstrate(); });
afterEach(() => { fs.rmSync(substrate.root, { recursive: true, force: true }); });
const today = () => new Date().toISOString().slice(0, 10);
it("AC-4: backfills a missing id + stage + verified on a ghost slice (idempotent re-run)", async () => {
const ghost = path.join(substrate.missionsRoot, "release-0.3.2", "slices", "05-ghost");
writeFile(path.join(ghost, "README.md"), "---\nstatus: placeholder\n---\n# ghost\n");
const ghostReadme = path.join(ghost, "README.md");
const r1 = await run(["slice", "repair", "05-ghost", "--mission", "release-0.3.2", "--json"], substrate.missionsRoot);
expect(r1.exitCode).toBe(0);
const fm1 = readFrontmatter(ghostReadme);
expect(fm1.id).toBe("OPR.0.3.2.5"); // minted from parent + NN, registered
expect(fm1.stage).toBe("wip"); // placeholder -> wip (migration map)
expect(fm1.verified).toBe(`${today()} against backfill (rig scope repair)`);
// Idempotent: a second run changes nothing.
await run(["slice", "repair", "05-ghost", "--mission", "release-0.3.2", "--json"], substrate.missionsRoot);
const fm2 = readFrontmatter(ghostReadme);
expect(fm2.id).toBe("OPR.0.3.2.5");
expect(fm2.stage).toBe("wip");
expect(fm2.verified).toBe(`${today()} against backfill (rig scope repair)`);
});
it("AC-4: does NOT clobber an existing real verified line", async () => {
const slice = path.join(substrate.missionsRoot, "release-0.3.2", "slices", "06-has-verified");
writeFile(path.join(slice, "README.md"), "---\nid: OPR.0.3.2.6\nstage: established\nverified: 2026-01-02 against runtime (npm+tag)\n---\n# v\n");
await run(["slice", "repair", "06-has-verified", "--mission", "release-0.3.2", "--json"], substrate.missionsRoot);
const fm = readFrontmatter(path.join(slice, "README.md"));
expect(fm.verified).toBe("2026-01-02 against runtime (npm+tag)"); // untouched
expect(fm.stage).toBe("established"); // untouched
});
it("AC-4: mission repair conforms the mission README frontmatter too (lazy parent-id)", async () => {
// A mission whose README has no id/stage/verified, plus a ghost slice.
const m = path.join(substrate.missionsRoot, "backlog-new");
writeFile(path.join(m, "README.md"), "---\nmission: backlog-new\n---\n# new\n");
writeFile(path.join(m, "slices", "01-foo", "README.md"), "---\nstatus: active\n---\n# foo\n");
const r = await run(["mission", "repair", "backlog-new", "--json"], substrate.missionsRoot);
expect(r.exitCode).toBe(0);
const mfm = readFrontmatter(path.join(m, "README.md"));
expect(typeof mfm.id).toBe("string"); // escape-band id minted
expect(mfm.stage).toBe("wip");
expect(mfm.verified).toBe(`${today()} against backfill (rig scope repair)`);
const sfm = readFrontmatter(path.join(m, "slices", "01-foo", "README.md"));
expect(typeof sfm.id).toBe("string"); // child id = parentId.1
expect(sfm.id).toBe(`${mfm.id}.1`);
expect(sfm.stage).toBe("established"); // active -> established (migration map)
});
});
// ---------------------------------------------------------------------
// OPR.0.4.1.6 — FR-5 read-time-trust in show (DERIVE, never store)
// ---------------------------------------------------------------------
describe("rig scope show read-time trust (OPR.0.4.1.6 FR-5)", () => {
let substrate: { root: string; missionsRoot: string };
beforeEach(() => { substrate = seedSubstrate(); });
afterEach(() => { fs.rmSync(substrate.root, { recursive: true, force: true }); });
const dAgo = (n: number) => new Date(Date.now() - n * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
function seedSlice(name: string, fm: string): string {
const dir = path.join(substrate.missionsRoot, "release-0.3.2", "slices", name);
writeFile(path.join(dir, "README.md"), fm);
return path.join(dir, "README.md");
}
it("AC-5: established + STALE verified shows effectively provisional (derived)", async () => {
seedSlice("07-stale", `---\nid: OPR.0.3.2.7\nstage: established\nverified: ${dAgo(200)} against runtime\n---\n# stale\n`);
const r = await run(["slice", "show", "07-stale", "--mission", "release-0.3.2", "--json"], substrate.missionsRoot);
const p = JSON.parse(r.stdout);
expect(p.slice.trust.declaredStage).toBe("established");
expect(p.slice.trust.effectiveStage).toBe("provisional");
expect(p.slice.trust.downgraded).toBe(true);
expect(p.slice.trust.verified.status).toBe("stale_verified");
});
it("AC-5: established + FRESH verified stays established", async () => {
seedSlice("08-fresh", `---\nid: OPR.0.3.2.8\nstage: established\nverified: ${dAgo(5)} against runtime\n---\n# fresh\n`);
const r = await run(["slice", "show", "08-fresh", "--mission", "release-0.3.2", "--json"], substrate.missionsRoot);
const p = JSON.parse(r.stdout);
expect(p.slice.trust.effectiveStage).toBe("established");
expect(p.slice.trust.downgraded).toBe(false);
expect(p.slice.trust.verified.status).toBe("verified");
});
it("AC-5: established + SCAFFOLD provenance shows effectively provisional regardless of date", async () => {
seedSlice("09-scaffold", `---\nid: OPR.0.3.2.9\nstage: established\nverified: ${dAgo(1)} against scaffold (rig scope create)\n---\n# s\n`);
const r = await run(["slice", "show", "09-scaffold", "--mission", "release-0.3.2", "--json"], substrate.missionsRoot);
const p = JSON.parse(r.stdout);
expect(p.slice.trust.effectiveStage).toBe("provisional");
expect(p.slice.trust.verified.status).toBe("unverified_provenance");
});
it("AC-5: show DERIVES trust and does NOT write it back to disk (no stored composite)", async () => {
const readmePath = seedSlice("10-nowrite", `---\nid: OPR.0.3.2.10\nstage: canonical\nverified: ${dAgo(200)} against runtime\n---\n# n\n`);
const before = fs.readFileSync(readmePath, "utf8");
await run(["slice", "show", "10-nowrite", "--mission", "release-0.3.2", "--json"], substrate.missionsRoot);
const after = fs.readFileSync(readmePath, "utf8");
expect(after).toBe(before); // byte-identical — nothing written back
const fm = readFrontmatter(readmePath);
expect(fm.trust).toBeUndefined();
expect(fm.effectiveStage).toBeUndefined();
});
it("mission show also derives read-time trust", async () => {
writeFile(
path.join(substrate.missionsRoot, "release-0.3.2", "README.md"),
`---\nid: OPR.0.3.2\nstage: canonical\nverified: ${dAgo(300)} against design\n---\n# release-0.3.2\n`,
);
const r = await run(["mission", "show", "release-0.3.2", "--json"], substrate.missionsRoot);
const p = JSON.parse(r.stdout);
expect(p.mission.trust.effectiveStage).toBe("provisional");
expect(p.mission.trust.downgraded).toBe(true);
});
});
// ---------------------------------------------------------------------
// OPR.0.4.1.6 — FR-3 create regression-lock (already ships; guard the templates)
// ---------------------------------------------------------------------
describe("rig scope create regression-lock (OPR.0.4.1.6 FR-3)", () => {
let substrate: { root: string; missionsRoot: string };
beforeEach(() => { substrate = seedSubstrate(); });
afterEach(() => { fs.rmSync(substrate.root, { recursive: true, force: true }); });
const WELL_FORMED_VERIFIED = /^\d{4}-\d{2}-\d{2} against .+$/;
it("AC-3: fresh slice create writes id + stage + a well-formed verified line", async () => {
const r = await run(["slice", "create", "release-0.3.2", "new-feature", "--json"], substrate.missionsRoot);
expect(r.exitCode).toBe(0);
const fm = readFrontmatter(path.join(substrate.missionsRoot, "release-0.3.2", "slices", "02-new-feature", "README.md"));
expect(fm.id).toBe("OPR.0.3.2.2");
expect(fm.stage).toBe("wip");
expect(String(fm.verified)).toMatch(WELL_FORMED_VERIFIED);
});
it("AC-3: fresh mission create writes id + stage + a well-formed verified line", async () => {
const r = await run(["mission", "create", "release-0.5.0", "--json"], substrate.missionsRoot);
expect(r.exitCode).toBe(0);
const fm = readFrontmatter(path.join(substrate.missionsRoot, "release-0.5.0", "README.md"));
expect(typeof fm.id).toBe("string");
expect((fm.id as string).length).toBeGreaterThan(0);
expect(fm.stage).toBe("wip");
expect(String(fm.verified)).toMatch(WELL_FORMED_VERIFIED);
});
});
// ---------------------------------------------------------------------
// OPR.0.4.1.6 — FR-6 help teaches the new verbs + enforcement rules
// ---------------------------------------------------------------------
describe("OPR.0.4.1.6 FR-6 — help", () => {
const renderHelp = (cmd: Command) => cmd.helpInformation().replace(/\s+/g, " ");
it("stage + verified verbs are registered on both tiers with descriptions", () => {
const cmd = scopeCommand();
for (const tierName of ["slice", "mission"]) {
const tier = cmd.commands.find((c) => c.name() === tierName)!;
const verbs = tier.commands.map((c) => c.name());
expect(verbs).toContain("stage");
expect(verbs).toContain("verified");
for (const verb of tier.commands) expect(verb.description()).toBeTruthy();
}
});
it("stage help teaches the superseded-needs-successor rule + the valid enum", () => {
const stage = scopeCommand().commands.find((c) => c.name() === "slice")!.commands.find((c) => c.name() === "stage")!;
const help = renderHelp(stage);
expect(help).toMatch(/superseded/i);
expect(help).toMatch(/successor/i);
expect(help).toMatch(/wip.*provisional.*established.*canonical/i);
});
it("verified help teaches provenance-mandatory", () => {
const verified = scopeCommand().commands.find((c) => c.name() === "slice")!.commands.find((c) => c.name() === "verified")!;
const help = renderHelp(verified);
expect(help).toMatch(/against|provenance|mandatory|source/i);
});
});
+88
View File
@@ -0,0 +1,88 @@
// OPR.0.4.1.6 — read-time-trust derivation for rig scope (FR-5).
// Mirrors the scope-and-versioning §2 keystone: trust is DERIVED at read time
// from (stage x verified), never stored. 90-day freshness window mirrors
// packages/daemon/src/domain/skill-audit.ts:34.
import { describe, expect, it } from "vitest";
import {
FRESHNESS_WINDOW_DAYS,
deriveVerifiedTrust,
deriveEffectiveStage,
} from "../src/lib/scope/trust.js";
const NOW = new Date("2026-06-20T00:00:00Z");
function daysAgo(n: number): string {
const d = new Date(NOW.getTime() - n * 24 * 60 * 60 * 1000);
return d.toISOString().slice(0, 10);
}
describe("deriveVerifiedTrust (mirrors skill-audit checkStaleness, 90-day window)", () => {
it("pins the freshness window at 90 days", () => {
expect(FRESHNESS_WINDOW_DAYS).toBe(90);
});
it("verified within the window with a real source => verified", () => {
const r = deriveVerifiedTrust(`${daysAgo(19)} against runtime (npm+tag)`, NOW);
expect(r.status).toBe("verified");
expect(r.source).toBe("runtime (npm+tag)");
});
it("BOUNDARY: 89 days => verified (fresh)", () => {
expect(deriveVerifiedTrust(`${daysAgo(89)} against founder review`, NOW).status).toBe("verified");
});
it("BOUNDARY: 91 days => stale_verified", () => {
expect(deriveVerifiedTrust(`${daysAgo(91)} against founder review`, NOW).status).toBe("stale_verified");
});
it("BOUNDARY: unparseable/bare date => bare_verified", () => {
expect(deriveVerifiedTrust("sometime against the docs", NOW).status).toBe("bare_verified");
expect(deriveVerifiedTrust("2026-13-99 against the docs", NOW).status).toBe("bare_verified");
});
it("missing/empty verified => missing_verified", () => {
expect(deriveVerifiedTrust(undefined, NOW).status).toBe("missing_verified");
expect(deriveVerifiedTrust(" ", NOW).status).toBe("missing_verified");
});
it("a scaffold/backfill/unverified provenance is NOT a real verification (regardless of date)", () => {
expect(deriveVerifiedTrust(`${daysAgo(1)} against scaffold (rig scope create)`, NOW).status).toBe("unverified_provenance");
expect(deriveVerifiedTrust(`${daysAgo(1)} against backfill (rig scope repair)`, NOW).status).toBe("unverified_provenance");
expect(deriveVerifiedTrust(`${daysAgo(1)} against (unverified)`, NOW).status).toBe("unverified_provenance");
});
});
describe("deriveEffectiveStage (the §2 self-correcting downgrade)", () => {
it("established + fresh verified stays established", () => {
const r = deriveEffectiveStage("established", "verified");
expect(r.effectiveStage).toBe("established");
expect(r.downgraded).toBe(false);
});
it("canonical + fresh verified stays canonical", () => {
expect(deriveEffectiveStage("canonical", "verified").effectiveStage).toBe("canonical");
});
it("established + stale verified => effectively provisional", () => {
const r = deriveEffectiveStage("established", "stale_verified");
expect(r.effectiveStage).toBe("provisional");
expect(r.downgraded).toBe(true);
expect(r.reason).toMatch(/stale/);
});
it("established + missing verified => effectively provisional", () => {
expect(deriveEffectiveStage("established", "missing_verified").effectiveStage).toBe("provisional");
});
it("canonical + bare/unverified provenance => effectively provisional", () => {
expect(deriveEffectiveStage("canonical", "bare_verified").effectiveStage).toBe("provisional");
expect(deriveEffectiveStage("canonical", "unverified_provenance").effectiveStage).toBe("provisional");
});
it("wip/provisional are never downgraded (only established/canonical can coast)", () => {
expect(deriveEffectiveStage("wip", "missing_verified").downgraded).toBe(false);
expect(deriveEffectiveStage("provisional", "stale_verified").downgraded).toBe(false);
expect(deriveEffectiveStage("provisional", "stale_verified").effectiveStage).toBe("provisional");
});
});