@fenglimg/fabric-server 2.0.0-rc.1 → 2.0.0-rc.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-NRWDWAVO.js → chunk-MMGHD42I.js} +1944 -271
- package/dist/{http-CHCOF6DJ.js → http-U5RL5Q6P.js} +12 -151
- package/dist/index.d.ts +131 -30
- package/dist/index.js +1306 -263
- package/package.json +4 -5
- package/dist/static/assets/index-DNSKn-El.js +0 -10
- package/dist/static/assets/index-FoBU5Kta.css +0 -1
- package/dist/static/index.html +0 -16
package/dist/index.js
CHANGED
|
@@ -3,32 +3,35 @@ import {
|
|
|
3
3
|
LEDGER_PATH,
|
|
4
4
|
LEGACY_LEDGER_PATH,
|
|
5
5
|
appendEventLedgerEvent,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
atomicWriteText,
|
|
7
|
+
buildKnowledgeMeta,
|
|
8
|
+
computeKnowledgeBasedAgentsMeta,
|
|
9
|
+
computeKnowledgeTestIndex,
|
|
10
|
+
deriveKnowledgeMetaLayer,
|
|
11
|
+
deriveKnowledgeMetaTopologyType,
|
|
12
|
+
ensureKnowledgeFresh,
|
|
13
|
+
ensureParentDirectory,
|
|
13
14
|
flushAndSyncEventLedger,
|
|
14
15
|
getEventLedgerPath,
|
|
15
16
|
getLedgerPath,
|
|
16
17
|
getLegacyLedgerPath,
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
isSameKnowledgeTestIndex,
|
|
19
|
+
normalizeKnowledgePath,
|
|
19
20
|
readAgentsMeta,
|
|
20
|
-
|
|
21
|
+
reconcileKnowledge,
|
|
21
22
|
resolveProjectRoot,
|
|
23
|
+
runDoctorApplyLint,
|
|
22
24
|
runDoctorFix,
|
|
23
25
|
runDoctorReport,
|
|
26
|
+
sha256,
|
|
24
27
|
stableStringify,
|
|
25
|
-
|
|
26
|
-
} from "./chunk-
|
|
28
|
+
writeKnowledgeMeta
|
|
29
|
+
} from "./chunk-MMGHD42I.js";
|
|
27
30
|
|
|
28
31
|
// src/index.ts
|
|
29
|
-
import { existsSync as
|
|
30
|
-
import { readFile as
|
|
31
|
-
import { join as
|
|
32
|
+
import { existsSync as existsSync4 } from "fs";
|
|
33
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
34
|
+
import { join as join5, resolve as resolve2 } from "path";
|
|
32
35
|
import { fileURLToPath } from "url";
|
|
33
36
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
34
37
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -54,15 +57,15 @@ function createInFlightTracker() {
|
|
|
54
57
|
drain(deadlineMs) {
|
|
55
58
|
const startedWith = active.size;
|
|
56
59
|
if (startedWith === 0) return Promise.resolve({ drained: 0, timed_out: 0 });
|
|
57
|
-
return new Promise((
|
|
60
|
+
return new Promise((resolve3) => {
|
|
58
61
|
const timer = setTimeout(() => {
|
|
59
62
|
const drainedCount = startedWith - active.size;
|
|
60
63
|
resolveDrained = null;
|
|
61
|
-
|
|
64
|
+
resolve3({ drained: drainedCount, timed_out: active.size });
|
|
62
65
|
}, deadlineMs);
|
|
63
66
|
resolveDrained = () => {
|
|
64
67
|
clearTimeout(timer);
|
|
65
|
-
|
|
68
|
+
resolve3({ drained: startedWith, timed_out: 0 });
|
|
66
69
|
};
|
|
67
70
|
});
|
|
68
71
|
},
|
|
@@ -72,12 +75,12 @@ function createInFlightTracker() {
|
|
|
72
75
|
};
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
// src/tools/
|
|
78
|
+
// src/tools/extract-knowledge.ts
|
|
76
79
|
import { randomUUID } from "crypto";
|
|
77
80
|
import {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
FabExtractKnowledgeInputShape,
|
|
82
|
+
FabExtractKnowledgeOutputSchema,
|
|
83
|
+
fabExtractKnowledgeAnnotations
|
|
81
84
|
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
82
85
|
import { enforcePayloadLimit } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
83
86
|
|
|
@@ -99,48 +102,381 @@ function readPayloadLimits(projectRoot) {
|
|
|
99
102
|
return readFabricConfig(projectRoot).mcpPayloadLimits;
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
// src/services/extract-knowledge.ts
|
|
106
|
+
import { existsSync as existsSync2 } from "fs";
|
|
107
|
+
import { readFile } from "fs/promises";
|
|
108
|
+
import { homedir } from "os";
|
|
109
|
+
import { join as join2, relative } from "path";
|
|
110
|
+
import {
|
|
111
|
+
PROPOSED_REASON_DESCRIPTIONS
|
|
112
|
+
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
113
|
+
var TEAM_PENDING_REL = ".fabric/knowledge/pending";
|
|
114
|
+
var SLUG_MAX_LENGTH = 40;
|
|
115
|
+
function pendingBase(layer, projectRoot) {
|
|
116
|
+
if (layer === "personal") {
|
|
117
|
+
return join2(resolvePersonalRoot(), ".fabric", "knowledge", "pending");
|
|
118
|
+
}
|
|
119
|
+
return join2(projectRoot, TEAM_PENDING_REL);
|
|
120
|
+
}
|
|
121
|
+
function resolvePersonalRoot() {
|
|
122
|
+
return process.env.FABRIC_HOME ?? homedir();
|
|
123
|
+
}
|
|
124
|
+
async function extractKnowledge(projectRoot, input) {
|
|
125
|
+
const sanitizedSlug = sanitizeSlug(input.slug);
|
|
126
|
+
const sourceSessions = Array.isArray(input.source_sessions) && input.source_sessions.length > 0 ? input.source_sessions : input.source_session !== void 0 && input.source_session.length > 0 ? [input.source_session] : [];
|
|
127
|
+
const primarySession = sourceSessions[0] ?? "";
|
|
128
|
+
const idempotencyKey = sha256(
|
|
129
|
+
JSON.stringify({
|
|
130
|
+
source_session: primarySession,
|
|
131
|
+
type: input.type,
|
|
132
|
+
slug: sanitizedSlug
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
const summary = input.user_messages_summary ?? "";
|
|
136
|
+
const summaryIsEmpty = summary.trim().length === 0;
|
|
137
|
+
const slugIsEmpty = sanitizedSlug.length === 0;
|
|
138
|
+
if (summaryIsEmpty || slugIsEmpty) {
|
|
139
|
+
await emitEventBestEffort(projectRoot, {
|
|
140
|
+
event_type: "knowledge_archive_attempted",
|
|
141
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
142
|
+
correlation_id: primarySession,
|
|
143
|
+
session_id: primarySession,
|
|
144
|
+
reason: `extract_knowledge:${sanitizedSlug || input.slug}`
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
pending_path: "",
|
|
148
|
+
idempotency_key: idempotencyKey
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const layer = input.layer ?? "team";
|
|
152
|
+
let relevanceScope = input.relevance_scope;
|
|
153
|
+
let relevancePaths = input.relevance_paths;
|
|
154
|
+
const shouldAutoDegrade = layer === "personal" && relevanceScope === "narrow";
|
|
155
|
+
if (shouldAutoDegrade) {
|
|
156
|
+
relevanceScope = "broad";
|
|
157
|
+
relevancePaths = [];
|
|
158
|
+
await emitEventBestEffort(projectRoot, {
|
|
159
|
+
event_type: "knowledge_scope_degraded",
|
|
160
|
+
stable_id: `pending:${idempotencyKey}`,
|
|
161
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
162
|
+
from_scope: "narrow",
|
|
163
|
+
to_scope: "broad",
|
|
164
|
+
reason: "personal-implies-broad"
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const baseDir = pendingBase(layer, projectRoot);
|
|
168
|
+
const absolutePath = join2(baseDir, input.type, `${sanitizedSlug}.md`);
|
|
169
|
+
const reportedPath = layer === "personal" ? `~/${relative(resolvePersonalRoot(), absolutePath)}` : relative(projectRoot, absolutePath);
|
|
170
|
+
await ensureParentDirectory(absolutePath);
|
|
171
|
+
if (existsSync2(absolutePath)) {
|
|
172
|
+
const existing = await readFile(absolutePath, "utf8");
|
|
173
|
+
const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
|
|
174
|
+
if (existingKey === idempotencyKey) {
|
|
175
|
+
const augmented = mergeEvidenceNotes(existing, summary, input.recent_paths);
|
|
176
|
+
await atomicWriteText(absolutePath, augmented);
|
|
177
|
+
await emitEventBestEffort(projectRoot, {
|
|
178
|
+
event_type: "knowledge_proposed",
|
|
179
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
180
|
+
correlation_id: primarySession,
|
|
181
|
+
session_id: primarySession,
|
|
182
|
+
reason: `extract_knowledge:${sanitizedSlug}`
|
|
183
|
+
});
|
|
184
|
+
return {
|
|
185
|
+
pending_path: reportedPath,
|
|
186
|
+
idempotency_key: idempotencyKey
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
await emitEventBestEffort(projectRoot, {
|
|
190
|
+
event_type: "knowledge_archive_attempted",
|
|
191
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
192
|
+
correlation_id: primarySession,
|
|
193
|
+
session_id: primarySession,
|
|
194
|
+
reason: `extract_knowledge:${sanitizedSlug}: slug-collision (existing key ${existingKey ?? "<none>"} != incoming ${idempotencyKey})`
|
|
195
|
+
});
|
|
196
|
+
throw new Error(
|
|
197
|
+
`slug collision: pending file ${reportedPath} already exists with a different idempotency_key (existing=${existingKey ?? "<missing>"}, incoming=${idempotencyKey}); rename slug or resolve upstream`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const fresh = renderFreshEntry({
|
|
201
|
+
type: input.type,
|
|
202
|
+
sourceSessions,
|
|
203
|
+
idempotencyKey,
|
|
204
|
+
summary,
|
|
205
|
+
recentPaths: input.recent_paths,
|
|
206
|
+
layer,
|
|
207
|
+
proposedReason: input.proposed_reason,
|
|
208
|
+
sessionContext: input.session_context,
|
|
209
|
+
relevanceScope,
|
|
210
|
+
relevancePaths
|
|
211
|
+
});
|
|
212
|
+
await atomicWriteText(absolutePath, fresh);
|
|
213
|
+
await emitEventBestEffort(projectRoot, {
|
|
214
|
+
event_type: "knowledge_proposed",
|
|
215
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
216
|
+
correlation_id: primarySession,
|
|
217
|
+
session_id: primarySession,
|
|
218
|
+
reason: `extract_knowledge:${sanitizedSlug}`
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
pending_path: reportedPath,
|
|
222
|
+
idempotency_key: idempotencyKey
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function sanitizeSlug(raw) {
|
|
226
|
+
const lower = raw.toLowerCase();
|
|
227
|
+
const collapsed = lower.replace(/[^a-z0-9]+/g, "-");
|
|
228
|
+
const trimmed = collapsed.replace(/^-+|-+$/g, "");
|
|
229
|
+
if (trimmed.length === 0) {
|
|
230
|
+
return "";
|
|
231
|
+
}
|
|
232
|
+
return trimmed.slice(0, SLUG_MAX_LENGTH).replace(/-+$/g, "");
|
|
233
|
+
}
|
|
234
|
+
function renderFreshEntry(args) {
|
|
235
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
236
|
+
const frontmatterLines = [
|
|
237
|
+
"---",
|
|
238
|
+
`type: ${args.type}`,
|
|
239
|
+
"maturity: draft",
|
|
240
|
+
`layer: ${args.layer}`,
|
|
241
|
+
`created_at: ${createdAt}`,
|
|
242
|
+
`source_sessions: [${args.sourceSessions.map((s) => JSON.stringify(s)).join(", ")}]`,
|
|
243
|
+
`proposed_reason: ${args.proposedReason}`,
|
|
244
|
+
"tags: []"
|
|
245
|
+
];
|
|
246
|
+
if (args.relevanceScope !== void 0) {
|
|
247
|
+
frontmatterLines.push(`relevance_scope: ${args.relevanceScope}`);
|
|
248
|
+
}
|
|
249
|
+
if (args.relevancePaths !== void 0) {
|
|
250
|
+
const pathsBody = args.relevancePaths.map((p) => quoteRelevancePath(p)).join(", ");
|
|
251
|
+
frontmatterLines.push(`relevance_paths: [${pathsBody}]`);
|
|
252
|
+
}
|
|
253
|
+
frontmatterLines.push(
|
|
254
|
+
`x-fabric-idempotency-key: ${args.idempotencyKey}`,
|
|
255
|
+
"---"
|
|
256
|
+
);
|
|
257
|
+
const frontmatter = frontmatterLines.join("\n");
|
|
258
|
+
const reasonExplanation = PROPOSED_REASON_DESCRIPTIONS[args.proposedReason];
|
|
259
|
+
const body = [
|
|
260
|
+
"",
|
|
261
|
+
"## Summary",
|
|
262
|
+
"",
|
|
263
|
+
args.summary,
|
|
264
|
+
"",
|
|
265
|
+
"## Why proposed",
|
|
266
|
+
"",
|
|
267
|
+
`${args.proposedReason} \u2014 ${reasonExplanation}`,
|
|
268
|
+
"",
|
|
269
|
+
"## Session context",
|
|
270
|
+
"",
|
|
271
|
+
args.sessionContext,
|
|
272
|
+
"",
|
|
273
|
+
"## Evidence",
|
|
274
|
+
"",
|
|
275
|
+
renderEvidenceBlock(args.summary, args.recentPaths),
|
|
276
|
+
""
|
|
277
|
+
].join("\n");
|
|
278
|
+
return `${frontmatter}
|
|
279
|
+
${body}`;
|
|
280
|
+
}
|
|
281
|
+
function quoteRelevancePath(value) {
|
|
282
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
283
|
+
}
|
|
284
|
+
function renderEvidenceBlock(summary, recentPaths) {
|
|
285
|
+
const pathLines = recentPaths.length === 0 ? "_(no recent paths reported)_" : recentPaths.map((p) => `- ${p}`).join("\n");
|
|
286
|
+
return [
|
|
287
|
+
"Recent paths:",
|
|
288
|
+
"",
|
|
289
|
+
pathLines,
|
|
290
|
+
"",
|
|
291
|
+
"Notes:",
|
|
292
|
+
"",
|
|
293
|
+
`- ${summary.trim()}`
|
|
294
|
+
].join("\n");
|
|
295
|
+
}
|
|
296
|
+
function mergeEvidenceNotes(existing, newSummary, newRecentPaths) {
|
|
297
|
+
const beforeMatch = /^([\s\S]*?)(\n## Evidence(?:\s*\(call \d+\))?\s*\n)/u.exec(
|
|
298
|
+
existing.endsWith("\n") ? existing : `${existing}
|
|
299
|
+
`
|
|
300
|
+
);
|
|
301
|
+
if (beforeMatch === null) {
|
|
302
|
+
const trimmed = existing.endsWith("\n") ? existing : `${existing}
|
|
303
|
+
`;
|
|
304
|
+
return `${trimmed}
|
|
305
|
+
## Evidence
|
|
306
|
+
|
|
307
|
+
${renderEvidenceBlock(newSummary, newRecentPaths)}
|
|
308
|
+
`;
|
|
309
|
+
}
|
|
310
|
+
const head = beforeMatch[1] ?? "";
|
|
311
|
+
const existingNotes = [];
|
|
312
|
+
const existingPaths = [];
|
|
313
|
+
const evidenceBlockRe = /\n## Evidence(?:\s*\(call \d+\))?\s*\n([\s\S]*?)(?=\n## |$)/gu;
|
|
314
|
+
let m;
|
|
315
|
+
while ((m = evidenceBlockRe.exec(`${existing}
|
|
316
|
+
`)) !== null) {
|
|
317
|
+
const block = m[1] ?? "";
|
|
318
|
+
const pathSection = /Recent paths:\s*\n([\s\S]*?)(?:\n\s*Notes:|$)/u.exec(block);
|
|
319
|
+
if (pathSection !== null) {
|
|
320
|
+
for (const rawLine of (pathSection[1] ?? "").split(/\r?\n/u)) {
|
|
321
|
+
const t = rawLine.trim();
|
|
322
|
+
if (t.startsWith("- ")) {
|
|
323
|
+
existingPaths.push(t.slice(2).trim());
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const notesSection = /Notes:\s*\n([\s\S]*?)$/u.exec(block);
|
|
328
|
+
const noteBody = (notesSection !== null ? notesSection[1] : block) ?? "";
|
|
329
|
+
const bulletLines = [];
|
|
330
|
+
let prose = [];
|
|
331
|
+
for (const rawLine of noteBody.split(/\r?\n/u)) {
|
|
332
|
+
const t = rawLine.trim();
|
|
333
|
+
if (t.length === 0) continue;
|
|
334
|
+
if (t.startsWith("- ")) {
|
|
335
|
+
if (prose.length > 0) {
|
|
336
|
+
existingNotes.push(prose.join(" ").trim());
|
|
337
|
+
prose = [];
|
|
338
|
+
}
|
|
339
|
+
bulletLines.push(t.slice(2).trim());
|
|
340
|
+
} else {
|
|
341
|
+
prose.push(t);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (prose.length > 0) existingNotes.push(prose.join(" ").trim());
|
|
345
|
+
for (const n of bulletLines) existingNotes.push(n);
|
|
346
|
+
}
|
|
347
|
+
const mergedNotes = [];
|
|
348
|
+
const seenNotes = /* @__PURE__ */ new Set();
|
|
349
|
+
const incomingNote = newSummary.trim();
|
|
350
|
+
const candidates = [...existingNotes, incomingNote];
|
|
351
|
+
for (const note of candidates) {
|
|
352
|
+
const key = note.replace(/\s+/gu, " ").trim();
|
|
353
|
+
if (key.length === 0) continue;
|
|
354
|
+
if (seenNotes.has(key)) continue;
|
|
355
|
+
seenNotes.add(key);
|
|
356
|
+
mergedNotes.push(note);
|
|
357
|
+
}
|
|
358
|
+
const mergedPaths = [];
|
|
359
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
360
|
+
for (const p of [...existingPaths, ...newRecentPaths]) {
|
|
361
|
+
const key = p.trim();
|
|
362
|
+
if (key.length === 0) continue;
|
|
363
|
+
if (seenPaths.has(key)) continue;
|
|
364
|
+
seenPaths.add(key);
|
|
365
|
+
mergedPaths.push(key);
|
|
366
|
+
}
|
|
367
|
+
const pathLines = mergedPaths.length === 0 ? "_(no recent paths reported)_" : mergedPaths.map((p) => `- ${p}`).join("\n");
|
|
368
|
+
const noteLines = mergedNotes.length === 0 ? "_(no notes recorded)_" : mergedNotes.map((n) => `- ${n}`).join("\n");
|
|
369
|
+
const evidenceBody = [
|
|
370
|
+
"Recent paths:",
|
|
371
|
+
"",
|
|
372
|
+
pathLines,
|
|
373
|
+
"",
|
|
374
|
+
"Notes:",
|
|
375
|
+
"",
|
|
376
|
+
noteLines
|
|
377
|
+
].join("\n");
|
|
378
|
+
return `${head}
|
|
379
|
+
## Evidence
|
|
380
|
+
|
|
381
|
+
${evidenceBody}
|
|
382
|
+
`;
|
|
383
|
+
}
|
|
384
|
+
function readFrontmatterKey(content, key) {
|
|
385
|
+
const match = /^---\n([\s\S]*?)\n---/u.exec(content);
|
|
386
|
+
if (match === null) {
|
|
387
|
+
return void 0;
|
|
388
|
+
}
|
|
389
|
+
const block = match[1];
|
|
390
|
+
if (block === void 0) {
|
|
391
|
+
return void 0;
|
|
392
|
+
}
|
|
393
|
+
for (const rawLine of block.split(/\r?\n/u)) {
|
|
394
|
+
const line = rawLine.trim();
|
|
395
|
+
const sep = line.indexOf(":");
|
|
396
|
+
if (sep === -1) continue;
|
|
397
|
+
const k = line.slice(0, sep).trim();
|
|
398
|
+
if (k === key) {
|
|
399
|
+
return line.slice(sep + 1).trim();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return void 0;
|
|
403
|
+
}
|
|
404
|
+
async function emitEventBestEffort(projectRoot, event) {
|
|
405
|
+
try {
|
|
406
|
+
await appendEventLedgerEvent(projectRoot, event);
|
|
407
|
+
} catch {
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/tools/extract-knowledge.ts
|
|
412
|
+
function registerExtractKnowledge(server, tracker) {
|
|
413
|
+
server.registerTool(
|
|
414
|
+
"fab_extract_knowledge",
|
|
415
|
+
{
|
|
416
|
+
description: "Persist a proposed pending knowledge entry under .fabric/knowledge/pending/<type>/<slug>.md. Idempotent on (source_session, type, slug); repeat calls append evidence rather than overwrite. Skill-side tool \u2014 invoked at session-stop.",
|
|
417
|
+
inputSchema: FabExtractKnowledgeInputShape,
|
|
418
|
+
outputSchema: FabExtractKnowledgeOutputSchema.shape,
|
|
419
|
+
annotations: fabExtractKnowledgeAnnotations
|
|
420
|
+
},
|
|
421
|
+
async (input) => {
|
|
422
|
+
const requestId = randomUUID();
|
|
423
|
+
tracker?.enter(requestId);
|
|
424
|
+
try {
|
|
425
|
+
const projectRoot = resolveProjectRoot();
|
|
426
|
+
const result = await extractKnowledge(projectRoot, input);
|
|
427
|
+
const response = { ...result };
|
|
428
|
+
const payloadLimits = readPayloadLimits(projectRoot);
|
|
429
|
+
const serialized = JSON.stringify(response);
|
|
430
|
+
enforcePayloadLimit(serialized, payloadLimits);
|
|
431
|
+
return {
|
|
432
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
433
|
+
structuredContent: response
|
|
434
|
+
};
|
|
435
|
+
} finally {
|
|
436
|
+
tracker?.exit(requestId);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/tools/plan-context.ts
|
|
443
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
444
|
+
import {
|
|
445
|
+
planContextAnnotations,
|
|
446
|
+
planContextInputSchema,
|
|
447
|
+
planContextOutputSchema
|
|
448
|
+
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
449
|
+
import { enforcePayloadLimit as enforcePayloadLimit2 } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
450
|
+
|
|
102
451
|
// src/services/plan-context.ts
|
|
452
|
+
import { minimatch } from "minimatch";
|
|
453
|
+
import { deriveAgentsMetaLayer } from "@fenglimg/fabric-shared";
|
|
103
454
|
var SELECTION_TOKEN_TTL_MS = 5 * 60 * 1e3;
|
|
104
455
|
var selectionTokenCache = /* @__PURE__ */ new Map();
|
|
105
456
|
async function planContext(projectRoot, input) {
|
|
106
457
|
const meta = await readAgentsMeta(projectRoot);
|
|
107
458
|
const stale = input.client_hash !== void 0 && input.client_hash !== meta.revision;
|
|
108
459
|
const uniquePaths = dedupePaths(input.paths);
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
(item) => includeDeprecated ? true : !isDeprecatedMaturity(item)
|
|
112
|
-
);
|
|
460
|
+
const allDescriptions = buildDescriptionIndex(meta);
|
|
461
|
+
const relevanceTargetPaths = input.target_paths ?? uniquePaths;
|
|
113
462
|
const entries = uniquePaths.map((path2) => {
|
|
114
463
|
const profile = buildRequirementProfile(path2, input);
|
|
115
|
-
const descriptionIndex = allDescriptions.filter((item) => shouldIncludeIndexItemForPath(item, meta, path2));
|
|
116
|
-
const requiredStableIds2 = descriptionIndex.filter((item) => item.required).map((item) => item.stable_id);
|
|
117
|
-
const aiSelectableStableIds2 = descriptionIndex.filter((item) => item.selectable).map((item) => item.stable_id);
|
|
464
|
+
const descriptionIndex = allDescriptions.filter((item) => shouldIncludeIndexItemForPath(item, meta, path2)).filter((item) => shouldIncludeByRelevance(item, relevanceTargetPaths));
|
|
118
465
|
return {
|
|
119
466
|
path: path2,
|
|
120
467
|
requirement_profile: profile,
|
|
121
|
-
description_index: descriptionIndex
|
|
122
|
-
required_stable_ids: requiredStableIds2,
|
|
123
|
-
ai_selectable_stable_ids: aiSelectableStableIds2,
|
|
124
|
-
initial_selected_stable_ids: requiredStableIds2,
|
|
125
|
-
selection_policy: {
|
|
126
|
-
required_levels: ["L0", "L2"],
|
|
127
|
-
ai_selectable_levels: ["L1"],
|
|
128
|
-
final_fetch_rule: "required_stable_ids + ai_selected_l1_stable_ids"
|
|
129
|
-
}
|
|
468
|
+
description_index: descriptionIndex
|
|
130
469
|
};
|
|
131
470
|
});
|
|
132
|
-
const requiredStableIds = dedupeStableIds(entries.flatMap((entry) => entry.required_stable_ids));
|
|
133
|
-
const aiSelectableStableIds = dedupeStableIds(entries.flatMap((entry) => entry.ai_selectable_stable_ids));
|
|
134
471
|
const sharedDescriptionIndex = dedupeDescriptionIndex(entries.flatMap((entry) => entry.description_index));
|
|
135
|
-
const
|
|
472
|
+
const sharedStableIds = sharedDescriptionIndex.map((item) => item.stable_id);
|
|
473
|
+
const selectionToken = createSelectionToken(meta.revision, uniquePaths, [], sharedStableIds);
|
|
136
474
|
const result = {
|
|
137
475
|
revision_hash: meta.revision,
|
|
138
476
|
stale,
|
|
139
477
|
selection_token: selectionToken,
|
|
140
478
|
entries,
|
|
141
479
|
shared: {
|
|
142
|
-
required_stable_ids: requiredStableIds,
|
|
143
|
-
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
144
480
|
description_index: sharedDescriptionIndex,
|
|
145
481
|
preflight_diagnostics: buildPreflightDiagnostics(meta)
|
|
146
482
|
}
|
|
@@ -149,9 +485,9 @@ async function planContext(projectRoot, input) {
|
|
|
149
485
|
await appendEventLedgerEvent(projectRoot, {
|
|
150
486
|
event_type: "knowledge_context_planned",
|
|
151
487
|
target_paths: uniquePaths,
|
|
152
|
-
required_stable_ids:
|
|
153
|
-
ai_selectable_stable_ids:
|
|
154
|
-
final_stable_ids:
|
|
488
|
+
required_stable_ids: [],
|
|
489
|
+
ai_selectable_stable_ids: sharedDescriptionIndex.map((item) => item.stable_id),
|
|
490
|
+
final_stable_ids: [],
|
|
155
491
|
selection_token: selectionToken,
|
|
156
492
|
client_hash: input.client_hash,
|
|
157
493
|
intent: input.intent,
|
|
@@ -191,7 +527,7 @@ function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSe
|
|
|
191
527
|
function dedupePaths(paths) {
|
|
192
528
|
const seenPaths = /* @__PURE__ */ new Set();
|
|
193
529
|
return paths.flatMap((path2) => {
|
|
194
|
-
const normalizedPath =
|
|
530
|
+
const normalizedPath = normalizeKnowledgePath(path2);
|
|
195
531
|
if (seenPaths.has(normalizedPath)) {
|
|
196
532
|
return [];
|
|
197
533
|
}
|
|
@@ -200,7 +536,7 @@ function dedupePaths(paths) {
|
|
|
200
536
|
});
|
|
201
537
|
}
|
|
202
538
|
function buildRequirementProfile(path2, input) {
|
|
203
|
-
const normalizedPath =
|
|
539
|
+
const normalizedPath = normalizeKnowledgePath(path2);
|
|
204
540
|
const extensionMatch = /(\.[^./\\]+)$/u.exec(normalizedPath);
|
|
205
541
|
const knownTech = dedupeStableIds([
|
|
206
542
|
...input.known_tech ?? [],
|
|
@@ -210,17 +546,14 @@ function buildRequirementProfile(path2, input) {
|
|
|
210
546
|
target_path: normalizedPath,
|
|
211
547
|
path_segments: normalizedPath.split("/").filter(Boolean),
|
|
212
548
|
extension: extensionMatch?.[1] ?? "",
|
|
213
|
-
inferred_domain: inferDomains(normalizedPath),
|
|
214
549
|
known_tech: knownTech,
|
|
215
550
|
user_intent: input.intent ?? "",
|
|
216
|
-
intent_tokens: tokenizeIntent(input.intent ?? ""),
|
|
217
|
-
impact_hints: inferImpactHints(input.intent ?? ""),
|
|
218
551
|
detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path2] ?? []
|
|
219
552
|
};
|
|
220
553
|
}
|
|
221
554
|
function buildDescriptionIndex(meta) {
|
|
222
555
|
return Object.entries(meta.nodes).flatMap(([nodeId, node]) => {
|
|
223
|
-
const level = node.
|
|
556
|
+
const level = deriveAgentsMetaLayer(node.file);
|
|
224
557
|
const description = node.description ?? descriptionFromLegacyActivation(node.activation?.description);
|
|
225
558
|
if (description === void 0) {
|
|
226
559
|
return [];
|
|
@@ -229,16 +562,46 @@ function buildDescriptionIndex(meta) {
|
|
|
229
562
|
return [{
|
|
230
563
|
stable_id: node.stable_id ?? nodeId,
|
|
231
564
|
level,
|
|
232
|
-
required:
|
|
233
|
-
selectable:
|
|
565
|
+
required: false,
|
|
566
|
+
selectable: false,
|
|
234
567
|
description,
|
|
235
568
|
type: description.knowledge_type,
|
|
236
569
|
maturity: description.maturity,
|
|
237
570
|
layer: description.knowledge_layer ?? inferredLayer,
|
|
238
|
-
layer_reason: description.layer_reason
|
|
571
|
+
layer_reason: description.layer_reason,
|
|
572
|
+
// v2.0-rc.5 C3 (TASK-012): surface relevance fields at the top level
|
|
573
|
+
// so the per-entry filter + downstream MCP clients can read them
|
|
574
|
+
// without reaching into description.*. Defaults (broad + []) are
|
|
575
|
+
// applied at the meta-builder layer; we just pass them through here.
|
|
576
|
+
relevance_scope: description.relevance_scope,
|
|
577
|
+
relevance_paths: description.relevance_paths
|
|
239
578
|
}];
|
|
240
579
|
}).sort(compareDescriptionIndexItems);
|
|
241
580
|
}
|
|
581
|
+
function matchesAnyPath(globs, targetPaths) {
|
|
582
|
+
if (globs.length === 0) {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
for (const rawGlob of globs) {
|
|
586
|
+
const glob = rawGlob.endsWith("/") ? `${rawGlob}**` : rawGlob;
|
|
587
|
+
for (const target of targetPaths) {
|
|
588
|
+
if (minimatch(target, glob, { dot: true, matchBase: false })) {
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
function shouldIncludeByRelevance(item, targetPaths) {
|
|
596
|
+
const scope = item.relevance_scope ?? "broad";
|
|
597
|
+
if (scope === "broad") {
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
if (targetPaths.length === 0) {
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
return matchesAnyPath(item.relevance_paths ?? [], targetPaths);
|
|
604
|
+
}
|
|
242
605
|
function inferKnowledgeLayerFromContentRef(contentRef) {
|
|
243
606
|
if (contentRef === void 0) {
|
|
244
607
|
return void 0;
|
|
@@ -251,11 +614,6 @@ function inferKnowledgeLayerFromContentRef(contentRef) {
|
|
|
251
614
|
}
|
|
252
615
|
return void 0;
|
|
253
616
|
}
|
|
254
|
-
function isDeprecatedMaturity(item) {
|
|
255
|
-
const a = item.maturity;
|
|
256
|
-
const b = item.description.maturity;
|
|
257
|
-
return a === "deprecated" || b === "deprecated";
|
|
258
|
-
}
|
|
259
617
|
function descriptionFromLegacyActivation(summary) {
|
|
260
618
|
if (summary === void 0) {
|
|
261
619
|
return void 0;
|
|
@@ -268,24 +626,8 @@ function descriptionFromLegacyActivation(summary) {
|
|
|
268
626
|
must_read_if: summary
|
|
269
627
|
};
|
|
270
628
|
}
|
|
271
|
-
function shouldIncludeIndexItemForPath(
|
|
272
|
-
|
|
273
|
-
return true;
|
|
274
|
-
}
|
|
275
|
-
const node = Object.values(meta.nodes).find((candidate) => candidate.stable_id === item.stable_id);
|
|
276
|
-
if (node === void 0) {
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
return node.scope_glob === path2 || minimatchSimple(path2, node.scope_glob);
|
|
280
|
-
}
|
|
281
|
-
function minimatchSimple(path2, glob) {
|
|
282
|
-
if (glob === "**") {
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
if (glob.endsWith("/**")) {
|
|
286
|
-
return path2.startsWith(glob.slice(0, -3));
|
|
287
|
-
}
|
|
288
|
-
return path2 === glob;
|
|
629
|
+
function shouldIncludeIndexItemForPath(_item, _meta, _path) {
|
|
630
|
+
return true;
|
|
289
631
|
}
|
|
290
632
|
function buildPreflightDiagnostics(meta) {
|
|
291
633
|
const missingDescriptionStableIds = Object.entries(meta.nodes).filter(([, node]) => node.description === void 0 && node.activation?.description === void 0).map(([nodeId, node]) => node.stable_id ?? nodeId).sort();
|
|
@@ -299,26 +641,6 @@ function buildPreflightDiagnostics(meta) {
|
|
|
299
641
|
message: `Resolved registry includes ${missingDescriptionStableIds.length} node(s) without structured descriptions.`
|
|
300
642
|
}];
|
|
301
643
|
}
|
|
302
|
-
function inferDomains(path2) {
|
|
303
|
-
const domains = [];
|
|
304
|
-
if (path2.includes("/ui/") || path2.toLowerCase().includes("ui")) {
|
|
305
|
-
domains.push("UI");
|
|
306
|
-
}
|
|
307
|
-
if (path2.includes("assets/scripts")) {
|
|
308
|
-
domains.push("Gameplay");
|
|
309
|
-
}
|
|
310
|
-
if (path2.includes("resources") || path2.includes("assets/resources")) {
|
|
311
|
-
domains.push("Asset");
|
|
312
|
-
}
|
|
313
|
-
return domains;
|
|
314
|
-
}
|
|
315
|
-
function tokenizeIntent(intent) {
|
|
316
|
-
const tokens = ["\u6027\u80FD", "\u4F18\u5316", "drawcall", "\u6E32\u67D3", "\u5361\u987F", "\u95EA\u70C1", "\u754C\u9762", "UI", "\u8D44\u6E90", "\u56FE\u96C6"].filter((token) => intent.toLowerCase().includes(token.toLowerCase()));
|
|
317
|
-
return dedupeStableIds(tokens);
|
|
318
|
-
}
|
|
319
|
-
function inferImpactHints(intent) {
|
|
320
|
-
return /性能|优化|drawcall|渲染|卡顿|闪烁/iu.test(intent) ? ["Performance"] : [];
|
|
321
|
-
}
|
|
322
644
|
function dedupeStableIds(stableIds) {
|
|
323
645
|
return Array.from(new Set(stableIds));
|
|
324
646
|
}
|
|
@@ -333,18 +655,7 @@ function dedupeDescriptionIndex(items) {
|
|
|
333
655
|
});
|
|
334
656
|
}
|
|
335
657
|
function compareDescriptionIndexItems(left, right) {
|
|
336
|
-
|
|
337
|
-
return levelDelta !== 0 ? levelDelta : left.stable_id.localeCompare(right.stable_id);
|
|
338
|
-
}
|
|
339
|
-
function levelOrder(level) {
|
|
340
|
-
switch (level) {
|
|
341
|
-
case "L0":
|
|
342
|
-
return 0;
|
|
343
|
-
case "L1":
|
|
344
|
-
return 1;
|
|
345
|
-
case "L2":
|
|
346
|
-
return 2;
|
|
347
|
-
}
|
|
658
|
+
return left.stable_id.localeCompare(right.stable_id);
|
|
348
659
|
}
|
|
349
660
|
|
|
350
661
|
// src/tools/plan-context.ts
|
|
@@ -357,12 +668,12 @@ function registerPlanContext(server, tracker) {
|
|
|
357
668
|
outputSchema: planContextOutputSchema,
|
|
358
669
|
annotations: planContextAnnotations
|
|
359
670
|
},
|
|
360
|
-
async ({ paths, intent, known_tech, detected_entities, client_hash, correlation_id, session_id
|
|
361
|
-
const requestId =
|
|
671
|
+
async ({ paths, intent, known_tech, detected_entities, client_hash, correlation_id, session_id }) => {
|
|
672
|
+
const requestId = randomUUID2();
|
|
362
673
|
tracker?.enter(requestId);
|
|
363
674
|
try {
|
|
364
675
|
const projectRoot = resolveProjectRoot();
|
|
365
|
-
const syncReport = await
|
|
676
|
+
const syncReport = await ensureKnowledgeFresh(projectRoot);
|
|
366
677
|
const result = await planContext(projectRoot, {
|
|
367
678
|
paths,
|
|
368
679
|
intent,
|
|
@@ -370,8 +681,7 @@ function registerPlanContext(server, tracker) {
|
|
|
370
681
|
detected_entities,
|
|
371
682
|
client_hash,
|
|
372
683
|
correlation_id,
|
|
373
|
-
session_id
|
|
374
|
-
include_deprecated
|
|
684
|
+
session_id
|
|
375
685
|
});
|
|
376
686
|
const response = {
|
|
377
687
|
...result,
|
|
@@ -379,7 +689,7 @@ function registerPlanContext(server, tracker) {
|
|
|
379
689
|
};
|
|
380
690
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
381
691
|
const serialized = JSON.stringify(response);
|
|
382
|
-
const guardResult =
|
|
692
|
+
const guardResult = enforcePayloadLimit2(serialized, payloadLimits);
|
|
383
693
|
if (guardResult.warning) {
|
|
384
694
|
response.warnings = [
|
|
385
695
|
...response.warnings,
|
|
@@ -401,20 +711,800 @@ function registerPlanContext(server, tracker) {
|
|
|
401
711
|
);
|
|
402
712
|
}
|
|
403
713
|
|
|
404
|
-
// src/tools/
|
|
405
|
-
import { randomUUID as
|
|
714
|
+
// src/tools/review.ts
|
|
715
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
406
716
|
import {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
717
|
+
FabReviewInputSchema,
|
|
718
|
+
FabReviewInputShape,
|
|
719
|
+
FabReviewOutputShape,
|
|
720
|
+
fabReviewAnnotations
|
|
410
721
|
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
411
|
-
import { enforcePayloadLimit as
|
|
722
|
+
import { enforcePayloadLimit as enforcePayloadLimit3 } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
412
723
|
|
|
413
|
-
// src/services/
|
|
414
|
-
import {
|
|
415
|
-
import {
|
|
416
|
-
import {
|
|
417
|
-
|
|
724
|
+
// src/services/review.ts
|
|
725
|
+
import { execFileSync } from "child_process";
|
|
726
|
+
import { existsSync as existsSync3 } from "fs";
|
|
727
|
+
import { readFile as readFile3, readdir, unlink } from "fs/promises";
|
|
728
|
+
import { homedir as homedir2 } from "os";
|
|
729
|
+
import { basename, join as join3, relative as relative2, resolve } from "path";
|
|
730
|
+
|
|
731
|
+
// src/services/knowledge-id-allocator.ts
|
|
732
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
733
|
+
import { dirname } from "path";
|
|
734
|
+
import { mkdir } from "fs/promises";
|
|
735
|
+
import {
|
|
736
|
+
AgentsMetaCountersSchema,
|
|
737
|
+
agentsMetaSchema,
|
|
738
|
+
allocateKnowledgeId,
|
|
739
|
+
defaultAgentsMetaCounters
|
|
740
|
+
} from "@fenglimg/fabric-shared";
|
|
741
|
+
import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
742
|
+
var KnowledgeIdAllocator = class {
|
|
743
|
+
constructor(metaPath) {
|
|
744
|
+
this.metaPath = metaPath;
|
|
745
|
+
}
|
|
746
|
+
metaPath;
|
|
747
|
+
/**
|
|
748
|
+
* Allocate the next stable_id for the given (layer, type) pair and persist
|
|
749
|
+
* the advanced counter to `agents.meta.json`.
|
|
750
|
+
*/
|
|
751
|
+
async allocate(layer, type) {
|
|
752
|
+
const meta = await this.readMeta();
|
|
753
|
+
const counters = this.normalizeCounters(meta.counters);
|
|
754
|
+
const { id, nextCounters } = allocateKnowledgeId(layer, type, counters);
|
|
755
|
+
await this.writeMetaAtomic({ ...meta, counters: nextCounters });
|
|
756
|
+
return id;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Returns the current counters envelope, defaulting to all-zero slots when
|
|
760
|
+
* the meta file is absent or pre-v2.0 (counters key missing).
|
|
761
|
+
*/
|
|
762
|
+
async getCounters() {
|
|
763
|
+
const meta = await this.readMeta();
|
|
764
|
+
return this.normalizeCounters(meta.counters);
|
|
765
|
+
}
|
|
766
|
+
// ---- internal helpers ------------------------------------------------
|
|
767
|
+
async readMeta() {
|
|
768
|
+
let raw;
|
|
769
|
+
try {
|
|
770
|
+
raw = await readFile2(this.metaPath, "utf8");
|
|
771
|
+
} catch (err) {
|
|
772
|
+
if (isNodeError(err) && err.code === "ENOENT") {
|
|
773
|
+
return { revision: "", nodes: {} };
|
|
774
|
+
}
|
|
775
|
+
throw err;
|
|
776
|
+
}
|
|
777
|
+
let parsed;
|
|
778
|
+
try {
|
|
779
|
+
parsed = JSON.parse(raw);
|
|
780
|
+
} catch {
|
|
781
|
+
return { revision: "", nodes: {} };
|
|
782
|
+
}
|
|
783
|
+
const validation = agentsMetaSchema.safeParse(parsed);
|
|
784
|
+
if (validation.success) {
|
|
785
|
+
return validation.data;
|
|
786
|
+
}
|
|
787
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
788
|
+
}
|
|
789
|
+
normalizeCounters(input) {
|
|
790
|
+
if (input === void 0 || input === null) {
|
|
791
|
+
return defaultAgentsMetaCounters();
|
|
792
|
+
}
|
|
793
|
+
const parsed = AgentsMetaCountersSchema.safeParse(input);
|
|
794
|
+
return parsed.success ? parsed.data : defaultAgentsMetaCounters();
|
|
795
|
+
}
|
|
796
|
+
async writeMetaAtomic(meta) {
|
|
797
|
+
await ensureParentDirectory2(this.metaPath);
|
|
798
|
+
await atomicWriteJson(this.metaPath, meta, { indent: 2 });
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
async function ensureParentDirectory2(filePath) {
|
|
802
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
803
|
+
}
|
|
804
|
+
function isNodeError(err) {
|
|
805
|
+
return err instanceof Error && typeof err.code === "string";
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// src/services/review.ts
|
|
809
|
+
var PENDING_BASE_TEAM_REL = ".fabric/knowledge/pending";
|
|
810
|
+
function pendingBaseAbs(layer, projectRoot) {
|
|
811
|
+
if (layer === "personal") {
|
|
812
|
+
return join3(resolvePersonalRoot2(), ".fabric", "knowledge", "pending");
|
|
813
|
+
}
|
|
814
|
+
return join3(projectRoot, PENDING_BASE_TEAM_REL);
|
|
815
|
+
}
|
|
816
|
+
var PLURAL_TYPES = [
|
|
817
|
+
"decisions",
|
|
818
|
+
"pitfalls",
|
|
819
|
+
"guidelines",
|
|
820
|
+
"models",
|
|
821
|
+
"processes"
|
|
822
|
+
];
|
|
823
|
+
var PLURAL_TO_SINGULAR = {
|
|
824
|
+
decisions: "decision",
|
|
825
|
+
pitfalls: "pitfall",
|
|
826
|
+
guidelines: "guideline",
|
|
827
|
+
models: "model",
|
|
828
|
+
processes: "process"
|
|
829
|
+
};
|
|
830
|
+
async function reviewKnowledge(projectRoot, input) {
|
|
831
|
+
switch (input.action) {
|
|
832
|
+
case "list":
|
|
833
|
+
return {
|
|
834
|
+
action: "list",
|
|
835
|
+
items: await listPending(projectRoot, input.filters)
|
|
836
|
+
};
|
|
837
|
+
case "approve":
|
|
838
|
+
return {
|
|
839
|
+
action: "approve",
|
|
840
|
+
approved: await approveAll(projectRoot, input.pending_paths)
|
|
841
|
+
};
|
|
842
|
+
case "reject":
|
|
843
|
+
return {
|
|
844
|
+
action: "reject",
|
|
845
|
+
rejected: await rejectAll(projectRoot, input.pending_paths, input.reason)
|
|
846
|
+
};
|
|
847
|
+
case "modify":
|
|
848
|
+
return await modifyEntry(projectRoot, input.pending_path, input.changes);
|
|
849
|
+
case "search":
|
|
850
|
+
return {
|
|
851
|
+
action: "search",
|
|
852
|
+
items: await searchEntries(projectRoot, input.query, input.filters)
|
|
853
|
+
};
|
|
854
|
+
case "defer":
|
|
855
|
+
return {
|
|
856
|
+
action: "defer",
|
|
857
|
+
deferred: await deferAll(
|
|
858
|
+
projectRoot,
|
|
859
|
+
input.pending_paths,
|
|
860
|
+
input.until,
|
|
861
|
+
input.reason
|
|
862
|
+
)
|
|
863
|
+
};
|
|
864
|
+
default: {
|
|
865
|
+
const exhaustive = input;
|
|
866
|
+
throw new Error(`unsupported action: ${JSON.stringify(exhaustive)}`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
function resolveSandboxedPath(projectRoot, candidate, options = {}) {
|
|
871
|
+
if (candidate.length === 0) {
|
|
872
|
+
throw new Error("path is empty");
|
|
873
|
+
}
|
|
874
|
+
const projectKnowledgeRoot = resolve(projectRoot, ".fabric", "knowledge");
|
|
875
|
+
const personalKnowledgeRoot = resolve(resolvePersonalRoot2(), ".fabric", "knowledge");
|
|
876
|
+
if (candidate.startsWith("~/")) {
|
|
877
|
+
if (options.allowPersonal !== true) {
|
|
878
|
+
throw new Error(`personal-root path not allowed for this action: ${candidate}`);
|
|
879
|
+
}
|
|
880
|
+
const abs = resolve(resolvePersonalRoot2(), candidate.slice(2));
|
|
881
|
+
if (abs !== personalKnowledgeRoot && !abs.startsWith(personalKnowledgeRoot + "/")) {
|
|
882
|
+
throw new Error(`path escapes personal knowledge root: ${candidate}`);
|
|
883
|
+
}
|
|
884
|
+
return { abs, isInProjectTree: false };
|
|
885
|
+
}
|
|
886
|
+
const projectAbs = resolve(projectRoot, candidate);
|
|
887
|
+
if (projectAbs === projectKnowledgeRoot || projectAbs.startsWith(projectKnowledgeRoot + "/")) {
|
|
888
|
+
return { abs: projectAbs, isInProjectTree: true };
|
|
889
|
+
}
|
|
890
|
+
if (options.allowPersonal === true) {
|
|
891
|
+
const personalAbs = resolve(resolvePersonalRoot2(), candidate);
|
|
892
|
+
if (personalAbs === personalKnowledgeRoot || personalAbs.startsWith(personalKnowledgeRoot + "/")) {
|
|
893
|
+
return { abs: personalAbs, isInProjectTree: false };
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
throw new Error(`path escapes knowledge root: ${candidate}`);
|
|
897
|
+
}
|
|
898
|
+
async function listPending(projectRoot, filters) {
|
|
899
|
+
const items = [];
|
|
900
|
+
const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
|
|
901
|
+
const sources = [
|
|
902
|
+
{ origin: "team", root: pendingBaseAbs("team", projectRoot) },
|
|
903
|
+
{ origin: "personal", root: pendingBaseAbs("personal", projectRoot) }
|
|
904
|
+
];
|
|
905
|
+
for (const source of sources) {
|
|
906
|
+
for (const type of typesToScan) {
|
|
907
|
+
const dir = join3(source.root, type);
|
|
908
|
+
if (!existsSync3(dir)) {
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
let entries;
|
|
912
|
+
try {
|
|
913
|
+
entries = await readdir(dir);
|
|
914
|
+
} catch {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
for (const name of entries) {
|
|
918
|
+
if (!name.endsWith(".md")) continue;
|
|
919
|
+
const absolutePath = join3(dir, name);
|
|
920
|
+
let content;
|
|
921
|
+
try {
|
|
922
|
+
content = await readFile3(absolutePath, "utf8");
|
|
923
|
+
} catch {
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
const fm = parseFrontmatter(content);
|
|
927
|
+
const layer = fm.layer ?? (source.origin === "personal" ? "personal" : "team");
|
|
928
|
+
const maturity = fm.maturity ?? "draft";
|
|
929
|
+
if (filters?.layer !== void 0 && filters.layer !== "both" && filters.layer !== layer) {
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
if (filters?.maturity !== void 0 && filters.maturity !== maturity) {
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
if (filters?.tags !== void 0 && filters.tags.length > 0) {
|
|
936
|
+
const itemTags = fm.tags ?? [];
|
|
937
|
+
const hasAll = filters.tags.every((t) => itemTags.includes(t));
|
|
938
|
+
if (!hasAll) continue;
|
|
939
|
+
}
|
|
940
|
+
if (filters?.created_after !== void 0) {
|
|
941
|
+
const createdAt = fm.created_at;
|
|
942
|
+
if (createdAt === void 0 || createdAt < filters.created_after) {
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const reportedPath = source.origin === "personal" ? `~/${relative2(resolvePersonalRoot2(), absolutePath)}` : relative2(projectRoot, absolutePath);
|
|
947
|
+
items.push({
|
|
948
|
+
pending_path: reportedPath,
|
|
949
|
+
type,
|
|
950
|
+
layer,
|
|
951
|
+
maturity,
|
|
952
|
+
origin: source.origin,
|
|
953
|
+
...fm.tags !== void 0 && fm.tags.length > 0 ? { tags: fm.tags } : {}
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return items;
|
|
959
|
+
}
|
|
960
|
+
async function approveAll(projectRoot, pendingPaths) {
|
|
961
|
+
const allocator = new KnowledgeIdAllocator(
|
|
962
|
+
join3(projectRoot, ".fabric", "agents.meta.json")
|
|
963
|
+
);
|
|
964
|
+
const approved = [];
|
|
965
|
+
for (const pendingPath of pendingPaths) {
|
|
966
|
+
const result = await approveOne(projectRoot, pendingPath, allocator);
|
|
967
|
+
if (result !== null) {
|
|
968
|
+
approved.push(result);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return approved;
|
|
972
|
+
}
|
|
973
|
+
async function approveOne(projectRoot, pendingPath, allocator) {
|
|
974
|
+
let sourceAbs;
|
|
975
|
+
let sourceOrigin;
|
|
976
|
+
try {
|
|
977
|
+
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
978
|
+
const teamPendingAbs = pendingBaseAbs("team", projectRoot);
|
|
979
|
+
const personalPendingAbs = pendingBaseAbs("personal", projectRoot);
|
|
980
|
+
const inTeamPending = sandboxed.abs === teamPendingAbs || sandboxed.abs.startsWith(teamPendingAbs + "/");
|
|
981
|
+
const inPersonalPending = sandboxed.abs === personalPendingAbs || sandboxed.abs.startsWith(personalPendingAbs + "/");
|
|
982
|
+
if (!inTeamPending && !inPersonalPending) {
|
|
983
|
+
throw new Error(`approve path is outside .fabric/knowledge/pending/: ${pendingPath}`);
|
|
984
|
+
}
|
|
985
|
+
sourceAbs = sandboxed.abs;
|
|
986
|
+
sourceOrigin = inPersonalPending ? "personal" : "team";
|
|
987
|
+
} catch (err) {
|
|
988
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
989
|
+
await emitEventBestEffort2(projectRoot, {
|
|
990
|
+
event_type: "knowledge_promote_failed",
|
|
991
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
992
|
+
reason: `approve:${pendingPath}: ${reason}`
|
|
993
|
+
});
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
const slug = basename(pendingPath).replace(/\.md$/u, "");
|
|
997
|
+
await emitEventBestEffort2(projectRoot, {
|
|
998
|
+
event_type: "knowledge_promote_started",
|
|
999
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1000
|
+
reason: `approve:${slug}`
|
|
1001
|
+
});
|
|
1002
|
+
let allocatedId;
|
|
1003
|
+
let targetAbs;
|
|
1004
|
+
let writtenTarget = false;
|
|
1005
|
+
try {
|
|
1006
|
+
const content = await readFile3(sourceAbs, "utf8");
|
|
1007
|
+
const fm = parseFrontmatter(content);
|
|
1008
|
+
const pluralType = fm.type;
|
|
1009
|
+
if (pluralType === void 0 || !PLURAL_TYPES.includes(pluralType)) {
|
|
1010
|
+
throw new Error(`pending file missing or invalid 'type' frontmatter: ${pendingPath}`);
|
|
1011
|
+
}
|
|
1012
|
+
const layer = fm.layer ?? "team";
|
|
1013
|
+
const singularType = PLURAL_TO_SINGULAR[pluralType];
|
|
1014
|
+
const stableId = await allocator.allocate(layer, singularType);
|
|
1015
|
+
allocatedId = stableId;
|
|
1016
|
+
const newFilename = `${stableId}--${slug}.md`;
|
|
1017
|
+
const layerRoot = layer === "personal" ? join3(resolvePersonalRoot2(), ".fabric") : join3(projectRoot, ".fabric");
|
|
1018
|
+
targetAbs = join3(layerRoot, "knowledge", pluralType, newFilename);
|
|
1019
|
+
await ensureParentDirectory(targetAbs);
|
|
1020
|
+
const rewritten = rewriteFrontmatterForPromote(content, stableId);
|
|
1021
|
+
await atomicWriteText(targetAbs, rewritten);
|
|
1022
|
+
writtenTarget = true;
|
|
1023
|
+
if (sourceOrigin === "team") {
|
|
1024
|
+
try {
|
|
1025
|
+
execFileSync("git", ["rm", "--quiet", "-f", pendingPath], {
|
|
1026
|
+
cwd: projectRoot,
|
|
1027
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1028
|
+
});
|
|
1029
|
+
} catch {
|
|
1030
|
+
if (existsSync3(sourceAbs)) {
|
|
1031
|
+
await unlink(sourceAbs);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
} else {
|
|
1035
|
+
if (existsSync3(sourceAbs)) {
|
|
1036
|
+
await unlink(sourceAbs);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1040
|
+
event_type: "knowledge_promoted",
|
|
1041
|
+
stable_id: stableId,
|
|
1042
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1043
|
+
reason: `approve:${slug}`
|
|
1044
|
+
});
|
|
1045
|
+
return { pending_path: pendingPath, stable_id: stableId };
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
if (writtenTarget && targetAbs !== void 0 && existsSync3(targetAbs)) {
|
|
1048
|
+
try {
|
|
1049
|
+
await unlink(targetAbs);
|
|
1050
|
+
} catch {
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1054
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1055
|
+
event_type: "knowledge_promote_failed",
|
|
1056
|
+
...allocatedId !== void 0 ? { stable_id: allocatedId } : {},
|
|
1057
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1058
|
+
reason: `approve:${slug}: ${reason}`
|
|
1059
|
+
});
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
async function rejectAll(projectRoot, pendingPaths, reason) {
|
|
1064
|
+
const rejected = [];
|
|
1065
|
+
for (const pendingPath of pendingPaths) {
|
|
1066
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1067
|
+
event_type: "knowledge_rejected",
|
|
1068
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1069
|
+
reason: `reject:${pendingPath}: ${reason}`
|
|
1070
|
+
});
|
|
1071
|
+
rejected.push(pendingPath);
|
|
1072
|
+
}
|
|
1073
|
+
return rejected;
|
|
1074
|
+
}
|
|
1075
|
+
async function modifyEntry(projectRoot, pendingPath, changes) {
|
|
1076
|
+
const target = resolveModifyTarget(projectRoot, pendingPath);
|
|
1077
|
+
if (target === null) {
|
|
1078
|
+
throw new Error(`modify target not found: ${pendingPath}`);
|
|
1079
|
+
}
|
|
1080
|
+
const content = await readFile3(target.absPath, "utf8");
|
|
1081
|
+
const fm = parseFrontmatter(content);
|
|
1082
|
+
const currentLayer = fm.layer ?? "team";
|
|
1083
|
+
if (changes.layer !== void 0 && changes.layer !== currentLayer) {
|
|
1084
|
+
return await modifyLayerFlip(projectRoot, target, content, fm, changes);
|
|
1085
|
+
}
|
|
1086
|
+
const merged = rewriteFrontmatterMerge(content, changes);
|
|
1087
|
+
await atomicWriteText(target.absPath, merged);
|
|
1088
|
+
return {
|
|
1089
|
+
action: "modify",
|
|
1090
|
+
pending_path: pendingPath
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
function resolveModifyTarget(projectRoot, pendingPath) {
|
|
1094
|
+
let sandboxed;
|
|
1095
|
+
try {
|
|
1096
|
+
sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
1097
|
+
} catch {
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1100
|
+
if (existsSync3(sandboxed.abs)) {
|
|
1101
|
+
return {
|
|
1102
|
+
absPath: sandboxed.abs,
|
|
1103
|
+
isInProjectTree: sandboxed.isInProjectTree,
|
|
1104
|
+
inferredType: inferTypeFromPath(pendingPath),
|
|
1105
|
+
slug: extractSlug(pendingPath)
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
function inferTypeFromPath(path2) {
|
|
1111
|
+
const match = /knowledge\/(?:pending\/)?([^/]+)\/[^/]+\.md$/u.exec(path2);
|
|
1112
|
+
if (match === null) return null;
|
|
1113
|
+
const seg = match[1];
|
|
1114
|
+
if (seg !== void 0 && PLURAL_TYPES.includes(seg)) {
|
|
1115
|
+
return seg;
|
|
1116
|
+
}
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
function extractSlug(path2) {
|
|
1120
|
+
const file = basename(path2).replace(/\.md$/u, "");
|
|
1121
|
+
return file.replace(/^K[PT]-(MOD|DEC|GLD|PIT|PRO)-\d+--/u, "");
|
|
1122
|
+
}
|
|
1123
|
+
async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
1124
|
+
const fromLayer = fm.layer ?? "team";
|
|
1125
|
+
const toLayer = changes.layer;
|
|
1126
|
+
const pluralType = fm.type ?? target.inferredType;
|
|
1127
|
+
if (pluralType === null || pluralType === void 0) {
|
|
1128
|
+
throw new Error(`layer-flip requires a known type; could not infer for ${target.absPath}`);
|
|
1129
|
+
}
|
|
1130
|
+
const slug = target.slug;
|
|
1131
|
+
const priorStableId = fm.id;
|
|
1132
|
+
const fromScope = fm.relevance_scope ?? "broad";
|
|
1133
|
+
const shouldAutoDegrade = fromScope === "narrow" && fromLayer === "team" && toLayer === "personal";
|
|
1134
|
+
const allocator = new KnowledgeIdAllocator(
|
|
1135
|
+
join3(projectRoot, ".fabric", "agents.meta.json")
|
|
1136
|
+
);
|
|
1137
|
+
const singularType = PLURAL_TO_SINGULAR[pluralType];
|
|
1138
|
+
const newStableId = await allocator.allocate(toLayer, singularType);
|
|
1139
|
+
const toRoot = toLayer === "personal" ? join3(resolvePersonalRoot2(), ".fabric") : join3(projectRoot, ".fabric");
|
|
1140
|
+
const toAbs = join3(toRoot, "knowledge", pluralType, `${newStableId}--${slug}.md`);
|
|
1141
|
+
await ensureParentDirectory(toAbs);
|
|
1142
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1143
|
+
event_type: "knowledge_promote_started",
|
|
1144
|
+
...priorStableId !== void 0 ? { stable_id: priorStableId } : {},
|
|
1145
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1146
|
+
reason: `layer_flip:${priorStableId ?? "<unassigned>"}->${newStableId}`
|
|
1147
|
+
});
|
|
1148
|
+
const effectivePatch = shouldAutoDegrade ? {
|
|
1149
|
+
...changes,
|
|
1150
|
+
layer: toLayer,
|
|
1151
|
+
relevance_scope: "broad",
|
|
1152
|
+
relevance_paths: []
|
|
1153
|
+
} : { ...changes, layer: toLayer };
|
|
1154
|
+
const rewritten = rewriteFrontmatterMerge(content, effectivePatch, { id: newStableId });
|
|
1155
|
+
await atomicWriteText(toAbs, rewritten);
|
|
1156
|
+
if (target.isInProjectTree) {
|
|
1157
|
+
const relSource = relative2(projectRoot, target.absPath);
|
|
1158
|
+
try {
|
|
1159
|
+
execFileSync("git", ["rm", "--quiet", "-f", relSource], {
|
|
1160
|
+
cwd: projectRoot,
|
|
1161
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1162
|
+
});
|
|
1163
|
+
} catch {
|
|
1164
|
+
if (existsSync3(target.absPath)) {
|
|
1165
|
+
await unlink(target.absPath);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
} else if (existsSync3(target.absPath)) {
|
|
1169
|
+
await unlink(target.absPath);
|
|
1170
|
+
}
|
|
1171
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1172
|
+
event_type: "knowledge_layer_changed",
|
|
1173
|
+
stable_id: newStableId,
|
|
1174
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1175
|
+
from_layer: fromLayer,
|
|
1176
|
+
to_layer: toLayer,
|
|
1177
|
+
reason: `layer_flip:${priorStableId ?? "<unassigned>"}->${newStableId}`
|
|
1178
|
+
});
|
|
1179
|
+
if (shouldAutoDegrade) {
|
|
1180
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1181
|
+
event_type: "knowledge_scope_degraded",
|
|
1182
|
+
stable_id: newStableId,
|
|
1183
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1184
|
+
from_scope: "narrow",
|
|
1185
|
+
to_scope: "broad",
|
|
1186
|
+
reason: "personal-implies-broad"
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
const responsePath = toLayer === "team" ? relative2(projectRoot, toAbs) : `~/${relative2(resolvePersonalRoot2(), toAbs)}`;
|
|
1190
|
+
return {
|
|
1191
|
+
action: "modify",
|
|
1192
|
+
pending_path: responsePath,
|
|
1193
|
+
...priorStableId !== void 0 ? { prior_stable_id: priorStableId } : {},
|
|
1194
|
+
new_stable_id: newStableId
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
async function searchEntries(projectRoot, query, filters) {
|
|
1198
|
+
const lowerQuery = query.toLowerCase();
|
|
1199
|
+
const items = [];
|
|
1200
|
+
const sources = [
|
|
1201
|
+
{ root: pendingBaseAbs("team", projectRoot), isPending: true, isPersonal: false },
|
|
1202
|
+
{ root: pendingBaseAbs("personal", projectRoot), isPending: true, isPersonal: true },
|
|
1203
|
+
{ root: join3(projectRoot, ".fabric", "knowledge"), isPending: false, isPersonal: false },
|
|
1204
|
+
{ root: join3(resolvePersonalRoot2(), ".fabric", "knowledge"), isPending: false, isPersonal: true }
|
|
1205
|
+
];
|
|
1206
|
+
const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
|
|
1207
|
+
for (const source of sources) {
|
|
1208
|
+
for (const type of typesToScan) {
|
|
1209
|
+
const dir = join3(source.root, type);
|
|
1210
|
+
if (!existsSync3(dir)) continue;
|
|
1211
|
+
let entries;
|
|
1212
|
+
try {
|
|
1213
|
+
entries = await readdir(dir);
|
|
1214
|
+
} catch {
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
for (const name of entries) {
|
|
1218
|
+
if (!name.endsWith(".md")) continue;
|
|
1219
|
+
const absolutePath = join3(dir, name);
|
|
1220
|
+
let content;
|
|
1221
|
+
try {
|
|
1222
|
+
content = await readFile3(absolutePath, "utf8");
|
|
1223
|
+
} catch {
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
const fm = parseFrontmatter(content);
|
|
1227
|
+
const layer = fm.layer ?? (source.isPersonal ? "personal" : "team");
|
|
1228
|
+
const maturity = fm.maturity ?? "draft";
|
|
1229
|
+
if (filters?.layer !== void 0 && filters.layer !== "both" && filters.layer !== layer) {
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
if (filters?.maturity !== void 0 && filters.maturity !== maturity) {
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
if (filters?.tags !== void 0 && filters.tags.length > 0) {
|
|
1236
|
+
const itemTags = fm.tags ?? [];
|
|
1237
|
+
const hasAll = filters.tags.every((t) => itemTags.includes(t));
|
|
1238
|
+
if (!hasAll) continue;
|
|
1239
|
+
}
|
|
1240
|
+
if (filters?.created_after !== void 0) {
|
|
1241
|
+
const createdAt = fm.created_at;
|
|
1242
|
+
if (createdAt === void 0 || createdAt < filters.created_after) {
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
const haystacks = [
|
|
1247
|
+
fm.title ?? "",
|
|
1248
|
+
fm.summary ?? "",
|
|
1249
|
+
...fm.tags ?? [],
|
|
1250
|
+
name
|
|
1251
|
+
].map((s) => s.toLowerCase());
|
|
1252
|
+
const matches = haystacks.some((h) => h.includes(lowerQuery));
|
|
1253
|
+
if (!matches) continue;
|
|
1254
|
+
const reportedPath = source.isPersonal ? `~/${relative2(resolvePersonalRoot2(), absolutePath)}` : relative2(projectRoot, absolutePath);
|
|
1255
|
+
items.push({
|
|
1256
|
+
pending_path: reportedPath,
|
|
1257
|
+
type,
|
|
1258
|
+
layer,
|
|
1259
|
+
maturity,
|
|
1260
|
+
// Only pending entries carry an origin tag (search results that are
|
|
1261
|
+
// canonical entries don't have a pending root to point back to).
|
|
1262
|
+
...source.isPending ? { origin: source.isPersonal ? "personal" : "team" } : {},
|
|
1263
|
+
...fm.tags !== void 0 && fm.tags.length > 0 ? { tags: fm.tags } : {},
|
|
1264
|
+
...fm.title !== void 0 ? { title: fm.title } : {},
|
|
1265
|
+
...fm.summary !== void 0 ? { summary: fm.summary } : {}
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return items;
|
|
1271
|
+
}
|
|
1272
|
+
async function deferAll(projectRoot, pendingPaths, until, reason) {
|
|
1273
|
+
const deferred = [];
|
|
1274
|
+
for (const pendingPath of pendingPaths) {
|
|
1275
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1276
|
+
event_type: "knowledge_deferred",
|
|
1277
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1278
|
+
...until !== void 0 ? { until } : {},
|
|
1279
|
+
...reason !== void 0 ? { reason } : {}
|
|
1280
|
+
});
|
|
1281
|
+
deferred.push(pendingPath);
|
|
1282
|
+
}
|
|
1283
|
+
return deferred;
|
|
1284
|
+
}
|
|
1285
|
+
function parseFrontmatter(content) {
|
|
1286
|
+
const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/u.exec(content);
|
|
1287
|
+
if (match === null) {
|
|
1288
|
+
return {};
|
|
1289
|
+
}
|
|
1290
|
+
const block = match[1];
|
|
1291
|
+
if (block === void 0) {
|
|
1292
|
+
return {};
|
|
1293
|
+
}
|
|
1294
|
+
const out = {};
|
|
1295
|
+
for (const rawLine of block.split(/\r?\n/u)) {
|
|
1296
|
+
const line = rawLine.trim();
|
|
1297
|
+
if (line.length === 0) continue;
|
|
1298
|
+
const sep = line.indexOf(":");
|
|
1299
|
+
if (sep === -1) continue;
|
|
1300
|
+
const key = line.slice(0, sep).trim();
|
|
1301
|
+
const value = line.slice(sep + 1).trim();
|
|
1302
|
+
switch (key) {
|
|
1303
|
+
case "id":
|
|
1304
|
+
out.id = stripQuotes(value);
|
|
1305
|
+
break;
|
|
1306
|
+
case "type":
|
|
1307
|
+
if (PLURAL_TYPES.includes(value)) {
|
|
1308
|
+
out.type = value;
|
|
1309
|
+
}
|
|
1310
|
+
break;
|
|
1311
|
+
case "layer":
|
|
1312
|
+
if (value === "team" || value === "personal") {
|
|
1313
|
+
out.layer = value;
|
|
1314
|
+
}
|
|
1315
|
+
break;
|
|
1316
|
+
case "maturity":
|
|
1317
|
+
if (value === "draft" || value === "verified" || value === "proven") {
|
|
1318
|
+
out.maturity = value;
|
|
1319
|
+
}
|
|
1320
|
+
break;
|
|
1321
|
+
case "source_session":
|
|
1322
|
+
out.source_session = stripQuotes(value);
|
|
1323
|
+
break;
|
|
1324
|
+
case "created_at":
|
|
1325
|
+
out.created_at = stripQuotes(value);
|
|
1326
|
+
break;
|
|
1327
|
+
case "tags":
|
|
1328
|
+
out.tags = parseFlowArray(value);
|
|
1329
|
+
break;
|
|
1330
|
+
case "title":
|
|
1331
|
+
out.title = stripQuotes(value);
|
|
1332
|
+
break;
|
|
1333
|
+
case "summary":
|
|
1334
|
+
out.summary = stripQuotes(value);
|
|
1335
|
+
break;
|
|
1336
|
+
case "relevance_scope":
|
|
1337
|
+
if (value === "narrow" || value === "broad") {
|
|
1338
|
+
out.relevance_scope = value;
|
|
1339
|
+
}
|
|
1340
|
+
break;
|
|
1341
|
+
case "relevance_paths":
|
|
1342
|
+
out.relevance_paths = parseFlowArray(value);
|
|
1343
|
+
break;
|
|
1344
|
+
default:
|
|
1345
|
+
break;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
return out;
|
|
1349
|
+
}
|
|
1350
|
+
function stripQuotes(value) {
|
|
1351
|
+
return value.replace(/^["'](.*)["']$/u, "$1");
|
|
1352
|
+
}
|
|
1353
|
+
function parseFlowArray(value) {
|
|
1354
|
+
const trimmed = value.trim();
|
|
1355
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) {
|
|
1356
|
+
return [];
|
|
1357
|
+
}
|
|
1358
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
1359
|
+
if (inner.length === 0) {
|
|
1360
|
+
return [];
|
|
1361
|
+
}
|
|
1362
|
+
return inner.split(",").map((item) => stripQuotes(item.trim())).filter((item) => item.length > 0);
|
|
1363
|
+
}
|
|
1364
|
+
function rewriteFrontmatterForPromote(content, stableId) {
|
|
1365
|
+
const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u.exec(content);
|
|
1366
|
+
if (match === null) {
|
|
1367
|
+
return `---
|
|
1368
|
+
id: ${stableId}
|
|
1369
|
+
---
|
|
1370
|
+
|
|
1371
|
+
${content}`;
|
|
1372
|
+
}
|
|
1373
|
+
const block = match[1] ?? "";
|
|
1374
|
+
const filteredLines = block.split(/\r?\n/u).filter((line) => !/^x-fabric-idempotency-key\s*:/u.test(line));
|
|
1375
|
+
filteredLines.unshift(`id: ${stableId}`);
|
|
1376
|
+
const newBlock = filteredLines.join("\n");
|
|
1377
|
+
const before = content.slice(0, match.index);
|
|
1378
|
+
const after = content.slice(match.index + match[0].length);
|
|
1379
|
+
return `${before}---
|
|
1380
|
+
${newBlock}
|
|
1381
|
+
---${after}`;
|
|
1382
|
+
}
|
|
1383
|
+
function rewriteFrontmatterMerge(content, patch, forced) {
|
|
1384
|
+
const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u.exec(content);
|
|
1385
|
+
if (match === null) {
|
|
1386
|
+
const synthLines = [];
|
|
1387
|
+
if (forced?.id !== void 0) synthLines.push(`id: ${forced.id}`);
|
|
1388
|
+
appendPatchLines(synthLines, patch);
|
|
1389
|
+
return `---
|
|
1390
|
+
${synthLines.join("\n")}
|
|
1391
|
+
---
|
|
1392
|
+
|
|
1393
|
+
${content}`;
|
|
1394
|
+
}
|
|
1395
|
+
const block = match[1] ?? "";
|
|
1396
|
+
const updates = {};
|
|
1397
|
+
if (forced?.id !== void 0) updates.id = `id: ${forced.id}`;
|
|
1398
|
+
if (patch.title !== void 0) updates.title = `title: ${quoteIfNeeded(patch.title)}`;
|
|
1399
|
+
if (patch.summary !== void 0) updates.summary = `summary: ${quoteIfNeeded(patch.summary)}`;
|
|
1400
|
+
if (patch.layer !== void 0) updates.layer = `layer: ${patch.layer}`;
|
|
1401
|
+
if (patch.maturity !== void 0) updates.maturity = `maturity: ${patch.maturity}`;
|
|
1402
|
+
if (patch.tags !== void 0) updates.tags = `tags: [${patch.tags.join(", ")}]`;
|
|
1403
|
+
if (patch.relevance_scope !== void 0) updates.relevance_scope = `relevance_scope: ${patch.relevance_scope}`;
|
|
1404
|
+
if (patch.relevance_paths !== void 0) updates.relevance_paths = `relevance_paths: [${patch.relevance_paths.join(", ")}]`;
|
|
1405
|
+
const lines = block.split(/\r?\n/u);
|
|
1406
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1407
|
+
const newLines = [];
|
|
1408
|
+
for (const line of lines) {
|
|
1409
|
+
const sep = line.indexOf(":");
|
|
1410
|
+
const key = sep === -1 ? "" : line.slice(0, sep).trim();
|
|
1411
|
+
if (key in updates) {
|
|
1412
|
+
newLines.push(updates[key]);
|
|
1413
|
+
seen.add(key);
|
|
1414
|
+
} else {
|
|
1415
|
+
newLines.push(line);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
for (const key of Object.keys(updates)) {
|
|
1419
|
+
if (!seen.has(key)) {
|
|
1420
|
+
newLines.push(updates[key]);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
const newBlock = newLines.join("\n");
|
|
1424
|
+
const before = content.slice(0, match.index);
|
|
1425
|
+
const after = content.slice(match.index + match[0].length);
|
|
1426
|
+
return `${before}---
|
|
1427
|
+
${newBlock}
|
|
1428
|
+
---${after}`;
|
|
1429
|
+
}
|
|
1430
|
+
function appendPatchLines(lines, patch) {
|
|
1431
|
+
if (patch.title !== void 0) lines.push(`title: ${quoteIfNeeded(patch.title)}`);
|
|
1432
|
+
if (patch.summary !== void 0) lines.push(`summary: ${quoteIfNeeded(patch.summary)}`);
|
|
1433
|
+
if (patch.layer !== void 0) lines.push(`layer: ${patch.layer}`);
|
|
1434
|
+
if (patch.maturity !== void 0) lines.push(`maturity: ${patch.maturity}`);
|
|
1435
|
+
if (patch.tags !== void 0) lines.push(`tags: [${patch.tags.join(", ")}]`);
|
|
1436
|
+
if (patch.relevance_scope !== void 0) lines.push(`relevance_scope: ${patch.relevance_scope}`);
|
|
1437
|
+
if (patch.relevance_paths !== void 0) lines.push(`relevance_paths: [${patch.relevance_paths.join(", ")}]`);
|
|
1438
|
+
}
|
|
1439
|
+
function quoteIfNeeded(value) {
|
|
1440
|
+
if (/[\n\r]/u.test(value)) {
|
|
1441
|
+
return JSON.stringify(value);
|
|
1442
|
+
}
|
|
1443
|
+
if (/[:#\[\]{}&*!|>'"%@`,]|^\s|\s$/u.test(value)) {
|
|
1444
|
+
return `"${value.replace(/"/gu, '\\"')}"`;
|
|
1445
|
+
}
|
|
1446
|
+
return value;
|
|
1447
|
+
}
|
|
1448
|
+
function resolvePersonalRoot2() {
|
|
1449
|
+
return process.env.FABRIC_HOME ?? homedir2();
|
|
1450
|
+
}
|
|
1451
|
+
async function emitEventBestEffort2(projectRoot, event) {
|
|
1452
|
+
try {
|
|
1453
|
+
await appendEventLedgerEvent(projectRoot, event);
|
|
1454
|
+
} catch {
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// src/tools/review.ts
|
|
1459
|
+
function registerReview(server, tracker) {
|
|
1460
|
+
server.registerTool(
|
|
1461
|
+
"fab_review",
|
|
1462
|
+
{
|
|
1463
|
+
description: "Review pending knowledge entries under .fabric/knowledge/pending/. Discriminated by `action`: list (enumerate), approve (allocate stable_id and promote to canonical layer/type path), reject/modify/search/defer (TASK-002). Skill-side tool \u2014 invoked by fabric-review.",
|
|
1464
|
+
// Flat ZodRawShape required by MCP SDK 1.29.0 registerTool. The
|
|
1465
|
+
// authoritative cross-field contract still lives in FabReviewInputSchema
|
|
1466
|
+
// (discriminatedUnion) and is enforced inside the handler via
|
|
1467
|
+
// `FabReviewInputSchema.parse(input)`.
|
|
1468
|
+
inputSchema: FabReviewInputShape,
|
|
1469
|
+
outputSchema: FabReviewOutputShape,
|
|
1470
|
+
annotations: fabReviewAnnotations
|
|
1471
|
+
},
|
|
1472
|
+
async (input) => {
|
|
1473
|
+
const requestId = randomUUID3();
|
|
1474
|
+
tracker?.enter(requestId);
|
|
1475
|
+
try {
|
|
1476
|
+
const narrowed = FabReviewInputSchema.parse(input);
|
|
1477
|
+
const projectRoot = resolveProjectRoot();
|
|
1478
|
+
const result = await reviewKnowledge(projectRoot, narrowed);
|
|
1479
|
+
const response = result;
|
|
1480
|
+
const payloadLimits = readPayloadLimits(projectRoot);
|
|
1481
|
+
const serialized = JSON.stringify(response);
|
|
1482
|
+
enforcePayloadLimit3(serialized, payloadLimits);
|
|
1483
|
+
return {
|
|
1484
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
1485
|
+
structuredContent: response
|
|
1486
|
+
};
|
|
1487
|
+
} finally {
|
|
1488
|
+
tracker?.exit(requestId);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// src/tools/knowledge-sections.ts
|
|
1495
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1496
|
+
import {
|
|
1497
|
+
knowledgeSectionsAnnotations,
|
|
1498
|
+
knowledgeSectionsInputSchema,
|
|
1499
|
+
knowledgeSectionsOutputSchema
|
|
1500
|
+
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
1501
|
+
import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
1502
|
+
|
|
1503
|
+
// src/services/knowledge-sections.ts
|
|
1504
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1505
|
+
import { homedir as homedir3 } from "os";
|
|
1506
|
+
import { join as join4 } from "path";
|
|
1507
|
+
var KNOWLEDGE_SECTION_NAMES = [
|
|
418
1508
|
"MISSION_STATEMENT",
|
|
419
1509
|
"MANDATORY_INJECTION",
|
|
420
1510
|
"BUSINESS_LOGIC_CHUNKS",
|
|
@@ -425,7 +1515,7 @@ var PRIORITY_ORDER = {
|
|
|
425
1515
|
medium: 1,
|
|
426
1516
|
low: 2
|
|
427
1517
|
};
|
|
428
|
-
function
|
|
1518
|
+
function parseKnowledgeSections(content) {
|
|
429
1519
|
const sections = /* @__PURE__ */ new Map();
|
|
430
1520
|
const lines = content.split(/\r?\n/u);
|
|
431
1521
|
let activeSection;
|
|
@@ -447,7 +1537,7 @@ function parseRuleSections(content) {
|
|
|
447
1537
|
const heading = /^(#{2,6})\s+\[([A-Z_]+)\]\s*$/u.exec(line.trim());
|
|
448
1538
|
if (heading !== null) {
|
|
449
1539
|
flush();
|
|
450
|
-
activeSection =
|
|
1540
|
+
activeSection = isKnowledgeSectionName(heading[2]) ? heading[2] : void 0;
|
|
451
1541
|
activeSectionDepth = activeSection === void 0 ? 0 : heading[1].length;
|
|
452
1542
|
continue;
|
|
453
1543
|
}
|
|
@@ -471,7 +1561,7 @@ function parseRuleSections(content) {
|
|
|
471
1561
|
Array.from(sections.entries()).map(([section, values]) => [section, values.join("\n\n")])
|
|
472
1562
|
);
|
|
473
1563
|
}
|
|
474
|
-
async function
|
|
1564
|
+
async function getKnowledgeSections(projectRoot, input) {
|
|
475
1565
|
const token = readSelectionToken(input.selection_token);
|
|
476
1566
|
if (token === void 0) {
|
|
477
1567
|
throw new Error("selection_token is missing or expired");
|
|
@@ -483,8 +1573,8 @@ async function getRuleSections(projectRoot, input) {
|
|
|
483
1573
|
const diagnostics = [];
|
|
484
1574
|
const rules = [];
|
|
485
1575
|
for (const rule of selectedRules) {
|
|
486
|
-
const content = await
|
|
487
|
-
const parsedSections =
|
|
1576
|
+
const content = await readFile4(resolveRuleSourcePath(projectRoot, rule.path), "utf8");
|
|
1577
|
+
const parsedSections = parseKnowledgeSections(content);
|
|
488
1578
|
const sections = {};
|
|
489
1579
|
for (const section of input.sections) {
|
|
490
1580
|
const sectionContent = parsedSections.get(section);
|
|
@@ -522,20 +1612,23 @@ async function getRuleSections(projectRoot, input) {
|
|
|
522
1612
|
rules,
|
|
523
1613
|
diagnostics
|
|
524
1614
|
};
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
1615
|
+
try {
|
|
1616
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1617
|
+
event_type: "knowledge_selection",
|
|
1618
|
+
selection_token: input.selection_token,
|
|
1619
|
+
target_paths: token.target_paths,
|
|
1620
|
+
required_stable_ids: token.required_stable_ids,
|
|
1621
|
+
ai_selectable_stable_ids: token.ai_selectable_stable_ids,
|
|
1622
|
+
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
1623
|
+
final_stable_ids: result.selected_stable_ids,
|
|
1624
|
+
ai_selection_reasons: pickSelectionReasons(input.ai_selected_stable_ids, input.ai_selection_reasons),
|
|
1625
|
+
rejected_stable_ids: [],
|
|
1626
|
+
ignored_stable_ids: [],
|
|
1627
|
+
correlation_id: input.correlation_id,
|
|
1628
|
+
session_id: input.session_id
|
|
1629
|
+
});
|
|
1630
|
+
} catch {
|
|
1631
|
+
}
|
|
539
1632
|
try {
|
|
540
1633
|
await appendEventLedgerEvent(projectRoot, {
|
|
541
1634
|
event_type: "knowledge_sections_fetched",
|
|
@@ -550,6 +1643,26 @@ async function getRuleSections(projectRoot, input) {
|
|
|
550
1643
|
});
|
|
551
1644
|
} catch {
|
|
552
1645
|
}
|
|
1646
|
+
const consumedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1647
|
+
const consumedClientHash = input.client_hash ?? "";
|
|
1648
|
+
const emittedConsumed = /* @__PURE__ */ new Set();
|
|
1649
|
+
for (const stableId of result.selected_stable_ids) {
|
|
1650
|
+
if (emittedConsumed.has(stableId)) {
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
emittedConsumed.add(stableId);
|
|
1654
|
+
try {
|
|
1655
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1656
|
+
event_type: "knowledge_consumed",
|
|
1657
|
+
stable_id: stableId,
|
|
1658
|
+
consumed_at: consumedAt,
|
|
1659
|
+
client_hash: consumedClientHash,
|
|
1660
|
+
correlation_id: input.correlation_id,
|
|
1661
|
+
session_id: input.session_id
|
|
1662
|
+
});
|
|
1663
|
+
} catch {
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
553
1666
|
return result;
|
|
554
1667
|
}
|
|
555
1668
|
function validateAiSelections(aiSelectableStableIds, aiSelectedStableIds, aiSelectionReasons) {
|
|
@@ -572,12 +1685,12 @@ function findRuleNode(meta, stableId) {
|
|
|
572
1685
|
if (nodeStableId !== stableId) {
|
|
573
1686
|
continue;
|
|
574
1687
|
}
|
|
575
|
-
const level = node.level ?? node.layer;
|
|
1688
|
+
const level = node.level ?? node.layer ?? "L2";
|
|
576
1689
|
return {
|
|
577
1690
|
stable_id: nodeStableId,
|
|
578
1691
|
level,
|
|
579
|
-
path:
|
|
580
|
-
priority: node.priority,
|
|
1692
|
+
path: normalizeKnowledgePath(node.content_ref ?? node.file),
|
|
1693
|
+
priority: node.priority ?? "medium",
|
|
581
1694
|
node
|
|
582
1695
|
};
|
|
583
1696
|
}
|
|
@@ -606,44 +1719,44 @@ function outputLevelOrder(level) {
|
|
|
606
1719
|
return 2;
|
|
607
1720
|
}
|
|
608
1721
|
}
|
|
609
|
-
function
|
|
610
|
-
return
|
|
1722
|
+
function isKnowledgeSectionName(value) {
|
|
1723
|
+
return KNOWLEDGE_SECTION_NAMES.includes(value);
|
|
611
1724
|
}
|
|
612
1725
|
function resolveRuleSourcePath(projectRoot, contentRef) {
|
|
613
1726
|
if (contentRef.startsWith("~/.fabric/knowledge/")) {
|
|
614
|
-
const home = process.env.FABRIC_HOME ??
|
|
615
|
-
return
|
|
1727
|
+
const home = process.env.FABRIC_HOME ?? homedir3();
|
|
1728
|
+
return join4(home, ".fabric", "knowledge", contentRef.slice("~/.fabric/knowledge/".length));
|
|
616
1729
|
}
|
|
617
|
-
return
|
|
1730
|
+
return join4(projectRoot, contentRef);
|
|
618
1731
|
}
|
|
619
1732
|
function pickSelectionReasons(selectedStableIds, reasons) {
|
|
620
1733
|
return Object.fromEntries(selectedStableIds.map((stableId) => [stableId, reasons[stableId] ?? ""]));
|
|
621
1734
|
}
|
|
622
1735
|
|
|
623
|
-
// src/tools/
|
|
624
|
-
function
|
|
1736
|
+
// src/tools/knowledge-sections.ts
|
|
1737
|
+
function registerKnowledgeSections(server, tracker) {
|
|
625
1738
|
server.registerTool(
|
|
626
|
-
"
|
|
1739
|
+
"fab_get_knowledge_sections",
|
|
627
1740
|
{
|
|
628
1741
|
description: "Fetch structured Fabric rule sections after fab_plan_context. Required L0/L2 rules are merged with AI-selected L1 rules server-side.",
|
|
629
|
-
inputSchema:
|
|
630
|
-
outputSchema:
|
|
631
|
-
annotations:
|
|
1742
|
+
inputSchema: knowledgeSectionsInputSchema,
|
|
1743
|
+
outputSchema: knowledgeSectionsOutputSchema,
|
|
1744
|
+
annotations: knowledgeSectionsAnnotations
|
|
632
1745
|
},
|
|
633
1746
|
async (input) => {
|
|
634
|
-
const requestId =
|
|
1747
|
+
const requestId = randomUUID4();
|
|
635
1748
|
tracker?.enter(requestId);
|
|
636
1749
|
try {
|
|
637
1750
|
const projectRoot = resolveProjectRoot();
|
|
638
|
-
const syncReport = await
|
|
639
|
-
const result = await
|
|
1751
|
+
const syncReport = await ensureKnowledgeFresh(projectRoot);
|
|
1752
|
+
const result = await getKnowledgeSections(projectRoot, input);
|
|
640
1753
|
const response = {
|
|
641
1754
|
...result,
|
|
642
1755
|
warnings: [...syncReport.warnings]
|
|
643
1756
|
};
|
|
644
1757
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
645
1758
|
const serialized = JSON.stringify(response);
|
|
646
|
-
const guardResult =
|
|
1759
|
+
const guardResult = enforcePayloadLimit4(serialized, payloadLimits);
|
|
647
1760
|
if (guardResult.warning) {
|
|
648
1761
|
response.warnings = [
|
|
649
1762
|
...response.warnings,
|
|
@@ -665,83 +1778,6 @@ function registerRuleSections(server, tracker) {
|
|
|
665
1778
|
);
|
|
666
1779
|
}
|
|
667
1780
|
|
|
668
|
-
// src/services/knowledge-id-allocator.ts
|
|
669
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
670
|
-
import { dirname } from "path";
|
|
671
|
-
import { mkdir } from "fs/promises";
|
|
672
|
-
import {
|
|
673
|
-
AgentsMetaCountersSchema,
|
|
674
|
-
agentsMetaSchema,
|
|
675
|
-
allocateKnowledgeId,
|
|
676
|
-
defaultAgentsMetaCounters
|
|
677
|
-
} from "@fenglimg/fabric-shared";
|
|
678
|
-
import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
679
|
-
var KnowledgeIdAllocator = class {
|
|
680
|
-
constructor(metaPath) {
|
|
681
|
-
this.metaPath = metaPath;
|
|
682
|
-
}
|
|
683
|
-
metaPath;
|
|
684
|
-
/**
|
|
685
|
-
* Allocate the next stable_id for the given (layer, type) pair and persist
|
|
686
|
-
* the advanced counter to `agents.meta.json`.
|
|
687
|
-
*/
|
|
688
|
-
async allocate(layer, type) {
|
|
689
|
-
const meta = await this.readMeta();
|
|
690
|
-
const counters = this.normalizeCounters(meta.counters);
|
|
691
|
-
const { id, nextCounters } = allocateKnowledgeId(layer, type, counters);
|
|
692
|
-
await this.writeMetaAtomic({ ...meta, counters: nextCounters });
|
|
693
|
-
return id;
|
|
694
|
-
}
|
|
695
|
-
/**
|
|
696
|
-
* Returns the current counters envelope, defaulting to all-zero slots when
|
|
697
|
-
* the meta file is absent or pre-v2.0 (counters key missing).
|
|
698
|
-
*/
|
|
699
|
-
async getCounters() {
|
|
700
|
-
const meta = await this.readMeta();
|
|
701
|
-
return this.normalizeCounters(meta.counters);
|
|
702
|
-
}
|
|
703
|
-
// ---- internal helpers ------------------------------------------------
|
|
704
|
-
async readMeta() {
|
|
705
|
-
let raw;
|
|
706
|
-
try {
|
|
707
|
-
raw = await readFile2(this.metaPath, "utf8");
|
|
708
|
-
} catch (err) {
|
|
709
|
-
if (isNodeError(err) && err.code === "ENOENT") {
|
|
710
|
-
return { revision: "", nodes: {} };
|
|
711
|
-
}
|
|
712
|
-
throw err;
|
|
713
|
-
}
|
|
714
|
-
let parsed;
|
|
715
|
-
try {
|
|
716
|
-
parsed = JSON.parse(raw);
|
|
717
|
-
} catch {
|
|
718
|
-
return { revision: "", nodes: {} };
|
|
719
|
-
}
|
|
720
|
-
const validation = agentsMetaSchema.safeParse(parsed);
|
|
721
|
-
if (validation.success) {
|
|
722
|
-
return validation.data;
|
|
723
|
-
}
|
|
724
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
725
|
-
}
|
|
726
|
-
normalizeCounters(input) {
|
|
727
|
-
if (input === void 0 || input === null) {
|
|
728
|
-
return defaultAgentsMetaCounters();
|
|
729
|
-
}
|
|
730
|
-
const parsed = AgentsMetaCountersSchema.safeParse(input);
|
|
731
|
-
return parsed.success ? parsed.data : defaultAgentsMetaCounters();
|
|
732
|
-
}
|
|
733
|
-
async writeMetaAtomic(meta) {
|
|
734
|
-
await ensureParentDirectory(this.metaPath);
|
|
735
|
-
await atomicWriteJson(this.metaPath, meta, { indent: 2 });
|
|
736
|
-
}
|
|
737
|
-
};
|
|
738
|
-
async function ensureParentDirectory(filePath) {
|
|
739
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
740
|
-
}
|
|
741
|
-
function isNodeError(err) {
|
|
742
|
-
return err instanceof Error && typeof err.code === "string";
|
|
743
|
-
}
|
|
744
|
-
|
|
745
1781
|
// src/services/serve-lock.ts
|
|
746
1782
|
import fs from "fs";
|
|
747
1783
|
import path from "path";
|
|
@@ -846,18 +1882,20 @@ function formatError(error) {
|
|
|
846
1882
|
}
|
|
847
1883
|
function formatPreexistingRootMessage(projectRoot) {
|
|
848
1884
|
const preexisting = [];
|
|
849
|
-
if (
|
|
850
|
-
if (
|
|
1885
|
+
if (existsSync4(join5(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
|
|
1886
|
+
if (existsSync4(join5(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
|
|
851
1887
|
if (preexisting.length === 0) return null;
|
|
852
1888
|
return `[startup] info: detected ${preexisting.join(", ")} at project root. Note: Fabric serves knowledge from .fabric/knowledge/ via MCP \u2014 root markdown files are not auto-loaded into the AI context.`;
|
|
853
1889
|
}
|
|
854
1890
|
function createFabricServer(tracker) {
|
|
855
1891
|
const server = new McpServer({
|
|
856
|
-
name: "fabric-
|
|
857
|
-
version: "2.0.0-rc.
|
|
1892
|
+
name: "fabric-knowledge-server",
|
|
1893
|
+
version: "2.0.0-rc.11"
|
|
858
1894
|
});
|
|
859
1895
|
registerPlanContext(server, tracker);
|
|
860
|
-
|
|
1896
|
+
registerKnowledgeSections(server, tracker);
|
|
1897
|
+
registerExtractKnowledge(server, tracker);
|
|
1898
|
+
registerReview(server, tracker);
|
|
861
1899
|
server.registerResource(
|
|
862
1900
|
"bootstrap README",
|
|
863
1901
|
AGENTS_MD_RESOURCE_URI,
|
|
@@ -867,10 +1905,10 @@ function createFabricServer(tracker) {
|
|
|
867
1905
|
},
|
|
868
1906
|
async (_uri) => {
|
|
869
1907
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
870
|
-
const path2 =
|
|
1908
|
+
const path2 = join5(projectRoot, ".fabric", "bootstrap", "README.md");
|
|
871
1909
|
let text = "";
|
|
872
|
-
if (
|
|
873
|
-
text = await
|
|
1910
|
+
if (existsSync4(path2)) {
|
|
1911
|
+
text = await readFile5(path2, "utf8");
|
|
874
1912
|
}
|
|
875
1913
|
return {
|
|
876
1914
|
contents: [
|
|
@@ -889,7 +1927,7 @@ async function startStdioServer() {
|
|
|
889
1927
|
const tracker = createInFlightTracker();
|
|
890
1928
|
const projectRoot = resolveProjectRoot();
|
|
891
1929
|
const syncStart = Date.now();
|
|
892
|
-
const reconcileResult = await
|
|
1930
|
+
const reconcileResult = await reconcileKnowledge(projectRoot, { trigger: "startup" });
|
|
893
1931
|
const syncDurationMs = Date.now() - syncStart;
|
|
894
1932
|
process.stderr.write(
|
|
895
1933
|
`[startup] rule sync: status=${reconcileResult.status}, events=${reconcileResult.events.length}, ${syncDurationMs}ms
|
|
@@ -952,9 +1990,9 @@ function createShutdownHandler(deps) {
|
|
|
952
1990
|
};
|
|
953
1991
|
}
|
|
954
1992
|
async function startHttpServer(options) {
|
|
955
|
-
const { createFabricHttpApp } = await import("./http-
|
|
956
|
-
const { port, projectRoot, host = "127.0.0.1", authToken
|
|
957
|
-
const app = createFabricHttpApp({ projectRoot, host, authToken
|
|
1993
|
+
const { createFabricHttpApp } = await import("./http-U5RL5Q6P.js");
|
|
1994
|
+
const { port, projectRoot, host = "127.0.0.1", authToken } = options;
|
|
1995
|
+
const app = createFabricHttpApp({ projectRoot, host, authToken });
|
|
958
1996
|
return await new Promise((resolveServer, rejectServer) => {
|
|
959
1997
|
const server = app.listen(port, host);
|
|
960
1998
|
server.once("close", () => {
|
|
@@ -970,7 +2008,7 @@ async function startHttpServer(options) {
|
|
|
970
2008
|
}
|
|
971
2009
|
var entrypoint = process.argv[1];
|
|
972
2010
|
var currentFilePath = fileURLToPath(import.meta.url);
|
|
973
|
-
var isMainModule = entrypoint !== void 0 &&
|
|
2011
|
+
var isMainModule = entrypoint !== void 0 && resolve2(entrypoint) === currentFilePath;
|
|
974
2012
|
if (isMainModule) {
|
|
975
2013
|
void startStdioServer().catch((error) => {
|
|
976
2014
|
writeStderr(formatError(error));
|
|
@@ -986,29 +2024,34 @@ export {
|
|
|
986
2024
|
ServeLockHeldError,
|
|
987
2025
|
acquireLock,
|
|
988
2026
|
appendEventLedgerEvent,
|
|
989
|
-
|
|
2027
|
+
buildKnowledgeMeta,
|
|
990
2028
|
checkLockOrThrow,
|
|
991
|
-
|
|
992
|
-
|
|
2029
|
+
computeKnowledgeBasedAgentsMeta,
|
|
2030
|
+
computeKnowledgeTestIndex,
|
|
993
2031
|
createFabricServer,
|
|
994
2032
|
createInFlightTracker,
|
|
995
2033
|
createShutdownHandler,
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
2034
|
+
deriveKnowledgeMetaLayer,
|
|
2035
|
+
deriveKnowledgeMetaTopologyType,
|
|
2036
|
+
ensureKnowledgeFresh,
|
|
2037
|
+
extractKnowledge,
|
|
999
2038
|
flushAndSyncEventLedger,
|
|
1000
2039
|
formatPreexistingRootMessage,
|
|
1001
2040
|
getEventLedgerPath,
|
|
1002
2041
|
getLedgerPath,
|
|
1003
2042
|
getLegacyLedgerPath,
|
|
1004
|
-
|
|
2043
|
+
isSameKnowledgeTestIndex,
|
|
2044
|
+
planContext,
|
|
1005
2045
|
readLockState,
|
|
1006
|
-
|
|
2046
|
+
readSelectionToken,
|
|
2047
|
+
reconcileKnowledge,
|
|
1007
2048
|
releaseLock,
|
|
2049
|
+
reviewKnowledge,
|
|
2050
|
+
runDoctorApplyLint,
|
|
1008
2051
|
runDoctorFix,
|
|
1009
2052
|
runDoctorReport,
|
|
1010
2053
|
stableStringify,
|
|
1011
2054
|
startHttpServer,
|
|
1012
2055
|
startStdioServer,
|
|
1013
|
-
|
|
2056
|
+
writeKnowledgeMeta
|
|
1014
2057
|
};
|