@fenglimg/fabric-server 2.0.0-rc.1 → 2.0.0-rc.10
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-AR2HV5JT.js} +1762 -273
- package/dist/{http-CHCOF6DJ.js → http-7HJQGQSN.js} +12 -151
- package/dist/index.d.ts +131 -30
- package/dist/index.js +1276 -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-AR2HV5JT.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,351 @@ 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
|
+
const baseDir = pendingBase(layer, projectRoot);
|
|
153
|
+
const absolutePath = join2(baseDir, input.type, `${sanitizedSlug}.md`);
|
|
154
|
+
const reportedPath = layer === "personal" ? `~/${relative(resolvePersonalRoot(), absolutePath)}` : relative(projectRoot, absolutePath);
|
|
155
|
+
await ensureParentDirectory(absolutePath);
|
|
156
|
+
if (existsSync2(absolutePath)) {
|
|
157
|
+
const existing = await readFile(absolutePath, "utf8");
|
|
158
|
+
const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
|
|
159
|
+
if (existingKey === idempotencyKey) {
|
|
160
|
+
const augmented = mergeEvidenceNotes(existing, summary, input.recent_paths);
|
|
161
|
+
await atomicWriteText(absolutePath, augmented);
|
|
162
|
+
await emitEventBestEffort(projectRoot, {
|
|
163
|
+
event_type: "knowledge_proposed",
|
|
164
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
165
|
+
correlation_id: primarySession,
|
|
166
|
+
session_id: primarySession,
|
|
167
|
+
reason: `extract_knowledge:${sanitizedSlug}`
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
pending_path: reportedPath,
|
|
171
|
+
idempotency_key: idempotencyKey
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
await emitEventBestEffort(projectRoot, {
|
|
175
|
+
event_type: "knowledge_archive_attempted",
|
|
176
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
177
|
+
correlation_id: primarySession,
|
|
178
|
+
session_id: primarySession,
|
|
179
|
+
reason: `extract_knowledge:${sanitizedSlug}: slug-collision (existing key ${existingKey ?? "<none>"} != incoming ${idempotencyKey})`
|
|
180
|
+
});
|
|
181
|
+
throw new Error(
|
|
182
|
+
`slug collision: pending file ${reportedPath} already exists with a different idempotency_key (existing=${existingKey ?? "<missing>"}, incoming=${idempotencyKey}); rename slug or resolve upstream`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
const fresh = renderFreshEntry({
|
|
186
|
+
type: input.type,
|
|
187
|
+
sourceSessions,
|
|
188
|
+
idempotencyKey,
|
|
189
|
+
summary,
|
|
190
|
+
recentPaths: input.recent_paths,
|
|
191
|
+
layer,
|
|
192
|
+
proposedReason: input.proposed_reason,
|
|
193
|
+
sessionContext: input.session_context
|
|
194
|
+
});
|
|
195
|
+
await atomicWriteText(absolutePath, fresh);
|
|
196
|
+
await emitEventBestEffort(projectRoot, {
|
|
197
|
+
event_type: "knowledge_proposed",
|
|
198
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
199
|
+
correlation_id: primarySession,
|
|
200
|
+
session_id: primarySession,
|
|
201
|
+
reason: `extract_knowledge:${sanitizedSlug}`
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
pending_path: reportedPath,
|
|
205
|
+
idempotency_key: idempotencyKey
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function sanitizeSlug(raw) {
|
|
209
|
+
const lower = raw.toLowerCase();
|
|
210
|
+
const collapsed = lower.replace(/[^a-z0-9]+/g, "-");
|
|
211
|
+
const trimmed = collapsed.replace(/^-+|-+$/g, "");
|
|
212
|
+
if (trimmed.length === 0) {
|
|
213
|
+
return "";
|
|
214
|
+
}
|
|
215
|
+
return trimmed.slice(0, SLUG_MAX_LENGTH).replace(/-+$/g, "");
|
|
216
|
+
}
|
|
217
|
+
function renderFreshEntry(args) {
|
|
218
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
219
|
+
const frontmatter = [
|
|
220
|
+
"---",
|
|
221
|
+
`type: ${args.type}`,
|
|
222
|
+
"maturity: draft",
|
|
223
|
+
`layer: ${args.layer}`,
|
|
224
|
+
`created_at: ${createdAt}`,
|
|
225
|
+
`source_sessions: [${args.sourceSessions.map((s) => JSON.stringify(s)).join(", ")}]`,
|
|
226
|
+
`proposed_reason: ${args.proposedReason}`,
|
|
227
|
+
"tags: []",
|
|
228
|
+
`x-fabric-idempotency-key: ${args.idempotencyKey}`,
|
|
229
|
+
"---"
|
|
230
|
+
].join("\n");
|
|
231
|
+
const reasonExplanation = PROPOSED_REASON_DESCRIPTIONS[args.proposedReason];
|
|
232
|
+
const body = [
|
|
233
|
+
"",
|
|
234
|
+
"## Summary",
|
|
235
|
+
"",
|
|
236
|
+
args.summary,
|
|
237
|
+
"",
|
|
238
|
+
"## Why proposed",
|
|
239
|
+
"",
|
|
240
|
+
`${args.proposedReason} \u2014 ${reasonExplanation}`,
|
|
241
|
+
"",
|
|
242
|
+
"## Session context",
|
|
243
|
+
"",
|
|
244
|
+
args.sessionContext,
|
|
245
|
+
"",
|
|
246
|
+
"## Evidence",
|
|
247
|
+
"",
|
|
248
|
+
renderEvidenceBlock(args.summary, args.recentPaths),
|
|
249
|
+
""
|
|
250
|
+
].join("\n");
|
|
251
|
+
return `${frontmatter}
|
|
252
|
+
${body}`;
|
|
253
|
+
}
|
|
254
|
+
function renderEvidenceBlock(summary, recentPaths) {
|
|
255
|
+
const pathLines = recentPaths.length === 0 ? "_(no recent paths reported)_" : recentPaths.map((p) => `- ${p}`).join("\n");
|
|
256
|
+
return [
|
|
257
|
+
"Recent paths:",
|
|
258
|
+
"",
|
|
259
|
+
pathLines,
|
|
260
|
+
"",
|
|
261
|
+
"Notes:",
|
|
262
|
+
"",
|
|
263
|
+
`- ${summary.trim()}`
|
|
264
|
+
].join("\n");
|
|
265
|
+
}
|
|
266
|
+
function mergeEvidenceNotes(existing, newSummary, newRecentPaths) {
|
|
267
|
+
const beforeMatch = /^([\s\S]*?)(\n## Evidence(?:\s*\(call \d+\))?\s*\n)/u.exec(
|
|
268
|
+
existing.endsWith("\n") ? existing : `${existing}
|
|
269
|
+
`
|
|
270
|
+
);
|
|
271
|
+
if (beforeMatch === null) {
|
|
272
|
+
const trimmed = existing.endsWith("\n") ? existing : `${existing}
|
|
273
|
+
`;
|
|
274
|
+
return `${trimmed}
|
|
275
|
+
## Evidence
|
|
276
|
+
|
|
277
|
+
${renderEvidenceBlock(newSummary, newRecentPaths)}
|
|
278
|
+
`;
|
|
279
|
+
}
|
|
280
|
+
const head = beforeMatch[1] ?? "";
|
|
281
|
+
const existingNotes = [];
|
|
282
|
+
const existingPaths = [];
|
|
283
|
+
const evidenceBlockRe = /\n## Evidence(?:\s*\(call \d+\))?\s*\n([\s\S]*?)(?=\n## |$)/gu;
|
|
284
|
+
let m;
|
|
285
|
+
while ((m = evidenceBlockRe.exec(`${existing}
|
|
286
|
+
`)) !== null) {
|
|
287
|
+
const block = m[1] ?? "";
|
|
288
|
+
const pathSection = /Recent paths:\s*\n([\s\S]*?)(?:\n\s*Notes:|$)/u.exec(block);
|
|
289
|
+
if (pathSection !== null) {
|
|
290
|
+
for (const rawLine of (pathSection[1] ?? "").split(/\r?\n/u)) {
|
|
291
|
+
const t = rawLine.trim();
|
|
292
|
+
if (t.startsWith("- ")) {
|
|
293
|
+
existingPaths.push(t.slice(2).trim());
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const notesSection = /Notes:\s*\n([\s\S]*?)$/u.exec(block);
|
|
298
|
+
const noteBody = (notesSection !== null ? notesSection[1] : block) ?? "";
|
|
299
|
+
const bulletLines = [];
|
|
300
|
+
let prose = [];
|
|
301
|
+
for (const rawLine of noteBody.split(/\r?\n/u)) {
|
|
302
|
+
const t = rawLine.trim();
|
|
303
|
+
if (t.length === 0) continue;
|
|
304
|
+
if (t.startsWith("- ")) {
|
|
305
|
+
if (prose.length > 0) {
|
|
306
|
+
existingNotes.push(prose.join(" ").trim());
|
|
307
|
+
prose = [];
|
|
308
|
+
}
|
|
309
|
+
bulletLines.push(t.slice(2).trim());
|
|
310
|
+
} else {
|
|
311
|
+
prose.push(t);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (prose.length > 0) existingNotes.push(prose.join(" ").trim());
|
|
315
|
+
for (const n of bulletLines) existingNotes.push(n);
|
|
316
|
+
}
|
|
317
|
+
const mergedNotes = [];
|
|
318
|
+
const seenNotes = /* @__PURE__ */ new Set();
|
|
319
|
+
const incomingNote = newSummary.trim();
|
|
320
|
+
const candidates = [...existingNotes, incomingNote];
|
|
321
|
+
for (const note of candidates) {
|
|
322
|
+
const key = note.replace(/\s+/gu, " ").trim();
|
|
323
|
+
if (key.length === 0) continue;
|
|
324
|
+
if (seenNotes.has(key)) continue;
|
|
325
|
+
seenNotes.add(key);
|
|
326
|
+
mergedNotes.push(note);
|
|
327
|
+
}
|
|
328
|
+
const mergedPaths = [];
|
|
329
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
330
|
+
for (const p of [...existingPaths, ...newRecentPaths]) {
|
|
331
|
+
const key = p.trim();
|
|
332
|
+
if (key.length === 0) continue;
|
|
333
|
+
if (seenPaths.has(key)) continue;
|
|
334
|
+
seenPaths.add(key);
|
|
335
|
+
mergedPaths.push(key);
|
|
336
|
+
}
|
|
337
|
+
const pathLines = mergedPaths.length === 0 ? "_(no recent paths reported)_" : mergedPaths.map((p) => `- ${p}`).join("\n");
|
|
338
|
+
const noteLines = mergedNotes.length === 0 ? "_(no notes recorded)_" : mergedNotes.map((n) => `- ${n}`).join("\n");
|
|
339
|
+
const evidenceBody = [
|
|
340
|
+
"Recent paths:",
|
|
341
|
+
"",
|
|
342
|
+
pathLines,
|
|
343
|
+
"",
|
|
344
|
+
"Notes:",
|
|
345
|
+
"",
|
|
346
|
+
noteLines
|
|
347
|
+
].join("\n");
|
|
348
|
+
return `${head}
|
|
349
|
+
## Evidence
|
|
350
|
+
|
|
351
|
+
${evidenceBody}
|
|
352
|
+
`;
|
|
353
|
+
}
|
|
354
|
+
function readFrontmatterKey(content, key) {
|
|
355
|
+
const match = /^---\n([\s\S]*?)\n---/u.exec(content);
|
|
356
|
+
if (match === null) {
|
|
357
|
+
return void 0;
|
|
358
|
+
}
|
|
359
|
+
const block = match[1];
|
|
360
|
+
if (block === void 0) {
|
|
361
|
+
return void 0;
|
|
362
|
+
}
|
|
363
|
+
for (const rawLine of block.split(/\r?\n/u)) {
|
|
364
|
+
const line = rawLine.trim();
|
|
365
|
+
const sep = line.indexOf(":");
|
|
366
|
+
if (sep === -1) continue;
|
|
367
|
+
const k = line.slice(0, sep).trim();
|
|
368
|
+
if (k === key) {
|
|
369
|
+
return line.slice(sep + 1).trim();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return void 0;
|
|
373
|
+
}
|
|
374
|
+
async function emitEventBestEffort(projectRoot, event) {
|
|
375
|
+
try {
|
|
376
|
+
await appendEventLedgerEvent(projectRoot, event);
|
|
377
|
+
} catch {
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/tools/extract-knowledge.ts
|
|
382
|
+
function registerExtractKnowledge(server, tracker) {
|
|
383
|
+
server.registerTool(
|
|
384
|
+
"fab_extract_knowledge",
|
|
385
|
+
{
|
|
386
|
+
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.",
|
|
387
|
+
inputSchema: FabExtractKnowledgeInputShape,
|
|
388
|
+
outputSchema: FabExtractKnowledgeOutputSchema.shape,
|
|
389
|
+
annotations: fabExtractKnowledgeAnnotations
|
|
390
|
+
},
|
|
391
|
+
async (input) => {
|
|
392
|
+
const requestId = randomUUID();
|
|
393
|
+
tracker?.enter(requestId);
|
|
394
|
+
try {
|
|
395
|
+
const projectRoot = resolveProjectRoot();
|
|
396
|
+
const result = await extractKnowledge(projectRoot, input);
|
|
397
|
+
const response = { ...result };
|
|
398
|
+
const payloadLimits = readPayloadLimits(projectRoot);
|
|
399
|
+
const serialized = JSON.stringify(response);
|
|
400
|
+
enforcePayloadLimit(serialized, payloadLimits);
|
|
401
|
+
return {
|
|
402
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
403
|
+
structuredContent: response
|
|
404
|
+
};
|
|
405
|
+
} finally {
|
|
406
|
+
tracker?.exit(requestId);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/tools/plan-context.ts
|
|
413
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
414
|
+
import {
|
|
415
|
+
planContextAnnotations,
|
|
416
|
+
planContextInputSchema,
|
|
417
|
+
planContextOutputSchema
|
|
418
|
+
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
419
|
+
import { enforcePayloadLimit as enforcePayloadLimit2 } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
420
|
+
|
|
102
421
|
// src/services/plan-context.ts
|
|
422
|
+
import { minimatch } from "minimatch";
|
|
423
|
+
import { deriveAgentsMetaLayer } from "@fenglimg/fabric-shared";
|
|
103
424
|
var SELECTION_TOKEN_TTL_MS = 5 * 60 * 1e3;
|
|
104
425
|
var selectionTokenCache = /* @__PURE__ */ new Map();
|
|
105
426
|
async function planContext(projectRoot, input) {
|
|
106
427
|
const meta = await readAgentsMeta(projectRoot);
|
|
107
428
|
const stale = input.client_hash !== void 0 && input.client_hash !== meta.revision;
|
|
108
429
|
const uniquePaths = dedupePaths(input.paths);
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
(item) => includeDeprecated ? true : !isDeprecatedMaturity(item)
|
|
112
|
-
);
|
|
430
|
+
const allDescriptions = buildDescriptionIndex(meta);
|
|
431
|
+
const relevanceTargetPaths = input.target_paths ?? uniquePaths;
|
|
113
432
|
const entries = uniquePaths.map((path2) => {
|
|
114
433
|
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);
|
|
434
|
+
const descriptionIndex = allDescriptions.filter((item) => shouldIncludeIndexItemForPath(item, meta, path2)).filter((item) => shouldIncludeByRelevance(item, relevanceTargetPaths));
|
|
118
435
|
return {
|
|
119
436
|
path: path2,
|
|
120
437
|
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
|
-
}
|
|
438
|
+
description_index: descriptionIndex
|
|
130
439
|
};
|
|
131
440
|
});
|
|
132
|
-
const requiredStableIds = dedupeStableIds(entries.flatMap((entry) => entry.required_stable_ids));
|
|
133
|
-
const aiSelectableStableIds = dedupeStableIds(entries.flatMap((entry) => entry.ai_selectable_stable_ids));
|
|
134
441
|
const sharedDescriptionIndex = dedupeDescriptionIndex(entries.flatMap((entry) => entry.description_index));
|
|
135
|
-
const
|
|
442
|
+
const sharedStableIds = sharedDescriptionIndex.map((item) => item.stable_id);
|
|
443
|
+
const selectionToken = createSelectionToken(meta.revision, uniquePaths, [], sharedStableIds);
|
|
136
444
|
const result = {
|
|
137
445
|
revision_hash: meta.revision,
|
|
138
446
|
stale,
|
|
139
447
|
selection_token: selectionToken,
|
|
140
448
|
entries,
|
|
141
449
|
shared: {
|
|
142
|
-
required_stable_ids: requiredStableIds,
|
|
143
|
-
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
144
450
|
description_index: sharedDescriptionIndex,
|
|
145
451
|
preflight_diagnostics: buildPreflightDiagnostics(meta)
|
|
146
452
|
}
|
|
@@ -149,9 +455,9 @@ async function planContext(projectRoot, input) {
|
|
|
149
455
|
await appendEventLedgerEvent(projectRoot, {
|
|
150
456
|
event_type: "knowledge_context_planned",
|
|
151
457
|
target_paths: uniquePaths,
|
|
152
|
-
required_stable_ids:
|
|
153
|
-
ai_selectable_stable_ids:
|
|
154
|
-
final_stable_ids:
|
|
458
|
+
required_stable_ids: [],
|
|
459
|
+
ai_selectable_stable_ids: sharedDescriptionIndex.map((item) => item.stable_id),
|
|
460
|
+
final_stable_ids: [],
|
|
155
461
|
selection_token: selectionToken,
|
|
156
462
|
client_hash: input.client_hash,
|
|
157
463
|
intent: input.intent,
|
|
@@ -191,7 +497,7 @@ function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSe
|
|
|
191
497
|
function dedupePaths(paths) {
|
|
192
498
|
const seenPaths = /* @__PURE__ */ new Set();
|
|
193
499
|
return paths.flatMap((path2) => {
|
|
194
|
-
const normalizedPath =
|
|
500
|
+
const normalizedPath = normalizeKnowledgePath(path2);
|
|
195
501
|
if (seenPaths.has(normalizedPath)) {
|
|
196
502
|
return [];
|
|
197
503
|
}
|
|
@@ -200,7 +506,7 @@ function dedupePaths(paths) {
|
|
|
200
506
|
});
|
|
201
507
|
}
|
|
202
508
|
function buildRequirementProfile(path2, input) {
|
|
203
|
-
const normalizedPath =
|
|
509
|
+
const normalizedPath = normalizeKnowledgePath(path2);
|
|
204
510
|
const extensionMatch = /(\.[^./\\]+)$/u.exec(normalizedPath);
|
|
205
511
|
const knownTech = dedupeStableIds([
|
|
206
512
|
...input.known_tech ?? [],
|
|
@@ -210,17 +516,14 @@ function buildRequirementProfile(path2, input) {
|
|
|
210
516
|
target_path: normalizedPath,
|
|
211
517
|
path_segments: normalizedPath.split("/").filter(Boolean),
|
|
212
518
|
extension: extensionMatch?.[1] ?? "",
|
|
213
|
-
inferred_domain: inferDomains(normalizedPath),
|
|
214
519
|
known_tech: knownTech,
|
|
215
520
|
user_intent: input.intent ?? "",
|
|
216
|
-
intent_tokens: tokenizeIntent(input.intent ?? ""),
|
|
217
|
-
impact_hints: inferImpactHints(input.intent ?? ""),
|
|
218
521
|
detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path2] ?? []
|
|
219
522
|
};
|
|
220
523
|
}
|
|
221
524
|
function buildDescriptionIndex(meta) {
|
|
222
525
|
return Object.entries(meta.nodes).flatMap(([nodeId, node]) => {
|
|
223
|
-
const level = node.
|
|
526
|
+
const level = deriveAgentsMetaLayer(node.file);
|
|
224
527
|
const description = node.description ?? descriptionFromLegacyActivation(node.activation?.description);
|
|
225
528
|
if (description === void 0) {
|
|
226
529
|
return [];
|
|
@@ -229,16 +532,46 @@ function buildDescriptionIndex(meta) {
|
|
|
229
532
|
return [{
|
|
230
533
|
stable_id: node.stable_id ?? nodeId,
|
|
231
534
|
level,
|
|
232
|
-
required:
|
|
233
|
-
selectable:
|
|
535
|
+
required: false,
|
|
536
|
+
selectable: false,
|
|
234
537
|
description,
|
|
235
538
|
type: description.knowledge_type,
|
|
236
539
|
maturity: description.maturity,
|
|
237
540
|
layer: description.knowledge_layer ?? inferredLayer,
|
|
238
|
-
layer_reason: description.layer_reason
|
|
541
|
+
layer_reason: description.layer_reason,
|
|
542
|
+
// v2.0-rc.5 C3 (TASK-012): surface relevance fields at the top level
|
|
543
|
+
// so the per-entry filter + downstream MCP clients can read them
|
|
544
|
+
// without reaching into description.*. Defaults (broad + []) are
|
|
545
|
+
// applied at the meta-builder layer; we just pass them through here.
|
|
546
|
+
relevance_scope: description.relevance_scope,
|
|
547
|
+
relevance_paths: description.relevance_paths
|
|
239
548
|
}];
|
|
240
549
|
}).sort(compareDescriptionIndexItems);
|
|
241
550
|
}
|
|
551
|
+
function matchesAnyPath(globs, targetPaths) {
|
|
552
|
+
if (globs.length === 0) {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
for (const rawGlob of globs) {
|
|
556
|
+
const glob = rawGlob.endsWith("/") ? `${rawGlob}**` : rawGlob;
|
|
557
|
+
for (const target of targetPaths) {
|
|
558
|
+
if (minimatch(target, glob, { dot: true, matchBase: false })) {
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
function shouldIncludeByRelevance(item, targetPaths) {
|
|
566
|
+
const scope = item.relevance_scope ?? "broad";
|
|
567
|
+
if (scope === "broad") {
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
if (targetPaths.length === 0) {
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
return matchesAnyPath(item.relevance_paths ?? [], targetPaths);
|
|
574
|
+
}
|
|
242
575
|
function inferKnowledgeLayerFromContentRef(contentRef) {
|
|
243
576
|
if (contentRef === void 0) {
|
|
244
577
|
return void 0;
|
|
@@ -251,11 +584,6 @@ function inferKnowledgeLayerFromContentRef(contentRef) {
|
|
|
251
584
|
}
|
|
252
585
|
return void 0;
|
|
253
586
|
}
|
|
254
|
-
function isDeprecatedMaturity(item) {
|
|
255
|
-
const a = item.maturity;
|
|
256
|
-
const b = item.description.maturity;
|
|
257
|
-
return a === "deprecated" || b === "deprecated";
|
|
258
|
-
}
|
|
259
587
|
function descriptionFromLegacyActivation(summary) {
|
|
260
588
|
if (summary === void 0) {
|
|
261
589
|
return void 0;
|
|
@@ -268,24 +596,8 @@ function descriptionFromLegacyActivation(summary) {
|
|
|
268
596
|
must_read_if: summary
|
|
269
597
|
};
|
|
270
598
|
}
|
|
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;
|
|
599
|
+
function shouldIncludeIndexItemForPath(_item, _meta, _path) {
|
|
600
|
+
return true;
|
|
289
601
|
}
|
|
290
602
|
function buildPreflightDiagnostics(meta) {
|
|
291
603
|
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 +611,6 @@ function buildPreflightDiagnostics(meta) {
|
|
|
299
611
|
message: `Resolved registry includes ${missingDescriptionStableIds.length} node(s) without structured descriptions.`
|
|
300
612
|
}];
|
|
301
613
|
}
|
|
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
614
|
function dedupeStableIds(stableIds) {
|
|
323
615
|
return Array.from(new Set(stableIds));
|
|
324
616
|
}
|
|
@@ -333,18 +625,7 @@ function dedupeDescriptionIndex(items) {
|
|
|
333
625
|
});
|
|
334
626
|
}
|
|
335
627
|
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
|
-
}
|
|
628
|
+
return left.stable_id.localeCompare(right.stable_id);
|
|
348
629
|
}
|
|
349
630
|
|
|
350
631
|
// src/tools/plan-context.ts
|
|
@@ -357,12 +638,12 @@ function registerPlanContext(server, tracker) {
|
|
|
357
638
|
outputSchema: planContextOutputSchema,
|
|
358
639
|
annotations: planContextAnnotations
|
|
359
640
|
},
|
|
360
|
-
async ({ paths, intent, known_tech, detected_entities, client_hash, correlation_id, session_id
|
|
361
|
-
const requestId =
|
|
641
|
+
async ({ paths, intent, known_tech, detected_entities, client_hash, correlation_id, session_id }) => {
|
|
642
|
+
const requestId = randomUUID2();
|
|
362
643
|
tracker?.enter(requestId);
|
|
363
644
|
try {
|
|
364
645
|
const projectRoot = resolveProjectRoot();
|
|
365
|
-
const syncReport = await
|
|
646
|
+
const syncReport = await ensureKnowledgeFresh(projectRoot);
|
|
366
647
|
const result = await planContext(projectRoot, {
|
|
367
648
|
paths,
|
|
368
649
|
intent,
|
|
@@ -370,8 +651,7 @@ function registerPlanContext(server, tracker) {
|
|
|
370
651
|
detected_entities,
|
|
371
652
|
client_hash,
|
|
372
653
|
correlation_id,
|
|
373
|
-
session_id
|
|
374
|
-
include_deprecated
|
|
654
|
+
session_id
|
|
375
655
|
});
|
|
376
656
|
const response = {
|
|
377
657
|
...result,
|
|
@@ -379,7 +659,7 @@ function registerPlanContext(server, tracker) {
|
|
|
379
659
|
};
|
|
380
660
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
381
661
|
const serialized = JSON.stringify(response);
|
|
382
|
-
const guardResult =
|
|
662
|
+
const guardResult = enforcePayloadLimit2(serialized, payloadLimits);
|
|
383
663
|
if (guardResult.warning) {
|
|
384
664
|
response.warnings = [
|
|
385
665
|
...response.warnings,
|
|
@@ -401,20 +681,800 @@ function registerPlanContext(server, tracker) {
|
|
|
401
681
|
);
|
|
402
682
|
}
|
|
403
683
|
|
|
404
|
-
// src/tools/
|
|
405
|
-
import { randomUUID as
|
|
684
|
+
// src/tools/review.ts
|
|
685
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
406
686
|
import {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
687
|
+
FabReviewInputSchema,
|
|
688
|
+
FabReviewInputShape,
|
|
689
|
+
FabReviewOutputShape,
|
|
690
|
+
fabReviewAnnotations
|
|
410
691
|
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
411
|
-
import { enforcePayloadLimit as
|
|
692
|
+
import { enforcePayloadLimit as enforcePayloadLimit3 } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
412
693
|
|
|
413
|
-
// src/services/
|
|
414
|
-
import {
|
|
415
|
-
import {
|
|
416
|
-
import {
|
|
417
|
-
|
|
694
|
+
// src/services/review.ts
|
|
695
|
+
import { execFileSync } from "child_process";
|
|
696
|
+
import { existsSync as existsSync3 } from "fs";
|
|
697
|
+
import { readFile as readFile3, readdir, unlink } from "fs/promises";
|
|
698
|
+
import { homedir as homedir2 } from "os";
|
|
699
|
+
import { basename, join as join3, relative as relative2, resolve } from "path";
|
|
700
|
+
|
|
701
|
+
// src/services/knowledge-id-allocator.ts
|
|
702
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
703
|
+
import { dirname } from "path";
|
|
704
|
+
import { mkdir } from "fs/promises";
|
|
705
|
+
import {
|
|
706
|
+
AgentsMetaCountersSchema,
|
|
707
|
+
agentsMetaSchema,
|
|
708
|
+
allocateKnowledgeId,
|
|
709
|
+
defaultAgentsMetaCounters
|
|
710
|
+
} from "@fenglimg/fabric-shared";
|
|
711
|
+
import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
712
|
+
var KnowledgeIdAllocator = class {
|
|
713
|
+
constructor(metaPath) {
|
|
714
|
+
this.metaPath = metaPath;
|
|
715
|
+
}
|
|
716
|
+
metaPath;
|
|
717
|
+
/**
|
|
718
|
+
* Allocate the next stable_id for the given (layer, type) pair and persist
|
|
719
|
+
* the advanced counter to `agents.meta.json`.
|
|
720
|
+
*/
|
|
721
|
+
async allocate(layer, type) {
|
|
722
|
+
const meta = await this.readMeta();
|
|
723
|
+
const counters = this.normalizeCounters(meta.counters);
|
|
724
|
+
const { id, nextCounters } = allocateKnowledgeId(layer, type, counters);
|
|
725
|
+
await this.writeMetaAtomic({ ...meta, counters: nextCounters });
|
|
726
|
+
return id;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Returns the current counters envelope, defaulting to all-zero slots when
|
|
730
|
+
* the meta file is absent or pre-v2.0 (counters key missing).
|
|
731
|
+
*/
|
|
732
|
+
async getCounters() {
|
|
733
|
+
const meta = await this.readMeta();
|
|
734
|
+
return this.normalizeCounters(meta.counters);
|
|
735
|
+
}
|
|
736
|
+
// ---- internal helpers ------------------------------------------------
|
|
737
|
+
async readMeta() {
|
|
738
|
+
let raw;
|
|
739
|
+
try {
|
|
740
|
+
raw = await readFile2(this.metaPath, "utf8");
|
|
741
|
+
} catch (err) {
|
|
742
|
+
if (isNodeError(err) && err.code === "ENOENT") {
|
|
743
|
+
return { revision: "", nodes: {} };
|
|
744
|
+
}
|
|
745
|
+
throw err;
|
|
746
|
+
}
|
|
747
|
+
let parsed;
|
|
748
|
+
try {
|
|
749
|
+
parsed = JSON.parse(raw);
|
|
750
|
+
} catch {
|
|
751
|
+
return { revision: "", nodes: {} };
|
|
752
|
+
}
|
|
753
|
+
const validation = agentsMetaSchema.safeParse(parsed);
|
|
754
|
+
if (validation.success) {
|
|
755
|
+
return validation.data;
|
|
756
|
+
}
|
|
757
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
758
|
+
}
|
|
759
|
+
normalizeCounters(input) {
|
|
760
|
+
if (input === void 0 || input === null) {
|
|
761
|
+
return defaultAgentsMetaCounters();
|
|
762
|
+
}
|
|
763
|
+
const parsed = AgentsMetaCountersSchema.safeParse(input);
|
|
764
|
+
return parsed.success ? parsed.data : defaultAgentsMetaCounters();
|
|
765
|
+
}
|
|
766
|
+
async writeMetaAtomic(meta) {
|
|
767
|
+
await ensureParentDirectory2(this.metaPath);
|
|
768
|
+
await atomicWriteJson(this.metaPath, meta, { indent: 2 });
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
async function ensureParentDirectory2(filePath) {
|
|
772
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
773
|
+
}
|
|
774
|
+
function isNodeError(err) {
|
|
775
|
+
return err instanceof Error && typeof err.code === "string";
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// src/services/review.ts
|
|
779
|
+
var PENDING_BASE_TEAM_REL = ".fabric/knowledge/pending";
|
|
780
|
+
function pendingBaseAbs(layer, projectRoot) {
|
|
781
|
+
if (layer === "personal") {
|
|
782
|
+
return join3(resolvePersonalRoot2(), ".fabric", "knowledge", "pending");
|
|
783
|
+
}
|
|
784
|
+
return join3(projectRoot, PENDING_BASE_TEAM_REL);
|
|
785
|
+
}
|
|
786
|
+
var PLURAL_TYPES = [
|
|
787
|
+
"decisions",
|
|
788
|
+
"pitfalls",
|
|
789
|
+
"guidelines",
|
|
790
|
+
"models",
|
|
791
|
+
"processes"
|
|
792
|
+
];
|
|
793
|
+
var PLURAL_TO_SINGULAR = {
|
|
794
|
+
decisions: "decision",
|
|
795
|
+
pitfalls: "pitfall",
|
|
796
|
+
guidelines: "guideline",
|
|
797
|
+
models: "model",
|
|
798
|
+
processes: "process"
|
|
799
|
+
};
|
|
800
|
+
async function reviewKnowledge(projectRoot, input) {
|
|
801
|
+
switch (input.action) {
|
|
802
|
+
case "list":
|
|
803
|
+
return {
|
|
804
|
+
action: "list",
|
|
805
|
+
items: await listPending(projectRoot, input.filters)
|
|
806
|
+
};
|
|
807
|
+
case "approve":
|
|
808
|
+
return {
|
|
809
|
+
action: "approve",
|
|
810
|
+
approved: await approveAll(projectRoot, input.pending_paths)
|
|
811
|
+
};
|
|
812
|
+
case "reject":
|
|
813
|
+
return {
|
|
814
|
+
action: "reject",
|
|
815
|
+
rejected: await rejectAll(projectRoot, input.pending_paths, input.reason)
|
|
816
|
+
};
|
|
817
|
+
case "modify":
|
|
818
|
+
return await modifyEntry(projectRoot, input.pending_path, input.changes);
|
|
819
|
+
case "search":
|
|
820
|
+
return {
|
|
821
|
+
action: "search",
|
|
822
|
+
items: await searchEntries(projectRoot, input.query, input.filters)
|
|
823
|
+
};
|
|
824
|
+
case "defer":
|
|
825
|
+
return {
|
|
826
|
+
action: "defer",
|
|
827
|
+
deferred: await deferAll(
|
|
828
|
+
projectRoot,
|
|
829
|
+
input.pending_paths,
|
|
830
|
+
input.until,
|
|
831
|
+
input.reason
|
|
832
|
+
)
|
|
833
|
+
};
|
|
834
|
+
default: {
|
|
835
|
+
const exhaustive = input;
|
|
836
|
+
throw new Error(`unsupported action: ${JSON.stringify(exhaustive)}`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function resolveSandboxedPath(projectRoot, candidate, options = {}) {
|
|
841
|
+
if (candidate.length === 0) {
|
|
842
|
+
throw new Error("path is empty");
|
|
843
|
+
}
|
|
844
|
+
const projectKnowledgeRoot = resolve(projectRoot, ".fabric", "knowledge");
|
|
845
|
+
const personalKnowledgeRoot = resolve(resolvePersonalRoot2(), ".fabric", "knowledge");
|
|
846
|
+
if (candidate.startsWith("~/")) {
|
|
847
|
+
if (options.allowPersonal !== true) {
|
|
848
|
+
throw new Error(`personal-root path not allowed for this action: ${candidate}`);
|
|
849
|
+
}
|
|
850
|
+
const abs = resolve(resolvePersonalRoot2(), candidate.slice(2));
|
|
851
|
+
if (abs !== personalKnowledgeRoot && !abs.startsWith(personalKnowledgeRoot + "/")) {
|
|
852
|
+
throw new Error(`path escapes personal knowledge root: ${candidate}`);
|
|
853
|
+
}
|
|
854
|
+
return { abs, isInProjectTree: false };
|
|
855
|
+
}
|
|
856
|
+
const projectAbs = resolve(projectRoot, candidate);
|
|
857
|
+
if (projectAbs === projectKnowledgeRoot || projectAbs.startsWith(projectKnowledgeRoot + "/")) {
|
|
858
|
+
return { abs: projectAbs, isInProjectTree: true };
|
|
859
|
+
}
|
|
860
|
+
if (options.allowPersonal === true) {
|
|
861
|
+
const personalAbs = resolve(resolvePersonalRoot2(), candidate);
|
|
862
|
+
if (personalAbs === personalKnowledgeRoot || personalAbs.startsWith(personalKnowledgeRoot + "/")) {
|
|
863
|
+
return { abs: personalAbs, isInProjectTree: false };
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
throw new Error(`path escapes knowledge root: ${candidate}`);
|
|
867
|
+
}
|
|
868
|
+
async function listPending(projectRoot, filters) {
|
|
869
|
+
const items = [];
|
|
870
|
+
const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
|
|
871
|
+
const sources = [
|
|
872
|
+
{ origin: "team", root: pendingBaseAbs("team", projectRoot) },
|
|
873
|
+
{ origin: "personal", root: pendingBaseAbs("personal", projectRoot) }
|
|
874
|
+
];
|
|
875
|
+
for (const source of sources) {
|
|
876
|
+
for (const type of typesToScan) {
|
|
877
|
+
const dir = join3(source.root, type);
|
|
878
|
+
if (!existsSync3(dir)) {
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
let entries;
|
|
882
|
+
try {
|
|
883
|
+
entries = await readdir(dir);
|
|
884
|
+
} catch {
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
for (const name of entries) {
|
|
888
|
+
if (!name.endsWith(".md")) continue;
|
|
889
|
+
const absolutePath = join3(dir, name);
|
|
890
|
+
let content;
|
|
891
|
+
try {
|
|
892
|
+
content = await readFile3(absolutePath, "utf8");
|
|
893
|
+
} catch {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
const fm = parseFrontmatter(content);
|
|
897
|
+
const layer = fm.layer ?? (source.origin === "personal" ? "personal" : "team");
|
|
898
|
+
const maturity = fm.maturity ?? "draft";
|
|
899
|
+
if (filters?.layer !== void 0 && filters.layer !== "both" && filters.layer !== layer) {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (filters?.maturity !== void 0 && filters.maturity !== maturity) {
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
if (filters?.tags !== void 0 && filters.tags.length > 0) {
|
|
906
|
+
const itemTags = fm.tags ?? [];
|
|
907
|
+
const hasAll = filters.tags.every((t) => itemTags.includes(t));
|
|
908
|
+
if (!hasAll) continue;
|
|
909
|
+
}
|
|
910
|
+
if (filters?.created_after !== void 0) {
|
|
911
|
+
const createdAt = fm.created_at;
|
|
912
|
+
if (createdAt === void 0 || createdAt < filters.created_after) {
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const reportedPath = source.origin === "personal" ? `~/${relative2(resolvePersonalRoot2(), absolutePath)}` : relative2(projectRoot, absolutePath);
|
|
917
|
+
items.push({
|
|
918
|
+
pending_path: reportedPath,
|
|
919
|
+
type,
|
|
920
|
+
layer,
|
|
921
|
+
maturity,
|
|
922
|
+
origin: source.origin,
|
|
923
|
+
...fm.tags !== void 0 && fm.tags.length > 0 ? { tags: fm.tags } : {}
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return items;
|
|
929
|
+
}
|
|
930
|
+
async function approveAll(projectRoot, pendingPaths) {
|
|
931
|
+
const allocator = new KnowledgeIdAllocator(
|
|
932
|
+
join3(projectRoot, ".fabric", "agents.meta.json")
|
|
933
|
+
);
|
|
934
|
+
const approved = [];
|
|
935
|
+
for (const pendingPath of pendingPaths) {
|
|
936
|
+
const result = await approveOne(projectRoot, pendingPath, allocator);
|
|
937
|
+
if (result !== null) {
|
|
938
|
+
approved.push(result);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return approved;
|
|
942
|
+
}
|
|
943
|
+
async function approveOne(projectRoot, pendingPath, allocator) {
|
|
944
|
+
let sourceAbs;
|
|
945
|
+
let sourceOrigin;
|
|
946
|
+
try {
|
|
947
|
+
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
948
|
+
const teamPendingAbs = pendingBaseAbs("team", projectRoot);
|
|
949
|
+
const personalPendingAbs = pendingBaseAbs("personal", projectRoot);
|
|
950
|
+
const inTeamPending = sandboxed.abs === teamPendingAbs || sandboxed.abs.startsWith(teamPendingAbs + "/");
|
|
951
|
+
const inPersonalPending = sandboxed.abs === personalPendingAbs || sandboxed.abs.startsWith(personalPendingAbs + "/");
|
|
952
|
+
if (!inTeamPending && !inPersonalPending) {
|
|
953
|
+
throw new Error(`approve path is outside .fabric/knowledge/pending/: ${pendingPath}`);
|
|
954
|
+
}
|
|
955
|
+
sourceAbs = sandboxed.abs;
|
|
956
|
+
sourceOrigin = inPersonalPending ? "personal" : "team";
|
|
957
|
+
} catch (err) {
|
|
958
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
959
|
+
await emitEventBestEffort2(projectRoot, {
|
|
960
|
+
event_type: "knowledge_promote_failed",
|
|
961
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
962
|
+
reason: `approve:${pendingPath}: ${reason}`
|
|
963
|
+
});
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
const slug = basename(pendingPath).replace(/\.md$/u, "");
|
|
967
|
+
await emitEventBestEffort2(projectRoot, {
|
|
968
|
+
event_type: "knowledge_promote_started",
|
|
969
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
970
|
+
reason: `approve:${slug}`
|
|
971
|
+
});
|
|
972
|
+
let allocatedId;
|
|
973
|
+
let targetAbs;
|
|
974
|
+
let writtenTarget = false;
|
|
975
|
+
try {
|
|
976
|
+
const content = await readFile3(sourceAbs, "utf8");
|
|
977
|
+
const fm = parseFrontmatter(content);
|
|
978
|
+
const pluralType = fm.type;
|
|
979
|
+
if (pluralType === void 0 || !PLURAL_TYPES.includes(pluralType)) {
|
|
980
|
+
throw new Error(`pending file missing or invalid 'type' frontmatter: ${pendingPath}`);
|
|
981
|
+
}
|
|
982
|
+
const layer = fm.layer ?? "team";
|
|
983
|
+
const singularType = PLURAL_TO_SINGULAR[pluralType];
|
|
984
|
+
const stableId = await allocator.allocate(layer, singularType);
|
|
985
|
+
allocatedId = stableId;
|
|
986
|
+
const newFilename = `${stableId}--${slug}.md`;
|
|
987
|
+
const layerRoot = layer === "personal" ? join3(resolvePersonalRoot2(), ".fabric") : join3(projectRoot, ".fabric");
|
|
988
|
+
targetAbs = join3(layerRoot, "knowledge", pluralType, newFilename);
|
|
989
|
+
await ensureParentDirectory(targetAbs);
|
|
990
|
+
const rewritten = rewriteFrontmatterForPromote(content, stableId);
|
|
991
|
+
await atomicWriteText(targetAbs, rewritten);
|
|
992
|
+
writtenTarget = true;
|
|
993
|
+
if (sourceOrigin === "team") {
|
|
994
|
+
try {
|
|
995
|
+
execFileSync("git", ["rm", "--quiet", "-f", pendingPath], {
|
|
996
|
+
cwd: projectRoot,
|
|
997
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
998
|
+
});
|
|
999
|
+
} catch {
|
|
1000
|
+
if (existsSync3(sourceAbs)) {
|
|
1001
|
+
await unlink(sourceAbs);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
} else {
|
|
1005
|
+
if (existsSync3(sourceAbs)) {
|
|
1006
|
+
await unlink(sourceAbs);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1010
|
+
event_type: "knowledge_promoted",
|
|
1011
|
+
stable_id: stableId,
|
|
1012
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1013
|
+
reason: `approve:${slug}`
|
|
1014
|
+
});
|
|
1015
|
+
return { pending_path: pendingPath, stable_id: stableId };
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
if (writtenTarget && targetAbs !== void 0 && existsSync3(targetAbs)) {
|
|
1018
|
+
try {
|
|
1019
|
+
await unlink(targetAbs);
|
|
1020
|
+
} catch {
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1024
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1025
|
+
event_type: "knowledge_promote_failed",
|
|
1026
|
+
...allocatedId !== void 0 ? { stable_id: allocatedId } : {},
|
|
1027
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1028
|
+
reason: `approve:${slug}: ${reason}`
|
|
1029
|
+
});
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
async function rejectAll(projectRoot, pendingPaths, reason) {
|
|
1034
|
+
const rejected = [];
|
|
1035
|
+
for (const pendingPath of pendingPaths) {
|
|
1036
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1037
|
+
event_type: "knowledge_rejected",
|
|
1038
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1039
|
+
reason: `reject:${pendingPath}: ${reason}`
|
|
1040
|
+
});
|
|
1041
|
+
rejected.push(pendingPath);
|
|
1042
|
+
}
|
|
1043
|
+
return rejected;
|
|
1044
|
+
}
|
|
1045
|
+
async function modifyEntry(projectRoot, pendingPath, changes) {
|
|
1046
|
+
const target = resolveModifyTarget(projectRoot, pendingPath);
|
|
1047
|
+
if (target === null) {
|
|
1048
|
+
throw new Error(`modify target not found: ${pendingPath}`);
|
|
1049
|
+
}
|
|
1050
|
+
const content = await readFile3(target.absPath, "utf8");
|
|
1051
|
+
const fm = parseFrontmatter(content);
|
|
1052
|
+
const currentLayer = fm.layer ?? "team";
|
|
1053
|
+
if (changes.layer !== void 0 && changes.layer !== currentLayer) {
|
|
1054
|
+
return await modifyLayerFlip(projectRoot, target, content, fm, changes);
|
|
1055
|
+
}
|
|
1056
|
+
const merged = rewriteFrontmatterMerge(content, changes);
|
|
1057
|
+
await atomicWriteText(target.absPath, merged);
|
|
1058
|
+
return {
|
|
1059
|
+
action: "modify",
|
|
1060
|
+
pending_path: pendingPath
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
function resolveModifyTarget(projectRoot, pendingPath) {
|
|
1064
|
+
let sandboxed;
|
|
1065
|
+
try {
|
|
1066
|
+
sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
1067
|
+
} catch {
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
if (existsSync3(sandboxed.abs)) {
|
|
1071
|
+
return {
|
|
1072
|
+
absPath: sandboxed.abs,
|
|
1073
|
+
isInProjectTree: sandboxed.isInProjectTree,
|
|
1074
|
+
inferredType: inferTypeFromPath(pendingPath),
|
|
1075
|
+
slug: extractSlug(pendingPath)
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
function inferTypeFromPath(path2) {
|
|
1081
|
+
const match = /knowledge\/(?:pending\/)?([^/]+)\/[^/]+\.md$/u.exec(path2);
|
|
1082
|
+
if (match === null) return null;
|
|
1083
|
+
const seg = match[1];
|
|
1084
|
+
if (seg !== void 0 && PLURAL_TYPES.includes(seg)) {
|
|
1085
|
+
return seg;
|
|
1086
|
+
}
|
|
1087
|
+
return null;
|
|
1088
|
+
}
|
|
1089
|
+
function extractSlug(path2) {
|
|
1090
|
+
const file = basename(path2).replace(/\.md$/u, "");
|
|
1091
|
+
return file.replace(/^K[PT]-(MOD|DEC|GLD|PIT|PRO)-\d+--/u, "");
|
|
1092
|
+
}
|
|
1093
|
+
async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
1094
|
+
const fromLayer = fm.layer ?? "team";
|
|
1095
|
+
const toLayer = changes.layer;
|
|
1096
|
+
const pluralType = fm.type ?? target.inferredType;
|
|
1097
|
+
if (pluralType === null || pluralType === void 0) {
|
|
1098
|
+
throw new Error(`layer-flip requires a known type; could not infer for ${target.absPath}`);
|
|
1099
|
+
}
|
|
1100
|
+
const slug = target.slug;
|
|
1101
|
+
const priorStableId = fm.id;
|
|
1102
|
+
const fromScope = fm.relevance_scope ?? "broad";
|
|
1103
|
+
const shouldAutoDegrade = fromScope === "narrow" && fromLayer === "team" && toLayer === "personal";
|
|
1104
|
+
const allocator = new KnowledgeIdAllocator(
|
|
1105
|
+
join3(projectRoot, ".fabric", "agents.meta.json")
|
|
1106
|
+
);
|
|
1107
|
+
const singularType = PLURAL_TO_SINGULAR[pluralType];
|
|
1108
|
+
const newStableId = await allocator.allocate(toLayer, singularType);
|
|
1109
|
+
const toRoot = toLayer === "personal" ? join3(resolvePersonalRoot2(), ".fabric") : join3(projectRoot, ".fabric");
|
|
1110
|
+
const toAbs = join3(toRoot, "knowledge", pluralType, `${newStableId}--${slug}.md`);
|
|
1111
|
+
await ensureParentDirectory(toAbs);
|
|
1112
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1113
|
+
event_type: "knowledge_promote_started",
|
|
1114
|
+
...priorStableId !== void 0 ? { stable_id: priorStableId } : {},
|
|
1115
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1116
|
+
reason: `layer_flip:${priorStableId ?? "<unassigned>"}->${newStableId}`
|
|
1117
|
+
});
|
|
1118
|
+
const effectivePatch = shouldAutoDegrade ? {
|
|
1119
|
+
...changes,
|
|
1120
|
+
layer: toLayer,
|
|
1121
|
+
relevance_scope: "broad",
|
|
1122
|
+
relevance_paths: []
|
|
1123
|
+
} : { ...changes, layer: toLayer };
|
|
1124
|
+
const rewritten = rewriteFrontmatterMerge(content, effectivePatch, { id: newStableId });
|
|
1125
|
+
await atomicWriteText(toAbs, rewritten);
|
|
1126
|
+
if (target.isInProjectTree) {
|
|
1127
|
+
const relSource = relative2(projectRoot, target.absPath);
|
|
1128
|
+
try {
|
|
1129
|
+
execFileSync("git", ["rm", "--quiet", "-f", relSource], {
|
|
1130
|
+
cwd: projectRoot,
|
|
1131
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1132
|
+
});
|
|
1133
|
+
} catch {
|
|
1134
|
+
if (existsSync3(target.absPath)) {
|
|
1135
|
+
await unlink(target.absPath);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
} else if (existsSync3(target.absPath)) {
|
|
1139
|
+
await unlink(target.absPath);
|
|
1140
|
+
}
|
|
1141
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1142
|
+
event_type: "knowledge_layer_changed",
|
|
1143
|
+
stable_id: newStableId,
|
|
1144
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1145
|
+
from_layer: fromLayer,
|
|
1146
|
+
to_layer: toLayer,
|
|
1147
|
+
reason: `layer_flip:${priorStableId ?? "<unassigned>"}->${newStableId}`
|
|
1148
|
+
});
|
|
1149
|
+
if (shouldAutoDegrade) {
|
|
1150
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1151
|
+
event_type: "knowledge_scope_degraded",
|
|
1152
|
+
stable_id: newStableId,
|
|
1153
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1154
|
+
from_scope: "narrow",
|
|
1155
|
+
to_scope: "broad",
|
|
1156
|
+
reason: "personal-implies-broad"
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
const responsePath = toLayer === "team" ? relative2(projectRoot, toAbs) : `~/${relative2(resolvePersonalRoot2(), toAbs)}`;
|
|
1160
|
+
return {
|
|
1161
|
+
action: "modify",
|
|
1162
|
+
pending_path: responsePath,
|
|
1163
|
+
...priorStableId !== void 0 ? { prior_stable_id: priorStableId } : {},
|
|
1164
|
+
new_stable_id: newStableId
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
async function searchEntries(projectRoot, query, filters) {
|
|
1168
|
+
const lowerQuery = query.toLowerCase();
|
|
1169
|
+
const items = [];
|
|
1170
|
+
const sources = [
|
|
1171
|
+
{ root: pendingBaseAbs("team", projectRoot), isPending: true, isPersonal: false },
|
|
1172
|
+
{ root: pendingBaseAbs("personal", projectRoot), isPending: true, isPersonal: true },
|
|
1173
|
+
{ root: join3(projectRoot, ".fabric", "knowledge"), isPending: false, isPersonal: false },
|
|
1174
|
+
{ root: join3(resolvePersonalRoot2(), ".fabric", "knowledge"), isPending: false, isPersonal: true }
|
|
1175
|
+
];
|
|
1176
|
+
const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
|
|
1177
|
+
for (const source of sources) {
|
|
1178
|
+
for (const type of typesToScan) {
|
|
1179
|
+
const dir = join3(source.root, type);
|
|
1180
|
+
if (!existsSync3(dir)) continue;
|
|
1181
|
+
let entries;
|
|
1182
|
+
try {
|
|
1183
|
+
entries = await readdir(dir);
|
|
1184
|
+
} catch {
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
for (const name of entries) {
|
|
1188
|
+
if (!name.endsWith(".md")) continue;
|
|
1189
|
+
const absolutePath = join3(dir, name);
|
|
1190
|
+
let content;
|
|
1191
|
+
try {
|
|
1192
|
+
content = await readFile3(absolutePath, "utf8");
|
|
1193
|
+
} catch {
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
const fm = parseFrontmatter(content);
|
|
1197
|
+
const layer = fm.layer ?? (source.isPersonal ? "personal" : "team");
|
|
1198
|
+
const maturity = fm.maturity ?? "draft";
|
|
1199
|
+
if (filters?.layer !== void 0 && filters.layer !== "both" && filters.layer !== layer) {
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
if (filters?.maturity !== void 0 && filters.maturity !== maturity) {
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
if (filters?.tags !== void 0 && filters.tags.length > 0) {
|
|
1206
|
+
const itemTags = fm.tags ?? [];
|
|
1207
|
+
const hasAll = filters.tags.every((t) => itemTags.includes(t));
|
|
1208
|
+
if (!hasAll) continue;
|
|
1209
|
+
}
|
|
1210
|
+
if (filters?.created_after !== void 0) {
|
|
1211
|
+
const createdAt = fm.created_at;
|
|
1212
|
+
if (createdAt === void 0 || createdAt < filters.created_after) {
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
const haystacks = [
|
|
1217
|
+
fm.title ?? "",
|
|
1218
|
+
fm.summary ?? "",
|
|
1219
|
+
...fm.tags ?? [],
|
|
1220
|
+
name
|
|
1221
|
+
].map((s) => s.toLowerCase());
|
|
1222
|
+
const matches = haystacks.some((h) => h.includes(lowerQuery));
|
|
1223
|
+
if (!matches) continue;
|
|
1224
|
+
const reportedPath = source.isPersonal ? `~/${relative2(resolvePersonalRoot2(), absolutePath)}` : relative2(projectRoot, absolutePath);
|
|
1225
|
+
items.push({
|
|
1226
|
+
pending_path: reportedPath,
|
|
1227
|
+
type,
|
|
1228
|
+
layer,
|
|
1229
|
+
maturity,
|
|
1230
|
+
// Only pending entries carry an origin tag (search results that are
|
|
1231
|
+
// canonical entries don't have a pending root to point back to).
|
|
1232
|
+
...source.isPending ? { origin: source.isPersonal ? "personal" : "team" } : {},
|
|
1233
|
+
...fm.tags !== void 0 && fm.tags.length > 0 ? { tags: fm.tags } : {},
|
|
1234
|
+
...fm.title !== void 0 ? { title: fm.title } : {},
|
|
1235
|
+
...fm.summary !== void 0 ? { summary: fm.summary } : {}
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return items;
|
|
1241
|
+
}
|
|
1242
|
+
async function deferAll(projectRoot, pendingPaths, until, reason) {
|
|
1243
|
+
const deferred = [];
|
|
1244
|
+
for (const pendingPath of pendingPaths) {
|
|
1245
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1246
|
+
event_type: "knowledge_deferred",
|
|
1247
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1248
|
+
...until !== void 0 ? { until } : {},
|
|
1249
|
+
...reason !== void 0 ? { reason } : {}
|
|
1250
|
+
});
|
|
1251
|
+
deferred.push(pendingPath);
|
|
1252
|
+
}
|
|
1253
|
+
return deferred;
|
|
1254
|
+
}
|
|
1255
|
+
function parseFrontmatter(content) {
|
|
1256
|
+
const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/u.exec(content);
|
|
1257
|
+
if (match === null) {
|
|
1258
|
+
return {};
|
|
1259
|
+
}
|
|
1260
|
+
const block = match[1];
|
|
1261
|
+
if (block === void 0) {
|
|
1262
|
+
return {};
|
|
1263
|
+
}
|
|
1264
|
+
const out = {};
|
|
1265
|
+
for (const rawLine of block.split(/\r?\n/u)) {
|
|
1266
|
+
const line = rawLine.trim();
|
|
1267
|
+
if (line.length === 0) continue;
|
|
1268
|
+
const sep = line.indexOf(":");
|
|
1269
|
+
if (sep === -1) continue;
|
|
1270
|
+
const key = line.slice(0, sep).trim();
|
|
1271
|
+
const value = line.slice(sep + 1).trim();
|
|
1272
|
+
switch (key) {
|
|
1273
|
+
case "id":
|
|
1274
|
+
out.id = stripQuotes(value);
|
|
1275
|
+
break;
|
|
1276
|
+
case "type":
|
|
1277
|
+
if (PLURAL_TYPES.includes(value)) {
|
|
1278
|
+
out.type = value;
|
|
1279
|
+
}
|
|
1280
|
+
break;
|
|
1281
|
+
case "layer":
|
|
1282
|
+
if (value === "team" || value === "personal") {
|
|
1283
|
+
out.layer = value;
|
|
1284
|
+
}
|
|
1285
|
+
break;
|
|
1286
|
+
case "maturity":
|
|
1287
|
+
if (value === "draft" || value === "verified" || value === "proven") {
|
|
1288
|
+
out.maturity = value;
|
|
1289
|
+
}
|
|
1290
|
+
break;
|
|
1291
|
+
case "source_session":
|
|
1292
|
+
out.source_session = stripQuotes(value);
|
|
1293
|
+
break;
|
|
1294
|
+
case "created_at":
|
|
1295
|
+
out.created_at = stripQuotes(value);
|
|
1296
|
+
break;
|
|
1297
|
+
case "tags":
|
|
1298
|
+
out.tags = parseFlowArray(value);
|
|
1299
|
+
break;
|
|
1300
|
+
case "title":
|
|
1301
|
+
out.title = stripQuotes(value);
|
|
1302
|
+
break;
|
|
1303
|
+
case "summary":
|
|
1304
|
+
out.summary = stripQuotes(value);
|
|
1305
|
+
break;
|
|
1306
|
+
case "relevance_scope":
|
|
1307
|
+
if (value === "narrow" || value === "broad") {
|
|
1308
|
+
out.relevance_scope = value;
|
|
1309
|
+
}
|
|
1310
|
+
break;
|
|
1311
|
+
case "relevance_paths":
|
|
1312
|
+
out.relevance_paths = parseFlowArray(value);
|
|
1313
|
+
break;
|
|
1314
|
+
default:
|
|
1315
|
+
break;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
return out;
|
|
1319
|
+
}
|
|
1320
|
+
function stripQuotes(value) {
|
|
1321
|
+
return value.replace(/^["'](.*)["']$/u, "$1");
|
|
1322
|
+
}
|
|
1323
|
+
function parseFlowArray(value) {
|
|
1324
|
+
const trimmed = value.trim();
|
|
1325
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) {
|
|
1326
|
+
return [];
|
|
1327
|
+
}
|
|
1328
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
1329
|
+
if (inner.length === 0) {
|
|
1330
|
+
return [];
|
|
1331
|
+
}
|
|
1332
|
+
return inner.split(",").map((item) => stripQuotes(item.trim())).filter((item) => item.length > 0);
|
|
1333
|
+
}
|
|
1334
|
+
function rewriteFrontmatterForPromote(content, stableId) {
|
|
1335
|
+
const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u.exec(content);
|
|
1336
|
+
if (match === null) {
|
|
1337
|
+
return `---
|
|
1338
|
+
id: ${stableId}
|
|
1339
|
+
---
|
|
1340
|
+
|
|
1341
|
+
${content}`;
|
|
1342
|
+
}
|
|
1343
|
+
const block = match[1] ?? "";
|
|
1344
|
+
const filteredLines = block.split(/\r?\n/u).filter((line) => !/^x-fabric-idempotency-key\s*:/u.test(line));
|
|
1345
|
+
filteredLines.unshift(`id: ${stableId}`);
|
|
1346
|
+
const newBlock = filteredLines.join("\n");
|
|
1347
|
+
const before = content.slice(0, match.index);
|
|
1348
|
+
const after = content.slice(match.index + match[0].length);
|
|
1349
|
+
return `${before}---
|
|
1350
|
+
${newBlock}
|
|
1351
|
+
---${after}`;
|
|
1352
|
+
}
|
|
1353
|
+
function rewriteFrontmatterMerge(content, patch, forced) {
|
|
1354
|
+
const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u.exec(content);
|
|
1355
|
+
if (match === null) {
|
|
1356
|
+
const synthLines = [];
|
|
1357
|
+
if (forced?.id !== void 0) synthLines.push(`id: ${forced.id}`);
|
|
1358
|
+
appendPatchLines(synthLines, patch);
|
|
1359
|
+
return `---
|
|
1360
|
+
${synthLines.join("\n")}
|
|
1361
|
+
---
|
|
1362
|
+
|
|
1363
|
+
${content}`;
|
|
1364
|
+
}
|
|
1365
|
+
const block = match[1] ?? "";
|
|
1366
|
+
const updates = {};
|
|
1367
|
+
if (forced?.id !== void 0) updates.id = `id: ${forced.id}`;
|
|
1368
|
+
if (patch.title !== void 0) updates.title = `title: ${quoteIfNeeded(patch.title)}`;
|
|
1369
|
+
if (patch.summary !== void 0) updates.summary = `summary: ${quoteIfNeeded(patch.summary)}`;
|
|
1370
|
+
if (patch.layer !== void 0) updates.layer = `layer: ${patch.layer}`;
|
|
1371
|
+
if (patch.maturity !== void 0) updates.maturity = `maturity: ${patch.maturity}`;
|
|
1372
|
+
if (patch.tags !== void 0) updates.tags = `tags: [${patch.tags.join(", ")}]`;
|
|
1373
|
+
if (patch.relevance_scope !== void 0) updates.relevance_scope = `relevance_scope: ${patch.relevance_scope}`;
|
|
1374
|
+
if (patch.relevance_paths !== void 0) updates.relevance_paths = `relevance_paths: [${patch.relevance_paths.join(", ")}]`;
|
|
1375
|
+
const lines = block.split(/\r?\n/u);
|
|
1376
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1377
|
+
const newLines = [];
|
|
1378
|
+
for (const line of lines) {
|
|
1379
|
+
const sep = line.indexOf(":");
|
|
1380
|
+
const key = sep === -1 ? "" : line.slice(0, sep).trim();
|
|
1381
|
+
if (key in updates) {
|
|
1382
|
+
newLines.push(updates[key]);
|
|
1383
|
+
seen.add(key);
|
|
1384
|
+
} else {
|
|
1385
|
+
newLines.push(line);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
for (const key of Object.keys(updates)) {
|
|
1389
|
+
if (!seen.has(key)) {
|
|
1390
|
+
newLines.push(updates[key]);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
const newBlock = newLines.join("\n");
|
|
1394
|
+
const before = content.slice(0, match.index);
|
|
1395
|
+
const after = content.slice(match.index + match[0].length);
|
|
1396
|
+
return `${before}---
|
|
1397
|
+
${newBlock}
|
|
1398
|
+
---${after}`;
|
|
1399
|
+
}
|
|
1400
|
+
function appendPatchLines(lines, patch) {
|
|
1401
|
+
if (patch.title !== void 0) lines.push(`title: ${quoteIfNeeded(patch.title)}`);
|
|
1402
|
+
if (patch.summary !== void 0) lines.push(`summary: ${quoteIfNeeded(patch.summary)}`);
|
|
1403
|
+
if (patch.layer !== void 0) lines.push(`layer: ${patch.layer}`);
|
|
1404
|
+
if (patch.maturity !== void 0) lines.push(`maturity: ${patch.maturity}`);
|
|
1405
|
+
if (patch.tags !== void 0) lines.push(`tags: [${patch.tags.join(", ")}]`);
|
|
1406
|
+
if (patch.relevance_scope !== void 0) lines.push(`relevance_scope: ${patch.relevance_scope}`);
|
|
1407
|
+
if (patch.relevance_paths !== void 0) lines.push(`relevance_paths: [${patch.relevance_paths.join(", ")}]`);
|
|
1408
|
+
}
|
|
1409
|
+
function quoteIfNeeded(value) {
|
|
1410
|
+
if (/[\n\r]/u.test(value)) {
|
|
1411
|
+
return JSON.stringify(value);
|
|
1412
|
+
}
|
|
1413
|
+
if (/[:#\[\]{}&*!|>'"%@`,]|^\s|\s$/u.test(value)) {
|
|
1414
|
+
return `"${value.replace(/"/gu, '\\"')}"`;
|
|
1415
|
+
}
|
|
1416
|
+
return value;
|
|
1417
|
+
}
|
|
1418
|
+
function resolvePersonalRoot2() {
|
|
1419
|
+
return process.env.FABRIC_HOME ?? homedir2();
|
|
1420
|
+
}
|
|
1421
|
+
async function emitEventBestEffort2(projectRoot, event) {
|
|
1422
|
+
try {
|
|
1423
|
+
await appendEventLedgerEvent(projectRoot, event);
|
|
1424
|
+
} catch {
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// src/tools/review.ts
|
|
1429
|
+
function registerReview(server, tracker) {
|
|
1430
|
+
server.registerTool(
|
|
1431
|
+
"fab_review",
|
|
1432
|
+
{
|
|
1433
|
+
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.",
|
|
1434
|
+
// Flat ZodRawShape required by MCP SDK 1.29.0 registerTool. The
|
|
1435
|
+
// authoritative cross-field contract still lives in FabReviewInputSchema
|
|
1436
|
+
// (discriminatedUnion) and is enforced inside the handler via
|
|
1437
|
+
// `FabReviewInputSchema.parse(input)`.
|
|
1438
|
+
inputSchema: FabReviewInputShape,
|
|
1439
|
+
outputSchema: FabReviewOutputShape,
|
|
1440
|
+
annotations: fabReviewAnnotations
|
|
1441
|
+
},
|
|
1442
|
+
async (input) => {
|
|
1443
|
+
const requestId = randomUUID3();
|
|
1444
|
+
tracker?.enter(requestId);
|
|
1445
|
+
try {
|
|
1446
|
+
const narrowed = FabReviewInputSchema.parse(input);
|
|
1447
|
+
const projectRoot = resolveProjectRoot();
|
|
1448
|
+
const result = await reviewKnowledge(projectRoot, narrowed);
|
|
1449
|
+
const response = result;
|
|
1450
|
+
const payloadLimits = readPayloadLimits(projectRoot);
|
|
1451
|
+
const serialized = JSON.stringify(response);
|
|
1452
|
+
enforcePayloadLimit3(serialized, payloadLimits);
|
|
1453
|
+
return {
|
|
1454
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
1455
|
+
structuredContent: response
|
|
1456
|
+
};
|
|
1457
|
+
} finally {
|
|
1458
|
+
tracker?.exit(requestId);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// src/tools/knowledge-sections.ts
|
|
1465
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1466
|
+
import {
|
|
1467
|
+
knowledgeSectionsAnnotations,
|
|
1468
|
+
knowledgeSectionsInputSchema,
|
|
1469
|
+
knowledgeSectionsOutputSchema
|
|
1470
|
+
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
1471
|
+
import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
1472
|
+
|
|
1473
|
+
// src/services/knowledge-sections.ts
|
|
1474
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1475
|
+
import { homedir as homedir3 } from "os";
|
|
1476
|
+
import { join as join4 } from "path";
|
|
1477
|
+
var KNOWLEDGE_SECTION_NAMES = [
|
|
418
1478
|
"MISSION_STATEMENT",
|
|
419
1479
|
"MANDATORY_INJECTION",
|
|
420
1480
|
"BUSINESS_LOGIC_CHUNKS",
|
|
@@ -425,7 +1485,7 @@ var PRIORITY_ORDER = {
|
|
|
425
1485
|
medium: 1,
|
|
426
1486
|
low: 2
|
|
427
1487
|
};
|
|
428
|
-
function
|
|
1488
|
+
function parseKnowledgeSections(content) {
|
|
429
1489
|
const sections = /* @__PURE__ */ new Map();
|
|
430
1490
|
const lines = content.split(/\r?\n/u);
|
|
431
1491
|
let activeSection;
|
|
@@ -447,7 +1507,7 @@ function parseRuleSections(content) {
|
|
|
447
1507
|
const heading = /^(#{2,6})\s+\[([A-Z_]+)\]\s*$/u.exec(line.trim());
|
|
448
1508
|
if (heading !== null) {
|
|
449
1509
|
flush();
|
|
450
|
-
activeSection =
|
|
1510
|
+
activeSection = isKnowledgeSectionName(heading[2]) ? heading[2] : void 0;
|
|
451
1511
|
activeSectionDepth = activeSection === void 0 ? 0 : heading[1].length;
|
|
452
1512
|
continue;
|
|
453
1513
|
}
|
|
@@ -471,7 +1531,7 @@ function parseRuleSections(content) {
|
|
|
471
1531
|
Array.from(sections.entries()).map(([section, values]) => [section, values.join("\n\n")])
|
|
472
1532
|
);
|
|
473
1533
|
}
|
|
474
|
-
async function
|
|
1534
|
+
async function getKnowledgeSections(projectRoot, input) {
|
|
475
1535
|
const token = readSelectionToken(input.selection_token);
|
|
476
1536
|
if (token === void 0) {
|
|
477
1537
|
throw new Error("selection_token is missing or expired");
|
|
@@ -483,8 +1543,8 @@ async function getRuleSections(projectRoot, input) {
|
|
|
483
1543
|
const diagnostics = [];
|
|
484
1544
|
const rules = [];
|
|
485
1545
|
for (const rule of selectedRules) {
|
|
486
|
-
const content = await
|
|
487
|
-
const parsedSections =
|
|
1546
|
+
const content = await readFile4(resolveRuleSourcePath(projectRoot, rule.path), "utf8");
|
|
1547
|
+
const parsedSections = parseKnowledgeSections(content);
|
|
488
1548
|
const sections = {};
|
|
489
1549
|
for (const section of input.sections) {
|
|
490
1550
|
const sectionContent = parsedSections.get(section);
|
|
@@ -522,20 +1582,23 @@ async function getRuleSections(projectRoot, input) {
|
|
|
522
1582
|
rules,
|
|
523
1583
|
diagnostics
|
|
524
1584
|
};
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
1585
|
+
try {
|
|
1586
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1587
|
+
event_type: "knowledge_selection",
|
|
1588
|
+
selection_token: input.selection_token,
|
|
1589
|
+
target_paths: token.target_paths,
|
|
1590
|
+
required_stable_ids: token.required_stable_ids,
|
|
1591
|
+
ai_selectable_stable_ids: token.ai_selectable_stable_ids,
|
|
1592
|
+
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
1593
|
+
final_stable_ids: result.selected_stable_ids,
|
|
1594
|
+
ai_selection_reasons: pickSelectionReasons(input.ai_selected_stable_ids, input.ai_selection_reasons),
|
|
1595
|
+
rejected_stable_ids: [],
|
|
1596
|
+
ignored_stable_ids: [],
|
|
1597
|
+
correlation_id: input.correlation_id,
|
|
1598
|
+
session_id: input.session_id
|
|
1599
|
+
});
|
|
1600
|
+
} catch {
|
|
1601
|
+
}
|
|
539
1602
|
try {
|
|
540
1603
|
await appendEventLedgerEvent(projectRoot, {
|
|
541
1604
|
event_type: "knowledge_sections_fetched",
|
|
@@ -550,6 +1613,26 @@ async function getRuleSections(projectRoot, input) {
|
|
|
550
1613
|
});
|
|
551
1614
|
} catch {
|
|
552
1615
|
}
|
|
1616
|
+
const consumedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1617
|
+
const consumedClientHash = input.client_hash ?? "";
|
|
1618
|
+
const emittedConsumed = /* @__PURE__ */ new Set();
|
|
1619
|
+
for (const stableId of result.selected_stable_ids) {
|
|
1620
|
+
if (emittedConsumed.has(stableId)) {
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
emittedConsumed.add(stableId);
|
|
1624
|
+
try {
|
|
1625
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1626
|
+
event_type: "knowledge_consumed",
|
|
1627
|
+
stable_id: stableId,
|
|
1628
|
+
consumed_at: consumedAt,
|
|
1629
|
+
client_hash: consumedClientHash,
|
|
1630
|
+
correlation_id: input.correlation_id,
|
|
1631
|
+
session_id: input.session_id
|
|
1632
|
+
});
|
|
1633
|
+
} catch {
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
553
1636
|
return result;
|
|
554
1637
|
}
|
|
555
1638
|
function validateAiSelections(aiSelectableStableIds, aiSelectedStableIds, aiSelectionReasons) {
|
|
@@ -572,12 +1655,12 @@ function findRuleNode(meta, stableId) {
|
|
|
572
1655
|
if (nodeStableId !== stableId) {
|
|
573
1656
|
continue;
|
|
574
1657
|
}
|
|
575
|
-
const level = node.level ?? node.layer;
|
|
1658
|
+
const level = node.level ?? node.layer ?? "L2";
|
|
576
1659
|
return {
|
|
577
1660
|
stable_id: nodeStableId,
|
|
578
1661
|
level,
|
|
579
|
-
path:
|
|
580
|
-
priority: node.priority,
|
|
1662
|
+
path: normalizeKnowledgePath(node.content_ref ?? node.file),
|
|
1663
|
+
priority: node.priority ?? "medium",
|
|
581
1664
|
node
|
|
582
1665
|
};
|
|
583
1666
|
}
|
|
@@ -606,44 +1689,44 @@ function outputLevelOrder(level) {
|
|
|
606
1689
|
return 2;
|
|
607
1690
|
}
|
|
608
1691
|
}
|
|
609
|
-
function
|
|
610
|
-
return
|
|
1692
|
+
function isKnowledgeSectionName(value) {
|
|
1693
|
+
return KNOWLEDGE_SECTION_NAMES.includes(value);
|
|
611
1694
|
}
|
|
612
1695
|
function resolveRuleSourcePath(projectRoot, contentRef) {
|
|
613
1696
|
if (contentRef.startsWith("~/.fabric/knowledge/")) {
|
|
614
|
-
const home = process.env.FABRIC_HOME ??
|
|
615
|
-
return
|
|
1697
|
+
const home = process.env.FABRIC_HOME ?? homedir3();
|
|
1698
|
+
return join4(home, ".fabric", "knowledge", contentRef.slice("~/.fabric/knowledge/".length));
|
|
616
1699
|
}
|
|
617
|
-
return
|
|
1700
|
+
return join4(projectRoot, contentRef);
|
|
618
1701
|
}
|
|
619
1702
|
function pickSelectionReasons(selectedStableIds, reasons) {
|
|
620
1703
|
return Object.fromEntries(selectedStableIds.map((stableId) => [stableId, reasons[stableId] ?? ""]));
|
|
621
1704
|
}
|
|
622
1705
|
|
|
623
|
-
// src/tools/
|
|
624
|
-
function
|
|
1706
|
+
// src/tools/knowledge-sections.ts
|
|
1707
|
+
function registerKnowledgeSections(server, tracker) {
|
|
625
1708
|
server.registerTool(
|
|
626
|
-
"
|
|
1709
|
+
"fab_get_knowledge_sections",
|
|
627
1710
|
{
|
|
628
1711
|
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:
|
|
1712
|
+
inputSchema: knowledgeSectionsInputSchema,
|
|
1713
|
+
outputSchema: knowledgeSectionsOutputSchema,
|
|
1714
|
+
annotations: knowledgeSectionsAnnotations
|
|
632
1715
|
},
|
|
633
1716
|
async (input) => {
|
|
634
|
-
const requestId =
|
|
1717
|
+
const requestId = randomUUID4();
|
|
635
1718
|
tracker?.enter(requestId);
|
|
636
1719
|
try {
|
|
637
1720
|
const projectRoot = resolveProjectRoot();
|
|
638
|
-
const syncReport = await
|
|
639
|
-
const result = await
|
|
1721
|
+
const syncReport = await ensureKnowledgeFresh(projectRoot);
|
|
1722
|
+
const result = await getKnowledgeSections(projectRoot, input);
|
|
640
1723
|
const response = {
|
|
641
1724
|
...result,
|
|
642
1725
|
warnings: [...syncReport.warnings]
|
|
643
1726
|
};
|
|
644
1727
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
645
1728
|
const serialized = JSON.stringify(response);
|
|
646
|
-
const guardResult =
|
|
1729
|
+
const guardResult = enforcePayloadLimit4(serialized, payloadLimits);
|
|
647
1730
|
if (guardResult.warning) {
|
|
648
1731
|
response.warnings = [
|
|
649
1732
|
...response.warnings,
|
|
@@ -665,83 +1748,6 @@ function registerRuleSections(server, tracker) {
|
|
|
665
1748
|
);
|
|
666
1749
|
}
|
|
667
1750
|
|
|
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
1751
|
// src/services/serve-lock.ts
|
|
746
1752
|
import fs from "fs";
|
|
747
1753
|
import path from "path";
|
|
@@ -846,18 +1852,20 @@ function formatError(error) {
|
|
|
846
1852
|
}
|
|
847
1853
|
function formatPreexistingRootMessage(projectRoot) {
|
|
848
1854
|
const preexisting = [];
|
|
849
|
-
if (
|
|
850
|
-
if (
|
|
1855
|
+
if (existsSync4(join5(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
|
|
1856
|
+
if (existsSync4(join5(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
|
|
851
1857
|
if (preexisting.length === 0) return null;
|
|
852
1858
|
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
1859
|
}
|
|
854
1860
|
function createFabricServer(tracker) {
|
|
855
1861
|
const server = new McpServer({
|
|
856
|
-
name: "fabric-
|
|
857
|
-
version: "2.0.0-rc.
|
|
1862
|
+
name: "fabric-knowledge-server",
|
|
1863
|
+
version: "2.0.0-rc.10"
|
|
858
1864
|
});
|
|
859
1865
|
registerPlanContext(server, tracker);
|
|
860
|
-
|
|
1866
|
+
registerKnowledgeSections(server, tracker);
|
|
1867
|
+
registerExtractKnowledge(server, tracker);
|
|
1868
|
+
registerReview(server, tracker);
|
|
861
1869
|
server.registerResource(
|
|
862
1870
|
"bootstrap README",
|
|
863
1871
|
AGENTS_MD_RESOURCE_URI,
|
|
@@ -867,10 +1875,10 @@ function createFabricServer(tracker) {
|
|
|
867
1875
|
},
|
|
868
1876
|
async (_uri) => {
|
|
869
1877
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
870
|
-
const path2 =
|
|
1878
|
+
const path2 = join5(projectRoot, ".fabric", "bootstrap", "README.md");
|
|
871
1879
|
let text = "";
|
|
872
|
-
if (
|
|
873
|
-
text = await
|
|
1880
|
+
if (existsSync4(path2)) {
|
|
1881
|
+
text = await readFile5(path2, "utf8");
|
|
874
1882
|
}
|
|
875
1883
|
return {
|
|
876
1884
|
contents: [
|
|
@@ -889,7 +1897,7 @@ async function startStdioServer() {
|
|
|
889
1897
|
const tracker = createInFlightTracker();
|
|
890
1898
|
const projectRoot = resolveProjectRoot();
|
|
891
1899
|
const syncStart = Date.now();
|
|
892
|
-
const reconcileResult = await
|
|
1900
|
+
const reconcileResult = await reconcileKnowledge(projectRoot, { trigger: "startup" });
|
|
893
1901
|
const syncDurationMs = Date.now() - syncStart;
|
|
894
1902
|
process.stderr.write(
|
|
895
1903
|
`[startup] rule sync: status=${reconcileResult.status}, events=${reconcileResult.events.length}, ${syncDurationMs}ms
|
|
@@ -952,9 +1960,9 @@ function createShutdownHandler(deps) {
|
|
|
952
1960
|
};
|
|
953
1961
|
}
|
|
954
1962
|
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
|
|
1963
|
+
const { createFabricHttpApp } = await import("./http-7HJQGQSN.js");
|
|
1964
|
+
const { port, projectRoot, host = "127.0.0.1", authToken } = options;
|
|
1965
|
+
const app = createFabricHttpApp({ projectRoot, host, authToken });
|
|
958
1966
|
return await new Promise((resolveServer, rejectServer) => {
|
|
959
1967
|
const server = app.listen(port, host);
|
|
960
1968
|
server.once("close", () => {
|
|
@@ -970,7 +1978,7 @@ async function startHttpServer(options) {
|
|
|
970
1978
|
}
|
|
971
1979
|
var entrypoint = process.argv[1];
|
|
972
1980
|
var currentFilePath = fileURLToPath(import.meta.url);
|
|
973
|
-
var isMainModule = entrypoint !== void 0 &&
|
|
1981
|
+
var isMainModule = entrypoint !== void 0 && resolve2(entrypoint) === currentFilePath;
|
|
974
1982
|
if (isMainModule) {
|
|
975
1983
|
void startStdioServer().catch((error) => {
|
|
976
1984
|
writeStderr(formatError(error));
|
|
@@ -986,29 +1994,34 @@ export {
|
|
|
986
1994
|
ServeLockHeldError,
|
|
987
1995
|
acquireLock,
|
|
988
1996
|
appendEventLedgerEvent,
|
|
989
|
-
|
|
1997
|
+
buildKnowledgeMeta,
|
|
990
1998
|
checkLockOrThrow,
|
|
991
|
-
|
|
992
|
-
|
|
1999
|
+
computeKnowledgeBasedAgentsMeta,
|
|
2000
|
+
computeKnowledgeTestIndex,
|
|
993
2001
|
createFabricServer,
|
|
994
2002
|
createInFlightTracker,
|
|
995
2003
|
createShutdownHandler,
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
2004
|
+
deriveKnowledgeMetaLayer,
|
|
2005
|
+
deriveKnowledgeMetaTopologyType,
|
|
2006
|
+
ensureKnowledgeFresh,
|
|
2007
|
+
extractKnowledge,
|
|
999
2008
|
flushAndSyncEventLedger,
|
|
1000
2009
|
formatPreexistingRootMessage,
|
|
1001
2010
|
getEventLedgerPath,
|
|
1002
2011
|
getLedgerPath,
|
|
1003
2012
|
getLegacyLedgerPath,
|
|
1004
|
-
|
|
2013
|
+
isSameKnowledgeTestIndex,
|
|
2014
|
+
planContext,
|
|
1005
2015
|
readLockState,
|
|
1006
|
-
|
|
2016
|
+
readSelectionToken,
|
|
2017
|
+
reconcileKnowledge,
|
|
1007
2018
|
releaseLock,
|
|
2019
|
+
reviewKnowledge,
|
|
2020
|
+
runDoctorApplyLint,
|
|
1008
2021
|
runDoctorFix,
|
|
1009
2022
|
runDoctorReport,
|
|
1010
2023
|
stableStringify,
|
|
1011
2024
|
startHttpServer,
|
|
1012
2025
|
startStdioServer,
|
|
1013
|
-
|
|
2026
|
+
writeKnowledgeMeta
|
|
1014
2027
|
};
|