Skip to content
This repository was archived by the owner on Oct 5, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ SVP doesn't call AI APIs or build compilers. SVP is an **enhancement layer** for
- **Toolchain**: `forge check` (validation), store (read/write), hash (change tracking)
- **Skills**: Generate structured context from the five-layer data model, fed into your existing AI tools

Similar to [OpenSpec](https://github.com/Fission-AI/OpenSpec) — we don't build AI, we feed AI better context. SVP's capabilities improve automatically as base models evolve.
We don't build AI, we feed AI better context. SVP's capabilities improve automatically as base models evolve.

### Works great with OpenSpec

[OpenSpec](https://github.com/Fission-AI/OpenSpec) focuses on **spec before code** — making sure requirements are clear before AI writes anything. SVP focuses on **verify after code** — making sure what AI wrote is consistent and correct across layers.

They're complementary: OpenSpec manages the input quality (what to build), SVP manages the output quality (was it built right). Use both for a complete spec → architecture → verification pipeline.

## Design Principles

Expand Down
8 changes: 7 additions & 1 deletion i18n/zh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ SVP 不自己调 AI API,不造编译器。SVP 是 AI 编码工具(Claude Cod
- **工具链**:`forge check`(校验)、store(读写)、hash(变更追踪)
- **Skills**:基于五层数据模型生成结构化 context,喂给用户已有的 AI 工具

类似 [OpenSpec](https://github.com/Fission-AI/OpenSpec) 的定位——不造 AI,给 AI 喂更好的上下文。SVP 的能力随 base model 进化自动提升。
不造 AI,给 AI 喂更好的上下文。SVP 的能力随 base model 进化自动提升。

### 推荐搭配 OpenSpec 使用

[OpenSpec](https://github.com/Fission-AI/OpenSpec) 专注**写代码之前**——确保需求规范清晰再让 AI 动手。SVP 专注**写代码之后**——确保 AI 写出来的东西在各层之间一致且正确。

两者互补:OpenSpec 管输入质量(要造什么),SVP 管输出质量(造对了没)。搭配使用可以形成完整的 需求 → 架构 → 验证 流水线。

## 设计原则

Expand Down
4 changes: 2 additions & 2 deletions packages/__tests__/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,11 @@ describe("E2E: SVP Pipeline", () => {
});
await writeL2(tmpDir, l2);

// Verify initially no issues
// Verify initially no errors (1 warning for missing node docs is expected)
let input = await loadCheckInput(tmpDir);
let report = check(input);
expect(report.summary.errors).toBe(0);
expect(report.summary.warnings).toBe(0);
expect(report.issues.filter((i) => i.code === "MISSING_NODE_DOCS")).toHaveLength(1);

// Modify L3 content (add a constraint) and rehash
const modifiedL3 = makeL3({
Expand Down
192 changes: 192 additions & 0 deletions packages/cli/commands/changeset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// forge changeset — cross-artifact version grouping
// Start, complete, list, view, and abandon changesets

import {
computeBaselineFromArtifacts,
computeDiff,
findActiveChangeset,
formatDiffSummary,
listChangesets,
readChangeset,
writeChangeset,
deleteChangeset,
} from "../../core/index.js";
import { loadCheckInput } from "../load.js";
import type { Changeset } from "../../core/index.js";
import type { Command } from "commander";

/** Convert a name to kebab-case id */
function toId(name: string): string {
return name
.toLowerCase()
.trim()
.replaceAll(/[^\da-z]+/g, "-")
.replaceAll(/^-+|-+$/g, "");
}

/** Build current rev map from CheckInput (same shape as baseline) */
function currentRevsFromInput(
input: Awaited<ReturnType<typeof loadCheckInput>>,
): Record<string, number> {
return computeBaselineFromArtifacts(input);
}

export function registerChangeset(program: Command): void {
const cmd = program.command("changeset").description("Cross-artifact version grouping");

// ── start ──
cmd
.command("start <name>")
.description("Start a new changeset (snapshot baseline)")
.requiredOption("--reason <reason>", "Why this change is being made")
.option("-r, --root <path>", "Project root directory", ".")
.action(async (name: string, options: { reason: string; root: string }) => {
const existing = await findActiveChangeset(options.root);
if (existing !== null) {
console.error(
`Error: Active changeset "${existing.id}" already exists. Complete or abandon it first.`,
);
process.exitCode = 1;
return;
}

const input = await loadCheckInput(options.root);
const baseline = computeBaselineFromArtifacts(input);
const id = toId(name);

const cs: Changeset = {
id,
name,
reason: options.reason,
status: "active",
baseline,
createdAt: new Date().toISOString(),
};

await writeChangeset(options.root, cs);
console.log(`Changeset "${id}" started.`);
console.log(`Baseline: ${String(Object.keys(baseline).length)} artifact(s) captured.`);
});

// ── complete ──
cmd
.command("complete")
.description("Complete the active changeset")
.option("-r, --root <path>", "Project root directory", ".")
.action(async (options: { root: string }) => {
const active = await findActiveChangeset(options.root);
if (active === null) {
console.error("Error: No active changeset to complete.");
process.exitCode = 1;
return;
}

const input = await loadCheckInput(options.root);
const current = currentRevsFromInput(input);
const diff = computeDiff(active.baseline, current);

const completed: Changeset = {
...active,
status: "completed",
completedAt: new Date().toISOString(),
};
await writeChangeset(options.root, completed);

console.log(`Changeset "${active.id}" completed.`);
console.log();
console.log(formatDiffSummary(diff));
});

// ── list ──
cmd
.command("list")
.description("List all changesets")
.option("-r, --root <path>", "Project root directory", ".")
.action(async (options: { root: string }) => {
const ids = await listChangesets(options.root);
if (ids.length === 0) {
console.log("No changesets found.");
return;
}

// Load all, show active first
const all: Changeset[] = [];
for (const id of ids) {
const cs = await readChangeset(options.root, id);
if (cs !== null) all.push(cs);
}

const active = all.filter((c) => c.status === "active");
const completed = all.filter((c) => c.status === "completed");

for (const cs of [...active, ...completed]) {
const marker = cs.status === "active" ? "* " : " ";
const date =
cs.status === "completed" && cs.completedAt !== undefined
? ` (completed ${cs.completedAt.slice(0, 10)})`
: "";
console.log(`${marker}${cs.id} — ${cs.reason}${date}`);
}
});

// ── view ──
cmd
.command("view [id]")
.description("View changeset diff (defaults to active)")
.option("-r, --root <path>", "Project root directory", ".")
.action(async (id: string | undefined, options: { root: string }) => {
const cs =
id === undefined
? await findActiveChangeset(options.root)
: await readChangeset(options.root, id);

if (cs === null) {
const msg =
id === undefined ? "Error: No active changeset." : `Error: Changeset "${id}" not found.`;
console.error(msg);
process.exitCode = 1;
return;
}

const input = await loadCheckInput(options.root);
const current = currentRevsFromInput(input);
const diff = computeDiff(cs.baseline, current);

console.log(`Changeset: ${cs.id}`);
console.log(`Reason: ${cs.reason}`);
console.log(`Status: ${cs.status}`);
console.log();
console.log(formatDiffSummary(diff));
});

// ── abandon ──
cmd
.command("abandon [id]")
.description("Delete an active changeset")
.option("-r, --root <path>", "Project root directory", ".")
.action(async (id: string | undefined, options: { root: string }) => {
const cs =
id === undefined
? await findActiveChangeset(options.root)
: await readChangeset(options.root, id);

if (cs === null) {
const msg =
id === undefined
? "Error: No active changeset to abandon."
: `Error: Changeset "${id}" not found.`;
console.error(msg);
process.exitCode = 1;
return;
}

if (cs.status !== "active") {
console.error(`Error: Changeset "${cs.id}" is already completed. Cannot abandon.`);
process.exitCode = 1;
return;
}

await deleteChangeset(options.root, cs.id);
console.log(`Changeset "${cs.id}" abandoned.`);
});
}
Loading
Loading