@fenglimg/fabric-server 2.0.0-rc.1 → 2.0.0-rc.11

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