🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@double-codeing/flow2spec

Package Overview
Dependencies
Maintainers
2
Versions
31
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@double-codeing/flow2spec - npm Package Compare versions

Comparing version
3.2.8-beta.0
to
3.2.8-beta.1
+1397
lib/knowledgeEngine.js
const fs = require("fs");
const path = require("path");
const { KNOWLEDGE_ROOT } = require("./agents");
const { loadFlow2specConfig } = require("./flow2specConfig");
const { resolveDeveloperContext, activeTaskDir } = require("./developerId");
const KNOWLEDGE_FILENAME = "manifest-routing.json";
const MATCHERS_FILENAME = "manifest-matchers.json";
const INDEX_FILENAME = "index.md";
const TOPIC_DIR = "topics";
const DELTA_FILENAME = "kb-delta.json";
const KB_COMMANDS = new Set([
"appendBody",
"replaceBody",
"updateFrontmatter",
"createTopic",
]);
const ALLOWED_TOPIC_PRIMARY = new Set(["policy", "config", "feature", "module"]);
const ALLOWED_TOPIC_CONFIDENCE = new Set(["manual", "inferred"]);
const TOPIC_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
const MATCHER_ID_RE = /^m-[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function writeJson(filePath, data) {
fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
}
function stableStringify(value) {
if (Array.isArray(value)) {
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
}
if (isPlainObject(value)) {
return `{${Object.keys(value)
.sort()
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
.join(",")}}`;
}
return JSON.stringify(value);
}
function resolveFromCwd(cwd, maybeRelativePath) {
return path.isAbsolute(maybeRelativePath)
? maybeRelativePath
: path.join(cwd, maybeRelativePath);
}
function isPlainObject(value) {
return (
value &&
typeof value === "object" &&
!Array.isArray(value) &&
Object.getPrototypeOf(value) === Object.prototype
);
}
function normalizeStringArray(values) {
const out = [];
const seen = new Set();
for (const value of Array.isArray(values) ? values : []) {
if (typeof value !== "string") continue;
const item = value.trim();
if (!item || seen.has(item)) continue;
seen.add(item);
out.push(item);
}
return out;
}
function parseInlineArray(raw) {
const source = String(raw || "").trim();
if (!source) return [];
const out = [];
let token = "";
let quote = null;
let escaped = false;
const pushToken = () => {
const value = token.trim();
if (value) out.push(parseFrontmatterScalar(value));
token = "";
};
for (const ch of source) {
if (escaped) {
token += ch;
escaped = false;
continue;
}
if (ch === "\\") {
token += ch;
escaped = true;
continue;
}
if (quote) {
token += ch;
if (ch === quote) quote = null;
continue;
}
if (ch === '"' || ch === "'") {
token += ch;
quote = ch;
continue;
}
if (ch === ",") {
pushToken();
continue;
}
token += ch;
}
pushToken();
return out;
}
function parseFrontmatterScalar(raw) {
const value = String(raw || "").trim();
if (value === "null" || value === "~") return null;
if (value === "true") return true;
if (value === "false") return false;
if (/^-?\d+$/.test(value)) return Number.parseInt(value, 10);
if (/^-?\d+\.\d+$/.test(value)) return Number.parseFloat(value);
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
return value.slice(1, -1).replace(/\\"/g, '"').replace(/\\'/g, "'");
}
if (value.startsWith("[") && value.endsWith("]")) {
return parseInlineArray(value.slice(1, -1));
}
return value;
}
function stringifyFrontmatterScalar(value) {
if (value === null) return "null";
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
const str = String(value);
if (!str) return '""';
if (/^[A-Za-z0-9_./:@-]+$/.test(str)) return str;
return JSON.stringify(str);
}
function stringifyFrontmatterValue(value) {
if (Array.isArray(value)) {
return `[${value.map((item) => stringifyFrontmatterScalar(item)).join(", ")}]`;
}
return stringifyFrontmatterScalar(value);
}
function parseFrontmatterBlock(block) {
const out = {};
const lines = String(block || "").split(/\r?\n/);
for (const line of lines) {
if (!line.trim()) continue;
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
if (!match) continue;
const key = match[1];
const value = match[2].trim();
if (!value) {
out[key] = "";
continue;
}
out[key] = parseFrontmatterScalar(value);
}
return out;
}
function parseTopicDocument(raw) {
const source = String(raw || "");
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
if (!match) {
return {
hasFrontmatter: false,
frontmatter: {},
body: source,
};
}
return {
hasFrontmatter: true,
frontmatter: parseFrontmatterBlock(match[1]),
body: source.slice(match[0].length),
};
}
function stringifyTopicDocument(frontmatter, body) {
const fm = stringifyFrontmatter(frontmatter);
const normalizedBody = String(body || "").replace(/^\n+/, "");
if (!fm) {
return normalizedBody.endsWith("\n") ? normalizedBody : `${normalizedBody}\n`;
}
const bodyText = normalizedBody.endsWith("\n")
? normalizedBody
: `${normalizedBody}\n`;
return `---\n${fm}---\n${bodyText}`;
}
function stringifyFrontmatter(frontmatter) {
const source = isPlainObject(frontmatter) ? frontmatter : {};
const preferredOrder = [
"id",
"revision",
"summary",
"dependsOn",
"primary",
"confidence",
];
const keys = [];
for (const key of preferredOrder) {
if (Object.prototype.hasOwnProperty.call(source, key)) keys.push(key);
}
for (const key of Object.keys(source).sort()) {
if (!keys.includes(key)) keys.push(key);
}
const lines = [];
for (const key of keys) {
const value = source[key];
if (value === undefined) continue;
lines.push(`${key}: ${stringifyFrontmatterValue(value)}`);
}
return lines.length ? `${lines.join("\n")}\n` : "";
}
function topicPathFor(topicId) {
return path.posix.join(KNOWLEDGE_ROOT, TOPIC_DIR, `${topicId}.md`);
}
function topicAbsPath(cwd, topicPath) {
return resolveFromCwd(cwd, topicPath);
}
function matcherPathFor(matcherId) {
return path.posix.join(KNOWLEDGE_ROOT, "matchers", `${matcherId}.json`);
}
function graphCwd(graph) {
if (graph.cwd) return graph.cwd;
if (graph.routingPath) {
return path.dirname(path.dirname(graph.routingPath));
}
return process.cwd();
}
function loadKnowledgeGraph(cwd) {
const routingPath = path.join(cwd, KNOWLEDGE_ROOT, KNOWLEDGE_FILENAME);
const matchersPath = path.join(cwd, KNOWLEDGE_ROOT, MATCHERS_FILENAME);
if (!fs.existsSync(routingPath)) {
throw new Error(
`缺少知识库路由清单:${path.join(KNOWLEDGE_ROOT, KNOWLEDGE_FILENAME)}`,
);
}
const routing = readJson(routingPath);
const matchers = fs.existsSync(matchersPath) ? readJson(matchersPath) : null;
const topicEntries = [];
const topicPaths = routing.topicPaths || {};
for (const [topicId, topicPath] of Object.entries(topicPaths)) {
const absPath = topicAbsPath(cwd, topicPath);
if (!fs.existsSync(absPath)) {
topicEntries.push({
topicId,
path: topicPath,
absPath,
exists: false,
});
continue;
}
const raw = fs.readFileSync(absPath, "utf8");
const parsed = parseTopicDocument(raw);
const meta = routing.topicMetadata?.[topicId] || {};
const frontmatter = isPlainObject(parsed.frontmatter)
? { ...parsed.frontmatter }
: {};
if (!Object.prototype.hasOwnProperty.call(frontmatter, "id")) {
frontmatter.id = topicId;
}
if (Object.prototype.hasOwnProperty.call(frontmatter, "dependsOn")) {
frontmatter.dependsOn = normalizeStringArray(frontmatter.dependsOn);
}
if (Object.prototype.hasOwnProperty.call(frontmatter, "primary")) {
frontmatter.primary = String(frontmatter.primary).trim();
} else if (meta.primary) {
frontmatter.primary = meta.primary;
}
if (Object.prototype.hasOwnProperty.call(frontmatter, "confidence")) {
frontmatter.confidence = String(frontmatter.confidence).trim();
} else if (meta.confidence) {
frontmatter.confidence = meta.confidence;
}
if (Object.prototype.hasOwnProperty.call(frontmatter, "tags")) {
frontmatter.tags = normalizeStringArray(frontmatter.tags);
} else if (Array.isArray(meta.tags)) {
frontmatter.tags = normalizeStringArray(meta.tags);
}
if (
Object.prototype.hasOwnProperty.call(frontmatter, "summary") &&
typeof frontmatter.summary !== "string"
) {
frontmatter.summary = String(frontmatter.summary);
}
topicEntries.push({
topicId,
path: topicPath,
absPath,
exists: true,
raw,
body: parsed.body,
hasFrontmatter: parsed.hasFrontmatter,
frontmatter,
routingMeta: meta,
});
}
return {
routingPath,
matchersPath,
routing,
matchers,
topics: topicEntries,
};
}
function deriveRoutingOverlayFromGraph(graph) {
const topicMetadata = {};
const topicDependencies = {};
for (const topic of graph.topics) {
if (!topic.exists) continue;
const fm = topic.frontmatter || {};
const entry = {};
if (Object.prototype.hasOwnProperty.call(fm, "primary")) {
const primary = String(fm.primary || "").trim();
if (ALLOWED_TOPIC_PRIMARY.has(primary)) {
entry.primary = primary;
}
}
if (Object.prototype.hasOwnProperty.call(fm, "confidence")) {
const confidence = String(fm.confidence || "").trim();
if (ALLOWED_TOPIC_CONFIDENCE.has(confidence)) {
entry.confidence = confidence;
}
}
if (Array.isArray(fm.tags)) {
const tags = normalizeStringArray(fm.tags).filter(
(tag) => ALLOWED_TOPIC_PRIMARY.has(tag) && tag !== entry.primary,
);
if (tags.length > 0) {
entry.tags = tags;
}
}
if (Object.keys(entry).length > 0) {
topicMetadata[topic.topicId] = entry;
}
if (Array.isArray(fm.dependsOn) && fm.dependsOn.length > 0) {
topicDependencies[topic.topicId] = normalizeStringArray(fm.dependsOn);
}
}
return { topicMetadata, topicDependencies };
}
function normalizeRoutingWithGraph(graph) {
const overlay = deriveRoutingOverlayFromGraph(graph);
const next = JSON.parse(JSON.stringify(graph.routing || {}));
let changed = false;
if (!isPlainObject(next.topicMetadata)) {
next.topicMetadata = {};
changed = true;
}
if (!isPlainObject(next.topicDependencies)) {
next.topicDependencies = {};
changed = true;
}
for (const [topicId, entry] of Object.entries(overlay.topicMetadata)) {
const raw = JSON.stringify(next.topicMetadata[topicId] || {});
const nextRaw = JSON.stringify(entry);
if (raw !== nextRaw) {
next.topicMetadata[topicId] = entry;
changed = true;
}
}
for (const topicId of Object.keys(next.topicMetadata)) {
if (!Object.prototype.hasOwnProperty.call(overlay.topicMetadata, topicId)) {
if (graph.routing.topicMetadata?.[topicId]) continue;
delete next.topicMetadata[topicId];
changed = true;
}
}
for (const [topicId, deps] of Object.entries(overlay.topicDependencies)) {
const raw = JSON.stringify(next.topicDependencies[topicId] || []);
const nextRaw = JSON.stringify(deps);
if (raw !== nextRaw) {
next.topicDependencies[topicId] = deps;
changed = true;
}
}
for (const topicId of Object.keys(next.topicDependencies)) {
if (!Object.prototype.hasOwnProperty.call(overlay.topicDependencies, topicId)) {
if (graph.routing.topicDependencies?.[topicId]) continue;
delete next.topicDependencies[topicId];
changed = true;
}
}
return { routing: next, changed };
}
function validateKnowledgeGraph(graph, options = {}) {
const issues = [];
const warnings = [];
const strictRevision = Boolean(options.strictRevision);
const topicIds = new Set();
if (!graph || typeof graph !== "object") {
return {
ok: false,
issues: ["knowledge graph is empty"],
warnings,
topicCount: 0,
};
}
const routing = graph.routing || {};
const topics = Array.isArray(graph.topics) ? graph.topics : [];
for (const topic of topics) {
topicIds.add(topic.topicId);
if (!topic.exists) {
issues.push(`topic missing: ${topic.topicId} -> ${topic.path}`);
continue;
}
const fm = topic.frontmatter || {};
if (Object.prototype.hasOwnProperty.call(fm, "id") && fm.id !== topic.topicId) {
issues.push(
`topic frontmatter id mismatch: ${topic.topicId} vs ${String(fm.id)}`,
);
}
if (Object.prototype.hasOwnProperty.call(fm, "revision")) {
const revision = Number(fm.revision);
if (!Number.isInteger(revision) || revision < 0) {
issues.push(`topic revision must be a non-negative integer: ${topic.topicId}`);
}
} else if (strictRevision) {
issues.push(`topic revision missing: ${topic.topicId}`);
} else {
warnings.push(`topic revision missing: ${topic.topicId}`);
}
if (Object.prototype.hasOwnProperty.call(fm, "primary")) {
const primary = String(fm.primary || "").trim();
if (!ALLOWED_TOPIC_PRIMARY.has(primary)) {
issues.push(`topic primary invalid: ${topic.topicId} -> ${primary}`);
}
}
if (Object.prototype.hasOwnProperty.call(fm, "confidence")) {
const confidence = String(fm.confidence || "").trim();
if (!ALLOWED_TOPIC_CONFIDENCE.has(confidence)) {
issues.push(`topic confidence invalid: ${topic.topicId} -> ${confidence}`);
}
}
if (Array.isArray(fm.dependsOn)) {
for (const depId of fm.dependsOn) {
if (!topic.topicId || typeof depId !== "string" || !depId.trim()) {
issues.push(`topic dependsOn contains empty value: ${topic.topicId}`);
continue;
}
if (!routing.topicPaths?.[depId]) {
issues.push(`topic dependsOn references missing topic: ${topic.topicId} -> ${depId}`);
}
}
}
}
if (!routing.topicPaths || typeof routing.topicPaths !== "object") {
issues.push("routing.topicPaths missing or invalid");
}
if (routing.fallbackTopic && !routing.topicPaths?.[routing.fallbackTopic]) {
issues.push(`fallbackTopic missing from topicPaths: ${routing.fallbackTopic}`);
}
if (routing.topicDependencies && typeof routing.topicDependencies === "object") {
for (const [topicId, deps] of Object.entries(routing.topicDependencies)) {
if (!routing.topicPaths?.[topicId]) {
issues.push(`topicDependencies references unknown topic: ${topicId}`);
}
if (!Array.isArray(deps)) {
issues.push(`topicDependencies.${topicId} must be an array`);
continue;
}
for (const depId of deps) {
if (!routing.topicPaths?.[depId]) {
issues.push(
`topicDependencies.${topicId} references unknown dependency: ${depId}`,
);
}
}
}
}
if (routing.topicMetadata && typeof routing.topicMetadata === "object") {
for (const [topicId, meta] of Object.entries(routing.topicMetadata)) {
if (!routing.topicPaths?.[topicId]) {
issues.push(`topicMetadata references unknown topic: ${topicId}`);
}
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
issues.push(`topicMetadata.${topicId} must be an object`);
continue;
}
if (
Object.prototype.hasOwnProperty.call(meta, "primary") &&
!ALLOWED_TOPIC_PRIMARY.has(String(meta.primary || "").trim())
) {
issues.push(`topicMetadata.${topicId}.primary invalid`);
}
if (
Object.prototype.hasOwnProperty.call(meta, "confidence") &&
!ALLOWED_TOPIC_CONFIDENCE.has(String(meta.confidence || "").trim())
) {
issues.push(`topicMetadata.${topicId}.confidence invalid`);
}
if (Array.isArray(meta.tags)) {
const seen = new Set();
for (const tag of meta.tags) {
const normalized = String(tag || "").trim();
if (!ALLOWED_TOPIC_PRIMARY.has(normalized)) {
issues.push(`topicMetadata.${topicId}.tags invalid value: ${normalized}`);
continue;
}
if (seen.has(normalized)) {
issues.push(`topicMetadata.${topicId}.tags contains duplicate: ${normalized}`);
}
seen.add(normalized);
}
}
}
}
const matcherMap =
graph.matchers && graph.matchers.matchers && typeof graph.matchers.matchers === "object"
? graph.matchers.matchers
: null;
if (graph.matchers && !matcherMap) {
issues.push("manifest-matchers structure invalid");
}
if (Array.isArray(routing.taskToTopicRules)) {
for (const rule of routing.taskToTopicRules) {
if (!rule || typeof rule !== "object") {
issues.push("taskToTopicRules contains a non-object rule");
continue;
}
if (!rule.task || typeof rule.task !== "string") {
issues.push("taskToTopicRules entry missing task");
}
if (!Array.isArray(rule.topics) || rule.topics.length === 0) {
issues.push(`taskToTopicRules(${rule.task || "unknown"}) must contain topics`);
} else {
for (const topicId of rule.topics) {
if (!routing.topicPaths?.[topicId]) {
issues.push(
`taskToTopicRules(${rule.task || "unknown"}) references unknown topic: ${topicId}`,
);
}
}
}
if (!rule.matcherId || typeof rule.matcherId !== "string") {
issues.push(`taskToTopicRules(${rule.task || "unknown"}) missing matcherId`);
}
if (!rule.matcherPath || typeof rule.matcherPath !== "string") {
issues.push(`taskToTopicRules(${rule.task || "unknown"}) missing matcherPath`);
} else {
const matcherAbs = resolveFromCwd(graph.cwd || process.cwd(), rule.matcherPath);
if (!fs.existsSync(matcherAbs)) {
issues.push(
`taskToTopicRules(${rule.task || "unknown"}) matcherPath missing: ${rule.matcherPath}`,
);
} else {
try {
const matcherShard = readJson(matcherAbs);
if (matcherShard.id !== rule.matcherId) {
issues.push(
`matcher id mismatch: ${rule.matcherPath} -> ${matcherShard.id} vs ${rule.matcherId}`,
);
}
if (!Array.isArray(matcherShard.includeAny)) {
issues.push(`matcher includeAny invalid: ${rule.matcherPath}`);
}
} catch (error) {
issues.push(`matcher JSON invalid: ${rule.matcherPath}`);
}
}
}
}
}
return {
ok: issues.length === 0,
issues,
warnings,
topicCount: topicIds.size,
};
}
function loadKnowledgeState(cwd) {
const graph = loadKnowledgeGraph(cwd);
graph.cwd = cwd;
const validation = validateKnowledgeGraph(graph);
return { graph, validation };
}
function parseKnowledgeDelta(input) {
const delta = typeof input === "string" ? readJson(input) : input;
if (!isPlainObject(delta)) {
throw new Error("kb delta 必须是对象");
}
if (!delta.taskId || typeof delta.taskId !== "string") {
throw new Error("kb delta 缺少 taskId");
}
if (!delta.developerId || typeof delta.developerId !== "string") {
throw new Error("kb delta 缺少 developerId");
}
const baseRevisions = isPlainObject(delta.baseRevisions)
? delta.baseRevisions
: {};
const normalizedBaseRevisions = {};
for (const [topicId, revision] of Object.entries(baseRevisions)) {
const nextRevision = Number(revision);
if (!topicId || !Number.isInteger(nextRevision) || nextRevision < 0) {
throw new Error(`kb delta baseRevisions 非法: ${topicId}`);
}
normalizedBaseRevisions[topicId] = nextRevision;
}
if (!Array.isArray(delta.changes) || delta.changes.length === 0) {
throw new Error("kb delta 需要至少一个 change");
}
const changes = delta.changes.map((change, index) =>
normalizeKnowledgeDeltaChange(change, index),
);
return {
taskId: delta.taskId.trim(),
developerId: delta.developerId.trim(),
baseRevisions: normalizedBaseRevisions,
changes,
notes: typeof delta.notes === "string" ? delta.notes : "",
};
}
function normalizeKnowledgeDeltaChange(change, index) {
if (!isPlainObject(change)) {
throw new Error(`kb delta change[${index}] 必须是对象`);
}
const type = String(change.type || "").trim();
if (!KB_COMMANDS.has(type)) {
throw new Error(`kb delta change[${index}] type 非法: ${type}`);
}
const targetTopic = String(change.targetTopic || "").trim();
if (!targetTopic) {
throw new Error(`kb delta change[${index}] 缺少 targetTopic`);
}
const normalized = {
type,
targetTopic,
};
if (Object.prototype.hasOwnProperty.call(change, "summary")) {
normalized.summary = String(change.summary || "").trim();
}
if (Object.prototype.hasOwnProperty.call(change, "content")) {
normalized.content = String(change.content || "");
}
if (Object.prototype.hasOwnProperty.call(change, "frontmatter")) {
if (!isPlainObject(change.frontmatter)) {
throw new Error(`kb delta change[${index}].frontmatter 必须是对象`);
}
normalized.frontmatter = JSON.parse(JSON.stringify(change.frontmatter));
}
if (Object.prototype.hasOwnProperty.call(change, "taskRule")) {
if (!isPlainObject(change.taskRule)) {
throw new Error(`kb delta change[${index}].taskRule 必须是对象`);
}
normalized.taskRule = normalizeDeltaTaskRule(
change.taskRule,
normalized.targetTopic,
index,
);
}
if (Object.prototype.hasOwnProperty.call(change, "matcher")) {
if (!isPlainObject(change.matcher)) {
throw new Error(`kb delta change[${index}].matcher 必须是对象`);
}
const matcherId =
normalized.taskRule?.matcherId ||
String(change.matcher.id || `m-${normalized.targetTopic}`).trim();
normalized.matcher = normalizeDeltaMatcher(
change.matcher,
matcherId,
index,
);
if (normalized.taskRule && !normalized.taskRule.matcherId) {
normalized.taskRule.matcherId = normalized.matcher.id;
normalized.taskRule.matcherPath = matcherPathFor(normalized.matcher.id);
}
}
if (normalized.taskRule && !normalized.matcher) {
throw new Error(
`kb delta change[${index}] 带 taskRule 时必须同时提供 matcher`,
);
}
if (
normalized.taskRule &&
normalized.matcher &&
normalized.taskRule.matcherId !== normalized.matcher.id
) {
throw new Error(
`kb delta change[${index}] matcher id 不一致: ${normalized.matcher.id} vs ${normalized.taskRule.matcherId}`,
);
}
if (normalized.type === "createTopic") {
if (!TOPIC_ID_RE.test(normalized.targetTopic)) {
throw new Error(
`kb delta change[${index}] targetTopic 非法: ${normalized.targetTopic}`,
);
}
if (!String(normalized.content || "").trim()) {
throw new Error(`createTopic change for ${normalized.targetTopic} 缺少 content`);
}
normalized.frontmatter = normalizeTopicFrontmatter(
normalized.targetTopic,
normalized.frontmatter || {},
{ defaultPrimary: "feature", defaultConfidence: "inferred" },
);
}
return normalized;
}
function normalizeDeltaTaskRule(rule, targetTopic, index) {
const task = String(rule.task || "").trim();
if (!task) {
throw new Error(`kb delta change[${index}].taskRule 缺少 task`);
}
const matcherIdRaw = String(rule.matcherId || `m-${task}`).trim();
if (!MATCHER_ID_RE.test(matcherIdRaw)) {
throw new Error(
`kb delta change[${index}].taskRule.matcherId 非法: ${matcherIdRaw}`,
);
}
const topics = normalizeStringArray(rule.topics || [targetTopic]);
if (!topics.includes(targetTopic)) topics.push(targetTopic);
return {
task,
matcherId: matcherIdRaw,
matcherPath:
typeof rule.matcherPath === "string" && rule.matcherPath.trim()
? rule.matcherPath.trim().replace(/\\/g, "/")
: matcherPathFor(matcherIdRaw),
topics,
};
}
function normalizeDeltaMatcher(matcher, matcherId, index) {
const id = String(matcher.id || matcherId || "").trim();
if (!MATCHER_ID_RE.test(id)) {
throw new Error(`kb delta change[${index}].matcher.id 非法: ${id}`);
}
const out = {
id,
version:
typeof matcher.version === "string" && matcher.version.trim()
? matcher.version.trim()
: "1.0.0",
schema:
typeof matcher.schema === "string" && matcher.schema.trim()
? matcher.schema.trim()
: "flow2spec.matcher.v1",
includeAny: normalizeStringArray(matcher.includeAny),
};
for (const key of ["includeAll", "excludeAny", "excludeAll"]) {
const values = normalizeStringArray(matcher[key]);
if (values.length > 0) out[key] = values;
}
if (out.includeAny.length === 0 && !out.includeAll?.length) {
throw new Error(`kb delta change[${index}].matcher 缺少 includeAny/includeAll`);
}
return out;
}
function normalizeTopicFrontmatter(topicId, frontmatter, options = {}) {
const source = isPlainObject(frontmatter) ? frontmatter : {};
const out = JSON.parse(JSON.stringify(source));
out.id = topicId;
const revision = Number(out.revision || 0);
out.revision = Number.isInteger(revision) && revision >= 0 ? revision : 0;
if (!out.primary) out.primary = options.defaultPrimary || "feature";
if (!out.confidence) out.confidence = options.defaultConfidence || "inferred";
if (Object.prototype.hasOwnProperty.call(out, "dependsOn")) {
out.dependsOn = normalizeStringArray(out.dependsOn);
}
if (Object.prototype.hasOwnProperty.call(out, "tags")) {
out.tags = normalizeStringArray(out.tags);
}
return out;
}
function inferSummaryFromBody(body) {
const heading = String(body || "")
.split(/\r?\n/)
.find((line) => line.trim().startsWith("#"));
return heading ? heading.replace(/^#+\s*/, "").trim() : "";
}
function frontmatterForRouting(topic, routing) {
const meta = routing.topicMetadata?.[topic.topicId] || {};
const deps = routing.topicDependencies?.[topic.topicId] || [];
const current = isPlainObject(topic.frontmatter) ? topic.frontmatter : {};
const out = JSON.parse(JSON.stringify(current));
let changed = false;
const setIfMissingOrInvalid = (key, value, isValid = (item) => item !== undefined) => {
if (!isValid(value)) return;
if (!Object.prototype.hasOwnProperty.call(out, key) || out[key] === "") {
out[key] = value;
changed = true;
}
};
if (out.id !== topic.topicId) {
out.id = topic.topicId;
changed = true;
}
const revision = Number(out.revision);
if (!Number.isInteger(revision) || revision < 0) {
out.revision = 0;
changed = true;
}
setIfMissingOrInvalid("summary", inferSummaryFromBody(topic.body), (item) => Boolean(item));
if (Array.isArray(deps) && deps.length > 0) {
const normalizedDeps = normalizeStringArray(deps);
if (JSON.stringify(out.dependsOn || []) !== JSON.stringify(normalizedDeps)) {
out.dependsOn = normalizedDeps;
changed = true;
}
}
if (meta.primary && ALLOWED_TOPIC_PRIMARY.has(meta.primary)) {
setIfMissingOrInvalid("primary", meta.primary);
}
if (meta.confidence && ALLOWED_TOPIC_CONFIDENCE.has(meta.confidence)) {
setIfMissingOrInvalid("confidence", meta.confidence);
}
if (Array.isArray(meta.tags) && meta.tags.length > 0) {
const tags = normalizeStringArray(meta.tags).filter((tag) =>
ALLOWED_TOPIC_PRIMARY.has(tag),
);
if (tags.length > 0 && JSON.stringify(out.tags || []) !== JSON.stringify(tags)) {
out.tags = tags;
changed = true;
}
}
return { frontmatter: out, changed };
}
function ensureTopicFrontmatterFromRouting(graph, options = {}) {
const dryRun = Boolean(options.dryRun);
const changedFiles = [];
for (const topic of graph.topics) {
if (!topic.exists) continue;
const next = frontmatterForRouting(topic, graph.routing);
if (!next.changed && topic.hasFrontmatter) continue;
const content = stringifyTopicDocument(next.frontmatter, topic.body);
topic.frontmatter = next.frontmatter;
topic.hasFrontmatter = true;
topic.raw = content;
if (!dryRun) {
fs.writeFileSync(topic.absPath, content, "utf8");
}
changedFiles.push(topic.path);
}
return { changedFiles };
}
function planKnowledgeDelta(graph, delta) {
const parsedDelta = typeof delta === "string" ? parseKnowledgeDelta(delta) : parseKnowledgeDelta(delta);
const working = new Map();
const originalRevisions = new Map();
const pendingTaskRules = new Set();
const pendingMatcherIds = new Set();
const pendingMatcherPaths = new Set();
for (const topic of graph.topics) {
if (!topic.exists) continue;
working.set(topic.topicId, JSON.parse(JSON.stringify(topic)));
originalRevisions.set(topic.topicId, Number(topic.frontmatter?.revision || 0));
}
const plan = [];
const conflicts = [];
for (const change of parsedDelta.changes) {
if (change.type === "createTopic") {
const createPlan = planCreateTopicChange(graph, working, change, {
pendingTaskRules,
pendingMatcherIds,
pendingMatcherPaths,
});
if (createPlan.conflict) {
conflicts.push(createPlan.conflict);
continue;
}
working.set(change.targetTopic, createPlan.topic);
originalRevisions.set(change.targetTopic, 0);
if (change.taskRule) {
pendingTaskRules.add(change.taskRule.task);
}
if (change.matcher) {
pendingMatcherIds.add(change.matcher.id);
pendingMatcherPaths.add(change.taskRule?.matcherPath || matcherPathFor(change.matcher.id));
}
plan.push(createPlan.plan);
continue;
}
const current = working.get(change.targetTopic);
if (!current) {
conflicts.push({
topicId: change.targetTopic,
reason: "topic missing",
change,
});
continue;
}
const currentRevision = Number(current.frontmatter?.revision || 0);
const originalRevision = originalRevisions.get(change.targetTopic) || 0;
const expected = parsedDelta.baseRevisions[change.targetTopic];
if (
Number.isInteger(expected) &&
expected >= 0 &&
expected !== originalRevision
) {
conflicts.push({
topicId: change.targetTopic,
reason: `revision mismatch ${expected} -> ${originalRevision}`,
change,
});
continue;
}
const next = applyTopicChangeDraft(current, change);
working.set(change.targetTopic, next);
plan.push({
topicId: change.targetTopic,
type: change.type,
beforeRevision: currentRevision,
afterRevision: next.frontmatter.revision,
summary: change.summary || "",
});
}
return {
delta: parsedDelta,
plan,
conflicts,
mergeable: conflicts.length === 0,
};
}
function planCreateTopicChange(graph, working, change, pending = {}) {
const topicId = change.targetTopic;
const cwd = graphCwd(graph);
const topic = createTopicDraft(cwd, change);
const topicPath = topic.path;
const absPath = topic.absPath;
if (working.has(topicId) || graph.routing.topicPaths?.[topicId] || fs.existsSync(absPath)) {
return {
conflict: {
topicId,
reason: "topic already exists",
change,
},
};
}
const deps = normalizeStringArray(change.frontmatter?.dependsOn);
for (const depId of deps) {
if (!working.has(depId) && !graph.routing.topicPaths?.[depId]) {
return {
conflict: {
topicId,
reason: `dependency missing: ${depId}`,
change,
},
};
}
}
if (change.taskRule) {
const rules = Array.isArray(graph.routing.taskToTopicRules)
? graph.routing.taskToTopicRules
: [];
const duplicateRule = rules.find(
(rule) =>
rule.task === change.taskRule.task ||
rule.matcherId === change.taskRule.matcherId ||
rule.matcherPath === change.taskRule.matcherPath,
);
if (duplicateRule) {
return {
conflict: {
topicId,
reason: `task rule already exists: ${duplicateRule.task}`,
change,
},
};
}
if (pending.pendingTaskRules?.has(change.taskRule.task)) {
return {
conflict: {
topicId,
reason: `task rule duplicated in delta: ${change.taskRule.task}`,
change,
},
};
}
}
if (change.matcher) {
const matcherId = change.matcher.id;
const matcherPath = change.taskRule?.matcherPath || matcherPathFor(matcherId);
const matcherAbs = resolveFromCwd(cwd, matcherPath);
const matcherMap = graph.matchers?.matchers || {};
if (matcherMap[matcherId] || fs.existsSync(matcherAbs)) {
return {
conflict: {
topicId,
reason: `matcher already exists: ${matcherId}`,
change,
},
};
}
if (
pending.pendingMatcherIds?.has(matcherId) ||
pending.pendingMatcherPaths?.has(matcherPath)
) {
return {
conflict: {
topicId,
reason: `matcher duplicated in delta: ${matcherId}`,
change,
},
};
}
if (change.taskRule && matcherId !== change.taskRule.matcherId) {
return {
conflict: {
topicId,
reason: `matcher id mismatch: ${matcherId} vs ${change.taskRule.matcherId}`,
change,
},
};
}
}
return {
topic,
plan: {
topicId,
type: change.type,
beforeRevision: null,
afterRevision: topic.frontmatter.revision,
summary: change.summary || "",
creates: {
topicPath,
matcherPath: change.matcher
? change.taskRule?.matcherPath || matcherPathFor(change.matcher.id)
: null,
taskRule: change.taskRule?.task || null,
},
},
};
}
function createTopicDraft(cwd, change) {
const topicId = change.targetTopic;
const topicPath = topicPathFor(topicId);
const absPath = topicAbsPath(cwd, topicPath);
const body = String(change.content || "");
const bodyText = body.endsWith("\n") ? body : `${body}\n`;
const frontmatter = normalizeTopicFrontmatter(topicId, change.frontmatter || {});
return {
topicId,
path: topicPath,
absPath,
exists: true,
raw: stringifyTopicDocument(frontmatter, bodyText),
body: bodyText,
hasFrontmatter: true,
frontmatter,
routingMeta: {},
};
}
function applyTopicChangeDraft(topic, change) {
const next = JSON.parse(JSON.stringify(topic));
const frontmatter = isPlainObject(next.frontmatter) ? next.frontmatter : {};
const currentRevision = Number(frontmatter.revision || 0);
let body = String(next.body || "");
frontmatter.id = topic.topicId;
if (change.type === "appendBody") {
const fragment = String(change.content || "").trim();
if (!fragment) {
throw new Error(`appendBody change for ${topic.topicId} 缺少 content`);
}
body = body.trimEnd();
body = body ? `${body}\n\n${fragment}\n` : `${fragment}\n`;
} else if (change.type === "replaceBody") {
body = String(change.content || "");
if (!body.trim()) {
throw new Error(`replaceBody change for ${topic.topicId} 缺少 content`);
}
if (!body.endsWith("\n")) body += "\n";
} else if (change.type === "updateFrontmatter") {
const incoming = isPlainObject(change.frontmatter) ? change.frontmatter : {};
for (const [key, value] of Object.entries(incoming)) {
if (value === undefined) continue;
if (key === "dependsOn") {
frontmatter.dependsOn = normalizeStringArray(value);
} else if (key === "revision") {
const revision = Number(value);
if (!Number.isInteger(revision) || revision < 0) {
throw new Error(`topic ${topic.topicId} revision 非法`);
}
frontmatter.revision = revision;
} else if (key === "tags") {
frontmatter.tags = normalizeStringArray(value);
} else {
frontmatter[key] = value;
}
}
}
frontmatter.revision = currentRevision + 1;
next.frontmatter = frontmatter;
next.body = body;
next.hasFrontmatter = true;
return next;
}
function applyKnowledgeDelta(cwd, deltaInput, options = {}) {
const graph = loadKnowledgeGraph(cwd);
graph.cwd = cwd;
const parsedDelta = parseKnowledgeDelta(deltaInput);
const dryRun = Boolean(options.dryRun);
const planResult = planKnowledgeDelta(graph, parsedDelta);
if (!planResult.mergeable) {
const error = new Error("kb delta 存在冲突,无法自动合并");
error.planResult = planResult;
throw error;
}
const changedFiles = [];
const changedTopicIds = [];
const createdTopicIds = [];
const matcherWrites = [];
const taskRuleWrites = [];
const drafts = new Map();
for (const topic of graph.topics) {
if (!topic.exists) continue;
drafts.set(topic.topicId, JSON.parse(JSON.stringify(topic)));
}
for (const change of parsedDelta.changes) {
if (change.type === "createTopic") {
const nextTopic = createTopicDraft(cwd, change);
drafts.set(change.targetTopic, nextTopic);
if (!changedTopicIds.includes(change.targetTopic)) {
changedTopicIds.push(change.targetTopic);
}
if (!createdTopicIds.includes(change.targetTopic)) {
createdTopicIds.push(change.targetTopic);
}
if (change.matcher) {
matcherWrites.push({
matcher: change.matcher,
matcherPath: change.taskRule?.matcherPath || matcherPathFor(change.matcher.id),
});
}
if (change.taskRule) {
taskRuleWrites.push(change.taskRule);
}
continue;
}
if (!drafts.has(change.targetTopic)) {
throw new Error(`未知 topic: ${change.targetTopic}`);
}
const topic = drafts.get(change.targetTopic);
const nextTopic = applyTopicChangeDraft(topic, change);
drafts.set(change.targetTopic, nextTopic);
if (!changedTopicIds.includes(change.targetTopic)) {
changedTopicIds.push(change.targetTopic);
}
}
for (const topicId of changedTopicIds) {
const topicIndex = graph.topics.findIndex((item) => item.topicId === topicId);
const nextTopic = drafts.get(topicId);
const nextContent = stringifyTopicDocument(nextTopic.frontmatter, nextTopic.body);
if (!dryRun) {
ensureDir(path.dirname(nextTopic.absPath));
fs.writeFileSync(nextTopic.absPath, nextContent, "utf8");
}
if (!changedFiles.includes(nextTopic.path)) {
changedFiles.push(nextTopic.path);
}
if (topicIndex >= 0) {
const topic = graph.topics[topicIndex];
graph.topics[topicIndex] = {
...topic,
...nextTopic,
raw: nextContent,
};
} else {
graph.topics.push({
...nextTopic,
raw: nextContent,
});
}
}
if (!isPlainObject(graph.routing.topicPaths)) {
graph.routing.topicPaths = {};
}
for (const topicId of createdTopicIds) {
const topic = drafts.get(topicId);
graph.routing.topicPaths[topicId] = topic.path;
}
if (!Array.isArray(graph.routing.taskToTopicRules)) {
graph.routing.taskToTopicRules = [];
}
for (const rule of taskRuleWrites) {
graph.routing.taskToTopicRules.push(rule);
}
for (const item of matcherWrites) {
const matcherAbs = resolveFromCwd(cwd, item.matcherPath);
if (!dryRun) {
ensureDir(path.dirname(matcherAbs));
writeJson(matcherAbs, item.matcher);
}
if (!changedFiles.includes(item.matcherPath)) {
changedFiles.push(item.matcherPath);
}
}
if (graph.matchersPath && fs.existsSync(graph.matchersPath) && matcherWrites.length > 0) {
const manifestMatchers = graph.matchers || {
version: "1.0.0",
generatedFrom: ".Knowledge/manifest-routing.json",
matcherKey: "matcherId",
sourceOfTruth: ".Knowledge/manifest-routing.json",
matchers: {},
};
if (!isPlainObject(manifestMatchers.matchers)) {
manifestMatchers.matchers = {};
}
for (const item of matcherWrites) {
const { id, ...matcherBody } = item.matcher;
manifestMatchers.matchers[id] = matcherBody;
}
graph.matchers = manifestMatchers;
if (!dryRun) {
writeJson(graph.matchersPath, manifestMatchers);
}
const manifestMatchersPath = path.posix.join(KNOWLEDGE_ROOT, MATCHERS_FILENAME);
if (!changedFiles.includes(manifestMatchersPath)) {
changedFiles.push(manifestMatchersPath);
}
}
const normalizedRouting = normalizeRoutingWithGraph(graph);
if (normalizedRouting.changed && !dryRun) {
writeJson(graph.routingPath, normalizedRouting.routing);
changedFiles.push(path.posix.join(KNOWLEDGE_ROOT, KNOWLEDGE_FILENAME));
}
return {
dryRun,
changedFiles,
plan: planResult.plan,
conflicts: planResult.conflicts,
delta: parsedDelta,
};
}
function scanTaskKnowledgeDeltas(cwd, taskRoot) {
const resolvedRoot = taskRoot || resolveDeveloperContext(loadFlow2specConfig(cwd), { cwd }).taskRoot;
const activeRoot = path.join(cwd, resolvedRoot, "active");
if (!fs.existsSync(activeRoot)) {
return [];
}
const tasks = [];
for (const name of fs.readdirSync(activeRoot)) {
const taskDir = path.join(activeRoot, name);
if (!fs.statSync(taskDir).isDirectory()) continue;
const deltaPath = path.join(taskDir, DELTA_FILENAME);
if (!fs.existsSync(deltaPath)) continue;
try {
const delta = parseKnowledgeDelta(deltaPath);
tasks.push({
taskName: name,
taskDir,
deltaPath,
delta,
});
} catch (error) {
tasks.push({
taskName: name,
taskDir,
deltaPath,
error: error.message || String(error),
});
}
}
return tasks;
}
function summarizeKnowledgeState(cwd, options = {}) {
const { graph, validation } = loadKnowledgeState(cwd);
const taskRoot = options.taskRoot ||
resolveDeveloperContext(loadFlow2specConfig(cwd), { cwd }).taskRoot;
const deltaFiles = scanTaskKnowledgeDeltas(cwd, taskRoot);
const normalizedRouting = normalizeRoutingWithGraph(graph);
const drift =
stableStringify(normalizedRouting.routing) !== stableStringify(graph.routing);
const tasks = deltaFiles.map((item) => {
if (item.error) {
return {
taskName: item.taskName,
deltaPath: item.deltaPath,
error: item.error,
};
}
const plan = planKnowledgeDelta(graph, item.delta);
return {
taskName: item.taskName,
deltaPath: item.deltaPath,
mergeable: plan.mergeable,
plan: plan.plan,
conflicts: plan.conflicts,
};
});
return {
cwd,
taskRoot,
topicCount: graph.topics.length,
validation,
routingDrift: drift,
tasks,
};
}
function buildKnowledgeGraph(cwd, options = {}) {
const graph = loadKnowledgeGraph(cwd);
graph.cwd = cwd;
const topicFrontmatter = options.writeTopicFrontmatter
? ensureTopicFrontmatterFromRouting(graph, {
dryRun: options.dryRun,
})
: { changedFiles: [] };
const normalizedRouting = normalizeRoutingWithGraph(graph);
const changed =
stableStringify(normalizedRouting.routing) !== stableStringify(graph.routing);
if (changed && !options.dryRun) {
writeJson(graph.routingPath, normalizedRouting.routing);
}
return {
changed: changed || topicFrontmatter.changedFiles.length > 0,
routingPath: graph.routingPath,
topicFrontmatterChanged: topicFrontmatter.changedFiles,
normalizedRouting: normalizedRouting.routing,
validation: validateKnowledgeGraph({
...graph,
routing: normalizedRouting.routing,
}),
};
}
module.exports = {
KNOWLEDGE_ROOT,
KNOWLEDGE_FILENAME,
MATCHERS_FILENAME,
INDEX_FILENAME,
TOPIC_DIR,
DELTA_FILENAME,
loadKnowledgeGraph,
loadKnowledgeState,
validateKnowledgeGraph,
parseTopicDocument,
stringifyTopicDocument,
parseKnowledgeDelta,
planKnowledgeDelta,
applyKnowledgeDelta,
scanTaskKnowledgeDeltas,
summarizeKnowledgeState,
buildKnowledgeGraph,
ensureTopicFrontmatterFromRouting,
normalizeRoutingWithGraph,
stableStringify,
topicPathFor,
};
MIT License
Copyright (c) 2026 兰涛
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# Flow2Spec — Let AI Always Know What You're Doing
> Cures the "amnesia" of Cursor / Claude Code — with one `init` command, AI
> remembers project context across sessions. No more re-explaining every time.
>
> 🌐 **[中文](./README.zh-CN.md)** · EN / 中
🎬 **[Live Demo](https://lands-1203.github.io/Flow2Spec/)** (13-slide HTML PPT, `←` `→` to navigate, `S` for presenter mode)
📖 **[Flow2Spec Introduction](./docs/en/Flow2Spec-Introduction.md)** · **[基础介绍(中文)](./docs/Flow2Spec基础介绍.md)** — long-form article: why Flow2Spec, knowledge graph vs project memory, with diagrams
🔧 **Quick start**:
```bash
npx @double-codeing/flow2spec@latest init
```
---
## Before / After
The exact same request, two conversations:
```
> Update the batch re-scoring of the review template library
```
**Without Flow2Spec**:
```
AI: Which module has this table?
AI: Is batchReScore sync or async?
AI: Is there a lock? What's the idempotency key?
AI: What's the response format? What's the error code?
AI: (Digging through 416 APIs, 796 files, 4.7 MB of source code…)
```
Repeated introductions · Repeated code searches · Repeated mistakes
**With Flow2Spec**:
```
[matcher hit] m-product-review-template-library
[loading deps] 4 topics · ~300 lines
AI: Known — fire-and-forget
Redis lock smp:product-review:template-library:batch-rescore:lock (TTL 10 min)
Max 100 items per batch · error code 101
AI: Starting implementation, 3 files affected.
```
4.7 MB → 300 lines · Pinpoint accuracy in seconds
---
## What Flow2Spec Does
**① Remembers project context across sessions**
`.Knowledge/` structured knowledge base: routing manifest (`manifest-routing.json`) + keyword indices (matchers) + topic shards (topics). AI only loads what's relevant — 4.7 MB of source code compressed to ~300 lines of precise context.
**② Routing manifest means AI doesn't dig through your repo**
Each task hits 1–4 topics, ~300 lines. Business constraints — Redis lock keys, error codes, batch limits — are all in the topics. AI doesn't have to guess from source code.
**③ f2s-* skills update knowledge as you code**
`/f2s-kb-feat` writes topics while writing features, `/f2s-kb-fix` corrects topics while fixing bugs, `/f2s-git-commit` checks topic coverage before committing. Changing code == updating knowledge. No separate "documentation maintenance."
**④ Full pipeline from requirements to code**
`/f2s-req-clarify` asks questions until requirements are unambiguous. `/f2s-req-tech` generates a ready-to-implement technical proposal into `req-docs/`. AI implements from the proposal — no relying on verbal agreements.
**⑤ Task checklists track progress across sessions**
When `changeTracking` is enabled, skills like `f2s-kb-feat` / `f2s-kb-fix` automatically create a `task.md` with checkboxes. Each step is checked off immediately to disk. New sessions auto-load the remaining checklist — no relying on memory. User-side todos (run SQL, set env vars, click approvals) go into `user-todos.md`, separate from AI steps.
**⑥ Document-driven: PDF / MD straight into the knowledge base**
`/f2s-kb-add` aggregates source files into draft → final → topics. `/f2s-doc-final` converts any PDF or MD into the canonical final-draft format. External docs and legacy proposals all become routable knowledge.
---
## Getting Started
**Minimum viable setup is an empty skeleton.**
```bash
npx @double-codeing/flow2spec@latest init
```
1 minute generates the directory structure + routing config. Empty, ready to use. **Next requirement hits whichever area → you document that area.** No upfront investment needed.
Real data from a production repo running for 3 months:
| Metric | Value |
|---|---|
| Public APIs | 416 |
| Source code | 796 files / 4.7 MB / ~100K lines |
| Flow2Spec per-task load | **≈ 300 lines** (99% noise removed) |
---
## Usage Flow
### Step 1: Initialize (one-time)
```bash
npx @double-codeing/flow2spec@latest init
```
Follow the prompts to completion — generates the `.Knowledge/` directory structure and routing config skeleton.
---
### Step 2: Build the Knowledge Base (one-time)
In your Agent tool (Cursor / Claude Code):
1. `/f2s-doc-arch` — Scan your project architecture, generate an architecture draft, and follow the flow until topics are created
> This step is done once. You won't need to repeat it for daily development.
2. `/f2s-kb-add <folder path>` — Import any feature modules that haven't been added yet
> Do this selectively before starting development when you notice a module's knowledge is missing from the knowledge base.
---
### Step 3: Daily Development (every feature or fix)
**Large features:**
```
/f2s-req-clarify one-line description or paste PRD ← clarify requirements
/f2s-req-tech ← generate technical proposal
natural language: implement the proposal above ← AI starts coding (task checklist auto-created when changeTracking is on)
(debug and verify)
/f2s-kb-feat add xxx capability ← if something's missing
/f2s-kb-fix fix xxx ← if there's a bug
/f2s-kb-sync ← sync knowledge base
/f2s-git-commit ← check and commit
```
**Small changes / quick fixes:**
```
/f2s-kb-feat add xxx capability ← missing feature
/f2s-kb-fix fix xxx ← bug fix
```
---
## Quick Command Reference
| Command | Purpose |
|---|---|
| `/f2s-req-clarify` | Clarify requirements |
| `/f2s-req-tech` | Generate technical proposal |
| `/f2s-kb-feat` | Add a new capability |
| `/f2s-kb-fix` | Fix a bug |
| `/f2s-kb-sync` | Sync knowledge base |
| `/f2s-git-commit` | Commit code; "quick commit" skips KB coverage check |
| `/f2s-kb-add <path>` | Import API module into knowledge base |
For the full command list, see [Usage Guide](./docs/en/usage-guide.md) · [Commands Reference](./docs/en/commands-reference.md)
---
## When NOT to Use
- **One-off scripts** — throwaway code is faster with a few Markdown files for AI context
- **Solo small projects** — a single CLAUDE.md is enough; routing overhead > benefits
- **Team won't maintain .Knowledge/** — tools can't replace discipline
---
## Documentation
**Start here** — product narrative and diagrams:
- [Flow2Spec Introduction](./docs/en/Flow2Spec-Introduction.md) (EN)
- [Flow2Spec 基础介绍](./docs/Flow2Spec基础介绍.md) (中文)
**Hands-on guides**
### English
- [Usage Guide](./docs/en/usage-guide.md) — skill chains, config details
- [Commands Reference](./docs/en/commands-reference.md) — all f2s-* command reference
- [Directory Conventions](./docs/en/directory-conventions.md)
- [Architecture & Principles](./docs/en/architecture.md)
- [Usage Scenarios](./docs/en/usage-scenarios.md)
- [Design Principles](./docs/en/design-principles.md)
- [Project Milestones](./docs/en/milestones.md)
### 中文
- [使用说明](./docs/使用说明.md)
- [命令说明](./docs/命令说明.md)
- [目录与路径约定](./docs/目录与路径约定.md)
- [体系与原理](./docs/体系与原理.md)
- [使用案例·模拟对话](./docs/使用案例-模拟对话.md)
- [设计说明](./docs/设计说明.md)
- [项目里程碑](./docs/项目里程碑.md)
## License
MIT. Copyright © 2026 兰涛
# Flow2Spec — 让 AI 一直知道你在做什么
> 解决 Cursor / Claude Code 的「失忆症」——用一个命令初始化,让 AI
> 跨会话记住项目上下文,不用每轮重新交代。
>
> 🌐 **[English](./README.md)** · 中 / EN
🎬 **[在线演示](https://lands-1203.github.io/Flow2Spec/)**(13 页 HTML PPT,`←` `→` 翻页,`S` 演讲者模式)
📖 **[Flow2Spec 基础介绍](./docs/Flow2Spec基础介绍.md)** · **[Introduction (EN)](./docs/en/Flow2Spec-Introduction.md)** — 长文:为什么做 Flow2Spec、知识图谱 vs 项目记忆,含配图与流程图
🔧 **快速体验**:
```bash
npx @double-codeing/flow2spec@latest init
```
---
## Before / After
同样一句话,两段对话:
```
> 改一下评价模板文案库的批量重评分
```
**没有 Flow2Spec**:
```
AI: 这个模块的表在哪?
AI: batchReScore 是同步还是异步?
AI: 有没有锁?幂等键是什么?
AI: 返回格式是什么?错误码是多少?
AI: (翻遍 416 个接口、796 份文件、4.7 MB 源码…)
```
反复介绍 · 反复翻代码 · 反复踩坑
**有 Flow2Spec**:
```
[matcher 命中] m-product-review-template-library
[加载依赖] 4 个 topic · 约 300 行
AI: 已知 — fire-and-forget
Redis 锁 smp:product-review:template-library:batch-rescore:lock(TTL 10 分钟)
单次最多 100 条 · 错误码 101
AI: 开始改,预计 3 处文件。
```
4.7 MB → 300 行 · 秒级定位到硬约束
---
## Flow2Spec 做这些事
**① 跨会话记住项目上下文**
`.Knowledge/` 结构化知识库:路由清单(manifest-routing.json)+ 关键词索引(matchers)+ 主题分片(topics)。AI 启动时只读该读的,4.7 MB 源码压到 300 行精准上下文。
**② 路由清单让 AI 不翻仓库,只拿该拿的**
每次需求命中 1~4 个 topic,约 300 行。业务的硬约束——锁的 key、错误码、上限——都在 topic 里,AI 不用从源码猜。
**③ f2s-* 技能改代码顺手更新知识**
`/f2s-kb-feat` 写功能时同步写 topic,`/f2s-kb-fix` 修 bug 时更正 topic,`/f2s-git-commit` 提交前检查 topic 覆盖。改代码就是记知识,没有"单独维护文档"这件事。
**④ 需求到实现全链路:澄清 → 技术方案 → 代码**
`/f2s-req-clarify` 反问到无歧义,`/f2s-req-tech` 生成可直接实现的技术方案文档落到 `req-docs/`,AI 按方案实现,不靠口头约定。
**⑤ 任务清单跨会话追踪进度**
开启 `changeTracking` 配置后,`f2s-kb-feat` / `f2s-kb-fix` 等技能执行时自动创建带 checkbox 的 `task.md`,每步完成立即打钩落盘。新会话续作时自动加载剩余清单,不靠记忆、不靠口头,任务进度永远在磁盘上。用户侧的代办(执行 SQL、配环境变量、点审批)单独写入 `user-todos.md`,不混在 AI 步骤里。
**⑥ 文档驱动:PDF / MD 一键入知识库**
`/f2s-kb-add` 把已落地能力的源码聚合成初稿 → 终稿 → topics,`/f2s-doc-final` 把 PDF 或任意 MD 转成规范终稿格式。外部文档、历史方案都能变成可路由的知识。
---
## 上手成本
**最小可用集是一个空骨架。**
```bash
npx @double-codeing/flow2spec@latest init
```
1 分钟生成目录结构 + 路由配置,空的,直接跑。**下次需求命中哪块,写哪块**,不提前建设。
真实仓库跑了三个月的数据:
| 指标 | 数值 |
|---|---|
| 对外接口数 | 416 |
| 源码体积 | 796 文件 / 4.7 MB / ~10 万行 |
| Flow2Spec 每次加载 | **≈ 300 行**(噪声切掉 99%) |
---
## 使用流程
### 第一步:初始化(一次性)
```bash
npx @double-codeing/flow2spec@latest init
```
跟着提示走完,生成 `.Knowledge/` 目录结构和路由配置骨架。
---
### 第二步:建知识库(一次性)
在 Agent 工具(Cursor / Claude Code)中执行:
1. `/f2s-doc-arch` — 扫描项目架构,生成架构说明初稿,跟着流程走直到生成主题(topics)
> 这一步只做一次,之后日常开发不需要重复。
2. `/f2s-kb-add <文件夹路径>` — 把还没入库的功能模块路径补进来
> 这一步在进入开发前,发现没有某个模块能力的知识的时候选择性的去做
---
### 第三步:日常开发(每次需求)
**大需求:**
```
/f2s-req-clarify 一句话需求或粘贴 PRD ← 需求澄清
/f2s-req-tech ← 生成技术方案
自然语言:实现上面的技术方案 ← AI 开始实现(开启 changeTracking 时自动建任务清单)
(调试验证)
/f2s-kb-feat 新增 xxx 能力 ← 功能缺失时补能力
/f2s-kb-fix 修复 xxx ← 有 BUG 时修复
/f2s-kb-sync ← 同步知识库
/f2s-git-commit ← 检查并提交
```
**小需求 / 日常改动:**
```
/f2s-kb-feat 新增 xxx 能力 ← 功能缺失
/f2s-kb-fix 修复 xxx ← 改 BUG
```
---
## 常用命令速查
| 命令 | 用途 |
|---|---|
| `/f2s-req-clarify` | 需求澄清 |
| `/f2s-req-tech` | 生成技术方案 |
| `/f2s-kb-feat` | 新增小功能 |
| `/f2s-kb-fix` | 改 BUG |
| `/f2s-kb-sync` | 同步知识库 |
| `/f2s-git-commit` | 提交代码;“快捷提交”跳过知识库覆盖检查 |
| `/f2s-kb-add <路径>` | 接口模块入知识库 |
更多命令详见 [使用说明](./docs/使用说明.md) · [命令说明](./docs/命令说明.md)
---
## 什么时候别用
- **一次性脚本** — 写完就删的东西,直接丢几个 Markdown 给 AI 更快
- **单人小项目** — 一份 CLAUDE.md 就够,路由和分片的开销大于收益
- **团队不愿同步 .Knowledge/** — 工具不能替代纪律
---
## 详细文档
**从这里开始** — 产品叙事与配图:
- [Flow2Spec 基础介绍](./docs/Flow2Spec基础介绍.md)(中文)
- [Flow2Spec Introduction](./docs/en/Flow2Spec-Introduction.md)(EN)
**上手与参考**
### 中文
- [使用说明](./docs/使用说明.md) — 技能链、配置详解
- [命令说明](./docs/命令说明.md) — 所有 f2s-* 命令速查
- [目录与路径约定](./docs/目录与路径约定.md)
- [体系与原理](./docs/体系与原理.md)
- [使用案例·模拟对话](./docs/使用案例-模拟对话.md)
- [设计说明](./docs/设计说明.md)
- [项目里程碑](./docs/项目里程碑.md)
### English
- [Usage Guide](./docs/en/usage-guide.md)
- [Commands Reference](./docs/en/commands-reference.md)
- [Directory Conventions](./docs/en/directory-conventions.md)
- [Architecture & Principles](./docs/en/architecture.md)
- [Usage Scenarios](./docs/en/usage-scenarios.md)
- [Design Principles](./docs/en/design-principles.md)
- [Project Milestones](./docs/en/milestones.md)
## 协议
MIT. Copyright © 2026 兰涛
+199
-2

@@ -17,2 +17,3 @@ #!/usr/bin/env node

} = require("./lib/flow2specConfig");
const knowledgeEngine = require("./lib/knowledgeEngine");

@@ -204,2 +205,29 @@ const { execFileSync } = require("child_process");

function printJson(data) {
console.log(`${JSON.stringify(data, null, 2)}\n`);
}
function printKnowledgeHelp() {
console.log(`
Flow2Spec KB - knowledge collaboration engine
用法:
flow2spec kb status [--json]
flow2spec kb check [--strict] [--json]
flow2spec kb plan <delta-file> [--json]
flow2spec kb apply <delta-file> [--dry-run] [--json]
flow2spec kb build [--fix-topics] [--json]
说明:
status - 汇总当前知识图、active task delta 与潜在漂移
check - 校验 manifest/topic/frontmatter/revision
plan - 预演一个 kb-delta 是否可自动合并
apply - 应用 kb-delta 并同步 topic frontmatter / routing
build - 基于 topic frontmatter 归一化 routing 元数据;--fix-topics 可为旧 topic 补 frontmatter/revision
delta changes:
appendBody / replaceBody / updateFrontmatter / createTopic
`);
}
const help = `

@@ -211,2 +239,3 @@ Flow2Spec - 统一知识库工作流(AI 配置入口) v${pkg.version}

flow2spec config 打印项目根 ${CONFIG_FILENAME} 的解析结果(缺省值合并后)
flow2spec kb 知识库协作引擎:status / check / plan / apply / build
flow2spec version 显示当前 flow2spec 版本

@@ -242,3 +271,4 @@ flow2spec update 更新 flow2spec 到最新版本;更新后提示执行 f2s-kb-upgrade

5. 每次 init 将当前 locale 包模板 knowledge/index.md 复制到 .Knowledge/template/index.template.md,供 f2s-kb-upgrade 技能与 .Knowledge/index.md 对照;不自动改写 index.md。(「知识库升级」指 f2s-kb-upgrade 技能,init 本身不是升级命令。)
6. 规则与技能在各 agent 配置根加载;其他模版类文件在 .Knowledge/template/ 等目录。
6. 非破坏式补充 .gitignore:忽略 .task/ 与 .Knowledge/update-check.json 这类本地运行态。
7. 规则与技能在各 agent 配置根加载;其他模版类文件在 .Knowledge/template/ 等目录。

@@ -302,2 +332,165 @@ 更多说明见 README.md 或 docs/使用说明.md

if (sub === "kb") {
const kbSub = args[1];
const kbFlags = new Set(args.slice(2).filter((arg) => String(arg || "").startsWith("--")));
const kbPositionals = args.slice(2).filter((arg) => !String(arg || "").startsWith("--"));
const cwd = process.cwd();
const jsonOut = kbFlags.has("--json");
try {
if (!kbSub || kbSub === "--help" || kbSub === "-h") {
printKnowledgeHelp();
process.exit(0);
}
if (kbSub === "status") {
const report = knowledgeEngine.summarizeKnowledgeState(cwd);
if (jsonOut) {
printJson(report);
} else {
console.log(`knowledge topics: ${report.topicCount}`);
console.log(`routing drift: ${report.routingDrift ? "yes" : "no"}`);
console.log(`validation: ${report.validation.ok ? "ok" : "has issues"}`);
if (report.validation.warnings.length) {
console.log(`warnings: ${report.validation.warnings.length}`);
}
if (report.tasks.length) {
console.log("active kb deltas:");
for (const task of report.tasks) {
if (task.error) {
console.log(`- ${task.taskName}: ${task.error}`);
continue;
}
console.log(
`- ${task.taskName}: ${task.mergeable ? "mergeable" : "conflict"} (${task.plan.length} changes, ${task.conflicts.length} conflicts)`,
);
}
}
}
process.exit(report.validation.ok ? 0 : 1);
}
if (kbSub === "check") {
const strict = kbFlags.has("--strict");
const graph = knowledgeEngine.loadKnowledgeGraph(cwd);
const validation = knowledgeEngine.validateKnowledgeGraph(graph, {
strictRevision: strict,
});
const normalized = knowledgeEngine.normalizeRoutingWithGraph(
graph,
);
const routingDrift =
knowledgeEngine.stableStringify(normalized.routing) !==
knowledgeEngine.stableStringify(graph.routing);
const report = knowledgeEngine.summarizeKnowledgeState(cwd);
const ok =
validation.issues.length === 0 &&
!routingDrift &&
(!strict || validation.warnings.length === 0);
const result = {
ok,
strict,
topicCount: report.topicCount,
issues: validation.issues,
warnings: validation.warnings,
routingDrift,
activeDeltas: report.tasks,
};
if (jsonOut) {
printJson(result);
} else {
console.log(`knowledge check: ${result.ok ? "ok" : "failed"}`);
console.log(`topics: ${result.topicCount}`);
console.log(`routing drift: ${routingDrift ? "yes" : "no"}`);
if (result.issues.length) {
console.log(`issues: ${result.issues.length}`);
for (const issue of result.issues.slice(0, 10)) {
console.log(`- ${issue}`);
}
}
if (result.warnings.length) {
console.log(`warnings: ${result.warnings.length}`);
}
}
process.exit(result.ok ? 0 : 1);
}
if (kbSub === "plan" || kbSub === "apply") {
const deltaArg = kbPositionals[0];
if (!deltaArg) {
console.error(`kb ${kbSub} 需要 delta 文件路径`);
process.exit(1);
}
const deltaPath = path.resolve(cwd, deltaArg);
const dryRun = kbFlags.has("--dry-run") || kbSub === "plan";
const graph = knowledgeEngine.loadKnowledgeGraph(cwd);
const delta = knowledgeEngine.parseKnowledgeDelta(deltaPath);
const plan = knowledgeEngine.planKnowledgeDelta(graph, delta);
if (kbSub === "plan") {
const result = {
ok: plan.mergeable,
deltaPath,
plan: plan.plan,
conflicts: plan.conflicts,
};
if (jsonOut) {
printJson(result);
} else {
console.log(`kb plan: ${result.ok ? "mergeable" : "conflict"}`);
for (const item of result.plan) {
console.log(
`- ${item.topicId}: ${item.type} ${item.beforeRevision} -> ${item.afterRevision}`,
);
}
for (const conflict of result.conflicts) {
console.log(`! ${conflict.topicId}: ${conflict.reason}`);
}
}
process.exit(result.ok ? 0 : 1);
}
const result = knowledgeEngine.applyKnowledgeDelta(cwd, deltaPath, {
dryRun,
});
if (jsonOut) {
printJson(result);
} else {
console.log(`kb apply: ${dryRun ? "dry-run" : "applied"}`);
for (const file of result.changedFiles) {
console.log(`- ${file}`);
}
}
process.exit(0);
}
if (kbSub === "build") {
const result = knowledgeEngine.buildKnowledgeGraph(cwd, {
writeTopicFrontmatter: kbFlags.has("--fix-topics"),
});
if (jsonOut) {
printJson(result);
} else {
console.log(`kb build: ${result.changed ? "updated" : "up-to-date"}`);
console.log(`routing: ${path.relative(cwd, result.routingPath)}`);
if (result.topicFrontmatterChanged?.length) {
console.log(`topic frontmatter: ${result.topicFrontmatterChanged.length} updated`);
for (const file of result.topicFrontmatterChanged.slice(0, 10)) {
console.log(`- ${file}`);
}
}
console.log(
`validation: ${result.validation.ok ? "ok" : "has issues"}`,
);
}
process.exit(result.validation.ok ? 0 : 1);
}
console.error(`unknown kb subcommand: ${kbSub}`);
printKnowledgeHelp();
process.exit(1);
} catch (e) {
console.error(e.message || e);
process.exit(1);
}
}
if (sub === "init") {

@@ -577,3 +770,3 @@ const rawArgs = args.slice(1);

)
.then(({ ids, knowledgeResult, routingUpgrade, indexSnapshot, projectConfig, locale, claudeHooksResult }) => {
.then(({ ids, knowledgeResult, routingUpgrade, indexSnapshot, gitignoreResult, projectConfig, locale, claudeHooksResult }) => {
const lines = ids.map((id) => {

@@ -605,2 +798,5 @@ const { root, label } = AGENTS[id];

const configLine = ` - ${CONFIG_FILENAME}:locale=${pc.locale || locale}, subAgent=${Boolean(pc.subAgent)}, switchAgentVerification=${Boolean(pc.switchAgentVerification)}`;
const gitignoreLine = gitignoreResult?.changed
? ` - .gitignore:已补充 ${gitignoreResult.added.join(", ")}`
: " - .gitignore:Flow2Spec 本地态忽略项已存在";
console.log(`

@@ -611,2 +807,3 @@ ✓ Flow2Spec init 完成

${indexLine}
${gitignoreLine}
${configLine}

@@ -613,0 +810,0 @@ ${lines.join("\n")}

@@ -47,2 +47,24 @@ const path = require("path");

function ensureFlow2specGitignore(cwd) {
const gitignorePath = path.join(cwd, ".gitignore");
const required = [".task/", ".Knowledge/update-check.json"];
const existing = fs.existsSync(gitignorePath)
? fs.readFileSync(gitignorePath, "utf8")
: "";
const lines = existing.split(/\r?\n/).map((line) => line.trim());
const missing = required.filter((item) => !lines.includes(item));
if (missing.length === 0) {
return { path: gitignorePath, changed: false, added: [] };
}
const additions = [];
if (!lines.includes("# Flow2Spec local state")) {
additions.push("# Flow2Spec local state");
}
additions.push(...missing);
const prefix = existing && !existing.endsWith("\n") ? `${existing}\n` : existing;
const separator = prefix && !prefix.endsWith("\n\n") ? "\n" : "";
fs.writeFileSync(gitignorePath, `${prefix}${separator}${additions.join("\n")}\n`, "utf8");
return { path: gitignorePath, changed: true, added: missing };
}
function ensureAgentDirs(cwd, agentId) {

@@ -1209,2 +1231,3 @@ const root = AGENTS[agentId].root;

removeKnowledgeUpdateCheckCache(cwd);
const gitignoreResult = ensureFlow2specGitignore(cwd);
ensureFlow2specProjectConfig(cwd, templatesDir, {

@@ -1251,2 +1274,3 @@ overwrite: false,

indexSnapshot,
gitignoreResult,
projectConfig,

@@ -1253,0 +1277,0 @@ locale,

+2
-2
{
"name": "@double-codeing/flow2spec",
"version": "3.2.8-beta.0",
"version": "3.2.8-beta.1",
"description": "在业务仓库初始化「文档驱动、可写回知识库」的 AI 协作骨架:项目根 .Knowledge 承载 stock-docs/req-docs 与机读路由,.cursor/.claude/.codex 写入 f2s-* 规则与技能(含 Karpathy 式编码行为准则,init 同步 rules / Codex topics / skills);init 只落结构与模板,业务内容由各 f2s-* 技能在对话中维护。",

@@ -28,3 +28,3 @@ "homepage": "https://github.com/Lands-1203/Flow2Spec#readme",

"scripts": {
"test": "node cli.js --help",
"test": "node cli.js --help && node cli.js kb check && node scripts/test-knowledge-engine.js && node scripts/test-template-knowledge.js && node scripts/test-init-gitignore.js",
"sync:agents": "node cli.js init cursor claude codex",

@@ -31,0 +31,0 @@ "prepublishOnly": "node cli.js --help",

{
"version": "3.1.5",
"projectRev": 1,
"projectRev": 2,
"knowledgeRoot": ".Knowledge",

@@ -5,0 +5,0 @@ "matcherKey": "matcherId",

@@ -0,1 +1,9 @@

---
id: config-precheck
revision: 0
summary: "config-precheck (routing summary)"
primary: config
confidence: manual
tags: [policy]
---
# config-precheck (routing summary)

@@ -2,0 +10,0 @@

@@ -0,1 +1,8 @@

---
id: fallback-triage
revision: 0
summary: fallback-triage
primary: policy
confidence: manual
---
# fallback-triage

@@ -2,0 +9,0 @@

@@ -0,1 +1,9 @@

---
id: implement-tech-design
revision: 0
summary: "implement-tech-design (routing summary)"
dependsOn: [f2s-doc-routing]
primary: policy
confidence: manual
---
# implement-tech-design (routing summary)

@@ -2,0 +10,0 @@

@@ -0,1 +1,9 @@

---
id: f2s-req-plan
revision: 0
summary: "f2s-req-plan (routing summary)"
dependsOn: [f2s-task]
primary: policy
confidence: manual
---
# f2s-req-plan (routing summary)

@@ -2,0 +10,0 @@

@@ -0,1 +1,8 @@

---
id: f2s-doc-routing
revision: 0
summary: "f2s-doc-routing (routing summary)"
primary: policy
confidence: manual
---
# f2s-doc-routing (routing summary)

@@ -2,0 +9,0 @@

@@ -0,1 +1,8 @@

---
id: f2s-task
revision: 0
summary: "f2s-task (routing summary)"
primary: policy
confidence: manual
---
# f2s-task (routing summary)

@@ -2,0 +9,0 @@

@@ -62,2 +62,13 @@ ---

**First perform the KB auto-merge preflight (required; do not ask the user to run commands manually):**
1. The agent runs `flow2spec kb check --json` and `flow2spec kb status --json` inside this step, or uses an equivalent built-in KB engine capability. Do not turn these commands into manual pre-commit chores for the user.
2. If `check` reports knowledge-structure errors, missing matchers, routing drift, or other health issues: stop this commit, report the concrete issues and suggested fix actions, and do not commit a broken knowledge base.
3. If `status.tasks` contains `kb-delta.json` under the current developer task root:
- If the current task line can be uniquely identified and `mergeable=true`: automatically run `plan -> apply -> build -> check` (via CLI or equivalent built-in capability), and include the written `.Knowledge/**` files in the commit file list.
- If `mergeable=false`, delta parsing failed, or multiple active deltas exist and the agent cannot determine which one belongs to this commit: stop the automatic write, list `topic / reason / deltaPath`, and tell the user that semantic merge or task-line selection is required. Do not guess the merge.
4. Only when there is no active `kb-delta.json` for the current task line, continue to the coarse coverage check below.
**When there is no auto-applicable delta, perform the coarse coverage check:**
1. Infer the **functional modules** touched by this change from `git diff HEAD` and untracked file paths (use actual repository directories/package names; do not invent business names that do not appear).

@@ -199,3 +210,3 @@ 2. Read the directory lists of `.Knowledge/topics/` and `.Knowledge/stock-docs/`.

3. Was `git add -A` / `git add .` used? Must be no.
4. Was the knowledge-base check performed or skipped with an explicit reason (quick commit / `.Knowledge` missing)? Must be yes.
4. Was the knowledge-base check performed or skipped with an explicit reason (quick commit / `.Knowledge` missing)? Must be yes. If an active `kb-delta.json` exists, was it automatically planned/applied/built/checked or was a conflict explicitly reported? Must be yes.
5. Was the Step 3 commit message generated from actual `git diff` content? Must be yes, not only `--stat`.

@@ -202,0 +213,0 @@ 6. Was the proposed first line **shown in the same reply** before executing commit? Must be yes; do **not** require the user to separately "confirm commit".

@@ -11,2 +11,11 @@ ---

## KB Auto-Merge Protocol (Required)
This skill must not make manual command execution part of the user flow. After the user triggers this skill, the agent performs knowledge candidate generation, merge planning, build, and validation by itself:
1. If reusable knowledge should be recorded, first form a `kb-delta` draft in the current task context with `taskId`, `developerId`, `baseRevisions`, `changes`, and evidence summary. If there is no explicit task directory, an equivalent in-memory object is acceptable; do not create `.task` only for this skill. `changes` may use `appendBody` / `replaceBody` / `updateFrontmatter`; when a new topic is needed, use `createTopic` and optionally include `taskRule` plus `matcher` so routing is connected in the same merge.
2. Before writing `.Knowledge`, run `flow2spec kb plan <delta>` or the equivalent internal capability. If a topic revision differs, stop automatic writing and switch to semantic-merge reporting.
3. When the change is auto-mergeable, run `flow2spec kb apply <delta>` or the equivalent internal capability, then run `flow2spec kb build` and `flow2spec kb check`.
4. The user should only see "knowledge base synced / semantic conflict needs confirmation / skipped with reason"; do not ask the user to manually run `kb plan/apply/build/check`.
## Orchestration (main / sub agent)

@@ -13,0 +22,0 @@

@@ -11,2 +11,11 @@ ---

## KB Auto-Merge Protocol (Required)
This skill must not make manual command execution part of the user flow. After the implementation is completed or confirmed to already exist, the agent performs knowledge candidate generation, merge planning, build, and validation by itself:
1. Convert this capability change into a `kb-delta` draft with `taskId`, `developerId`, `baseRevisions`, `changes`, and implementation evidence. If `changeTracking.feat=true` and a task directory already exists, the delta may be written to `TASK_ROOT/active/<task-name>/kb-delta.json`; otherwise an equivalent in-memory object is acceptable. `changes` may use `appendBody` / `replaceBody` / `updateFrontmatter`; when a new topic is needed, use `createTopic` and optionally include `taskRule` plus `matcher` so routing is connected in the same merge.
2. Before writing `.Knowledge`, run `flow2spec kb plan <delta>` or the equivalent internal capability. If a topic revision differs, stop automatic writing and switch to semantic-merge reporting.
3. When the change is auto-mergeable, run `flow2spec kb apply <delta>` or the equivalent internal capability, then run `flow2spec kb build` and `flow2spec kb check`.
4. The user should only see "capability and knowledge base synced / semantic conflict needs confirmation / skipped with reason"; do not ask the user to manually run `kb plan/apply/build/check`.
## Orchestration (main / sub-agent)

@@ -13,0 +22,0 @@

@@ -11,2 +11,11 @@ ---

## KB Auto-Merge Protocol (Required)
This skill must not make manual command execution part of the user flow. After the fix is completed, the agent performs knowledge candidate generation, merge planning, build, and validation by itself:
1. Convert the corrected rule or implementation boundary into a `kb-delta` draft with `taskId`, `developerId`, `baseRevisions`, `changes`, and fix evidence. If `changeTracking.fix=true` and a task directory already exists, the delta may be written to `TASK_ROOT/active/<task-name>/kb-delta.json`; otherwise an equivalent in-memory object is acceptable. `changes` may use `appendBody` / `replaceBody` / `updateFrontmatter`; when a new topic is needed, use `createTopic` and optionally include `taskRule` plus `matcher` so routing is connected in the same merge.
2. Before writing `.Knowledge`, run `flow2spec kb plan <delta>` or the equivalent internal capability. If a topic revision differs, stop automatic writing and switch to semantic-merge reporting.
3. When the change is auto-mergeable, run `flow2spec kb apply <delta>` or the equivalent internal capability, then run `flow2spec kb build` and `flow2spec kb check`.
4. The user should only see "fix and knowledge base synced / semantic conflict needs confirmation / skipped with reason"; do not ask the user to manually run `kb plan/apply/build/check`.
## Orchestration (main / sub-agent)

@@ -13,0 +22,0 @@

@@ -8,2 +8,11 @@ ---

## KB Auto-Merge Protocol (Required)
This skill must not make manual command execution part of the user flow. After the user confirms the sync outline, the agent performs knowledge candidate generation, merge planning, build, and validation by itself:
1. Convert the confirmed outline into one or more `kb-delta` drafts with `taskId`, `developerId`, `baseRevisions`, `changes`, and evidence summary. If there is no explicit task directory, an equivalent in-memory object is acceptable; do not create `.task` only for this skill. `changes` may use `appendBody` / `replaceBody` / `updateFrontmatter`; when a new topic is needed, use `createTopic` and optionally include `taskRule` plus `matcher` so routing is connected in the same merge.
2. Before writing `.Knowledge`, run `flow2spec kb plan <delta>` or the equivalent internal capability. If a topic revision differs, stop automatic writing and switch to semantic-merge reporting.
3. When the change is auto-mergeable, run `flow2spec kb apply <delta>` or the equivalent internal capability, then run `flow2spec kb build` and `flow2spec kb check`.
4. The user should only see "knowledge base synced / semantic conflict needs confirmation / skipped with reason"; do not ask the user to manually run `kb plan/apply/build/check`.
## Orchestration (main / sub-agent)

@@ -10,0 +19,0 @@

@@ -241,2 +241,3 @@ ---

- The topic is frequently matched by multiple unrelated task types (can be judged from `taskToTopicRules` and matcher term breadth).
7. **Automatic old-topic frontmatter repair**: in the full flow, the agent must run `flow2spec kb build --fix-topics` (or the equivalent internal capability) to add `id`, `revision`, and `summary` to existing topics that lack frontmatter / `revision`, and to fill `dependsOn` / `primary` / `confidence` / `tags` from `manifest-routing.json`. Then run `flow2spec kb check --strict`; if strict validation fails, stop and list the concrete topic / reason in the summary. Do not ask the user to manually add topic headers one by one.

@@ -328,2 +329,3 @@ ### Step 3b: `index.md` Merge and `template/index.template.md` (Required)

- **topicMetadata (existing audit)**: `filled` / `pending user confirmation` / `not executed on fast path`; list added / fixed / deleted topicIds
- **topic frontmatter**: `auto-filled N topics` / `already complete` / `strict validation failed` / `not executed on fast path`
- **f2s-kb-upgrade SKILL**: `unchanged after init` / `reran N rounds from step 2c per new SKILL (no second init)` / `loop skipped on fast path` / `pending confirmation`

@@ -357,7 +359,8 @@ - **`projectRev` write-back**: `written to project manifest (value=pkgRev)` / `not executed on fast path` / `pkgRev=null, field untouched`

8. **On full flow**: **Step 3a** was executed: `topicMetadata` audited, with no orphan keys / illegal primary / illegal confidence; missing old topics were filled with `inferred` based on evidence or listed as pending confirmation.
9. **On full flow**: **Step 3b** was executed: `index.md` was **merged** (from **`Topic Overview`** section through before "Match and Execute" is project-maintained; the rest matches the package version), and `topicPaths` were checked; **at the end of full flow**, the project-side `projectRev` was **written back** to `pkgRev` (if `pkgRev=null`, the field was left unchanged).
10. **On fast path**: steps 3 / 3a / 3b were actually skipped (no unrelated scans), and the summary explicitly labels "not executed on fast path".
11. Manifest and key-path verification results were output.
12. If failed, a concrete next command suggestion was provided.
13. Step 3b `index.md` merge was completed and written by the main agent, with no unauthorized sub-agent write (applies only on full flow).
14. After successful upgrade, `.Knowledge/update-check.json` was deleted to avoid stale upgrade hints in new sessions that day.
9. **On full flow**: `flow2spec kb build --fix-topics` or an equivalent internal capability was executed, followed by `flow2spec kb check --strict`, ensuring existing topics have `revision`.
10. **On full flow**: **Step 3b** was executed: `index.md` was **merged** (from **`Topic Overview`** section through before "Match and Execute" is project-maintained; the rest matches the package version), and `topicPaths` were checked; **at the end of full flow**, the project-side `projectRev` was **written back** to `pkgRev` (if `pkgRev=null`, the field was left unchanged).
11. **On fast path**: steps 3 / 3a / 3b were actually skipped (no unrelated scans), and the summary explicitly labels "not executed on fast path".
12. Manifest and key-path verification results were output.
13. If failed, a concrete next command suggestion was provided.
14. Step 3b `index.md` merge was completed and written by the main agent, with no unauthorized sub-agent write (applies only on full flow).
15. After successful upgrade, `.Knowledge/update-check.json` was deleted to avoid stale upgrade hints in new sessions that day.
{
"version": "3.1.5",
"projectRev": 1,
"projectRev": 2,
"knowledgeRoot": ".Knowledge",

@@ -5,0 +5,0 @@ "matcherKey": "matcherId",

@@ -0,1 +1,9 @@

---
id: config-precheck
revision: 0
summary: "config-precheck(路由摘要)"
primary: config
confidence: manual
tags: [policy]
---
# config-precheck(路由摘要)

@@ -2,0 +10,0 @@

@@ -0,1 +1,8 @@

---
id: fallback-triage
revision: 0
summary: fallback-triage
primary: policy
confidence: manual
---
# fallback-triage

@@ -2,0 +9,0 @@

@@ -0,1 +1,9 @@

---
id: implement-tech-design
revision: 0
summary: "implement-tech-design(路由摘要)"
dependsOn: [f2s-doc-routing]
primary: policy
confidence: manual
---
# implement-tech-design(路由摘要)

@@ -2,0 +10,0 @@

@@ -0,1 +1,9 @@

---
id: f2s-req-plan
revision: 0
summary: "f2s-req-plan(路由摘要)"
dependsOn: [f2s-task]
primary: policy
confidence: manual
---
# f2s-req-plan(路由摘要)

@@ -2,0 +10,0 @@

@@ -0,1 +1,8 @@

---
id: f2s-doc-routing
revision: 0
summary: "f2s-doc-routing(路由摘要)"
primary: policy
confidence: manual
---
# f2s-doc-routing(路由摘要)

@@ -2,0 +9,0 @@

@@ -0,1 +1,8 @@

---
id: f2s-task
revision: 0
summary: "f2s-task(路由摘要)"
primary: policy
confidence: manual
---
# f2s-task(路由摘要)

@@ -2,0 +9,0 @@

@@ -62,2 +62,13 @@ ---

**先执行 KB 自动合并预检(必须,不让用户手动跑命令):**
1. Agent 在本步骤内部执行 `flow2spec kb check --json` 与 `flow2spec kb status --json`,或使用等价的内置 KB 引擎能力;不得把这些命令变成用户要手动执行的提交前置工作。
2. 若 `check` 返回知识库结构错误、matcher 缺失、routing drift 等健康问题:终止本次 commit,报告具体问题与建议修复动作;不要把损坏的知识库一起提交。
3. 若 `status.tasks` 中存在当前 developer 任务根下的 `kb-delta.json`:
- 能唯一定位当前任务线且 `mergeable=true`:自动执行 `plan → apply → build → check`(CLI 或等价内置能力均可),并把被写入的 `.Knowledge/**` 文件纳入本次提交文件列表。
- `mergeable=false`、delta 解析失败,或存在多个 active delta 且无法判断哪个属于本次提交:停止自动写入,列出 `topic / reason / deltaPath`,提示用户需要语义合并或选择任务线;不得猜测合并。
4. 若没有当前任务线的 active `kb-delta.json`,才进入下面的粗粒度覆盖判断。
**没有可自动应用的 delta 时,执行粗粒度覆盖检查:**
1. 从 `git diff HEAD` 及 untracked 文件路径推断本次变更涉及的**功能模块**(以仓库内目录/包名为准,勿臆测未出现的业务名)。

@@ -199,3 +210,3 @@ 2. 读取 `.Knowledge/topics/` 目录列表与 `.Knowledge/stock-docs/` 目录列表。

3. 是否用了 `git add -A` / `git add .`(必须为否)。
4. 知识库检查是否执行或有明确跳过理由(快捷提交 / `.Knowledge` 不存在)(必须为是)。
4. 知识库检查是否执行或有明确跳过理由(快捷提交 / `.Knowledge` 不存在)(必须为是);若存在 active `kb-delta.json`,是否已自动 plan/apply/build/check 或明确报告冲突(必须为是)。
5. 步骤 3 是否基于 `git diff` 实际内容生成提交信息(必须为是,而非仅 `--stat`)。

@@ -202,0 +213,0 @@ 6. 执行 commit 前是否在当条回复中**展示了拟提交首行**(必须为是);**不得**要求用户单独「确认 commit」才执行(与策略一致)。

@@ -11,2 +11,11 @@ ---

## KB 自动合并协议(必须)
本技能不得把“人工执行命令”作为用户流程。用户触发本技能后,由 agent 自己完成知识候选生成、合并、构建与校验:
1. 若本轮存在可沉淀知识,先在当前任务上下文中形成 `kb-delta` 草稿,记录 `taskId`、`developerId`、`baseRevisions`、`changes` 与证据摘要;没有显式任务目录时可在内存中形成等价对象,不强制为了本技能创建 `.task`。`changes` 可使用 `appendBody` / `replaceBody` / `updateFrontmatter`;确需新主题时使用 `createTopic`,并可携带 `taskRule` 与 `matcher` 让路由一并接入。
2. 写入 `.Knowledge` 前,必须用 `flow2spec kb plan <delta>` 或等价内部能力预演;若 topic revision 不一致,停止自动写入,转入语义合并说明。
3. 可自动合并时,由 agent 调用 `flow2spec kb apply <delta>` 或等价内部能力写入 topic,并随后执行 `flow2spec kb build` 与 `flow2spec kb check`。
4. 用户只看到“知识库已同步 / 有语义冲突需确认 / 已跳过入库及原因”,不要求用户手动执行 `kb plan/apply/build/check`。
## 编排(主 / 子 agent)

@@ -13,0 +22,0 @@

@@ -11,2 +11,11 @@ ---

## KB 自动合并协议(必须)
本技能不得把“人工执行命令”作为用户流程。代码实现完成或确认已有实现后,由 agent 自己完成知识候选生成、合并、构建与校验:
1. 将本次能力变更转换为 `kb-delta` 草稿,记录 `taskId`、`developerId`、`baseRevisions`、`changes` 与实现证据;若 `changeTracking.feat=true` 且已有任务目录,可把 delta 落在当前 `TASK_ROOT/active/<task-name>/kb-delta.json`,否则可在内存中形成等价对象。`changes` 可使用 `appendBody` / `replaceBody` / `updateFrontmatter`;确需新主题时使用 `createTopic`,并可携带 `taskRule` 与 `matcher` 让路由一并接入。
2. 写入 `.Knowledge` 前,必须用 `flow2spec kb plan <delta>` 或等价内部能力预演;若 topic revision 不一致,停止自动写入,转入语义合并说明。
3. 可自动合并时,由 agent 调用 `flow2spec kb apply <delta>` 或等价内部能力写入 topic,并随后执行 `flow2spec kb build` 与 `flow2spec kb check`。
4. 用户只看到“能力与知识库已同步 / 有语义冲突需确认 / 已跳过入库及原因”,不要求用户手动执行 `kb plan/apply/build/check`。
## 编排(主 / 子 agent)

@@ -13,0 +22,0 @@

@@ -11,2 +11,11 @@ ---

## KB 自动合并协议(必须)
本技能不得把“人工执行命令”作为用户流程。修复完成后,由 agent 自己完成知识候选生成、合并、构建与校验:
1. 将本次修复后的正确规则/实现边界转换为 `kb-delta` 草稿,记录 `taskId`、`developerId`、`baseRevisions`、`changes` 与修复证据;若 `changeTracking.fix=true` 且已有任务目录,可把 delta 落在当前 `TASK_ROOT/active/<task-name>/kb-delta.json`,否则可在内存中形成等价对象。`changes` 可使用 `appendBody` / `replaceBody` / `updateFrontmatter`;确需新主题时使用 `createTopic`,并可携带 `taskRule` 与 `matcher` 让路由一并接入。
2. 写入 `.Knowledge` 前,必须用 `flow2spec kb plan <delta>` 或等价内部能力预演;若 topic revision 不一致,停止自动写入,转入语义合并说明。
3. 可自动合并时,由 agent 调用 `flow2spec kb apply <delta>` 或等价内部能力写入 topic,并随后执行 `flow2spec kb build` 与 `flow2spec kb check`。
4. 用户只看到“修复与知识库已同步 / 有语义冲突需确认 / 已跳过入库及原因”,不要求用户手动执行 `kb plan/apply/build/check`。
## 编排(主 / 子 agent)

@@ -13,0 +22,0 @@

@@ -8,2 +8,11 @@ ---

## KB 自动合并协议(必须)
本技能不得把“人工执行命令”作为用户流程。用户确认同步大纲后,由 agent 自己完成知识候选生成、合并、构建与校验:
1. 将已确认的大纲转换为一个或多个 `kb-delta` 草稿,记录 `taskId`、`developerId`、`baseRevisions`、`changes` 与证据摘要;没有显式任务目录时可在内存中形成等价对象,不强制为了本技能创建 `.task`。`changes` 可使用 `appendBody` / `replaceBody` / `updateFrontmatter`;确需新主题时使用 `createTopic`,并可携带 `taskRule` 与 `matcher` 让路由一并接入。
2. 写入 `.Knowledge` 前,必须用 `flow2spec kb plan <delta>` 或等价内部能力预演;若 topic revision 不一致,停止自动写入,转入语义合并说明。
3. 可自动合并时,由 agent 调用 `flow2spec kb apply <delta>` 或等价内部能力写入 topic,并随后执行 `flow2spec kb build` 与 `flow2spec kb check`。
4. 用户只看到“知识库已同步 / 有语义冲突需确认 / 已跳过入库及原因”,不要求用户手动执行 `kb plan/apply/build/check`。
## 编排(主 / 子 agent)

@@ -10,0 +19,0 @@

@@ -241,2 +241,3 @@ ---

- 该 topic 同时被多种不相干任务类型频繁命中(可从 `taskToTopicRules` 和 matcher 词宽度判断)。
7. **旧 topic frontmatter 自动补齐**:完整流程中必须由 agent 自行执行 `flow2spec kb build --fix-topics`(或等价内部能力),为缺少 frontmatter / `revision` 的存量 topic 补 `id`、`revision`、`summary`,并按 `manifest-routing.json` 补 `dependsOn` / `primary` / `confidence` / `tags`。随后执行 `flow2spec kb check --strict`;若 strict 失败,停止并在摘要中列出具体 topic / reason。不得要求用户手动逐个 topic 添加头部。

@@ -328,2 +329,3 @@ ### 步骤 3b:`index.md` 融合与 `template/index.template.md`(必须执行)

- **topicMetadata(存量审计)**:`已补齐` / `待用户确认` / `快速路径下未执行`;列出新增 / 修正 / 删除的 topicId
- **topic frontmatter**:`已自动补齐 N 个` / `已完整无需补齐` / `strict 校验失败` / `快速路径下未执行`
- **f2s-kb-upgrade SKILL**:`init 后无变化` / `已按新版从 2c 起重跑 N 轮(不再次 init)` / `快速路径下跳过该闭环` / `待确认`

@@ -357,7 +359,8 @@ - **`projectRev` 回写**:`已写入项目 manifest(值=pkgRev)` / `快速路径下未执行` / `pkgRev=null 未动`

8. **完整流程时**:是否已执行 **步骤 3a**:审计 `topicMetadata`,确保无孤儿 key / 非法 primary / 非法 confidence;缺失旧主题已按证据补 `inferred` 或列为待确认。
9. **完整流程时**:是否已执行 **步骤 3b**:**融合** `index.md`(**主题一览**节起至命中与执行前为项目维护区,其余同包版),并核对 `topicPaths`;**完整流程末尾**是否已**回写** 项目侧 `projectRev = pkgRev`(`pkgRev=null` 则保留原值)。
10. **快速路径时**:步骤 3 / 3a / 3b 是否真的跳过(未做无关扫描),摘要中明确标注「快速路径下未执行」。
11. 是否输出了 manifest 与关键路径校验结果。
12. 若失败,是否给出下一步具体命令建议。
13. 步骤 3b 的 `index.md` 融合由主 agent 完成并落盘,无子 agent 越权写入(仅在完整流程时适用)。
14. 成功升级后是否删除 `.Knowledge/update-check.json`,避免当天新会话继续提示旧升级信息。
9. **完整流程时**:是否已执行 `flow2spec kb build --fix-topics` 或等价内部能力,并随后执行 `flow2spec kb check --strict`,确保存量 topic 已具备 `revision`。
10. **完整流程时**:是否已执行 **步骤 3b**:**融合** `index.md`(**主题一览**节起至命中与执行前为项目维护区,其余同包版),并核对 `topicPaths`;**完整流程末尾**是否已**回写** 项目侧 `projectRev = pkgRev`(`pkgRev=null` 则保留原值)。
11. **快速路径时**:步骤 3 / 3a / 3b 是否真的跳过(未做无关扫描),摘要中明确标注「快速路径下未执行」。
12. 是否输出了 manifest 与关键路径校验结果。
13. 若失败,是否给出下一步具体命令建议。
14. 步骤 3b 的 `index.md` 融合由主 agent 完成并落盘,无子 agent 越权写入(仅在完整流程时适用)。
15. 成功升级后是否删除 `.Knowledge/update-check.json`,避免当天新会话继续提示旧升级信息。