@fenglimg/fabric-server 1.8.0-rc.3 → 2.0.0-rc.8
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-AR2HV5JT.js +4061 -0
- package/dist/{http-PXFWUKCA.js → http-AR26GYEV.js} +19 -169
- package/dist/index.d.ts +174 -35
- package/dist/index.js +1622 -64
- package/package.json +4 -5
- package/dist/chunk-ZZGARZL5.js +0 -3019
- package/dist/static/assets/index-BSbndc76.js +0 -10
- package/dist/static/assets/index-FoBU5Kta.css +0 -1
- package/dist/static/index.html +0 -16
package/dist/index.js
CHANGED
|
@@ -1,37 +1,44 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AGENTS_MD_RESOURCE_URI,
|
|
3
2
|
EVENT_LEDGER_PATH,
|
|
4
3
|
LEDGER_PATH,
|
|
5
4
|
LEGACY_LEDGER_PATH,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
appendEventLedgerEvent,
|
|
6
|
+
atomicWriteText,
|
|
7
|
+
buildKnowledgeMeta,
|
|
8
|
+
computeKnowledgeBasedAgentsMeta,
|
|
9
|
+
computeKnowledgeTestIndex,
|
|
10
|
+
deriveKnowledgeMetaLayer,
|
|
11
|
+
deriveKnowledgeMetaTopologyType,
|
|
12
|
+
ensureKnowledgeFresh,
|
|
13
|
+
ensureParentDirectory,
|
|
12
14
|
flushAndSyncEventLedger,
|
|
13
15
|
getEventLedgerPath,
|
|
14
16
|
getLedgerPath,
|
|
15
17
|
getLegacyLedgerPath,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
isSameKnowledgeTestIndex,
|
|
19
|
+
normalizeKnowledgePath,
|
|
20
|
+
readAgentsMeta,
|
|
21
|
+
reconcileKnowledge,
|
|
20
22
|
resolveProjectRoot,
|
|
23
|
+
runDoctorApplyLint,
|
|
21
24
|
runDoctorFix,
|
|
22
25
|
runDoctorReport,
|
|
26
|
+
sha256,
|
|
23
27
|
stableStringify,
|
|
24
|
-
|
|
25
|
-
} from "./chunk-
|
|
28
|
+
writeKnowledgeMeta
|
|
29
|
+
} from "./chunk-AR2HV5JT.js";
|
|
26
30
|
|
|
27
31
|
// src/index.ts
|
|
28
|
-
import { existsSync as
|
|
29
|
-
import { readFile } from "fs/promises";
|
|
30
|
-
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";
|
|
31
35
|
import { fileURLToPath } from "url";
|
|
32
36
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
33
37
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
34
38
|
|
|
39
|
+
// src/constants.ts
|
|
40
|
+
var AGENTS_MD_RESOURCE_URI = "fabric://bootstrap-readme";
|
|
41
|
+
|
|
35
42
|
// src/services/in-flight-tracker.ts
|
|
36
43
|
function createInFlightTracker() {
|
|
37
44
|
const active = /* @__PURE__ */ new Map();
|
|
@@ -50,15 +57,15 @@ function createInFlightTracker() {
|
|
|
50
57
|
drain(deadlineMs) {
|
|
51
58
|
const startedWith = active.size;
|
|
52
59
|
if (startedWith === 0) return Promise.resolve({ drained: 0, timed_out: 0 });
|
|
53
|
-
return new Promise((
|
|
60
|
+
return new Promise((resolve3) => {
|
|
54
61
|
const timer = setTimeout(() => {
|
|
55
62
|
const drainedCount = startedWith - active.size;
|
|
56
63
|
resolveDrained = null;
|
|
57
|
-
|
|
64
|
+
resolve3({ drained: drainedCount, timed_out: active.size });
|
|
58
65
|
}, deadlineMs);
|
|
59
66
|
resolveDrained = () => {
|
|
60
67
|
clearTimeout(timer);
|
|
61
|
-
|
|
68
|
+
resolve3({ drained: startedWith, timed_out: 0 });
|
|
62
69
|
};
|
|
63
70
|
});
|
|
64
71
|
},
|
|
@@ -68,12 +75,12 @@ function createInFlightTracker() {
|
|
|
68
75
|
};
|
|
69
76
|
}
|
|
70
77
|
|
|
71
|
-
// src/tools/
|
|
78
|
+
// src/tools/extract-knowledge.ts
|
|
72
79
|
import { randomUUID } from "crypto";
|
|
73
80
|
import {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
FabExtractKnowledgeInputShape,
|
|
82
|
+
FabExtractKnowledgeOutputSchema,
|
|
83
|
+
fabExtractKnowledgeAnnotations
|
|
77
84
|
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
78
85
|
import { enforcePayloadLimit } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
79
86
|
|
|
@@ -95,6 +102,532 @@ function readPayloadLimits(projectRoot) {
|
|
|
95
102
|
return readFabricConfig(projectRoot).mcpPayloadLimits;
|
|
96
103
|
}
|
|
97
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
|
+
|
|
421
|
+
// src/services/plan-context.ts
|
|
422
|
+
import { minimatch } from "minimatch";
|
|
423
|
+
import { deriveAgentsMetaLayer } from "@fenglimg/fabric-shared";
|
|
424
|
+
var SELECTION_TOKEN_TTL_MS = 5 * 60 * 1e3;
|
|
425
|
+
var selectionTokenCache = /* @__PURE__ */ new Map();
|
|
426
|
+
async function planContext(projectRoot, input) {
|
|
427
|
+
const meta = await readAgentsMeta(projectRoot);
|
|
428
|
+
const stale = input.client_hash !== void 0 && input.client_hash !== meta.revision;
|
|
429
|
+
const uniquePaths = dedupePaths(input.paths);
|
|
430
|
+
const allDescriptions = buildDescriptionIndex(meta);
|
|
431
|
+
const relevanceTargetPaths = input.target_paths ?? uniquePaths;
|
|
432
|
+
const entries = uniquePaths.map((path2) => {
|
|
433
|
+
const profile = buildRequirementProfile(path2, input);
|
|
434
|
+
const descriptionIndex = allDescriptions.filter((item) => shouldIncludeIndexItemForPath(item, meta, path2)).filter((item) => shouldIncludeByRelevance(item, relevanceTargetPaths));
|
|
435
|
+
return {
|
|
436
|
+
path: path2,
|
|
437
|
+
requirement_profile: profile,
|
|
438
|
+
description_index: descriptionIndex
|
|
439
|
+
};
|
|
440
|
+
});
|
|
441
|
+
const sharedDescriptionIndex = dedupeDescriptionIndex(entries.flatMap((entry) => entry.description_index));
|
|
442
|
+
const sharedStableIds = sharedDescriptionIndex.map((item) => item.stable_id);
|
|
443
|
+
const selectionToken = createSelectionToken(meta.revision, uniquePaths, [], sharedStableIds);
|
|
444
|
+
const result = {
|
|
445
|
+
revision_hash: meta.revision,
|
|
446
|
+
stale,
|
|
447
|
+
selection_token: selectionToken,
|
|
448
|
+
entries,
|
|
449
|
+
shared: {
|
|
450
|
+
description_index: sharedDescriptionIndex,
|
|
451
|
+
preflight_diagnostics: buildPreflightDiagnostics(meta)
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
try {
|
|
455
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
456
|
+
event_type: "knowledge_context_planned",
|
|
457
|
+
target_paths: uniquePaths,
|
|
458
|
+
required_stable_ids: [],
|
|
459
|
+
ai_selectable_stable_ids: sharedDescriptionIndex.map((item) => item.stable_id),
|
|
460
|
+
final_stable_ids: [],
|
|
461
|
+
selection_token: selectionToken,
|
|
462
|
+
client_hash: input.client_hash,
|
|
463
|
+
intent: input.intent,
|
|
464
|
+
known_tech: input.known_tech,
|
|
465
|
+
diagnostics: result.shared.preflight_diagnostics,
|
|
466
|
+
correlation_id: input.correlation_id,
|
|
467
|
+
session_id: input.session_id
|
|
468
|
+
});
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
return result;
|
|
472
|
+
}
|
|
473
|
+
function readSelectionToken(token, now = Date.now()) {
|
|
474
|
+
const state = selectionTokenCache.get(token);
|
|
475
|
+
if (state === void 0) {
|
|
476
|
+
return void 0;
|
|
477
|
+
}
|
|
478
|
+
if (state.expires_at <= now) {
|
|
479
|
+
selectionTokenCache.delete(token);
|
|
480
|
+
return void 0;
|
|
481
|
+
}
|
|
482
|
+
return state;
|
|
483
|
+
}
|
|
484
|
+
function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSelectableStableIds, now = Date.now()) {
|
|
485
|
+
const token = `selection:${revisionHash}:${now.toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
486
|
+
selectionTokenCache.set(token, {
|
|
487
|
+
token,
|
|
488
|
+
revision_hash: revisionHash,
|
|
489
|
+
target_paths: targetPaths,
|
|
490
|
+
required_stable_ids: requiredStableIds,
|
|
491
|
+
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
492
|
+
created_at: now,
|
|
493
|
+
expires_at: now + SELECTION_TOKEN_TTL_MS
|
|
494
|
+
});
|
|
495
|
+
return token;
|
|
496
|
+
}
|
|
497
|
+
function dedupePaths(paths) {
|
|
498
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
499
|
+
return paths.flatMap((path2) => {
|
|
500
|
+
const normalizedPath = normalizeKnowledgePath(path2);
|
|
501
|
+
if (seenPaths.has(normalizedPath)) {
|
|
502
|
+
return [];
|
|
503
|
+
}
|
|
504
|
+
seenPaths.add(normalizedPath);
|
|
505
|
+
return [normalizedPath];
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
function buildRequirementProfile(path2, input) {
|
|
509
|
+
const normalizedPath = normalizeKnowledgePath(path2);
|
|
510
|
+
const extensionMatch = /(\.[^./\\]+)$/u.exec(normalizedPath);
|
|
511
|
+
const knownTech = dedupeStableIds([
|
|
512
|
+
...input.known_tech ?? [],
|
|
513
|
+
...extensionMatch?.[1] === ".ts" ? ["TypeScript"] : []
|
|
514
|
+
]);
|
|
515
|
+
return {
|
|
516
|
+
target_path: normalizedPath,
|
|
517
|
+
path_segments: normalizedPath.split("/").filter(Boolean),
|
|
518
|
+
extension: extensionMatch?.[1] ?? "",
|
|
519
|
+
known_tech: knownTech,
|
|
520
|
+
user_intent: input.intent ?? "",
|
|
521
|
+
detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path2] ?? []
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function buildDescriptionIndex(meta) {
|
|
525
|
+
return Object.entries(meta.nodes).flatMap(([nodeId, node]) => {
|
|
526
|
+
const level = deriveAgentsMetaLayer(node.file);
|
|
527
|
+
const description = node.description ?? descriptionFromLegacyActivation(node.activation?.description);
|
|
528
|
+
if (description === void 0) {
|
|
529
|
+
return [];
|
|
530
|
+
}
|
|
531
|
+
const inferredLayer = inferKnowledgeLayerFromContentRef(node.content_ref ?? node.file);
|
|
532
|
+
return [{
|
|
533
|
+
stable_id: node.stable_id ?? nodeId,
|
|
534
|
+
level,
|
|
535
|
+
required: false,
|
|
536
|
+
selectable: false,
|
|
537
|
+
description,
|
|
538
|
+
type: description.knowledge_type,
|
|
539
|
+
maturity: description.maturity,
|
|
540
|
+
layer: description.knowledge_layer ?? inferredLayer,
|
|
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
|
|
548
|
+
}];
|
|
549
|
+
}).sort(compareDescriptionIndexItems);
|
|
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
|
+
}
|
|
575
|
+
function inferKnowledgeLayerFromContentRef(contentRef) {
|
|
576
|
+
if (contentRef === void 0) {
|
|
577
|
+
return void 0;
|
|
578
|
+
}
|
|
579
|
+
if (contentRef.startsWith("~/.fabric/knowledge/")) {
|
|
580
|
+
return "personal";
|
|
581
|
+
}
|
|
582
|
+
if (contentRef.startsWith(".fabric/knowledge/")) {
|
|
583
|
+
return "team";
|
|
584
|
+
}
|
|
585
|
+
return void 0;
|
|
586
|
+
}
|
|
587
|
+
function descriptionFromLegacyActivation(summary) {
|
|
588
|
+
if (summary === void 0) {
|
|
589
|
+
return void 0;
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
summary,
|
|
593
|
+
intent_clues: [],
|
|
594
|
+
tech_stack: [],
|
|
595
|
+
impact: [],
|
|
596
|
+
must_read_if: summary
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
function shouldIncludeIndexItemForPath(_item, _meta, _path) {
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
function buildPreflightDiagnostics(meta) {
|
|
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();
|
|
604
|
+
if (missingDescriptionStableIds.length === 0) {
|
|
605
|
+
return [];
|
|
606
|
+
}
|
|
607
|
+
return [{
|
|
608
|
+
code: "missing_description",
|
|
609
|
+
severity: "warn",
|
|
610
|
+
stable_ids: missingDescriptionStableIds,
|
|
611
|
+
message: `Resolved registry includes ${missingDescriptionStableIds.length} node(s) without structured descriptions.`
|
|
612
|
+
}];
|
|
613
|
+
}
|
|
614
|
+
function dedupeStableIds(stableIds) {
|
|
615
|
+
return Array.from(new Set(stableIds));
|
|
616
|
+
}
|
|
617
|
+
function dedupeDescriptionIndex(items) {
|
|
618
|
+
const seenStableIds = /* @__PURE__ */ new Set();
|
|
619
|
+
return items.filter((item) => {
|
|
620
|
+
if (seenStableIds.has(item.stable_id)) {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
seenStableIds.add(item.stable_id);
|
|
624
|
+
return true;
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
function compareDescriptionIndexItems(left, right) {
|
|
628
|
+
return left.stable_id.localeCompare(right.stable_id);
|
|
629
|
+
}
|
|
630
|
+
|
|
98
631
|
// src/tools/plan-context.ts
|
|
99
632
|
function registerPlanContext(server, tracker) {
|
|
100
633
|
server.registerTool(
|
|
@@ -106,11 +639,11 @@ function registerPlanContext(server, tracker) {
|
|
|
106
639
|
annotations: planContextAnnotations
|
|
107
640
|
},
|
|
108
641
|
async ({ paths, intent, known_tech, detected_entities, client_hash, correlation_id, session_id }) => {
|
|
109
|
-
const requestId =
|
|
642
|
+
const requestId = randomUUID2();
|
|
110
643
|
tracker?.enter(requestId);
|
|
111
644
|
try {
|
|
112
645
|
const projectRoot = resolveProjectRoot();
|
|
113
|
-
const syncReport = await
|
|
646
|
+
const syncReport = await ensureKnowledgeFresh(projectRoot);
|
|
114
647
|
const result = await planContext(projectRoot, {
|
|
115
648
|
paths,
|
|
116
649
|
intent,
|
|
@@ -126,7 +659,7 @@ function registerPlanContext(server, tracker) {
|
|
|
126
659
|
};
|
|
127
660
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
128
661
|
const serialized = JSON.stringify(response);
|
|
129
|
-
const guardResult =
|
|
662
|
+
const guardResult = enforcePayloadLimit2(serialized, payloadLimits);
|
|
130
663
|
if (guardResult.warning) {
|
|
131
664
|
response.warnings = [
|
|
132
665
|
...response.warnings,
|
|
@@ -148,37 +681,1049 @@ function registerPlanContext(server, tracker) {
|
|
|
148
681
|
);
|
|
149
682
|
}
|
|
150
683
|
|
|
151
|
-
// src/tools/
|
|
152
|
-
import { randomUUID as
|
|
684
|
+
// src/tools/review.ts
|
|
685
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
153
686
|
import {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
687
|
+
FabReviewInputSchema,
|
|
688
|
+
FabReviewOutputSchema,
|
|
689
|
+
fabReviewAnnotations
|
|
157
690
|
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
158
|
-
import { enforcePayloadLimit as
|
|
159
|
-
|
|
691
|
+
import { enforcePayloadLimit as enforcePayloadLimit3 } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
692
|
+
|
|
693
|
+
// src/services/review.ts
|
|
694
|
+
import { execFileSync } from "child_process";
|
|
695
|
+
import { existsSync as existsSync3 } from "fs";
|
|
696
|
+
import { readFile as readFile3, readdir, unlink } from "fs/promises";
|
|
697
|
+
import { homedir as homedir2 } from "os";
|
|
698
|
+
import { basename, join as join3, relative as relative2, resolve } from "path";
|
|
699
|
+
|
|
700
|
+
// src/services/knowledge-id-allocator.ts
|
|
701
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
702
|
+
import { dirname } from "path";
|
|
703
|
+
import { mkdir } from "fs/promises";
|
|
704
|
+
import {
|
|
705
|
+
AgentsMetaCountersSchema,
|
|
706
|
+
agentsMetaSchema,
|
|
707
|
+
allocateKnowledgeId,
|
|
708
|
+
defaultAgentsMetaCounters
|
|
709
|
+
} from "@fenglimg/fabric-shared";
|
|
710
|
+
import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
711
|
+
var KnowledgeIdAllocator = class {
|
|
712
|
+
constructor(metaPath) {
|
|
713
|
+
this.metaPath = metaPath;
|
|
714
|
+
}
|
|
715
|
+
metaPath;
|
|
716
|
+
/**
|
|
717
|
+
* Allocate the next stable_id for the given (layer, type) pair and persist
|
|
718
|
+
* the advanced counter to `agents.meta.json`.
|
|
719
|
+
*/
|
|
720
|
+
async allocate(layer, type) {
|
|
721
|
+
const meta = await this.readMeta();
|
|
722
|
+
const counters = this.normalizeCounters(meta.counters);
|
|
723
|
+
const { id, nextCounters } = allocateKnowledgeId(layer, type, counters);
|
|
724
|
+
await this.writeMetaAtomic({ ...meta, counters: nextCounters });
|
|
725
|
+
return id;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Returns the current counters envelope, defaulting to all-zero slots when
|
|
729
|
+
* the meta file is absent or pre-v2.0 (counters key missing).
|
|
730
|
+
*/
|
|
731
|
+
async getCounters() {
|
|
732
|
+
const meta = await this.readMeta();
|
|
733
|
+
return this.normalizeCounters(meta.counters);
|
|
734
|
+
}
|
|
735
|
+
// ---- internal helpers ------------------------------------------------
|
|
736
|
+
async readMeta() {
|
|
737
|
+
let raw;
|
|
738
|
+
try {
|
|
739
|
+
raw = await readFile2(this.metaPath, "utf8");
|
|
740
|
+
} catch (err) {
|
|
741
|
+
if (isNodeError(err) && err.code === "ENOENT") {
|
|
742
|
+
return { revision: "", nodes: {} };
|
|
743
|
+
}
|
|
744
|
+
throw err;
|
|
745
|
+
}
|
|
746
|
+
let parsed;
|
|
747
|
+
try {
|
|
748
|
+
parsed = JSON.parse(raw);
|
|
749
|
+
} catch {
|
|
750
|
+
return { revision: "", nodes: {} };
|
|
751
|
+
}
|
|
752
|
+
const validation = agentsMetaSchema.safeParse(parsed);
|
|
753
|
+
if (validation.success) {
|
|
754
|
+
return validation.data;
|
|
755
|
+
}
|
|
756
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
757
|
+
}
|
|
758
|
+
normalizeCounters(input) {
|
|
759
|
+
if (input === void 0 || input === null) {
|
|
760
|
+
return defaultAgentsMetaCounters();
|
|
761
|
+
}
|
|
762
|
+
const parsed = AgentsMetaCountersSchema.safeParse(input);
|
|
763
|
+
return parsed.success ? parsed.data : defaultAgentsMetaCounters();
|
|
764
|
+
}
|
|
765
|
+
async writeMetaAtomic(meta) {
|
|
766
|
+
await ensureParentDirectory2(this.metaPath);
|
|
767
|
+
await atomicWriteJson(this.metaPath, meta, { indent: 2 });
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
async function ensureParentDirectory2(filePath) {
|
|
771
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
772
|
+
}
|
|
773
|
+
function isNodeError(err) {
|
|
774
|
+
return err instanceof Error && typeof err.code === "string";
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// src/services/review.ts
|
|
778
|
+
var PENDING_BASE_TEAM_REL = ".fabric/knowledge/pending";
|
|
779
|
+
function pendingBaseAbs(layer, projectRoot) {
|
|
780
|
+
if (layer === "personal") {
|
|
781
|
+
return join3(resolvePersonalRoot2(), ".fabric", "knowledge", "pending");
|
|
782
|
+
}
|
|
783
|
+
return join3(projectRoot, PENDING_BASE_TEAM_REL);
|
|
784
|
+
}
|
|
785
|
+
var PLURAL_TYPES = [
|
|
786
|
+
"decisions",
|
|
787
|
+
"pitfalls",
|
|
788
|
+
"guidelines",
|
|
789
|
+
"models",
|
|
790
|
+
"processes"
|
|
791
|
+
];
|
|
792
|
+
var PLURAL_TO_SINGULAR = {
|
|
793
|
+
decisions: "decision",
|
|
794
|
+
pitfalls: "pitfall",
|
|
795
|
+
guidelines: "guideline",
|
|
796
|
+
models: "model",
|
|
797
|
+
processes: "process"
|
|
798
|
+
};
|
|
799
|
+
async function reviewKnowledge(projectRoot, input) {
|
|
800
|
+
switch (input.action) {
|
|
801
|
+
case "list":
|
|
802
|
+
return {
|
|
803
|
+
action: "list",
|
|
804
|
+
items: await listPending(projectRoot, input.filters)
|
|
805
|
+
};
|
|
806
|
+
case "approve":
|
|
807
|
+
return {
|
|
808
|
+
action: "approve",
|
|
809
|
+
approved: await approveAll(projectRoot, input.pending_paths)
|
|
810
|
+
};
|
|
811
|
+
case "reject":
|
|
812
|
+
return {
|
|
813
|
+
action: "reject",
|
|
814
|
+
rejected: await rejectAll(projectRoot, input.pending_paths, input.reason)
|
|
815
|
+
};
|
|
816
|
+
case "modify":
|
|
817
|
+
return await modifyEntry(projectRoot, input.pending_path, input.changes);
|
|
818
|
+
case "search":
|
|
819
|
+
return {
|
|
820
|
+
action: "search",
|
|
821
|
+
items: await searchEntries(projectRoot, input.query, input.filters)
|
|
822
|
+
};
|
|
823
|
+
case "defer":
|
|
824
|
+
return {
|
|
825
|
+
action: "defer",
|
|
826
|
+
deferred: await deferAll(
|
|
827
|
+
projectRoot,
|
|
828
|
+
input.pending_paths,
|
|
829
|
+
input.until,
|
|
830
|
+
input.reason
|
|
831
|
+
)
|
|
832
|
+
};
|
|
833
|
+
default: {
|
|
834
|
+
const exhaustive = input;
|
|
835
|
+
throw new Error(`unsupported action: ${JSON.stringify(exhaustive)}`);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function resolveSandboxedPath(projectRoot, candidate, options = {}) {
|
|
840
|
+
if (candidate.length === 0) {
|
|
841
|
+
throw new Error("path is empty");
|
|
842
|
+
}
|
|
843
|
+
const projectKnowledgeRoot = resolve(projectRoot, ".fabric", "knowledge");
|
|
844
|
+
const personalKnowledgeRoot = resolve(resolvePersonalRoot2(), ".fabric", "knowledge");
|
|
845
|
+
if (candidate.startsWith("~/")) {
|
|
846
|
+
if (options.allowPersonal !== true) {
|
|
847
|
+
throw new Error(`personal-root path not allowed for this action: ${candidate}`);
|
|
848
|
+
}
|
|
849
|
+
const abs = resolve(resolvePersonalRoot2(), candidate.slice(2));
|
|
850
|
+
if (abs !== personalKnowledgeRoot && !abs.startsWith(personalKnowledgeRoot + "/")) {
|
|
851
|
+
throw new Error(`path escapes personal knowledge root: ${candidate}`);
|
|
852
|
+
}
|
|
853
|
+
return { abs, isInProjectTree: false };
|
|
854
|
+
}
|
|
855
|
+
const projectAbs = resolve(projectRoot, candidate);
|
|
856
|
+
if (projectAbs === projectKnowledgeRoot || projectAbs.startsWith(projectKnowledgeRoot + "/")) {
|
|
857
|
+
return { abs: projectAbs, isInProjectTree: true };
|
|
858
|
+
}
|
|
859
|
+
if (options.allowPersonal === true) {
|
|
860
|
+
const personalAbs = resolve(resolvePersonalRoot2(), candidate);
|
|
861
|
+
if (personalAbs === personalKnowledgeRoot || personalAbs.startsWith(personalKnowledgeRoot + "/")) {
|
|
862
|
+
return { abs: personalAbs, isInProjectTree: false };
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
throw new Error(`path escapes knowledge root: ${candidate}`);
|
|
866
|
+
}
|
|
867
|
+
async function listPending(projectRoot, filters) {
|
|
868
|
+
const items = [];
|
|
869
|
+
const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
|
|
870
|
+
const sources = [
|
|
871
|
+
{ origin: "team", root: pendingBaseAbs("team", projectRoot) },
|
|
872
|
+
{ origin: "personal", root: pendingBaseAbs("personal", projectRoot) }
|
|
873
|
+
];
|
|
874
|
+
for (const source of sources) {
|
|
875
|
+
for (const type of typesToScan) {
|
|
876
|
+
const dir = join3(source.root, type);
|
|
877
|
+
if (!existsSync3(dir)) {
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
let entries;
|
|
881
|
+
try {
|
|
882
|
+
entries = await readdir(dir);
|
|
883
|
+
} catch {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
for (const name of entries) {
|
|
887
|
+
if (!name.endsWith(".md")) continue;
|
|
888
|
+
const absolutePath = join3(dir, name);
|
|
889
|
+
let content;
|
|
890
|
+
try {
|
|
891
|
+
content = await readFile3(absolutePath, "utf8");
|
|
892
|
+
} catch {
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
const fm = parseFrontmatter(content);
|
|
896
|
+
const layer = fm.layer ?? (source.origin === "personal" ? "personal" : "team");
|
|
897
|
+
const maturity = fm.maturity ?? "draft";
|
|
898
|
+
if (filters?.layer !== void 0 && filters.layer !== "both" && filters.layer !== layer) {
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (filters?.maturity !== void 0 && filters.maturity !== maturity) {
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
if (filters?.tags !== void 0 && filters.tags.length > 0) {
|
|
905
|
+
const itemTags = fm.tags ?? [];
|
|
906
|
+
const hasAll = filters.tags.every((t) => itemTags.includes(t));
|
|
907
|
+
if (!hasAll) continue;
|
|
908
|
+
}
|
|
909
|
+
if (filters?.created_after !== void 0) {
|
|
910
|
+
const createdAt = fm.created_at;
|
|
911
|
+
if (createdAt === void 0 || createdAt < filters.created_after) {
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
const reportedPath = source.origin === "personal" ? `~/${relative2(resolvePersonalRoot2(), absolutePath)}` : relative2(projectRoot, absolutePath);
|
|
916
|
+
items.push({
|
|
917
|
+
pending_path: reportedPath,
|
|
918
|
+
type,
|
|
919
|
+
layer,
|
|
920
|
+
maturity,
|
|
921
|
+
origin: source.origin,
|
|
922
|
+
...fm.tags !== void 0 && fm.tags.length > 0 ? { tags: fm.tags } : {}
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return items;
|
|
928
|
+
}
|
|
929
|
+
async function approveAll(projectRoot, pendingPaths) {
|
|
930
|
+
const allocator = new KnowledgeIdAllocator(
|
|
931
|
+
join3(projectRoot, ".fabric", "agents.meta.json")
|
|
932
|
+
);
|
|
933
|
+
const approved = [];
|
|
934
|
+
for (const pendingPath of pendingPaths) {
|
|
935
|
+
const result = await approveOne(projectRoot, pendingPath, allocator);
|
|
936
|
+
if (result !== null) {
|
|
937
|
+
approved.push(result);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return approved;
|
|
941
|
+
}
|
|
942
|
+
async function approveOne(projectRoot, pendingPath, allocator) {
|
|
943
|
+
let sourceAbs;
|
|
944
|
+
let sourceOrigin;
|
|
945
|
+
try {
|
|
946
|
+
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
947
|
+
const teamPendingAbs = pendingBaseAbs("team", projectRoot);
|
|
948
|
+
const personalPendingAbs = pendingBaseAbs("personal", projectRoot);
|
|
949
|
+
const inTeamPending = sandboxed.abs === teamPendingAbs || sandboxed.abs.startsWith(teamPendingAbs + "/");
|
|
950
|
+
const inPersonalPending = sandboxed.abs === personalPendingAbs || sandboxed.abs.startsWith(personalPendingAbs + "/");
|
|
951
|
+
if (!inTeamPending && !inPersonalPending) {
|
|
952
|
+
throw new Error(`approve path is outside .fabric/knowledge/pending/: ${pendingPath}`);
|
|
953
|
+
}
|
|
954
|
+
sourceAbs = sandboxed.abs;
|
|
955
|
+
sourceOrigin = inPersonalPending ? "personal" : "team";
|
|
956
|
+
} catch (err) {
|
|
957
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
958
|
+
await emitEventBestEffort2(projectRoot, {
|
|
959
|
+
event_type: "knowledge_promote_failed",
|
|
960
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
961
|
+
reason: `approve:${pendingPath}: ${reason}`
|
|
962
|
+
});
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
const slug = basename(pendingPath).replace(/\.md$/u, "");
|
|
966
|
+
await emitEventBestEffort2(projectRoot, {
|
|
967
|
+
event_type: "knowledge_promote_started",
|
|
968
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
969
|
+
reason: `approve:${slug}`
|
|
970
|
+
});
|
|
971
|
+
let allocatedId;
|
|
972
|
+
let targetAbs;
|
|
973
|
+
let writtenTarget = false;
|
|
974
|
+
try {
|
|
975
|
+
const content = await readFile3(sourceAbs, "utf8");
|
|
976
|
+
const fm = parseFrontmatter(content);
|
|
977
|
+
const pluralType = fm.type;
|
|
978
|
+
if (pluralType === void 0 || !PLURAL_TYPES.includes(pluralType)) {
|
|
979
|
+
throw new Error(`pending file missing or invalid 'type' frontmatter: ${pendingPath}`);
|
|
980
|
+
}
|
|
981
|
+
const layer = fm.layer ?? "team";
|
|
982
|
+
const singularType = PLURAL_TO_SINGULAR[pluralType];
|
|
983
|
+
const stableId = await allocator.allocate(layer, singularType);
|
|
984
|
+
allocatedId = stableId;
|
|
985
|
+
const newFilename = `${stableId}--${slug}.md`;
|
|
986
|
+
const layerRoot = layer === "personal" ? join3(resolvePersonalRoot2(), ".fabric") : join3(projectRoot, ".fabric");
|
|
987
|
+
targetAbs = join3(layerRoot, "knowledge", pluralType, newFilename);
|
|
988
|
+
await ensureParentDirectory(targetAbs);
|
|
989
|
+
const rewritten = rewriteFrontmatterForPromote(content, stableId);
|
|
990
|
+
await atomicWriteText(targetAbs, rewritten);
|
|
991
|
+
writtenTarget = true;
|
|
992
|
+
if (sourceOrigin === "team") {
|
|
993
|
+
try {
|
|
994
|
+
execFileSync("git", ["rm", "--quiet", "-f", pendingPath], {
|
|
995
|
+
cwd: projectRoot,
|
|
996
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
997
|
+
});
|
|
998
|
+
} catch {
|
|
999
|
+
if (existsSync3(sourceAbs)) {
|
|
1000
|
+
await unlink(sourceAbs);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
} else {
|
|
1004
|
+
if (existsSync3(sourceAbs)) {
|
|
1005
|
+
await unlink(sourceAbs);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1009
|
+
event_type: "knowledge_promoted",
|
|
1010
|
+
stable_id: stableId,
|
|
1011
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1012
|
+
reason: `approve:${slug}`
|
|
1013
|
+
});
|
|
1014
|
+
return { pending_path: pendingPath, stable_id: stableId };
|
|
1015
|
+
} catch (err) {
|
|
1016
|
+
if (writtenTarget && targetAbs !== void 0 && existsSync3(targetAbs)) {
|
|
1017
|
+
try {
|
|
1018
|
+
await unlink(targetAbs);
|
|
1019
|
+
} catch {
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1023
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1024
|
+
event_type: "knowledge_promote_failed",
|
|
1025
|
+
...allocatedId !== void 0 ? { stable_id: allocatedId } : {},
|
|
1026
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1027
|
+
reason: `approve:${slug}: ${reason}`
|
|
1028
|
+
});
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
async function rejectAll(projectRoot, pendingPaths, reason) {
|
|
1033
|
+
const rejected = [];
|
|
1034
|
+
for (const pendingPath of pendingPaths) {
|
|
1035
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1036
|
+
event_type: "knowledge_rejected",
|
|
1037
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1038
|
+
reason: `reject:${pendingPath}: ${reason}`
|
|
1039
|
+
});
|
|
1040
|
+
rejected.push(pendingPath);
|
|
1041
|
+
}
|
|
1042
|
+
return rejected;
|
|
1043
|
+
}
|
|
1044
|
+
async function modifyEntry(projectRoot, pendingPath, changes) {
|
|
1045
|
+
const target = resolveModifyTarget(projectRoot, pendingPath);
|
|
1046
|
+
if (target === null) {
|
|
1047
|
+
throw new Error(`modify target not found: ${pendingPath}`);
|
|
1048
|
+
}
|
|
1049
|
+
const content = await readFile3(target.absPath, "utf8");
|
|
1050
|
+
const fm = parseFrontmatter(content);
|
|
1051
|
+
const currentLayer = fm.layer ?? "team";
|
|
1052
|
+
if (changes.layer !== void 0 && changes.layer !== currentLayer) {
|
|
1053
|
+
return await modifyLayerFlip(projectRoot, target, content, fm, changes);
|
|
1054
|
+
}
|
|
1055
|
+
const merged = rewriteFrontmatterMerge(content, changes);
|
|
1056
|
+
await atomicWriteText(target.absPath, merged);
|
|
1057
|
+
return {
|
|
1058
|
+
action: "modify",
|
|
1059
|
+
pending_path: pendingPath
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
function resolveModifyTarget(projectRoot, pendingPath) {
|
|
1063
|
+
let sandboxed;
|
|
1064
|
+
try {
|
|
1065
|
+
sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
1066
|
+
} catch {
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
if (existsSync3(sandboxed.abs)) {
|
|
1070
|
+
return {
|
|
1071
|
+
absPath: sandboxed.abs,
|
|
1072
|
+
isInProjectTree: sandboxed.isInProjectTree,
|
|
1073
|
+
inferredType: inferTypeFromPath(pendingPath),
|
|
1074
|
+
slug: extractSlug(pendingPath)
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1079
|
+
function inferTypeFromPath(path2) {
|
|
1080
|
+
const match = /knowledge\/(?:pending\/)?([^/]+)\/[^/]+\.md$/u.exec(path2);
|
|
1081
|
+
if (match === null) return null;
|
|
1082
|
+
const seg = match[1];
|
|
1083
|
+
if (seg !== void 0 && PLURAL_TYPES.includes(seg)) {
|
|
1084
|
+
return seg;
|
|
1085
|
+
}
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
function extractSlug(path2) {
|
|
1089
|
+
const file = basename(path2).replace(/\.md$/u, "");
|
|
1090
|
+
return file.replace(/^K[PT]-(MOD|DEC|GLD|PIT|PRO)-\d+--/u, "");
|
|
1091
|
+
}
|
|
1092
|
+
async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
1093
|
+
const fromLayer = fm.layer ?? "team";
|
|
1094
|
+
const toLayer = changes.layer;
|
|
1095
|
+
const pluralType = fm.type ?? target.inferredType;
|
|
1096
|
+
if (pluralType === null || pluralType === void 0) {
|
|
1097
|
+
throw new Error(`layer-flip requires a known type; could not infer for ${target.absPath}`);
|
|
1098
|
+
}
|
|
1099
|
+
const slug = target.slug;
|
|
1100
|
+
const priorStableId = fm.id;
|
|
1101
|
+
const fromScope = fm.relevance_scope ?? "broad";
|
|
1102
|
+
const shouldAutoDegrade = fromScope === "narrow" && fromLayer === "team" && toLayer === "personal";
|
|
1103
|
+
const allocator = new KnowledgeIdAllocator(
|
|
1104
|
+
join3(projectRoot, ".fabric", "agents.meta.json")
|
|
1105
|
+
);
|
|
1106
|
+
const singularType = PLURAL_TO_SINGULAR[pluralType];
|
|
1107
|
+
const newStableId = await allocator.allocate(toLayer, singularType);
|
|
1108
|
+
const toRoot = toLayer === "personal" ? join3(resolvePersonalRoot2(), ".fabric") : join3(projectRoot, ".fabric");
|
|
1109
|
+
const toAbs = join3(toRoot, "knowledge", pluralType, `${newStableId}--${slug}.md`);
|
|
1110
|
+
await ensureParentDirectory(toAbs);
|
|
1111
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1112
|
+
event_type: "knowledge_promote_started",
|
|
1113
|
+
...priorStableId !== void 0 ? { stable_id: priorStableId } : {},
|
|
1114
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1115
|
+
reason: `layer_flip:${priorStableId ?? "<unassigned>"}->${newStableId}`
|
|
1116
|
+
});
|
|
1117
|
+
const effectivePatch = shouldAutoDegrade ? {
|
|
1118
|
+
...changes,
|
|
1119
|
+
layer: toLayer,
|
|
1120
|
+
relevance_scope: "broad",
|
|
1121
|
+
relevance_paths: []
|
|
1122
|
+
} : { ...changes, layer: toLayer };
|
|
1123
|
+
const rewritten = rewriteFrontmatterMerge(content, effectivePatch, { id: newStableId });
|
|
1124
|
+
await atomicWriteText(toAbs, rewritten);
|
|
1125
|
+
if (target.isInProjectTree) {
|
|
1126
|
+
const relSource = relative2(projectRoot, target.absPath);
|
|
1127
|
+
try {
|
|
1128
|
+
execFileSync("git", ["rm", "--quiet", "-f", relSource], {
|
|
1129
|
+
cwd: projectRoot,
|
|
1130
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1131
|
+
});
|
|
1132
|
+
} catch {
|
|
1133
|
+
if (existsSync3(target.absPath)) {
|
|
1134
|
+
await unlink(target.absPath);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
} else if (existsSync3(target.absPath)) {
|
|
1138
|
+
await unlink(target.absPath);
|
|
1139
|
+
}
|
|
1140
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1141
|
+
event_type: "knowledge_layer_changed",
|
|
1142
|
+
stable_id: newStableId,
|
|
1143
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1144
|
+
from_layer: fromLayer,
|
|
1145
|
+
to_layer: toLayer,
|
|
1146
|
+
reason: `layer_flip:${priorStableId ?? "<unassigned>"}->${newStableId}`
|
|
1147
|
+
});
|
|
1148
|
+
if (shouldAutoDegrade) {
|
|
1149
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1150
|
+
event_type: "knowledge_scope_degraded",
|
|
1151
|
+
stable_id: newStableId,
|
|
1152
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1153
|
+
from_scope: "narrow",
|
|
1154
|
+
to_scope: "broad",
|
|
1155
|
+
reason: "personal-implies-broad"
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
const responsePath = toLayer === "team" ? relative2(projectRoot, toAbs) : `~/${relative2(resolvePersonalRoot2(), toAbs)}`;
|
|
1159
|
+
return {
|
|
1160
|
+
action: "modify",
|
|
1161
|
+
pending_path: responsePath,
|
|
1162
|
+
...priorStableId !== void 0 ? { prior_stable_id: priorStableId } : {},
|
|
1163
|
+
new_stable_id: newStableId
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
async function searchEntries(projectRoot, query, filters) {
|
|
1167
|
+
const lowerQuery = query.toLowerCase();
|
|
1168
|
+
const items = [];
|
|
1169
|
+
const sources = [
|
|
1170
|
+
{ root: pendingBaseAbs("team", projectRoot), isPending: true, isPersonal: false },
|
|
1171
|
+
{ root: pendingBaseAbs("personal", projectRoot), isPending: true, isPersonal: true },
|
|
1172
|
+
{ root: join3(projectRoot, ".fabric", "knowledge"), isPending: false, isPersonal: false },
|
|
1173
|
+
{ root: join3(resolvePersonalRoot2(), ".fabric", "knowledge"), isPending: false, isPersonal: true }
|
|
1174
|
+
];
|
|
1175
|
+
const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
|
|
1176
|
+
for (const source of sources) {
|
|
1177
|
+
for (const type of typesToScan) {
|
|
1178
|
+
const dir = join3(source.root, type);
|
|
1179
|
+
if (!existsSync3(dir)) continue;
|
|
1180
|
+
let entries;
|
|
1181
|
+
try {
|
|
1182
|
+
entries = await readdir(dir);
|
|
1183
|
+
} catch {
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
for (const name of entries) {
|
|
1187
|
+
if (!name.endsWith(".md")) continue;
|
|
1188
|
+
const absolutePath = join3(dir, name);
|
|
1189
|
+
let content;
|
|
1190
|
+
try {
|
|
1191
|
+
content = await readFile3(absolutePath, "utf8");
|
|
1192
|
+
} catch {
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
const fm = parseFrontmatter(content);
|
|
1196
|
+
const layer = fm.layer ?? (source.isPersonal ? "personal" : "team");
|
|
1197
|
+
const maturity = fm.maturity ?? "draft";
|
|
1198
|
+
if (filters?.layer !== void 0 && filters.layer !== "both" && filters.layer !== layer) {
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
if (filters?.maturity !== void 0 && filters.maturity !== maturity) {
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
if (filters?.tags !== void 0 && filters.tags.length > 0) {
|
|
1205
|
+
const itemTags = fm.tags ?? [];
|
|
1206
|
+
const hasAll = filters.tags.every((t) => itemTags.includes(t));
|
|
1207
|
+
if (!hasAll) continue;
|
|
1208
|
+
}
|
|
1209
|
+
if (filters?.created_after !== void 0) {
|
|
1210
|
+
const createdAt = fm.created_at;
|
|
1211
|
+
if (createdAt === void 0 || createdAt < filters.created_after) {
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
const haystacks = [
|
|
1216
|
+
fm.title ?? "",
|
|
1217
|
+
fm.summary ?? "",
|
|
1218
|
+
...fm.tags ?? [],
|
|
1219
|
+
name
|
|
1220
|
+
].map((s) => s.toLowerCase());
|
|
1221
|
+
const matches = haystacks.some((h) => h.includes(lowerQuery));
|
|
1222
|
+
if (!matches) continue;
|
|
1223
|
+
const reportedPath = source.isPersonal ? `~/${relative2(resolvePersonalRoot2(), absolutePath)}` : relative2(projectRoot, absolutePath);
|
|
1224
|
+
items.push({
|
|
1225
|
+
pending_path: reportedPath,
|
|
1226
|
+
type,
|
|
1227
|
+
layer,
|
|
1228
|
+
maturity,
|
|
1229
|
+
// Only pending entries carry an origin tag (search results that are
|
|
1230
|
+
// canonical entries don't have a pending root to point back to).
|
|
1231
|
+
...source.isPending ? { origin: source.isPersonal ? "personal" : "team" } : {},
|
|
1232
|
+
...fm.tags !== void 0 && fm.tags.length > 0 ? { tags: fm.tags } : {},
|
|
1233
|
+
...fm.title !== void 0 ? { title: fm.title } : {},
|
|
1234
|
+
...fm.summary !== void 0 ? { summary: fm.summary } : {}
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return items;
|
|
1240
|
+
}
|
|
1241
|
+
async function deferAll(projectRoot, pendingPaths, until, reason) {
|
|
1242
|
+
const deferred = [];
|
|
1243
|
+
for (const pendingPath of pendingPaths) {
|
|
1244
|
+
await emitEventBestEffort2(projectRoot, {
|
|
1245
|
+
event_type: "knowledge_deferred",
|
|
1246
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1247
|
+
...until !== void 0 ? { until } : {},
|
|
1248
|
+
...reason !== void 0 ? { reason } : {}
|
|
1249
|
+
});
|
|
1250
|
+
deferred.push(pendingPath);
|
|
1251
|
+
}
|
|
1252
|
+
return deferred;
|
|
1253
|
+
}
|
|
1254
|
+
function parseFrontmatter(content) {
|
|
1255
|
+
const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/u.exec(content);
|
|
1256
|
+
if (match === null) {
|
|
1257
|
+
return {};
|
|
1258
|
+
}
|
|
1259
|
+
const block = match[1];
|
|
1260
|
+
if (block === void 0) {
|
|
1261
|
+
return {};
|
|
1262
|
+
}
|
|
1263
|
+
const out = {};
|
|
1264
|
+
for (const rawLine of block.split(/\r?\n/u)) {
|
|
1265
|
+
const line = rawLine.trim();
|
|
1266
|
+
if (line.length === 0) continue;
|
|
1267
|
+
const sep = line.indexOf(":");
|
|
1268
|
+
if (sep === -1) continue;
|
|
1269
|
+
const key = line.slice(0, sep).trim();
|
|
1270
|
+
const value = line.slice(sep + 1).trim();
|
|
1271
|
+
switch (key) {
|
|
1272
|
+
case "id":
|
|
1273
|
+
out.id = stripQuotes(value);
|
|
1274
|
+
break;
|
|
1275
|
+
case "type":
|
|
1276
|
+
if (PLURAL_TYPES.includes(value)) {
|
|
1277
|
+
out.type = value;
|
|
1278
|
+
}
|
|
1279
|
+
break;
|
|
1280
|
+
case "layer":
|
|
1281
|
+
if (value === "team" || value === "personal") {
|
|
1282
|
+
out.layer = value;
|
|
1283
|
+
}
|
|
1284
|
+
break;
|
|
1285
|
+
case "maturity":
|
|
1286
|
+
if (value === "draft" || value === "verified" || value === "proven") {
|
|
1287
|
+
out.maturity = value;
|
|
1288
|
+
}
|
|
1289
|
+
break;
|
|
1290
|
+
case "source_session":
|
|
1291
|
+
out.source_session = stripQuotes(value);
|
|
1292
|
+
break;
|
|
1293
|
+
case "created_at":
|
|
1294
|
+
out.created_at = stripQuotes(value);
|
|
1295
|
+
break;
|
|
1296
|
+
case "tags":
|
|
1297
|
+
out.tags = parseFlowArray(value);
|
|
1298
|
+
break;
|
|
1299
|
+
case "title":
|
|
1300
|
+
out.title = stripQuotes(value);
|
|
1301
|
+
break;
|
|
1302
|
+
case "summary":
|
|
1303
|
+
out.summary = stripQuotes(value);
|
|
1304
|
+
break;
|
|
1305
|
+
case "relevance_scope":
|
|
1306
|
+
if (value === "narrow" || value === "broad") {
|
|
1307
|
+
out.relevance_scope = value;
|
|
1308
|
+
}
|
|
1309
|
+
break;
|
|
1310
|
+
case "relevance_paths":
|
|
1311
|
+
out.relevance_paths = parseFlowArray(value);
|
|
1312
|
+
break;
|
|
1313
|
+
default:
|
|
1314
|
+
break;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
return out;
|
|
1318
|
+
}
|
|
1319
|
+
function stripQuotes(value) {
|
|
1320
|
+
return value.replace(/^["'](.*)["']$/u, "$1");
|
|
1321
|
+
}
|
|
1322
|
+
function parseFlowArray(value) {
|
|
1323
|
+
const trimmed = value.trim();
|
|
1324
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) {
|
|
1325
|
+
return [];
|
|
1326
|
+
}
|
|
1327
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
1328
|
+
if (inner.length === 0) {
|
|
1329
|
+
return [];
|
|
1330
|
+
}
|
|
1331
|
+
return inner.split(",").map((item) => stripQuotes(item.trim())).filter((item) => item.length > 0);
|
|
1332
|
+
}
|
|
1333
|
+
function rewriteFrontmatterForPromote(content, stableId) {
|
|
1334
|
+
const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u.exec(content);
|
|
1335
|
+
if (match === null) {
|
|
1336
|
+
return `---
|
|
1337
|
+
id: ${stableId}
|
|
1338
|
+
---
|
|
1339
|
+
|
|
1340
|
+
${content}`;
|
|
1341
|
+
}
|
|
1342
|
+
const block = match[1] ?? "";
|
|
1343
|
+
const filteredLines = block.split(/\r?\n/u).filter((line) => !/^x-fabric-idempotency-key\s*:/u.test(line));
|
|
1344
|
+
filteredLines.unshift(`id: ${stableId}`);
|
|
1345
|
+
const newBlock = filteredLines.join("\n");
|
|
1346
|
+
const before = content.slice(0, match.index);
|
|
1347
|
+
const after = content.slice(match.index + match[0].length);
|
|
1348
|
+
return `${before}---
|
|
1349
|
+
${newBlock}
|
|
1350
|
+
---${after}`;
|
|
1351
|
+
}
|
|
1352
|
+
function rewriteFrontmatterMerge(content, patch, forced) {
|
|
1353
|
+
const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u.exec(content);
|
|
1354
|
+
if (match === null) {
|
|
1355
|
+
const synthLines = [];
|
|
1356
|
+
if (forced?.id !== void 0) synthLines.push(`id: ${forced.id}`);
|
|
1357
|
+
appendPatchLines(synthLines, patch);
|
|
1358
|
+
return `---
|
|
1359
|
+
${synthLines.join("\n")}
|
|
1360
|
+
---
|
|
1361
|
+
|
|
1362
|
+
${content}`;
|
|
1363
|
+
}
|
|
1364
|
+
const block = match[1] ?? "";
|
|
1365
|
+
const updates = {};
|
|
1366
|
+
if (forced?.id !== void 0) updates.id = `id: ${forced.id}`;
|
|
1367
|
+
if (patch.title !== void 0) updates.title = `title: ${quoteIfNeeded(patch.title)}`;
|
|
1368
|
+
if (patch.summary !== void 0) updates.summary = `summary: ${quoteIfNeeded(patch.summary)}`;
|
|
1369
|
+
if (patch.layer !== void 0) updates.layer = `layer: ${patch.layer}`;
|
|
1370
|
+
if (patch.maturity !== void 0) updates.maturity = `maturity: ${patch.maturity}`;
|
|
1371
|
+
if (patch.tags !== void 0) updates.tags = `tags: [${patch.tags.join(", ")}]`;
|
|
1372
|
+
if (patch.relevance_scope !== void 0) updates.relevance_scope = `relevance_scope: ${patch.relevance_scope}`;
|
|
1373
|
+
if (patch.relevance_paths !== void 0) updates.relevance_paths = `relevance_paths: [${patch.relevance_paths.join(", ")}]`;
|
|
1374
|
+
const lines = block.split(/\r?\n/u);
|
|
1375
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1376
|
+
const newLines = [];
|
|
1377
|
+
for (const line of lines) {
|
|
1378
|
+
const sep = line.indexOf(":");
|
|
1379
|
+
const key = sep === -1 ? "" : line.slice(0, sep).trim();
|
|
1380
|
+
if (key in updates) {
|
|
1381
|
+
newLines.push(updates[key]);
|
|
1382
|
+
seen.add(key);
|
|
1383
|
+
} else {
|
|
1384
|
+
newLines.push(line);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
for (const key of Object.keys(updates)) {
|
|
1388
|
+
if (!seen.has(key)) {
|
|
1389
|
+
newLines.push(updates[key]);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
const newBlock = newLines.join("\n");
|
|
1393
|
+
const before = content.slice(0, match.index);
|
|
1394
|
+
const after = content.slice(match.index + match[0].length);
|
|
1395
|
+
return `${before}---
|
|
1396
|
+
${newBlock}
|
|
1397
|
+
---${after}`;
|
|
1398
|
+
}
|
|
1399
|
+
function appendPatchLines(lines, patch) {
|
|
1400
|
+
if (patch.title !== void 0) lines.push(`title: ${quoteIfNeeded(patch.title)}`);
|
|
1401
|
+
if (patch.summary !== void 0) lines.push(`summary: ${quoteIfNeeded(patch.summary)}`);
|
|
1402
|
+
if (patch.layer !== void 0) lines.push(`layer: ${patch.layer}`);
|
|
1403
|
+
if (patch.maturity !== void 0) lines.push(`maturity: ${patch.maturity}`);
|
|
1404
|
+
if (patch.tags !== void 0) lines.push(`tags: [${patch.tags.join(", ")}]`);
|
|
1405
|
+
if (patch.relevance_scope !== void 0) lines.push(`relevance_scope: ${patch.relevance_scope}`);
|
|
1406
|
+
if (patch.relevance_paths !== void 0) lines.push(`relevance_paths: [${patch.relevance_paths.join(", ")}]`);
|
|
1407
|
+
}
|
|
1408
|
+
function quoteIfNeeded(value) {
|
|
1409
|
+
if (/[\n\r]/u.test(value)) {
|
|
1410
|
+
return JSON.stringify(value);
|
|
1411
|
+
}
|
|
1412
|
+
if (/[:#\[\]{}&*!|>'"%@`,]|^\s|\s$/u.test(value)) {
|
|
1413
|
+
return `"${value.replace(/"/gu, '\\"')}"`;
|
|
1414
|
+
}
|
|
1415
|
+
return value;
|
|
1416
|
+
}
|
|
1417
|
+
function resolvePersonalRoot2() {
|
|
1418
|
+
return process.env.FABRIC_HOME ?? homedir2();
|
|
1419
|
+
}
|
|
1420
|
+
async function emitEventBestEffort2(projectRoot, event) {
|
|
1421
|
+
try {
|
|
1422
|
+
await appendEventLedgerEvent(projectRoot, event);
|
|
1423
|
+
} catch {
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// src/tools/review.ts
|
|
1428
|
+
function registerReview(server, tracker) {
|
|
160
1429
|
server.registerTool(
|
|
161
|
-
"
|
|
1430
|
+
"fab_review",
|
|
1431
|
+
{
|
|
1432
|
+
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.",
|
|
1433
|
+
// Discriminated union schemas — passed whole (registerTool accepts
|
|
1434
|
+
// `AnySchema` in addition to `ZodRawShape`; see @modelcontextprotocol/sdk
|
|
1435
|
+
// mcp.d.ts:150).
|
|
1436
|
+
inputSchema: FabReviewInputSchema,
|
|
1437
|
+
outputSchema: FabReviewOutputSchema,
|
|
1438
|
+
annotations: fabReviewAnnotations
|
|
1439
|
+
},
|
|
1440
|
+
async (input) => {
|
|
1441
|
+
const requestId = randomUUID3();
|
|
1442
|
+
tracker?.enter(requestId);
|
|
1443
|
+
try {
|
|
1444
|
+
const projectRoot = resolveProjectRoot();
|
|
1445
|
+
const result = await reviewKnowledge(projectRoot, input);
|
|
1446
|
+
const response = result;
|
|
1447
|
+
const payloadLimits = readPayloadLimits(projectRoot);
|
|
1448
|
+
const serialized = JSON.stringify(response);
|
|
1449
|
+
enforcePayloadLimit3(serialized, payloadLimits);
|
|
1450
|
+
return {
|
|
1451
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
1452
|
+
structuredContent: response
|
|
1453
|
+
};
|
|
1454
|
+
} finally {
|
|
1455
|
+
tracker?.exit(requestId);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// src/tools/knowledge-sections.ts
|
|
1462
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1463
|
+
import {
|
|
1464
|
+
knowledgeSectionsAnnotations,
|
|
1465
|
+
knowledgeSectionsInputSchema,
|
|
1466
|
+
knowledgeSectionsOutputSchema
|
|
1467
|
+
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
1468
|
+
import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
1469
|
+
|
|
1470
|
+
// src/services/knowledge-sections.ts
|
|
1471
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1472
|
+
import { homedir as homedir3 } from "os";
|
|
1473
|
+
import { join as join4 } from "path";
|
|
1474
|
+
var KNOWLEDGE_SECTION_NAMES = [
|
|
1475
|
+
"MISSION_STATEMENT",
|
|
1476
|
+
"MANDATORY_INJECTION",
|
|
1477
|
+
"BUSINESS_LOGIC_CHUNKS",
|
|
1478
|
+
"CONTEXT_INFO"
|
|
1479
|
+
];
|
|
1480
|
+
var PRIORITY_ORDER = {
|
|
1481
|
+
high: 0,
|
|
1482
|
+
medium: 1,
|
|
1483
|
+
low: 2
|
|
1484
|
+
};
|
|
1485
|
+
function parseKnowledgeSections(content) {
|
|
1486
|
+
const sections = /* @__PURE__ */ new Map();
|
|
1487
|
+
const lines = content.split(/\r?\n/u);
|
|
1488
|
+
let activeSection;
|
|
1489
|
+
let activeSectionDepth = 0;
|
|
1490
|
+
let buffer = [];
|
|
1491
|
+
const flush = () => {
|
|
1492
|
+
if (activeSection === void 0) {
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
const text = buffer.join("\n").trim();
|
|
1496
|
+
if (text.length === 0) {
|
|
1497
|
+
buffer = [];
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
sections.set(activeSection, [...sections.get(activeSection) ?? [], text]);
|
|
1501
|
+
buffer = [];
|
|
1502
|
+
};
|
|
1503
|
+
for (const line of lines) {
|
|
1504
|
+
const heading = /^(#{2,6})\s+\[([A-Z_]+)\]\s*$/u.exec(line.trim());
|
|
1505
|
+
if (heading !== null) {
|
|
1506
|
+
flush();
|
|
1507
|
+
activeSection = isKnowledgeSectionName(heading[2]) ? heading[2] : void 0;
|
|
1508
|
+
activeSectionDepth = activeSection === void 0 ? 0 : heading[1].length;
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
const ordinaryHeading = /^(#{1,6})\s+/u.exec(line.trim());
|
|
1512
|
+
if (ordinaryHeading !== null) {
|
|
1513
|
+
if (activeSection !== void 0 && ordinaryHeading[1].length > activeSectionDepth) {
|
|
1514
|
+
buffer.push(line);
|
|
1515
|
+
continue;
|
|
1516
|
+
}
|
|
1517
|
+
flush();
|
|
1518
|
+
activeSection = void 0;
|
|
1519
|
+
activeSectionDepth = 0;
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
if (activeSection !== void 0) {
|
|
1523
|
+
buffer.push(line);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
flush();
|
|
1527
|
+
return new Map(
|
|
1528
|
+
Array.from(sections.entries()).map(([section, values]) => [section, values.join("\n\n")])
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
async function getKnowledgeSections(projectRoot, input) {
|
|
1532
|
+
const token = readSelectionToken(input.selection_token);
|
|
1533
|
+
if (token === void 0) {
|
|
1534
|
+
throw new Error("selection_token is missing or expired");
|
|
1535
|
+
}
|
|
1536
|
+
validateAiSelections(token.ai_selectable_stable_ids, input.ai_selected_stable_ids, input.ai_selection_reasons);
|
|
1537
|
+
const meta = await readAgentsMeta(projectRoot);
|
|
1538
|
+
const selectedStableIds = [...token.required_stable_ids, ...input.ai_selected_stable_ids];
|
|
1539
|
+
const selectedRules = sortRuleNodes(selectedStableIds.map((stableId) => findRuleNode(meta, stableId)));
|
|
1540
|
+
const diagnostics = [];
|
|
1541
|
+
const rules = [];
|
|
1542
|
+
for (const rule of selectedRules) {
|
|
1543
|
+
const content = await readFile4(resolveRuleSourcePath(projectRoot, rule.path), "utf8");
|
|
1544
|
+
const parsedSections = parseKnowledgeSections(content);
|
|
1545
|
+
const sections = {};
|
|
1546
|
+
for (const section of input.sections) {
|
|
1547
|
+
const sectionContent = parsedSections.get(section);
|
|
1548
|
+
sections[section] = sectionContent ?? "";
|
|
1549
|
+
if (sectionContent === void 0) {
|
|
1550
|
+
diagnostics.push({
|
|
1551
|
+
code: "missing_section",
|
|
1552
|
+
severity: "warn",
|
|
1553
|
+
stable_id: rule.stable_id,
|
|
1554
|
+
section,
|
|
1555
|
+
message: `Rule ${rule.stable_id} does not define section ${section}.`
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
const description = rule.node.description;
|
|
1560
|
+
if (description !== void 0 && description.knowledge_type === void 0 && description.knowledge_layer === void 0) {
|
|
1561
|
+
diagnostics.push({
|
|
1562
|
+
code: "missing_knowledge_metadata",
|
|
1563
|
+
severity: "warn",
|
|
1564
|
+
stable_id: rule.stable_id,
|
|
1565
|
+
message: `Rule ${rule.stable_id} has no knowledge metadata (type/layer) \u2014 likely an un-migrated v1.x entry.`
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
rules.push({
|
|
1569
|
+
stable_id: rule.stable_id,
|
|
1570
|
+
level: rule.level,
|
|
1571
|
+
path: rule.path,
|
|
1572
|
+
sections
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
const result = {
|
|
1576
|
+
revision_hash: meta.revision,
|
|
1577
|
+
precedence: ["L2", "L1", "L0"],
|
|
1578
|
+
selected_stable_ids: rules.map((rule) => rule.stable_id),
|
|
1579
|
+
rules,
|
|
1580
|
+
diagnostics
|
|
1581
|
+
};
|
|
1582
|
+
try {
|
|
1583
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1584
|
+
event_type: "knowledge_selection",
|
|
1585
|
+
selection_token: input.selection_token,
|
|
1586
|
+
target_paths: token.target_paths,
|
|
1587
|
+
required_stable_ids: token.required_stable_ids,
|
|
1588
|
+
ai_selectable_stable_ids: token.ai_selectable_stable_ids,
|
|
1589
|
+
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
1590
|
+
final_stable_ids: result.selected_stable_ids,
|
|
1591
|
+
ai_selection_reasons: pickSelectionReasons(input.ai_selected_stable_ids, input.ai_selection_reasons),
|
|
1592
|
+
rejected_stable_ids: [],
|
|
1593
|
+
ignored_stable_ids: [],
|
|
1594
|
+
correlation_id: input.correlation_id,
|
|
1595
|
+
session_id: input.session_id
|
|
1596
|
+
});
|
|
1597
|
+
} catch {
|
|
1598
|
+
}
|
|
1599
|
+
try {
|
|
1600
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1601
|
+
event_type: "knowledge_sections_fetched",
|
|
1602
|
+
selection_token: input.selection_token,
|
|
1603
|
+
target_paths: token.target_paths,
|
|
1604
|
+
requested_sections: input.sections,
|
|
1605
|
+
final_stable_ids: result.selected_stable_ids,
|
|
1606
|
+
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
1607
|
+
diagnostics,
|
|
1608
|
+
correlation_id: input.correlation_id,
|
|
1609
|
+
session_id: input.session_id
|
|
1610
|
+
});
|
|
1611
|
+
} catch {
|
|
1612
|
+
}
|
|
1613
|
+
const consumedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1614
|
+
const consumedClientHash = input.client_hash ?? "";
|
|
1615
|
+
const emittedConsumed = /* @__PURE__ */ new Set();
|
|
1616
|
+
for (const stableId of result.selected_stable_ids) {
|
|
1617
|
+
if (emittedConsumed.has(stableId)) {
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
emittedConsumed.add(stableId);
|
|
1621
|
+
try {
|
|
1622
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1623
|
+
event_type: "knowledge_consumed",
|
|
1624
|
+
stable_id: stableId,
|
|
1625
|
+
consumed_at: consumedAt,
|
|
1626
|
+
client_hash: consumedClientHash,
|
|
1627
|
+
correlation_id: input.correlation_id,
|
|
1628
|
+
session_id: input.session_id
|
|
1629
|
+
});
|
|
1630
|
+
} catch {
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
return result;
|
|
1634
|
+
}
|
|
1635
|
+
function validateAiSelections(aiSelectableStableIds, aiSelectedStableIds, aiSelectionReasons) {
|
|
1636
|
+
const selectable = new Set(aiSelectableStableIds);
|
|
1637
|
+
for (const stableId of aiSelectedStableIds) {
|
|
1638
|
+
if (!selectable.has(stableId)) {
|
|
1639
|
+
throw new Error(`Invalid L1 rule selection: ${stableId}`);
|
|
1640
|
+
}
|
|
1641
|
+
if (aiSelectionReasons[stableId]?.trim() === "") {
|
|
1642
|
+
throw new Error(`Missing AI selection reason for ${stableId}`);
|
|
1643
|
+
}
|
|
1644
|
+
if (aiSelectionReasons[stableId] === void 0) {
|
|
1645
|
+
throw new Error(`Missing AI selection reason for ${stableId}`);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
function findRuleNode(meta, stableId) {
|
|
1650
|
+
for (const [nodeId, node] of Object.entries(meta.nodes)) {
|
|
1651
|
+
const nodeStableId = node.stable_id ?? nodeId;
|
|
1652
|
+
if (nodeStableId !== stableId) {
|
|
1653
|
+
continue;
|
|
1654
|
+
}
|
|
1655
|
+
const level = node.level ?? node.layer ?? "L2";
|
|
1656
|
+
return {
|
|
1657
|
+
stable_id: nodeStableId,
|
|
1658
|
+
level,
|
|
1659
|
+
path: normalizeKnowledgePath(node.content_ref ?? node.file),
|
|
1660
|
+
priority: node.priority ?? "medium",
|
|
1661
|
+
node
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
throw new Error(`Selected rule is not present in agents.meta.json: ${stableId}`);
|
|
1665
|
+
}
|
|
1666
|
+
function sortRuleNodes(rules) {
|
|
1667
|
+
return [...rules].sort((left, right) => {
|
|
1668
|
+
const levelDelta = outputLevelOrder(left.level) - outputLevelOrder(right.level);
|
|
1669
|
+
if (levelDelta !== 0) {
|
|
1670
|
+
return levelDelta;
|
|
1671
|
+
}
|
|
1672
|
+
const priorityDelta = PRIORITY_ORDER[left.priority] - PRIORITY_ORDER[right.priority];
|
|
1673
|
+
if (priorityDelta !== 0) {
|
|
1674
|
+
return priorityDelta;
|
|
1675
|
+
}
|
|
1676
|
+
return left.stable_id.localeCompare(right.stable_id);
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
function outputLevelOrder(level) {
|
|
1680
|
+
switch (level) {
|
|
1681
|
+
case "L0":
|
|
1682
|
+
return 0;
|
|
1683
|
+
case "L1":
|
|
1684
|
+
return 1;
|
|
1685
|
+
case "L2":
|
|
1686
|
+
return 2;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
function isKnowledgeSectionName(value) {
|
|
1690
|
+
return KNOWLEDGE_SECTION_NAMES.includes(value);
|
|
1691
|
+
}
|
|
1692
|
+
function resolveRuleSourcePath(projectRoot, contentRef) {
|
|
1693
|
+
if (contentRef.startsWith("~/.fabric/knowledge/")) {
|
|
1694
|
+
const home = process.env.FABRIC_HOME ?? homedir3();
|
|
1695
|
+
return join4(home, ".fabric", "knowledge", contentRef.slice("~/.fabric/knowledge/".length));
|
|
1696
|
+
}
|
|
1697
|
+
return join4(projectRoot, contentRef);
|
|
1698
|
+
}
|
|
1699
|
+
function pickSelectionReasons(selectedStableIds, reasons) {
|
|
1700
|
+
return Object.fromEntries(selectedStableIds.map((stableId) => [stableId, reasons[stableId] ?? ""]));
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
// src/tools/knowledge-sections.ts
|
|
1704
|
+
function registerKnowledgeSections(server, tracker) {
|
|
1705
|
+
server.registerTool(
|
|
1706
|
+
"fab_get_knowledge_sections",
|
|
162
1707
|
{
|
|
163
1708
|
description: "Fetch structured Fabric rule sections after fab_plan_context. Required L0/L2 rules are merged with AI-selected L1 rules server-side.",
|
|
164
|
-
inputSchema:
|
|
165
|
-
outputSchema:
|
|
166
|
-
annotations:
|
|
1709
|
+
inputSchema: knowledgeSectionsInputSchema,
|
|
1710
|
+
outputSchema: knowledgeSectionsOutputSchema,
|
|
1711
|
+
annotations: knowledgeSectionsAnnotations
|
|
167
1712
|
},
|
|
168
1713
|
async (input) => {
|
|
169
|
-
const requestId =
|
|
1714
|
+
const requestId = randomUUID4();
|
|
170
1715
|
tracker?.enter(requestId);
|
|
171
1716
|
try {
|
|
172
1717
|
const projectRoot = resolveProjectRoot();
|
|
173
|
-
const syncReport = await
|
|
174
|
-
const result = await
|
|
1718
|
+
const syncReport = await ensureKnowledgeFresh(projectRoot);
|
|
1719
|
+
const result = await getKnowledgeSections(projectRoot, input);
|
|
175
1720
|
const response = {
|
|
176
1721
|
...result,
|
|
177
1722
|
warnings: [...syncReport.warnings]
|
|
178
1723
|
};
|
|
179
1724
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
180
1725
|
const serialized = JSON.stringify(response);
|
|
181
|
-
const guardResult =
|
|
1726
|
+
const guardResult = enforcePayloadLimit4(serialized, payloadLimits);
|
|
182
1727
|
if (guardResult.warning) {
|
|
183
1728
|
response.warnings = [
|
|
184
1729
|
...response.warnings,
|
|
@@ -304,34 +1849,40 @@ function formatError(error) {
|
|
|
304
1849
|
}
|
|
305
1850
|
function formatPreexistingRootMessage(projectRoot) {
|
|
306
1851
|
const preexisting = [];
|
|
307
|
-
if (
|
|
308
|
-
if (
|
|
1852
|
+
if (existsSync4(join5(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
|
|
1853
|
+
if (existsSync4(join5(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
|
|
309
1854
|
if (preexisting.length === 0) return null;
|
|
310
|
-
return `[startup] info: detected ${preexisting.join(", ")} at project root. Note: Fabric serves
|
|
1855
|
+
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.`;
|
|
311
1856
|
}
|
|
312
1857
|
function createFabricServer(tracker) {
|
|
313
1858
|
const server = new McpServer({
|
|
314
|
-
name: "fabric-
|
|
315
|
-
version: "
|
|
1859
|
+
name: "fabric-knowledge-server",
|
|
1860
|
+
version: "2.0.0-rc.8"
|
|
316
1861
|
});
|
|
317
1862
|
registerPlanContext(server, tracker);
|
|
318
|
-
|
|
1863
|
+
registerKnowledgeSections(server, tracker);
|
|
1864
|
+
registerExtractKnowledge(server, tracker);
|
|
1865
|
+
registerReview(server, tracker);
|
|
319
1866
|
server.registerResource(
|
|
320
1867
|
"bootstrap README",
|
|
321
1868
|
AGENTS_MD_RESOURCE_URI,
|
|
322
1869
|
{
|
|
323
|
-
description: "
|
|
1870
|
+
description: "Legacy v1.x bootstrap anchor (deprecated in v2.0; kept as MCP contract shim)",
|
|
324
1871
|
mimeType: "text/markdown"
|
|
325
1872
|
},
|
|
326
1873
|
async (_uri) => {
|
|
327
1874
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
328
|
-
const
|
|
1875
|
+
const path2 = join5(projectRoot, ".fabric", "bootstrap", "README.md");
|
|
1876
|
+
let text = "";
|
|
1877
|
+
if (existsSync4(path2)) {
|
|
1878
|
+
text = await readFile5(path2, "utf8");
|
|
1879
|
+
}
|
|
329
1880
|
return {
|
|
330
1881
|
contents: [
|
|
331
1882
|
{
|
|
332
1883
|
uri: AGENTS_MD_RESOURCE_URI,
|
|
333
1884
|
mimeType: "text/markdown",
|
|
334
|
-
text
|
|
1885
|
+
text
|
|
335
1886
|
}
|
|
336
1887
|
]
|
|
337
1888
|
};
|
|
@@ -343,7 +1894,7 @@ async function startStdioServer() {
|
|
|
343
1894
|
const tracker = createInFlightTracker();
|
|
344
1895
|
const projectRoot = resolveProjectRoot();
|
|
345
1896
|
const syncStart = Date.now();
|
|
346
|
-
const reconcileResult = await
|
|
1897
|
+
const reconcileResult = await reconcileKnowledge(projectRoot, { trigger: "startup" });
|
|
347
1898
|
const syncDurationMs = Date.now() - syncStart;
|
|
348
1899
|
process.stderr.write(
|
|
349
1900
|
`[startup] rule sync: status=${reconcileResult.status}, events=${reconcileResult.events.length}, ${syncDurationMs}ms
|
|
@@ -406,9 +1957,9 @@ function createShutdownHandler(deps) {
|
|
|
406
1957
|
};
|
|
407
1958
|
}
|
|
408
1959
|
async function startHttpServer(options) {
|
|
409
|
-
const { createFabricHttpApp } = await import("./http-
|
|
410
|
-
const { port, projectRoot, host = "127.0.0.1", authToken
|
|
411
|
-
const app = createFabricHttpApp({ projectRoot, host, authToken
|
|
1960
|
+
const { createFabricHttpApp } = await import("./http-AR26GYEV.js");
|
|
1961
|
+
const { port, projectRoot, host = "127.0.0.1", authToken } = options;
|
|
1962
|
+
const app = createFabricHttpApp({ projectRoot, host, authToken });
|
|
412
1963
|
return await new Promise((resolveServer, rejectServer) => {
|
|
413
1964
|
const server = app.listen(port, host);
|
|
414
1965
|
server.once("close", () => {
|
|
@@ -424,7 +1975,7 @@ async function startHttpServer(options) {
|
|
|
424
1975
|
}
|
|
425
1976
|
var entrypoint = process.argv[1];
|
|
426
1977
|
var currentFilePath = fileURLToPath(import.meta.url);
|
|
427
|
-
var isMainModule = entrypoint !== void 0 &&
|
|
1978
|
+
var isMainModule = entrypoint !== void 0 && resolve2(entrypoint) === currentFilePath;
|
|
428
1979
|
if (isMainModule) {
|
|
429
1980
|
void startStdioServer().catch((error) => {
|
|
430
1981
|
writeStderr(formatError(error));
|
|
@@ -434,33 +1985,40 @@ if (isMainModule) {
|
|
|
434
1985
|
export {
|
|
435
1986
|
AGENTS_MD_RESOURCE_URI,
|
|
436
1987
|
EVENT_LEDGER_PATH,
|
|
1988
|
+
KnowledgeIdAllocator,
|
|
437
1989
|
LEDGER_PATH,
|
|
438
1990
|
LEGACY_LEDGER_PATH,
|
|
439
1991
|
ServeLockHeldError,
|
|
440
1992
|
acquireLock,
|
|
441
|
-
|
|
1993
|
+
appendEventLedgerEvent,
|
|
1994
|
+
buildKnowledgeMeta,
|
|
442
1995
|
checkLockOrThrow,
|
|
443
|
-
|
|
444
|
-
|
|
1996
|
+
computeKnowledgeBasedAgentsMeta,
|
|
1997
|
+
computeKnowledgeTestIndex,
|
|
445
1998
|
createFabricServer,
|
|
446
1999
|
createInFlightTracker,
|
|
447
2000
|
createShutdownHandler,
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
2001
|
+
deriveKnowledgeMetaLayer,
|
|
2002
|
+
deriveKnowledgeMetaTopologyType,
|
|
2003
|
+
ensureKnowledgeFresh,
|
|
2004
|
+
extractKnowledge,
|
|
451
2005
|
flushAndSyncEventLedger,
|
|
452
2006
|
formatPreexistingRootMessage,
|
|
453
2007
|
getEventLedgerPath,
|
|
454
2008
|
getLedgerPath,
|
|
455
2009
|
getLegacyLedgerPath,
|
|
456
|
-
|
|
2010
|
+
isSameKnowledgeTestIndex,
|
|
2011
|
+
planContext,
|
|
457
2012
|
readLockState,
|
|
458
|
-
|
|
2013
|
+
readSelectionToken,
|
|
2014
|
+
reconcileKnowledge,
|
|
459
2015
|
releaseLock,
|
|
2016
|
+
reviewKnowledge,
|
|
2017
|
+
runDoctorApplyLint,
|
|
460
2018
|
runDoctorFix,
|
|
461
2019
|
runDoctorReport,
|
|
462
2020
|
stableStringify,
|
|
463
2021
|
startHttpServer,
|
|
464
2022
|
startStdioServer,
|
|
465
|
-
|
|
2023
|
+
writeKnowledgeMeta
|
|
466
2024
|
};
|