@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.
@@ -6,7 +6,7 @@ var ContextCache = class {
6
6
  defaultTtlMs;
7
7
  // Slot 1: raw AgentsMeta keyed by projectRoot
8
8
  metaSlot = /* @__PURE__ */ new Map();
9
- // Slot 2: GetRulesContext keyed by projectRoot
9
+ // Slot 2: GetKnowledgeContext keyed by projectRoot
10
10
  contextSlot = /* @__PURE__ */ new Map();
11
11
  // Slot 3: audit sliding-window cursor keyed by projectRoot
12
12
  auditSlot = /* @__PURE__ */ new Map();
@@ -276,20 +276,20 @@ function flushAndSyncEventLedger(projectRoot) {
276
276
  }
277
277
  }
278
278
 
279
- // src/services/rule-meta-builder.ts
279
+ // src/services/knowledge-meta-builder.ts
280
280
  import { mkdir as mkdir2, readdir, readFile as readFile3 } from "fs/promises";
281
281
  import { existsSync as existsSync2, statSync } from "fs";
282
282
  import { homedir } from "os";
283
283
  import { isAbsolute, join as join3, relative, resolve as resolve2, sep as sep2 } from "path";
284
284
  import {
285
- RULE_TEST_INDEX_SCHEMA_VERSION,
285
+ KNOWLEDGE_TEST_INDEX_SCHEMA_VERSION,
286
286
  agentsMetaSchema as agentsMetaSchema3,
287
287
  defaultAgentsMetaCounters,
288
288
  deriveAgentsMetaLayer,
289
289
  deriveAgentsMetaStableId,
290
290
  deriveAgentsMetaTopologyType,
291
291
  isKnowledgeStableId,
292
- ruleTestIndexSchema,
292
+ knowledgeTestIndexSchema,
293
293
  KnowledgeTypeSchema,
294
294
  MaturitySchema,
295
295
  LayerSchema,
@@ -297,34 +297,35 @@ import {
297
297
  parseKnowledgeId
298
298
  } from "@fenglimg/fabric-shared";
299
299
  import { atomicWriteText as atomicWriteText2 } from "@fenglimg/fabric-shared/node/atomic-write";
300
- async function buildRuleMeta(projectRootInput) {
300
+ async function buildKnowledgeMeta(projectRootInput) {
301
301
  const projectRoot = normalizeProjectRoot(projectRootInput);
302
302
  assertExistingDirectory(projectRoot);
303
303
  const metaPath = join3(projectRoot, ".fabric", "agents.meta.json");
304
- const ruleTestIndexPath = join3(projectRoot, ".fabric", "rule-test.index.json");
304
+ const knowledgeTestIndexPath = join3(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
305
305
  const existingMeta = await readExistingMeta(metaPath);
306
- const existingRuleTestIndex = await readExistingRuleTestIndex(ruleTestIndexPath);
307
- const meta = await computeRulesBasedAgentsMeta(projectRoot, existingMeta);
308
- const ruleTestIndex = await computeRuleTestIndex(projectRoot, meta, existingRuleTestIndex);
306
+ const existingKnowledgeTestIndex = await readExistingKnowledgeTestIndex(knowledgeTestIndexPath);
307
+ const meta = await computeKnowledgeBasedAgentsMeta(projectRoot, existingMeta);
308
+ const knowledgeTestIndex = await computeKnowledgeTestIndex(projectRoot, meta, existingKnowledgeTestIndex);
309
309
  return {
310
310
  meta,
311
- ruleTestIndex,
312
- changed: existingMeta === void 0 || stableStringify(existingMeta) !== stableStringify(meta) || existingRuleTestIndex === void 0 || !isSameRuleTestIndex(existingRuleTestIndex, ruleTestIndex)
311
+ knowledgeTestIndex,
312
+ changed: existingMeta === void 0 || stableStringify(existingMeta) !== stableStringify(meta) || existingKnowledgeTestIndex === void 0 || !isSameKnowledgeTestIndex(existingKnowledgeTestIndex, knowledgeTestIndex)
313
313
  };
314
314
  }
315
- async function writeRuleMeta(projectRootInput, options) {
315
+ async function writeKnowledgeMeta(projectRootInput, options) {
316
316
  const projectRoot = normalizeProjectRoot(projectRootInput);
317
317
  const metaPath = join3(projectRoot, ".fabric", "agents.meta.json");
318
- const ruleTestIndexPath = join3(projectRoot, ".fabric", "rule-test.index.json");
318
+ const knowledgeTestIndexPath = join3(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
319
319
  const existingMeta = await readExistingMeta(metaPath);
320
- const result = await buildRuleMeta(projectRoot);
320
+ const result = await buildKnowledgeMeta(projectRoot);
321
321
  if (!result.changed) {
322
322
  return result;
323
323
  }
324
324
  await ensureParentDirectory(metaPath);
325
325
  await atomicWriteText2(metaPath, `${JSON.stringify(result.meta, null, 2)}
326
326
  `);
327
- await atomicWriteText2(ruleTestIndexPath, `${JSON.stringify(result.ruleTestIndex, null, 2)}
327
+ await ensureParentDirectory(knowledgeTestIndexPath);
328
+ await atomicWriteText2(knowledgeTestIndexPath, `${JSON.stringify(result.knowledgeTestIndex, null, 2)}
328
329
  `);
329
330
  if (existingMeta === void 0 || stableStringify(existingMeta) !== stableStringify(result.meta)) {
330
331
  await recordBaselineSynced(projectRoot, {
@@ -338,7 +339,7 @@ async function writeRuleMeta(projectRootInput, options) {
338
339
  }
339
340
  return result;
340
341
  }
341
- async function computeRulesBasedAgentsMeta(projectRootInput, existingMeta) {
342
+ async function computeKnowledgeBasedAgentsMeta(projectRootInput, existingMeta) {
342
343
  const projectRoot = normalizeProjectRoot(projectRootInput);
343
344
  assertExistingDirectory(projectRoot);
344
345
  const previousMeta = existingMeta ?? await readExistingMeta(join3(projectRoot, ".fabric", "agents.meta.json"));
@@ -372,7 +373,7 @@ async function computeRulesBasedAgentsMeta(projectRootInput, existingMeta) {
372
373
  counters
373
374
  };
374
375
  }
375
- async function computeRuleTestIndex(projectRootInput, computedMeta, previousIndex) {
376
+ async function computeKnowledgeTestIndex(projectRootInput, computedMeta, previousIndex) {
376
377
  const projectRoot = normalizeProjectRoot(projectRootInput);
377
378
  assertExistingDirectory(projectRoot);
378
379
  const previousLinks = indexPreviousRuleTestEntries(previousIndex?.links ?? []);
@@ -408,7 +409,7 @@ async function computeRuleTestIndex(projectRootInput, computedMeta, previousInde
408
409
  });
409
410
  }
410
411
  return {
411
- schema_version: RULE_TEST_INDEX_SCHEMA_VERSION,
412
+ schema_version: KNOWLEDGE_TEST_INDEX_SCHEMA_VERSION,
412
413
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
413
414
  revision: computedMeta.revision,
414
415
  previous_revision: previousIndex?.revision !== void 0 && previousIndex.revision !== computedMeta.revision ? previousIndex.revision : previousIndex?.previous_revision,
@@ -416,14 +417,14 @@ async function computeRuleTestIndex(projectRootInput, computedMeta, previousInde
416
417
  orphan_annotations: orphanAnnotations.sort(compareRuleTestEntries)
417
418
  };
418
419
  }
419
- function deriveRuleMetaLayer(relativePath) {
420
+ function deriveKnowledgeMetaLayer(relativePath) {
420
421
  return deriveAgentsMetaLayer(toAgentsCompatiblePath(relativePath));
421
422
  }
422
- function deriveRuleMetaTopologyType(relativePath) {
423
+ function deriveKnowledgeMetaTopologyType(relativePath) {
423
424
  return deriveAgentsMetaTopologyType(toAgentsCompatiblePath(relativePath));
424
425
  }
425
- function isSameRuleTestIndex(left, right) {
426
- return stableStringify(toComparableRuleTestIndex(left)) === stableStringify(toComparableRuleTestIndex(right));
426
+ function isSameKnowledgeTestIndex(left, right) {
427
+ return stableStringify(toComparableKnowledgeTestIndex(left)) === stableStringify(toComparableKnowledgeTestIndex(right));
427
428
  }
428
429
  function stableStringify(value) {
429
430
  return JSON.stringify(value, Object.keys(flattenKeys(value)).sort());
@@ -452,7 +453,7 @@ async function readExistingMeta(metaPath) {
452
453
  return void 0;
453
454
  }
454
455
  }
455
- async function readExistingRuleTestIndex(indexPath) {
456
+ async function readExistingKnowledgeTestIndex(indexPath) {
456
457
  let raw;
457
458
  try {
458
459
  raw = await readFile3(indexPath, "utf8");
@@ -463,7 +464,7 @@ async function readExistingRuleTestIndex(indexPath) {
463
464
  throw error;
464
465
  }
465
466
  try {
466
- return ruleTestIndexSchema.parse(JSON.parse(raw));
467
+ return knowledgeTestIndexSchema.parse(JSON.parse(raw));
467
468
  } catch {
468
469
  return void 0;
469
470
  }
@@ -615,7 +616,7 @@ function compareRuleTestEntries(left, right) {
615
616
  function compareAnnotationEntries(left, right) {
616
617
  return left.stableId.localeCompare(right.stableId) || left.testFile.localeCompare(right.testFile) || left.line - right.line;
617
618
  }
618
- function toComparableRuleTestIndex(index) {
619
+ function toComparableKnowledgeTestIndex(index) {
619
620
  const { generated_at: _generatedAt, ...comparable } = index;
620
621
  return comparable;
621
622
  }
@@ -627,7 +628,7 @@ function indexExistingNodesByContentRef(existingMeta) {
627
628
  return byContentRef;
628
629
  }
629
630
  function deriveNodeId(file) {
630
- const layer = deriveRuleMetaLayer(file);
631
+ const layer = deriveKnowledgeMetaLayer(file);
631
632
  const relativeStem = getRuleRelativeStem(file);
632
633
  if (file.startsWith(PERSONAL_CONTENT_REF_PREFIX)) {
633
634
  return `${layer}/personal/${relativeStem}`;
@@ -638,8 +639,8 @@ function deriveNodeId(file) {
638
639
  return `${layer}/${relativeStem}`;
639
640
  }
640
641
  function createDefaultNodeMeta(contentRef) {
641
- const layer = deriveRuleMetaLayer(contentRef);
642
- const topologyType = deriveRuleMetaTopologyType(contentRef);
642
+ const layer = deriveKnowledgeMetaLayer(contentRef);
643
+ const topologyType = deriveKnowledgeMetaTopologyType(contentRef);
643
644
  return {
644
645
  file: contentRef,
645
646
  content_ref: contentRef,
@@ -676,8 +677,12 @@ function toAgentsCompatiblePath(contentRef) {
676
677
  function sortNodes(nodes) {
677
678
  return Object.fromEntries(Object.entries(nodes).sort(([left], [right]) => left.localeCompare(right)));
678
679
  }
680
+ function isPendingNode(node) {
681
+ const ref = node.content_ref ?? node.file ?? "";
682
+ return ref.startsWith(".fabric/knowledge/pending/") || ref.startsWith("~/.fabric/knowledge/pending/");
683
+ }
679
684
  function computeRevision(nodes) {
680
- const revisionSource = Object.entries(sortNodes(nodes)).map(([id, node]) => [id, node.hash, node.stable_id ?? "", node.identity_source ?? ""].join("|")).join("\n");
685
+ const revisionSource = Object.entries(sortNodes(nodes)).filter(([, node]) => !isPendingNode(node)).map(([id, node]) => [id, node.hash, node.stable_id ?? "", node.identity_source ?? ""].join("|")).join("\n");
681
686
  return sha256(revisionSource);
682
687
  }
683
688
  function collectSyncedFiles(existingMeta, computedMeta) {
@@ -810,7 +815,10 @@ function extractRuleDescription(source) {
810
815
  knowledge_layer: void 0,
811
816
  layer_reason: void 0,
812
817
  created_at: void 0,
813
- tags: void 0
818
+ tags: void 0,
819
+ // v2.0-rc.5 (C1): default-safe values when there is no frontmatter at all.
820
+ relevance_scope: "broad",
821
+ relevance_paths: []
814
822
  };
815
823
  }
816
824
  function extractRuleSections(source) {
@@ -836,7 +844,9 @@ function extractDescriptionFromFrontmatter(frontmatter) {
836
844
  knowledge_layer: knowledge.knowledge_layer,
837
845
  layer_reason: knowledge.layer_reason,
838
846
  created_at: knowledge.created_at,
839
- tags: knowledge.tags
847
+ tags: knowledge.tags,
848
+ relevance_scope: knowledge.relevance_scope,
849
+ relevance_paths: knowledge.relevance_paths
840
850
  };
841
851
  }
842
852
  function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
@@ -907,6 +917,9 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
907
917
  }
908
918
  }
909
919
  const tags = extractInlineArray(frontmatter, "tags");
920
+ const rawRelevanceScope = extractScalar(frontmatter, "relevance_scope");
921
+ const relevance_scope = rawRelevanceScope === "narrow" || rawRelevanceScope === "broad" ? rawRelevanceScope : "broad";
922
+ const relevance_paths = extractInlineArray(frontmatter, "relevance_paths");
910
923
  return {
911
924
  id,
912
925
  knowledge_type,
@@ -914,7 +927,9 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
914
927
  knowledge_layer,
915
928
  layer_reason: rawLayerReason,
916
929
  created_at,
917
- tags: tags.length > 0 ? tags : void 0
930
+ tags: tags.length > 0 ? tags : void 0,
931
+ relevance_scope,
932
+ relevance_paths
918
933
  };
919
934
  }
920
935
  function extractScalar(frontmatter, key) {
@@ -943,7 +958,7 @@ function isNodeError3(error) {
943
958
  return error instanceof Error;
944
959
  }
945
960
 
946
- // src/services/rule-sync.ts
961
+ // src/services/knowledge-sync.ts
947
962
  import { readdir as readdir2, readFile as readFile4, stat } from "fs/promises";
948
963
  import { existsSync as existsSync3, statSync as statSync2 } from "fs";
949
964
  import { join as join4, relative as relative2, sep as sep3 } from "path";
@@ -951,7 +966,7 @@ import { RuleValidationError } from "@fenglimg/fabric-shared/errors";
951
966
  var lastSyncState = /* @__PURE__ */ new Map();
952
967
  var freshSyncCooldown = /* @__PURE__ */ new Map();
953
968
  var SYNC_COOLDOWN_MS = 500;
954
- function invalidateRuleSyncCooldown(projectRoot) {
969
+ function invalidateKnowledgeSyncCooldown(projectRoot) {
955
970
  freshSyncCooldown.delete(projectRoot);
956
971
  }
957
972
  async function readMetaEntries(projectRoot) {
@@ -1125,14 +1140,14 @@ async function appendRuleSyncEvents(projectRoot, events) {
1125
1140
  });
1126
1141
  }
1127
1142
  }
1128
- async function ensureRulesFresh(projectRoot, opts) {
1143
+ async function ensureKnowledgeFresh(projectRoot, opts) {
1129
1144
  const mode = opts?.mode ?? "incremental";
1130
1145
  const cooldownExpiry = freshSyncCooldown.get(projectRoot);
1131
1146
  if (cooldownExpiry !== void 0 && Date.now() < cooldownExpiry && mode !== "full") {
1132
1147
  return { status: "fresh", events: [], warnings: [] };
1133
1148
  }
1134
1149
  const throwOnInvalidFrontmatter = opts?.throwOnInvalidFrontmatter ?? false;
1135
- const source = "ensureRulesFresh";
1150
+ const source = "ensureKnowledgeFresh";
1136
1151
  const events = [];
1137
1152
  const warnings = [];
1138
1153
  const metaEntries = await readMetaEntries(projectRoot);
@@ -1181,11 +1196,11 @@ async function ensureRulesFresh(projectRoot, opts) {
1181
1196
  reconciled_files: events.map((e) => e.path)
1182
1197
  };
1183
1198
  }
1184
- async function reconcileRules(projectRoot, opts) {
1199
+ async function reconcileKnowledge(projectRoot, opts) {
1185
1200
  freshSyncCooldown.delete(projectRoot);
1186
1201
  const trigger = opts?.trigger;
1187
1202
  const startTime = Date.now();
1188
- const source = "reconcileRules";
1203
+ const source = "reconcileKnowledge";
1189
1204
  const events = [];
1190
1205
  const warnings = [];
1191
1206
  const metaEntries = await readMetaEntries(projectRoot);
@@ -1217,7 +1232,7 @@ async function reconcileRules(projectRoot, opts) {
1217
1232
  }
1218
1233
  }
1219
1234
  if (events.length > 0) {
1220
- await writeRuleMeta(projectRoot, { source: "sync_meta" });
1235
+ await writeKnowledgeMeta(projectRoot, { source: "sync_meta" });
1221
1236
  await appendRuleSyncEvents(projectRoot, events);
1222
1237
  contextCache.invalidate("file_watch", projectRoot);
1223
1238
  }
@@ -1229,7 +1244,7 @@ async function reconcileRules(projectRoot, opts) {
1229
1244
  event_type: "meta_reconciled_on_startup",
1230
1245
  reconciled_files: reconciledFiles,
1231
1246
  duration_ms,
1232
- source: "reconcileRules"
1247
+ source: "reconcileKnowledge"
1233
1248
  });
1234
1249
  } else {
1235
1250
  await appendEventLedgerEvent(projectRoot, {
@@ -1237,7 +1252,7 @@ async function reconcileRules(projectRoot, opts) {
1237
1252
  reconciled_files: reconciledFiles,
1238
1253
  duration_ms,
1239
1254
  trigger,
1240
- source: "reconcileRules"
1255
+ source: "reconcileKnowledge"
1241
1256
  });
1242
1257
  }
1243
1258
  }
@@ -1254,19 +1269,59 @@ async function reconcileRules(projectRoot, opts) {
1254
1269
  }
1255
1270
 
1256
1271
  // src/services/doctor.ts
1272
+ import { execFileSync } from "child_process";
1257
1273
  import { existsSync as existsSync4, readdirSync, readFileSync, statSync as statSync3 } from "fs";
1258
- import { access, mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
1274
+ import { access, mkdir as mkdir3, readFile as readFile5, rename, writeFile as writeFile2 } from "fs/promises";
1259
1275
  import { constants } from "fs";
1260
- import { isAbsolute as isAbsolute2, join as join5, posix, resolve as resolve3 } from "path";
1276
+ import { homedir as homedir2 } from "os";
1277
+ import { isAbsolute as isAbsolute2, join as join5, posix, relative as nodeRelative, resolve as resolve3, sep as sep4 } from "path";
1278
+ import { minimatch } from "minimatch";
1261
1279
  import {
1262
1280
  agentsMetaSchema as agentsMetaSchema4,
1263
1281
  AgentsMetaCountersSchema,
1264
1282
  forensicReportSchema,
1265
1283
  parseKnowledgeId as parseKnowledgeId2,
1266
- ruleTestIndexSchema as ruleTestIndexSchema2
1284
+ knowledgeTestIndexSchema as knowledgeTestIndexSchema2
1267
1285
  } from "@fenglimg/fabric-shared";
1268
1286
  import { detectFramework } from "@fenglimg/fabric-shared/node";
1269
- import { atomicWriteJson as atomicWriteJson2 } from "@fenglimg/fabric-shared/node/atomic-write";
1287
+ import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText3 } from "@fenglimg/fabric-shared/node/atomic-write";
1288
+ var ORPHAN_DEMOTE_THRESHOLD_DAYS = {
1289
+ stable: 90,
1290
+ endorsed: 30,
1291
+ draft: 14
1292
+ };
1293
+ var STALE_ARCHIVE_ADDITIONAL_DAYS = 90;
1294
+ var PENDING_OVERDUE_THRESHOLD_DAYS = 14;
1295
+ var PENDING_AUTO_ARCHIVE_THRESHOLD_DAYS = 30;
1296
+ var DEFAULT_UNDERSEED_NODE_THRESHOLD = 10;
1297
+ var SESSION_HINTS_STALE_DAYS = 7;
1298
+ var SESSION_HINTS_FILE_PREFIX = "session-hints-";
1299
+ var SESSION_HINTS_FILE_SUFFIX = ".json";
1300
+ var NARROW_RATIO_THRESHOLD = 0.2;
1301
+ var NARROW_MIN_TOTAL = 10;
1302
+ var SILENCE_RATE_THRESHOLD = 0.95;
1303
+ var SILENCE_WINDOW_DAYS = 30;
1304
+ var EDIT_COUNTER_FILE_REL = posix.join(".fabric", ".cache", "edit-counter");
1305
+ var HINT_SILENCE_COUNTER_FILE_REL = posix.join(
1306
+ ".fabric",
1307
+ ".cache",
1308
+ "hint-silence-counter"
1309
+ );
1310
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
1311
+ var MATURITY_LINE_PATTERN = /^maturity:\s*("?)(stable|endorsed|draft)\1\s*$/mu;
1312
+ var CREATED_AT_LINE_PATTERN = /^created_at:\s*("?)([^"\n]+)\1\s*$/mu;
1313
+ var RELEVANCE_SCOPE_LINE_PATTERN = /^relevance_scope:\s*("?)(narrow|broad)\1\s*$/mu;
1314
+ var RELEVANCE_PATHS_LINE_PATTERN = /^relevance_paths:\s*\[([^\]]*)\]\s*$/mu;
1315
+ var RELEVANCE_PATHS_DRIFT_WINDOW_DAYS = 90;
1316
+ var SYNTHESIZED_PROMOTED_REASON = "[synthesized] filesystem-edit-fallback";
1317
+ var KNOWLEDGE_CANONICAL_TYPE_DIRS = [
1318
+ "decisions",
1319
+ "pitfalls",
1320
+ "guidelines",
1321
+ "models",
1322
+ "processes"
1323
+ ];
1324
+ var CANONICAL_KNOWLEDGE_FILENAME_PATTERN = /^(K[PT]-(?:MOD|DEC|GLD|PIT|PRO)-\d{4,})--[a-z0-9][a-z0-9-]*\.md$/u;
1270
1325
  var KNOWLEDGE_SUBDIRS2 = ["decisions", "pitfalls", "guidelines", "models", "processes", "pending"];
1271
1326
  var COUNTER_TYPE_CODES = ["MOD", "DEC", "GLD", "PIT", "PRO"];
1272
1327
  var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
@@ -1285,7 +1340,7 @@ var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
1285
1340
  var TARGET_FILE_PATHS = [
1286
1341
  ".fabric/forensic.json",
1287
1342
  ".fabric/agents.meta.json",
1288
- ".fabric/rule-test.index.json",
1343
+ ".fabric/.cache/knowledge-test.index.json",
1289
1344
  ".fabric/events.jsonl",
1290
1345
  ".fabric/knowledge"
1291
1346
  ];
@@ -1297,12 +1352,12 @@ async function runDoctorReport(target) {
1297
1352
  forensic,
1298
1353
  meta,
1299
1354
  eventLedger,
1300
- ruleTestIndex
1355
+ knowledgeTestIndex
1301
1356
  ] = await Promise.all([
1302
1357
  inspectForensic(projectRoot),
1303
1358
  inspectMeta(projectRoot),
1304
1359
  inspectEventLedger(projectRoot),
1305
- inspectRuleTestIndex(projectRoot)
1360
+ inspectKnowledgeTestIndex(projectRoot)
1306
1361
  ]);
1307
1362
  const mcpConfigInWrongFile = inspectMcpConfigInWrongFile(projectRoot);
1308
1363
  const metaManuallyDiverged = await inspectMetaManuallyDiverged(projectRoot);
@@ -1312,6 +1367,21 @@ async function runDoctorReport(target) {
1312
1367
  const counterDesync = inspectCounterDesync(meta);
1313
1368
  const preexistingRootFiles = inspectPreexistingRootFiles(projectRoot);
1314
1369
  const bootstrapAnchor = inspectBootstrapAnchor(projectRoot);
1370
+ const filesystemEditFallback = eventLedger.exists && eventLedger.writable && eventLedger.parseable ? await inspectFilesystemEditFallback(projectRoot) : { synthesized: 0, synthesizedStableIds: [] };
1371
+ const lintNow = Date.now();
1372
+ const orphanDemote = await inspectOrphanDemote(projectRoot, lintNow);
1373
+ const staleArchive = await inspectStaleArchive(projectRoot, lintNow);
1374
+ const pendingOverdue = inspectPendingOverdue(projectRoot, lintNow);
1375
+ const stableIdDuplicate = inspectStableIdDuplicate(projectRoot);
1376
+ const layerMismatch = inspectLayerMismatch(projectRoot);
1377
+ const indexDrift = inspectIndexDrift(projectRoot, meta);
1378
+ const underseeded = inspectUnderseeded(projectRoot);
1379
+ const narrowNoPaths = inspectNarrowNoPaths(projectRoot);
1380
+ const relevancePathsDangling = inspectRelevancePathsDangling(projectRoot);
1381
+ const relevancePathsDrift = inspectRelevancePathsDrift(projectRoot);
1382
+ const narrowTooFew = inspectNarrowTooFew(projectRoot, lintNow);
1383
+ const sessionHintsStale = inspectSessionHintsStale(projectRoot, lintNow);
1384
+ const relevanceFieldsMissing = inspectRelevanceFieldsMissing(projectRoot);
1315
1385
  const checks = [
1316
1386
  createBootstrapAnchorCheck(bootstrapAnchor),
1317
1387
  createKnowledgeDirMissingCheck(knowledgeDirMissing),
@@ -1327,7 +1397,7 @@ async function runDoctorReport(target) {
1327
1397
  // [MANDATORY_INJECTION] sections out of legacy rule files, a structural
1328
1398
  // concept that has no v2 equivalent. rc.4 will introduce a dedicated v2
1329
1399
  // lint suite for the new knowledge frontmatter contract.
1330
- createRuleTestIndexCheck(ruleTestIndex),
1400
+ createKnowledgeTestIndexCheck(knowledgeTestIndex),
1331
1401
  createEventLedgerCheck(eventLedger),
1332
1402
  createEventLedgerPartialWriteCheck(eventLedger),
1333
1403
  createMcpConfigInWrongFileCheck(mcpConfigInWrongFile),
@@ -1335,6 +1405,42 @@ async function runDoctorReport(target) {
1335
1405
  createKnowledgeDirUnindexedCheck(knowledgeDirUnindexed),
1336
1406
  createStableIdCollisionCheck(stableIdCollision),
1337
1407
  createCounterDesyncCheck(counterDesync),
1408
+ createFilesystemEditFallbackCheck(filesystemEditFallback),
1409
+ // rc.4 TASK-001: read-side lint checks #16-18. Findings only — mutation
1410
+ // + event emission lands in TASK-003 behind --apply-lint.
1411
+ createOrphanDemoteCheck(orphanDemote),
1412
+ createStaleArchiveCheck(staleArchive),
1413
+ createPendingOverdueCheck(pendingOverdue),
1414
+ // rc.4 TASK-002: read-side integrity checks #19-21. Stable_id duplicate
1415
+ // runs first in this trio — it is the most critical integrity break and
1416
+ // surfaces ahead of layer-mismatch / index-drift in the report so a
1417
+ // human operator triages the collision before reasoning about counter
1418
+ // state. Index drift is the only fixable_error of the three; stable_id
1419
+ // duplicate and layer mismatch require manual triage (rename / move).
1420
+ createStableIdDuplicateCheck(stableIdDuplicate),
1421
+ createLayerMismatchCheck(layerMismatch),
1422
+ createIndexDriftCheck(indexDrift),
1423
+ // rc.5 TASK-010: read-side underseeded-corpus check (#22). Info kind —
1424
+ // does not bump report status. Recommends running the fabric-import skill
1425
+ // to backfill knowledge when the corpus is below the threshold floor.
1426
+ createUnderseededCheck(underseeded),
1427
+ // rc.5 TASK-013 (C4): relevance_paths hygiene checks #23/#24/#25.
1428
+ // All three are flag-only in rc.5 (no apply-lint mutations).
1429
+ // #23 narrow_no_paths — warning kind (silent recall risk)
1430
+ // #24 relevance_paths_dangling — warning kind (glob → zero matches)
1431
+ // #25 relevance_paths_drift — info kind (git-log heuristic; noisy)
1432
+ createNarrowNoPathsCheck(narrowNoPaths),
1433
+ createRelevancePathsDanglingCheck(relevancePathsDangling),
1434
+ createRelevancePathsDriftCheck(relevancePathsDrift),
1435
+ // rc.6 TASK-023 (E6): narrow_too_few (lint #26). Info kind; both arms
1436
+ // (structural + telemetry) recommend the same fabric-import action.
1437
+ createNarrowTooFewCheck(narrowTooFew),
1438
+ // rc.6 TASK-021 (E3): session-hints cache hygiene (lint #27). Info kind.
1439
+ createSessionHintsStaleCheck(sessionHintsStale),
1440
+ // v2.0.0-rc.9 TASK-003 (A3): relevance fields back-fill (lint #28).
1441
+ // Info kind — applies to pending entries only; canonical entries get
1442
+ // the fields written verbatim by fab_review.approve/modify.
1443
+ createRelevanceFieldsMissingCheck(relevanceFieldsMissing),
1338
1444
  createPreexistingRootFilesCheck(preexistingRootFiles)
1339
1445
  // v2.0 / rc.2: `createLegacyClientPathCheck` removed. The schema now
1340
1446
  // rejects retired clientPaths keys (windsurf/rooCode/geminiCLI) at Zod
@@ -1401,19 +1507,19 @@ async function runDoctorFix(target) {
1401
1507
  (issue) => [
1402
1508
  "agents_meta_missing",
1403
1509
  "agents_meta_stale",
1404
- "rule_test_index_missing",
1405
- "rule_test_index_stale",
1510
+ "knowledge_test_index_missing",
1511
+ "knowledge_test_index_stale",
1406
1512
  "content_ref_missing",
1407
1513
  "knowledge_dir_unindexed"
1408
1514
  ].includes(issue.code)
1409
1515
  )) {
1410
- await reconcileRules(projectRoot, { trigger: "doctor" });
1516
+ await reconcileKnowledge(projectRoot, { trigger: "doctor" });
1411
1517
  for (const issue of before.fixable_errors.filter(
1412
1518
  (candidate) => [
1413
1519
  "agents_meta_missing",
1414
1520
  "agents_meta_stale",
1415
- "rule_test_index_missing",
1416
- "rule_test_index_stale",
1521
+ "knowledge_test_index_missing",
1522
+ "knowledge_test_index_stale",
1417
1523
  "content_ref_missing",
1418
1524
  "knowledge_dir_unindexed"
1419
1525
  ].includes(candidate.code)
@@ -1449,6 +1555,394 @@ async function runDoctorFix(target) {
1449
1555
  report
1450
1556
  };
1451
1557
  }
1558
+ var MANUAL_LINT_ERROR_CODES = /* @__PURE__ */ new Set([
1559
+ "knowledge_stable_id_duplicate",
1560
+ "knowledge_layer_mismatch"
1561
+ ]);
1562
+ async function runDoctorApplyLint(target) {
1563
+ const projectRoot = normalizeTarget(target);
1564
+ const before = await runDoctorReport(projectRoot);
1565
+ const mutations = [];
1566
+ const blockingManual = before.manual_errors.find(
1567
+ (issue) => MANUAL_LINT_ERROR_CODES.has(issue.code)
1568
+ );
1569
+ if (blockingManual !== void 0) {
1570
+ return {
1571
+ changed: false,
1572
+ mutations: [],
1573
+ manual_errors: before.manual_errors,
1574
+ aborted: true,
1575
+ abort_reason: `Manual repair required for ${blockingManual.code}: ${blockingManual.message} - apply-lint cannot resolve this safely; triage by hand.`,
1576
+ message: `apply-lint aborted: ${blockingManual.code} requires manual repair.`,
1577
+ report: before
1578
+ };
1579
+ }
1580
+ const now = Date.now();
1581
+ const orphanDemote = await inspectOrphanDemote(projectRoot, now);
1582
+ for (const candidate of orphanDemote.candidates) {
1583
+ if (candidate.next_maturity === null) {
1584
+ continue;
1585
+ }
1586
+ mutations.push(await applyOrphanDemote(projectRoot, candidate, now));
1587
+ }
1588
+ const staleArchive = await inspectStaleArchive(projectRoot, now);
1589
+ for (const candidate of staleArchive.candidates) {
1590
+ mutations.push(await applyStaleArchive(projectRoot, candidate, now));
1591
+ }
1592
+ const pendingAutoArchive = inspectPendingAutoArchive(projectRoot, now);
1593
+ for (const candidate of pendingAutoArchive.candidates) {
1594
+ mutations.push(await applyPendingAutoArchive(projectRoot, candidate, now));
1595
+ }
1596
+ const sessionHintsStale = inspectSessionHintsStale(projectRoot, now);
1597
+ for (const candidate of sessionHintsStale.candidates) {
1598
+ mutations.push(await applySessionHintsStaleCleanup(projectRoot, candidate));
1599
+ }
1600
+ const relevanceFieldsMissing = inspectRelevanceFieldsMissing(projectRoot);
1601
+ let relevanceTouchedCount = 0;
1602
+ for (const candidate of relevanceFieldsMissing.candidates) {
1603
+ const mutation = await applyRelevanceFieldsMissing(candidate);
1604
+ mutations.push(mutation);
1605
+ if (mutation.applied) {
1606
+ relevanceTouchedCount += 1;
1607
+ }
1608
+ }
1609
+ try {
1610
+ await appendEventLedgerEvent(projectRoot, {
1611
+ event_type: "relevance_migration_run",
1612
+ timestamp: new Date(now).toISOString(),
1613
+ scanned_count: relevanceFieldsMissing.scanned_count,
1614
+ touched_count: relevanceTouchedCount
1615
+ });
1616
+ } catch {
1617
+ }
1618
+ const meta = await inspectMeta(projectRoot);
1619
+ const indexDrift = inspectIndexDrift(projectRoot, meta);
1620
+ if (indexDrift.drifts.length > 0) {
1621
+ mutations.push(await applyIndexDriftFix(projectRoot, indexDrift));
1622
+ }
1623
+ contextCache.invalidate("meta_write", projectRoot);
1624
+ const after = await runDoctorReport(projectRoot);
1625
+ const successCount = mutations.filter((m) => m.applied).length;
1626
+ const failureCount = mutations.length - successCount;
1627
+ return {
1628
+ changed: successCount > 0,
1629
+ mutations,
1630
+ manual_errors: after.manual_errors,
1631
+ aborted: false,
1632
+ message: createApplyLintMessage(successCount, failureCount, after.manual_errors.length),
1633
+ report: after
1634
+ };
1635
+ }
1636
+ function createApplyLintMessage(succeeded, failed, manualErrorCount) {
1637
+ const parts = [];
1638
+ if (succeeded === 0 && failed === 0) {
1639
+ parts.push("No apply-lint mutations were needed.");
1640
+ } else {
1641
+ parts.push(`Applied ${succeeded} apply-lint mutation${succeeded === 1 ? "" : "s"}.`);
1642
+ if (failed > 0) {
1643
+ parts.push(`${failed} mutation${failed === 1 ? "" : "s"} failed.`);
1644
+ }
1645
+ }
1646
+ parts.push(
1647
+ manualErrorCount === 0 ? "No manual errors remain." : `${manualErrorCount} manual error${manualErrorCount === 1 ? "" : "s"} remain.`
1648
+ );
1649
+ return parts.join(" ");
1650
+ }
1651
+ function rewriteFrontmatterMaturity(source, newMaturity) {
1652
+ const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
1653
+ const fm = FM_PATTERN.exec(source);
1654
+ if (fm === null) {
1655
+ return null;
1656
+ }
1657
+ const block = fm[1];
1658
+ if (!MATURITY_LINE_PATTERN.test(block)) {
1659
+ return null;
1660
+ }
1661
+ const replacedBlock = block.replace(
1662
+ MATURITY_LINE_PATTERN,
1663
+ (line) => line.replace(/(stable|endorsed|draft)/u, newMaturity)
1664
+ );
1665
+ const blockStart = source.indexOf(block);
1666
+ if (blockStart < 0) {
1667
+ return null;
1668
+ }
1669
+ return source.slice(0, blockStart) + replacedBlock + source.slice(blockStart + block.length);
1670
+ }
1671
+ async function applyOrphanDemote(projectRoot, candidate, now) {
1672
+ const next = candidate.next_maturity;
1673
+ if (next === null) {
1674
+ return {
1675
+ kind: "knowledge_orphan_demote_required",
1676
+ path: candidate.path,
1677
+ detail: `${candidate.maturity} -> (none, already at terminal tier)`,
1678
+ applied: false,
1679
+ error: "next_maturity is null; orphan-demote not applicable"
1680
+ };
1681
+ }
1682
+ const detail = `${candidate.maturity} -> ${next}`;
1683
+ const absPath = join5(projectRoot, candidate.path);
1684
+ try {
1685
+ const source = await readFile5(absPath, "utf8");
1686
+ const rewritten = rewriteFrontmatterMaturity(source, next);
1687
+ if (rewritten === null) {
1688
+ return {
1689
+ kind: "knowledge_orphan_demote_required",
1690
+ path: candidate.path,
1691
+ detail,
1692
+ applied: false,
1693
+ error: "frontmatter missing maturity field; cannot rewrite"
1694
+ };
1695
+ }
1696
+ if (rewritten === source) {
1697
+ return {
1698
+ kind: "knowledge_orphan_demote_required",
1699
+ path: candidate.path,
1700
+ detail,
1701
+ applied: false,
1702
+ error: "rewrite produced byte-identical output"
1703
+ };
1704
+ }
1705
+ await atomicWriteText3(absPath, rewritten);
1706
+ try {
1707
+ await appendEventLedgerEvent(projectRoot, {
1708
+ event_type: "knowledge_demoted",
1709
+ stable_id: candidate.stable_id,
1710
+ timestamp: new Date(now).toISOString(),
1711
+ reason: `lint:orphan_demote ${candidate.maturity}->${next} after ${candidate.age_days}d inactive`
1712
+ });
1713
+ } catch (ledgerError) {
1714
+ try {
1715
+ await atomicWriteText3(absPath, source);
1716
+ } catch (rollbackError) {
1717
+ return {
1718
+ kind: "knowledge_orphan_demote_required",
1719
+ path: candidate.path,
1720
+ detail,
1721
+ applied: false,
1722
+ error: `ledger append failed (${truncateErrorMessage(ledgerError)}); rollback also failed (${truncateErrorMessage(rollbackError)}); disk may be in inconsistent state`
1723
+ };
1724
+ }
1725
+ return {
1726
+ kind: "knowledge_orphan_demote_required",
1727
+ path: candidate.path,
1728
+ detail,
1729
+ applied: false,
1730
+ error: `ledger append failed (${truncateErrorMessage(ledgerError)}); frontmatter rolled back`
1731
+ };
1732
+ }
1733
+ return {
1734
+ kind: "knowledge_orphan_demote_required",
1735
+ path: candidate.path,
1736
+ detail,
1737
+ applied: true
1738
+ };
1739
+ } catch (error) {
1740
+ return {
1741
+ kind: "knowledge_orphan_demote_required",
1742
+ path: candidate.path,
1743
+ detail,
1744
+ applied: false,
1745
+ error: truncateErrorMessage(error)
1746
+ };
1747
+ }
1748
+ }
1749
+ async function applyStaleArchive(projectRoot, candidate, now) {
1750
+ const sourceAbs = join5(projectRoot, candidate.path);
1751
+ const destAbs = join5(projectRoot, candidate.archive_path);
1752
+ const detail = `${candidate.path} -> ${candidate.archive_path}`;
1753
+ try {
1754
+ await mkdir3(join5(destAbs, ".."), { recursive: true });
1755
+ try {
1756
+ await rename(sourceAbs, destAbs);
1757
+ } catch (renameError) {
1758
+ if (renameError instanceof Error && "code" in renameError && renameError.code === "EXDEV") {
1759
+ const data = await readFile5(sourceAbs);
1760
+ await writeFile2(destAbs, data);
1761
+ const { unlink } = await import("fs/promises");
1762
+ await unlink(sourceAbs);
1763
+ } else {
1764
+ throw renameError;
1765
+ }
1766
+ }
1767
+ try {
1768
+ await appendEventLedgerEvent(projectRoot, {
1769
+ event_type: "knowledge_archived",
1770
+ stable_id: candidate.stable_id,
1771
+ timestamp: new Date(now).toISOString(),
1772
+ reason: `lint:stale_archive ${candidate.path} -> ${candidate.archive_path} after ${candidate.age_days}d inactive`
1773
+ });
1774
+ } catch (ledgerError) {
1775
+ try {
1776
+ await rename(destAbs, sourceAbs);
1777
+ } catch (rollbackError) {
1778
+ return {
1779
+ kind: "knowledge_stale_archive_required",
1780
+ path: candidate.path,
1781
+ detail,
1782
+ applied: false,
1783
+ error: `ledger append failed (${truncateErrorMessage(ledgerError)}); rollback also failed (${truncateErrorMessage(rollbackError)}); file may be stranded at ${candidate.archive_path}`
1784
+ };
1785
+ }
1786
+ return {
1787
+ kind: "knowledge_stale_archive_required",
1788
+ path: candidate.path,
1789
+ detail,
1790
+ applied: false,
1791
+ error: `ledger append failed (${truncateErrorMessage(ledgerError)}); archive rolled back`
1792
+ };
1793
+ }
1794
+ return {
1795
+ kind: "knowledge_stale_archive_required",
1796
+ path: candidate.path,
1797
+ detail,
1798
+ applied: true
1799
+ };
1800
+ } catch (error) {
1801
+ return {
1802
+ kind: "knowledge_stale_archive_required",
1803
+ path: candidate.path,
1804
+ detail,
1805
+ applied: false,
1806
+ error: truncateErrorMessage(error)
1807
+ };
1808
+ }
1809
+ }
1810
+ async function applyPendingAutoArchive(projectRoot, candidate, now) {
1811
+ const detail = `${candidate.pending_path} -> ${candidate.archived_to}`;
1812
+ try {
1813
+ await mkdir3(join5(candidate.archived_to_abs, ".."), { recursive: true });
1814
+ let moved = false;
1815
+ if (candidate.layer === "team") {
1816
+ try {
1817
+ const relSource = relativePosix(projectRoot, candidate.pending_path_abs);
1818
+ const relDest = relativePosix(projectRoot, candidate.archived_to_abs);
1819
+ execFileSync("git", ["mv", "-f", relSource, relDest], {
1820
+ cwd: projectRoot,
1821
+ stdio: ["ignore", "pipe", "pipe"]
1822
+ });
1823
+ moved = true;
1824
+ } catch {
1825
+ }
1826
+ }
1827
+ if (!moved) {
1828
+ try {
1829
+ await rename(candidate.pending_path_abs, candidate.archived_to_abs);
1830
+ } catch (renameError) {
1831
+ if (renameError instanceof Error && "code" in renameError && renameError.code === "EXDEV") {
1832
+ const data = await readFile5(candidate.pending_path_abs);
1833
+ await writeFile2(candidate.archived_to_abs, data);
1834
+ const { unlink } = await import("fs/promises");
1835
+ await unlink(candidate.pending_path_abs);
1836
+ } else {
1837
+ throw renameError;
1838
+ }
1839
+ }
1840
+ }
1841
+ try {
1842
+ await appendEventLedgerEvent(projectRoot, {
1843
+ event_type: "pending_auto_archived",
1844
+ pending_path: candidate.pending_path,
1845
+ archived_to: candidate.archived_to,
1846
+ reason: "auto_archive_30d"
1847
+ });
1848
+ } catch (ledgerError) {
1849
+ try {
1850
+ await rename(candidate.archived_to_abs, candidate.pending_path_abs);
1851
+ } catch (rollbackError) {
1852
+ return {
1853
+ kind: "knowledge_pending_auto_archive",
1854
+ path: candidate.pending_path,
1855
+ detail,
1856
+ applied: false,
1857
+ error: `ledger append failed (${truncateErrorMessage(ledgerError)}); rollback also failed (${truncateErrorMessage(rollbackError)}); file may be stranded at ${candidate.archived_to}`
1858
+ };
1859
+ }
1860
+ return {
1861
+ kind: "knowledge_pending_auto_archive",
1862
+ path: candidate.pending_path,
1863
+ detail,
1864
+ applied: false,
1865
+ error: `ledger append failed (${truncateErrorMessage(ledgerError)}); archive rolled back`
1866
+ };
1867
+ }
1868
+ return {
1869
+ kind: "knowledge_pending_auto_archive",
1870
+ path: candidate.pending_path,
1871
+ detail,
1872
+ applied: true
1873
+ };
1874
+ } catch (error) {
1875
+ return {
1876
+ kind: "knowledge_pending_auto_archive",
1877
+ path: candidate.pending_path,
1878
+ detail,
1879
+ applied: false,
1880
+ error: truncateErrorMessage(error)
1881
+ };
1882
+ }
1883
+ }
1884
+ function relativePosix(projectRoot, absolutePath) {
1885
+ const rel = nodeRelative(projectRoot, absolutePath);
1886
+ return rel.split(sep4).join("/");
1887
+ }
1888
+ async function applySessionHintsStaleCleanup(projectRoot, candidate) {
1889
+ const detail = `deleted (${candidate.age_days}d old)`;
1890
+ const absPath = join5(projectRoot, candidate.path);
1891
+ try {
1892
+ const { unlink } = await import("fs/promises");
1893
+ await unlink(absPath);
1894
+ return {
1895
+ kind: "knowledge_session_hints_stale_cleanup",
1896
+ path: candidate.path,
1897
+ detail,
1898
+ applied: true
1899
+ };
1900
+ } catch (error) {
1901
+ return {
1902
+ kind: "knowledge_session_hints_stale_cleanup",
1903
+ path: candidate.path,
1904
+ detail,
1905
+ applied: false,
1906
+ error: truncateErrorMessage(error)
1907
+ };
1908
+ }
1909
+ }
1910
+ async function applyIndexDriftFix(projectRoot, inspection) {
1911
+ const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
1912
+ const detailParts = [];
1913
+ try {
1914
+ const meta = agentsMetaSchema4.parse(JSON.parse(await readFile5(metaPath, "utf8")));
1915
+ const baseCounters = AgentsMetaCountersSchema.parse(meta.counters ?? void 0);
1916
+ const updatedCounters = {
1917
+ KP: { ...baseCounters.KP },
1918
+ KT: { ...baseCounters.KT }
1919
+ };
1920
+ for (const drift of inspection.drifts) {
1921
+ updatedCounters[drift.layer][drift.type] = drift.proposed_after;
1922
+ detailParts.push(`${drift.layer}.${drift.type}: ${drift.counter} -> ${drift.proposed_after}`);
1923
+ }
1924
+ const updated = { ...meta, counters: updatedCounters };
1925
+ await atomicWriteJson2(metaPath, updated, { indent: 2 });
1926
+ return {
1927
+ kind: "knowledge_index_drift",
1928
+ path: "agents.meta.json#counters",
1929
+ detail: detailParts.join("; "),
1930
+ applied: true
1931
+ };
1932
+ } catch (error) {
1933
+ return {
1934
+ kind: "knowledge_index_drift",
1935
+ path: "agents.meta.json#counters",
1936
+ detail: detailParts.join("; ") || "(no counters processed)",
1937
+ applied: false,
1938
+ error: truncateErrorMessage(error)
1939
+ };
1940
+ }
1941
+ }
1942
+ function truncateErrorMessage(error) {
1943
+ const raw = error instanceof Error ? error.message : String(error);
1944
+ return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
1945
+ }
1452
1946
  async function inspectForensic(projectRoot) {
1453
1947
  const path = join5(projectRoot, ".fabric", "forensic.json");
1454
1948
  try {
@@ -1537,7 +2031,7 @@ async function inspectMeta(projectRoot) {
1537
2031
  }
1538
2032
  async function tryBuildRuleMeta(projectRoot) {
1539
2033
  try {
1540
- return await buildRuleMeta(projectRoot);
2034
+ return await buildKnowledgeMeta(projectRoot);
1541
2035
  } catch {
1542
2036
  return null;
1543
2037
  }
@@ -1597,15 +2091,15 @@ async function inspectEventLedger(projectRoot) {
1597
2091
  };
1598
2092
  }
1599
2093
  }
1600
- async function inspectRuleTestIndex(projectRoot) {
1601
- const path = join5(projectRoot, ".fabric", "rule-test.index.json");
2094
+ async function inspectKnowledgeTestIndex(projectRoot) {
2095
+ const path = join5(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
1602
2096
  const built = await tryBuildRuleMeta(projectRoot);
1603
2097
  try {
1604
- const index = ruleTestIndexSchema2.parse(JSON.parse(await readFile5(path, "utf8")));
2098
+ const index = knowledgeTestIndexSchema2.parse(JSON.parse(await readFile5(path, "utf8")));
1605
2099
  return {
1606
2100
  present: true,
1607
2101
  valid: true,
1608
- stale: built === null ? false : !isSameRuleTestIndex(index, built.ruleTestIndex),
2102
+ stale: built === null ? false : !isSameKnowledgeTestIndex(index, built.knowledgeTestIndex),
1609
2103
  linkCount: index.links.length,
1610
2104
  orphanCount: index.orphan_annotations.length
1611
2105
  };
@@ -1616,7 +2110,7 @@ async function inspectRuleTestIndex(projectRoot) {
1616
2110
  stale: true,
1617
2111
  linkCount: 0,
1618
2112
  orphanCount: 0,
1619
- error: isMissingFileError(error) ? ".fabric/rule-test.index.json is missing." : error instanceof Error ? error.message : String(error)
2113
+ error: isMissingFileError(error) ? ".fabric/.cache/knowledge-test.index.json is missing." : error instanceof Error ? error.message : String(error)
1620
2114
  };
1621
2115
  }
1622
2116
  }
@@ -1732,17 +2226,17 @@ function createRuleContentRefCheck(meta) {
1732
2226
  }
1733
2227
  return okCheck("Rule content refs", "All content_ref entries resolve to .fabric/knowledge files.");
1734
2228
  }
1735
- function createRuleTestIndexCheck(index) {
2229
+ function createKnowledgeTestIndexCheck(index) {
1736
2230
  if (!index.present) {
1737
- return issueCheck("Rule-test index", "error", "fixable_error", "rule_test_index_missing", index.error, "Run `fab doctor --fix` to rebuild .fabric/rule-test.index.json.");
2231
+ return issueCheck("Knowledge-test index", "error", "fixable_error", "knowledge_test_index_missing", index.error, "Run `fab doctor --fix` to rebuild .fabric/.cache/knowledge-test.index.json.");
1738
2232
  }
1739
2233
  if (!index.valid) {
1740
- return issueCheck("Rule-test index", "error", "manual_error", "rule_test_index_invalid", index.error, "Delete .fabric/rule-test.index.json and run `fab doctor --fix` to regenerate it.");
2234
+ return issueCheck("Knowledge-test index", "error", "manual_error", "knowledge_test_index_invalid", index.error, "Delete .fabric/.cache/knowledge-test.index.json and run `fab doctor --fix` to regenerate it.");
1741
2235
  }
1742
2236
  if (index.stale) {
1743
- return issueCheck("Rule-test index", "error", "fixable_error", "rule_test_index_stale", ".fabric/rule-test.index.json is stale.", "Run `fab doctor --fix` to rebuild the rule-test index.");
2237
+ return issueCheck("Knowledge-test index", "error", "fixable_error", "knowledge_test_index_stale", ".fabric/.cache/knowledge-test.index.json is stale.", "Run `fab doctor --fix` to rebuild the knowledge-test index.");
1744
2238
  }
1745
- return okCheck("Rule-test index", `${index.linkCount} link${index.linkCount === 1 ? "" : "s"} and ${index.orphanCount} orphan annotation${index.orphanCount === 1 ? "" : "s"} indexed.`);
2239
+ return okCheck("Knowledge-test index", `${index.linkCount} link${index.linkCount === 1 ? "" : "s"} and ${index.orphanCount} orphan annotation${index.orphanCount === 1 ? "" : "s"} indexed.`);
1746
2240
  }
1747
2241
  function createEventLedgerCheck(ledger) {
1748
2242
  if (!ledger.exists) {
@@ -2071,6 +2565,83 @@ function inspectPreexistingRootFiles(projectRoot) {
2071
2565
  const detected = candidates.filter((name) => existsSync4(join5(projectRoot, name)));
2072
2566
  return { detected };
2073
2567
  }
2568
+ async function inspectFilesystemEditFallback(projectRoot) {
2569
+ const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
2570
+ if (!existsSync4(knowledgeRoot)) {
2571
+ return { synthesized: 0, synthesizedStableIds: [] };
2572
+ }
2573
+ const canonicalIds = /* @__PURE__ */ new Set();
2574
+ for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
2575
+ const dir = join5(knowledgeRoot, typeDir);
2576
+ if (!existsSync4(dir)) {
2577
+ continue;
2578
+ }
2579
+ let entries;
2580
+ try {
2581
+ entries = readdirSync(dir, { withFileTypes: true });
2582
+ } catch {
2583
+ continue;
2584
+ }
2585
+ for (const entry of entries) {
2586
+ if (!entry.isFile()) {
2587
+ continue;
2588
+ }
2589
+ const match = CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(entry.name);
2590
+ if (match === null) {
2591
+ continue;
2592
+ }
2593
+ canonicalIds.add(match[1]);
2594
+ }
2595
+ }
2596
+ if (canonicalIds.size === 0) {
2597
+ return { synthesized: 0, synthesizedStableIds: [] };
2598
+ }
2599
+ let promotedIds = /* @__PURE__ */ new Set();
2600
+ try {
2601
+ const { events } = await readEventLedger(projectRoot, { event_type: "knowledge_promoted" });
2602
+ promotedIds = new Set(
2603
+ events.map((event) => event.event_type === "knowledge_promoted" ? event.stable_id : void 0).filter((id) => typeof id === "string")
2604
+ );
2605
+ } catch {
2606
+ promotedIds = /* @__PURE__ */ new Set();
2607
+ }
2608
+ const orphanIds = [];
2609
+ for (const id of canonicalIds) {
2610
+ if (!promotedIds.has(id)) {
2611
+ orphanIds.push(id);
2612
+ }
2613
+ }
2614
+ orphanIds.sort();
2615
+ for (const stable_id of orphanIds) {
2616
+ await appendEventLedgerEvent(projectRoot, {
2617
+ event_type: "knowledge_promoted",
2618
+ stable_id,
2619
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2620
+ reason: SYNTHESIZED_PROMOTED_REASON,
2621
+ correlation_id: "doctor-synthesized",
2622
+ session_id: "doctor-synthesized"
2623
+ });
2624
+ }
2625
+ return { synthesized: orphanIds.length, synthesizedStableIds: orphanIds };
2626
+ }
2627
+ function createFilesystemEditFallbackCheck(inspection) {
2628
+ if (inspection.synthesized === 0) {
2629
+ return okCheck(
2630
+ "Filesystem-edit fallback",
2631
+ "No orphan canonical knowledge entries detected; events.jsonl promotion trail is complete."
2632
+ );
2633
+ }
2634
+ const sample = inspection.synthesizedStableIds.slice(0, 3).join(", ");
2635
+ return {
2636
+ name: "Filesystem-edit fallback",
2637
+ status: "ok",
2638
+ kind: "info",
2639
+ code: "knowledge_promoted_synthesized",
2640
+ fixable: false,
2641
+ message: `Synthesized ${inspection.synthesized} knowledge_promoted event${inspection.synthesized === 1 ? "" : "s"} for orphan canonical entries (${sample}${inspection.synthesizedStableIds.length > 3 ? ", ..." : ""}). Reason='${SYNTHESIZED_PROMOTED_REASON}'.`,
2642
+ actionHint: "These entries were moved into .fabric/knowledge/<type>/ outside fab_review.approve. The synthesized events restore audit-trail completeness."
2643
+ };
2644
+ }
2074
2645
  function createPreexistingRootFilesCheck(inspection) {
2075
2646
  if (inspection.detected.length === 0) {
2076
2647
  return okCheck("Preexisting root markdown", "No CLAUDE.md or AGENTS.md detected at project root.");
@@ -2085,82 +2656,1288 @@ function createPreexistingRootFilesCheck(inspection) {
2085
2656
  actionHint: "Move knowledge content to `.fabric/knowledge/{type}/` if you want it available in MCP responses."
2086
2657
  };
2087
2658
  }
2088
- async function fixMcpConfigInWrongFile(projectRoot) {
2089
- const settingsPath = join5(projectRoot, ".claude", "settings.json");
2090
- if (!existsSync4(settingsPath)) {
2091
- return;
2092
- }
2093
- let settings;
2659
+ async function buildLastConsumedIndex(projectRoot) {
2660
+ const map = /* @__PURE__ */ new Map();
2661
+ let events;
2094
2662
  try {
2095
- const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
2096
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2097
- return;
2098
- }
2099
- settings = parsed;
2663
+ ({ events } = await readEventLedger(projectRoot));
2100
2664
  } catch {
2101
- return;
2102
- }
2103
- const mcpServers = settings.mcpServers;
2104
- if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
2105
- return;
2106
- }
2107
- const { fabric: _removed, ...remainingServers } = mcpServers;
2108
- const cleaned = { ...settings };
2109
- if (Object.keys(remainingServers).length === 0) {
2110
- delete cleaned.mcpServers;
2111
- } else {
2112
- cleaned.mcpServers = remainingServers;
2665
+ return map;
2113
2666
  }
2114
- await atomicWriteJson2(settingsPath, cleaned, { indent: 2 });
2115
- await appendEventLedgerEvent(projectRoot, {
2116
- event_type: "mcp_config_migrated",
2117
- source: "doctor_fix",
2118
- removed_from: ".claude/settings.json"
2119
- });
2120
- }
2121
- async function ensureKnowledgeSubdirs(projectRoot) {
2122
- for (const sub of KNOWLEDGE_SUBDIRS2) {
2123
- await mkdir3(join5(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
2667
+ for (const event of events) {
2668
+ if (event.event_type !== "knowledge_consumed" && event.event_type !== "knowledge_demoted" && event.event_type !== "knowledge_archived") {
2669
+ continue;
2670
+ }
2671
+ const ts = event.ts;
2672
+ if (typeof ts !== "number" || !Number.isFinite(ts)) {
2673
+ continue;
2674
+ }
2675
+ const stableId = event.stable_id;
2676
+ if (typeof stableId !== "string" || stableId.length === 0) {
2677
+ continue;
2678
+ }
2679
+ const prev = map.get(stableId);
2680
+ if (prev === void 0 || ts > prev) {
2681
+ map.set(stableId, ts);
2682
+ }
2124
2683
  }
2684
+ return map;
2125
2685
  }
2126
- async function fixCounterDesync(projectRoot) {
2127
- const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
2128
- if (!existsSync4(metaPath)) {
2129
- return;
2130
- }
2131
- let meta;
2686
+ async function buildLastActiveIndex(projectRoot) {
2687
+ const map = /* @__PURE__ */ new Map();
2688
+ let events;
2132
2689
  try {
2133
- meta = agentsMetaSchema4.parse(JSON.parse(await readFile5(metaPath, "utf8")));
2690
+ ({ events } = await readEventLedger(projectRoot));
2134
2691
  } catch {
2135
- return;
2692
+ return map;
2136
2693
  }
2137
- const synthetic = {
2138
- present: true,
2139
- valid: true,
2140
- meta,
2141
- revision: meta.revision,
2142
- computedRevision: null,
2143
- ruleCount: 0,
2144
- missingContentRefs: [],
2145
- invalidContentRefs: [],
2146
- stale: false,
2147
- changed: false
2148
- };
2149
- const desync = inspectCounterDesync(synthetic);
2150
- if (desync.desyncs.length === 0 || desync.correctedCounters === null) {
2151
- return;
2694
+ for (const event of events) {
2695
+ const ts = event.ts;
2696
+ if (typeof ts !== "number" || !Number.isFinite(ts)) {
2697
+ continue;
2698
+ }
2699
+ const ids = [];
2700
+ switch (event.event_type) {
2701
+ case "knowledge_proposed":
2702
+ case "knowledge_promote_started":
2703
+ case "knowledge_promoted":
2704
+ case "knowledge_promote_failed":
2705
+ case "knowledge_layer_changed":
2706
+ case "knowledge_slug_renamed":
2707
+ case "knowledge_demoted":
2708
+ case "knowledge_archived":
2709
+ case "knowledge_archive_attempted":
2710
+ case "knowledge_deferred":
2711
+ case "knowledge_rejected": {
2712
+ if (typeof event.stable_id === "string" && event.stable_id.length > 0) {
2713
+ ids.push(event.stable_id);
2714
+ }
2715
+ break;
2716
+ }
2717
+ case "knowledge_context_planned": {
2718
+ ids.push(...event.required_stable_ids, ...event.ai_selectable_stable_ids, ...event.final_stable_ids);
2719
+ break;
2720
+ }
2721
+ case "knowledge_selection": {
2722
+ ids.push(
2723
+ ...event.required_stable_ids,
2724
+ ...event.ai_selectable_stable_ids,
2725
+ ...event.ai_selected_stable_ids,
2726
+ ...event.final_stable_ids
2727
+ );
2728
+ break;
2729
+ }
2730
+ case "knowledge_sections_fetched": {
2731
+ ids.push(...event.final_stable_ids, ...event.ai_selected_stable_ids);
2732
+ break;
2733
+ }
2734
+ default:
2735
+ break;
2736
+ }
2737
+ for (const id of ids) {
2738
+ const prev = map.get(id);
2739
+ if (prev === void 0 || ts > prev) {
2740
+ map.set(id, ts);
2741
+ }
2742
+ }
2152
2743
  }
2153
- const updated = { ...meta, counters: desync.correctedCounters };
2154
- await atomicWriteJson2(metaPath, updated, { indent: 2 });
2744
+ return map;
2155
2745
  }
2156
- async function ensureEventLedger(projectRoot) {
2157
- const path = getEventLedgerPath(projectRoot);
2158
- await ensureParentDirectory(path);
2159
- await writeFile2(path, "", { encoding: "utf8", flag: "a" });
2746
+ function maturityThresholdDays(maturity) {
2747
+ return ORPHAN_DEMOTE_THRESHOLD_DAYS[maturity];
2160
2748
  }
2161
- function createFixMessage(fixed, report) {
2162
- const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
2163
- const manualText = report.manual_errors.length === 0 ? "No manual errors remain." : `${report.manual_errors.length} manual error${report.manual_errors.length === 1 ? "" : "s"} remain.`;
2749
+ function nextLowerMaturity(current) {
2750
+ if (current === "stable") return "endorsed";
2751
+ if (current === "endorsed") return "draft";
2752
+ return null;
2753
+ }
2754
+ function extractKnowledgeFrontmatterMaturity(source) {
2755
+ const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
2756
+ const fm = FM_PATTERN.exec(source);
2757
+ if (fm === null) {
2758
+ return null;
2759
+ }
2760
+ const match = MATURITY_LINE_PATTERN.exec(fm[1]);
2761
+ return match === null ? null : match[2];
2762
+ }
2763
+ function extractKnowledgeFrontmatterCreatedAt(source) {
2764
+ const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
2765
+ const fm = FM_PATTERN.exec(source);
2766
+ if (fm === null) {
2767
+ return null;
2768
+ }
2769
+ const match = CREATED_AT_LINE_PATTERN.exec(fm[1]);
2770
+ if (match === null) {
2771
+ return null;
2772
+ }
2773
+ const parsed = Date.parse(match[2]);
2774
+ return Number.isFinite(parsed) ? parsed : null;
2775
+ }
2776
+ function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
2777
+ const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
2778
+ if (!existsSync4(knowledgeRoot)) {
2779
+ return;
2780
+ }
2781
+ for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
2782
+ const dir = join5(knowledgeRoot, typeDir);
2783
+ if (!existsSync4(dir)) {
2784
+ continue;
2785
+ }
2786
+ let entries;
2787
+ try {
2788
+ entries = readdirSync(dir, { withFileTypes: true });
2789
+ } catch {
2790
+ continue;
2791
+ }
2792
+ for (const entry of entries) {
2793
+ if (!entry.isFile()) {
2794
+ continue;
2795
+ }
2796
+ const match = CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(entry.name);
2797
+ if (match === null) {
2798
+ continue;
2799
+ }
2800
+ const stableId = match[1];
2801
+ const absPath = join5(dir, entry.name);
2802
+ let source;
2803
+ try {
2804
+ source = readFileSync(absPath, "utf8");
2805
+ } catch {
2806
+ continue;
2807
+ }
2808
+ const maturity = extractKnowledgeFrontmatterMaturity(source);
2809
+ if (maturity === null) {
2810
+ continue;
2811
+ }
2812
+ const createdAt = extractKnowledgeFrontmatterCreatedAt(source);
2813
+ const eventTs = lastActiveIndex.get(stableId) ?? 0;
2814
+ let lastReferenceMs = Math.max(createdAt ?? 0, eventTs);
2815
+ if (lastReferenceMs === 0) {
2816
+ try {
2817
+ lastReferenceMs = statSync3(absPath).mtimeMs;
2818
+ } catch {
2819
+ lastReferenceMs = 0;
2820
+ }
2821
+ }
2822
+ const relPath = posix.join(
2823
+ ".fabric/knowledge",
2824
+ typeDir,
2825
+ entry.name
2826
+ );
2827
+ yield { stable_id: stableId, maturity, type: typeDir, absPath, relPath, lastReferenceMs };
2828
+ }
2829
+ }
2830
+ }
2831
+ async function inspectOrphanDemote(projectRoot, now) {
2832
+ const lastConsumedIndex = await buildLastConsumedIndex(projectRoot);
2833
+ const candidates = [];
2834
+ for (const entry of iterateCanonicalEntries(projectRoot, lastConsumedIndex)) {
2835
+ const ageMs = entry.lastReferenceMs > 0 ? now - entry.lastReferenceMs : now;
2836
+ const ageDays = Math.floor(ageMs / MS_PER_DAY);
2837
+ const threshold = maturityThresholdDays(entry.maturity);
2838
+ if (ageDays <= threshold) {
2839
+ continue;
2840
+ }
2841
+ candidates.push({
2842
+ stable_id: entry.stable_id,
2843
+ path: entry.relPath,
2844
+ age_days: ageDays,
2845
+ maturity: entry.maturity,
2846
+ next_maturity: nextLowerMaturity(entry.maturity)
2847
+ });
2848
+ }
2849
+ candidates.sort((a, b) => a.path.localeCompare(b.path));
2850
+ return { candidates };
2851
+ }
2852
+ async function inspectStaleArchive(projectRoot, now) {
2853
+ const lastActiveIndex = await buildLastActiveIndex(projectRoot);
2854
+ const candidates = [];
2855
+ for (const entry of iterateCanonicalEntries(projectRoot, lastActiveIndex)) {
2856
+ if (entry.maturity !== "draft") {
2857
+ continue;
2858
+ }
2859
+ const ageMs = entry.lastReferenceMs > 0 ? now - entry.lastReferenceMs : now;
2860
+ const ageDays = Math.floor(ageMs / MS_PER_DAY);
2861
+ const requiredQuiet = ORPHAN_DEMOTE_THRESHOLD_DAYS.draft + STALE_ARCHIVE_ADDITIONAL_DAYS;
2862
+ if (ageDays <= requiredQuiet) {
2863
+ continue;
2864
+ }
2865
+ const filename = posix.basename(entry.relPath);
2866
+ candidates.push({
2867
+ stable_id: entry.stable_id,
2868
+ path: entry.relPath,
2869
+ age_days: ageDays,
2870
+ archive_path: posix.join(".fabric/.archive", entry.type, filename)
2871
+ });
2872
+ }
2873
+ candidates.sort((a, b) => a.path.localeCompare(b.path));
2874
+ return { candidates };
2875
+ }
2876
+ function* iteratePendingFiles(projectRoot, now) {
2877
+ const teamRoot = join5(projectRoot, ".fabric", "knowledge", "pending");
2878
+ const personalRoot = join5(resolvePersonalRootForPending(), ".fabric", "knowledge", "pending");
2879
+ for (const [layer, root, displayPrefix] of [
2880
+ ["team", teamRoot, ".fabric/knowledge/pending"],
2881
+ ["personal", personalRoot, "~/.fabric/knowledge/pending"]
2882
+ ]) {
2883
+ if (!existsSync4(root)) {
2884
+ continue;
2885
+ }
2886
+ let typeDirs = [];
2887
+ try {
2888
+ typeDirs = readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
2889
+ } catch {
2890
+ continue;
2891
+ }
2892
+ for (const typeDir of typeDirs) {
2893
+ const dir = join5(root, typeDir);
2894
+ let entries;
2895
+ try {
2896
+ entries = readdirSync(dir, { withFileTypes: true });
2897
+ } catch {
2898
+ continue;
2899
+ }
2900
+ for (const entry of entries) {
2901
+ if (!entry.isFile() || !entry.name.endsWith(".md")) {
2902
+ continue;
2903
+ }
2904
+ const absPath = join5(dir, entry.name);
2905
+ let source = "";
2906
+ try {
2907
+ source = readFileSync(absPath, "utf8");
2908
+ } catch {
2909
+ continue;
2910
+ }
2911
+ const createdAt = extractKnowledgeFrontmatterCreatedAt(source);
2912
+ let mtimeMs = 0;
2913
+ try {
2914
+ mtimeMs = statSync3(absPath).mtimeMs;
2915
+ } catch {
2916
+ mtimeMs = 0;
2917
+ }
2918
+ const referenceMs = createdAt ?? mtimeMs;
2919
+ const displayPath = posix.join(displayPrefix, typeDir, entry.name);
2920
+ if (referenceMs === 0) {
2921
+ yield {
2922
+ layer,
2923
+ type: typeDir,
2924
+ filename: entry.name,
2925
+ pending_path: displayPath,
2926
+ pending_path_abs: absPath,
2927
+ stable_id: void 0,
2928
+ age_days: PENDING_OVERDUE_THRESHOLD_DAYS + 1
2929
+ };
2930
+ continue;
2931
+ }
2932
+ const ageDays = Math.floor((now - referenceMs) / MS_PER_DAY);
2933
+ const stableId = extractKnowledgeFrontmatterId(source) ?? void 0;
2934
+ yield {
2935
+ layer,
2936
+ type: typeDir,
2937
+ filename: entry.name,
2938
+ pending_path: displayPath,
2939
+ pending_path_abs: absPath,
2940
+ stable_id: stableId,
2941
+ age_days: ageDays
2942
+ };
2943
+ }
2944
+ }
2945
+ }
2946
+ }
2947
+ function resolvePersonalRootForPending() {
2948
+ return process.env.FABRIC_HOME ?? homedir2();
2949
+ }
2950
+ function inspectPendingOverdue(projectRoot, now) {
2951
+ const candidates = [];
2952
+ for (const visit of iteratePendingFiles(projectRoot, now)) {
2953
+ if (visit.age_days <= PENDING_OVERDUE_THRESHOLD_DAYS) {
2954
+ continue;
2955
+ }
2956
+ candidates.push({
2957
+ stable_id: visit.stable_id,
2958
+ path: visit.pending_path,
2959
+ age_days: visit.age_days
2960
+ });
2961
+ }
2962
+ candidates.sort((a, b) => a.path.localeCompare(b.path));
2963
+ return { candidates };
2964
+ }
2965
+ function inspectPendingAutoArchive(projectRoot, now) {
2966
+ const candidates = [];
2967
+ for (const visit of iteratePendingFiles(projectRoot, now)) {
2968
+ if (visit.age_days <= PENDING_AUTO_ARCHIVE_THRESHOLD_DAYS) {
2969
+ continue;
2970
+ }
2971
+ if (visit.layer === "team") {
2972
+ const archivedToRel = posix.join(".fabric/.archive/pending", visit.type, visit.filename);
2973
+ candidates.push({
2974
+ layer: "team",
2975
+ type: visit.type,
2976
+ pending_path: visit.pending_path,
2977
+ pending_path_abs: visit.pending_path_abs,
2978
+ archived_to: archivedToRel,
2979
+ archived_to_abs: join5(projectRoot, archivedToRel),
2980
+ age_days: visit.age_days
2981
+ });
2982
+ } else {
2983
+ const archivedToDisplay = posix.join(
2984
+ "~/.fabric/.archive/pending",
2985
+ visit.type,
2986
+ visit.filename
2987
+ );
2988
+ const archivedToAbs = join5(
2989
+ resolvePersonalRootForPending(),
2990
+ ".fabric",
2991
+ ".archive",
2992
+ "pending",
2993
+ visit.type,
2994
+ visit.filename
2995
+ );
2996
+ candidates.push({
2997
+ layer: "personal",
2998
+ type: visit.type,
2999
+ pending_path: visit.pending_path,
3000
+ pending_path_abs: visit.pending_path_abs,
3001
+ archived_to: archivedToDisplay,
3002
+ archived_to_abs: archivedToAbs,
3003
+ age_days: visit.age_days
3004
+ });
3005
+ }
3006
+ }
3007
+ candidates.sort((a, b) => a.pending_path.localeCompare(b.pending_path));
3008
+ return { candidates };
3009
+ }
3010
+ function inspectUnderseeded(projectRoot) {
3011
+ const threshold = readUnderseedThresholdFromConfig(projectRoot);
3012
+ const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
3013
+ let nodeCount = 0;
3014
+ if (existsSync4(knowledgeRoot)) {
3015
+ for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
3016
+ const dir = join5(knowledgeRoot, typeDir);
3017
+ if (!existsSync4(dir)) continue;
3018
+ let entries;
3019
+ try {
3020
+ entries = readdirSync(dir, { withFileTypes: true });
3021
+ } catch {
3022
+ continue;
3023
+ }
3024
+ for (const entry of entries) {
3025
+ if (entry.isFile() && entry.name.endsWith(".md")) {
3026
+ nodeCount += 1;
3027
+ }
3028
+ }
3029
+ }
3030
+ }
3031
+ return {
3032
+ node_count: nodeCount,
3033
+ threshold,
3034
+ underseeded: nodeCount < threshold
3035
+ };
3036
+ }
3037
+ function inspectSessionHintsStale(projectRoot, now) {
3038
+ const cacheDir = join5(projectRoot, ".fabric", ".cache");
3039
+ if (!existsSync4(cacheDir)) {
3040
+ return { candidates: [] };
3041
+ }
3042
+ let entries;
3043
+ try {
3044
+ entries = readdirSync(cacheDir, { withFileTypes: true });
3045
+ } catch {
3046
+ return { candidates: [] };
3047
+ }
3048
+ const candidates = [];
3049
+ for (const entry of entries) {
3050
+ if (!entry.isFile()) continue;
3051
+ if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
3052
+ if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
3053
+ const absPath = join5(cacheDir, entry.name);
3054
+ let mtimeMs = 0;
3055
+ try {
3056
+ mtimeMs = statSync3(absPath).mtimeMs;
3057
+ } catch {
3058
+ continue;
3059
+ }
3060
+ const ageDays = Math.floor((now - mtimeMs) / MS_PER_DAY);
3061
+ if (ageDays < SESSION_HINTS_STALE_DAYS) continue;
3062
+ candidates.push({
3063
+ path: posix.join(".fabric", ".cache", entry.name),
3064
+ age_days: ageDays
3065
+ });
3066
+ }
3067
+ candidates.sort((a, b) => a.path.localeCompare(b.path));
3068
+ return { candidates };
3069
+ }
3070
+ function inspectNarrowTooFew(projectRoot, now) {
3071
+ let total = 0;
3072
+ let narrowWithPaths = 0;
3073
+ for (const { scope, paths } of iterateRelevanceFrontmatter(projectRoot)) {
3074
+ total += 1;
3075
+ if (scope === "narrow" && paths.length > 0) {
3076
+ narrowWithPaths += 1;
3077
+ }
3078
+ }
3079
+ const narrowRatio = total === 0 ? 0 : narrowWithPaths / total;
3080
+ const structuralFlagged = total >= NARROW_MIN_TOTAL && narrowRatio < NARROW_RATIO_THRESHOLD;
3081
+ const windowStartMs = now - SILENCE_WINDOW_DAYS * MS_PER_DAY;
3082
+ const editFires = readCounterTimestamps(
3083
+ join5(projectRoot, EDIT_COUNTER_FILE_REL),
3084
+ windowStartMs
3085
+ );
3086
+ const silenceFires = readCounterTimestamps(
3087
+ join5(projectRoot, HINT_SILENCE_COUNTER_FILE_REL),
3088
+ windowStartMs
3089
+ );
3090
+ const telemetrySkipped = editFires === 0;
3091
+ const silenceRate = editFires === 0 ? 0 : silenceFires / editFires;
3092
+ const telemetryFlagged = !telemetrySkipped && silenceRate > SILENCE_RATE_THRESHOLD;
3093
+ return {
3094
+ total_canonical_entries: total,
3095
+ narrow_with_paths_count: narrowWithPaths,
3096
+ narrow_ratio: narrowRatio,
3097
+ structural_flagged: structuralFlagged,
3098
+ total_edit_fires_in_window: editFires,
3099
+ silence_fires_in_window: silenceFires,
3100
+ silence_rate: silenceRate,
3101
+ telemetry_skipped: telemetrySkipped,
3102
+ telemetry_flagged: telemetryFlagged
3103
+ };
3104
+ }
3105
+ function readCounterTimestamps(absPath, windowStartMs) {
3106
+ if (!existsSync4(absPath)) return 0;
3107
+ let raw;
3108
+ try {
3109
+ raw = readFileSync(absPath, "utf8");
3110
+ } catch {
3111
+ return 0;
3112
+ }
3113
+ let count = 0;
3114
+ for (const line of raw.split(/\r?\n/u)) {
3115
+ const trimmed = line.trim();
3116
+ if (trimmed.length === 0) continue;
3117
+ const ts = Date.parse(trimmed);
3118
+ if (!Number.isFinite(ts)) continue;
3119
+ if (ts < windowStartMs) continue;
3120
+ count += 1;
3121
+ }
3122
+ return count;
3123
+ }
3124
+ function readUnderseedThresholdFromConfig(projectRoot) {
3125
+ const configPath = join5(projectRoot, ".fabric", "fabric-config.json");
3126
+ if (!existsSync4(configPath)) return DEFAULT_UNDERSEED_NODE_THRESHOLD;
3127
+ try {
3128
+ const raw = readFileSync(configPath, "utf8");
3129
+ const parsed = JSON.parse(raw);
3130
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3131
+ const v = parsed.underseed_node_threshold;
3132
+ if (typeof v === "number" && Number.isFinite(v) && v > 0) {
3133
+ return v;
3134
+ }
3135
+ }
3136
+ } catch {
3137
+ }
3138
+ return DEFAULT_UNDERSEED_NODE_THRESHOLD;
3139
+ }
3140
+ function createOrphanDemoteCheck(inspection) {
3141
+ if (inspection.candidates.length === 0) {
3142
+ return okCheck(
3143
+ "Knowledge orphan demote",
3144
+ "No canonical knowledge entries exceed their maturity-keyed inactivity threshold."
3145
+ );
3146
+ }
3147
+ const first = inspection.candidates[0];
3148
+ const detail = `${first.stable_id} (${first.maturity}, ${first.age_days}d inactive at ${first.path})`;
3149
+ return issueCheck(
3150
+ "Knowledge orphan demote",
3151
+ "warn",
3152
+ "warning",
3153
+ "knowledge_orphan_demote_required",
3154
+ `${inspection.candidates.length} canonical knowledge entr${inspection.candidates.length === 1 ? "y exceeds" : "ies exceed"} their maturity-keyed inactivity threshold (stable=${ORPHAN_DEMOTE_THRESHOLD_DAYS.stable}d / endorsed=${ORPHAN_DEMOTE_THRESHOLD_DAYS.endorsed}d / draft=${ORPHAN_DEMOTE_THRESHOLD_DAYS.draft}d). First: ${detail}.`,
3155
+ "Run `fab doctor --apply-lint` (rc.4 TASK-003) to demote orphan entries one maturity tier."
3156
+ );
3157
+ }
3158
+ function createStaleArchiveCheck(inspection) {
3159
+ if (inspection.candidates.length === 0) {
3160
+ return okCheck(
3161
+ "Knowledge stale archive",
3162
+ "No draft knowledge entries exceed the additional stale-archive quiet window."
3163
+ );
3164
+ }
3165
+ const first = inspection.candidates[0];
3166
+ const detail = `${first.stable_id} (${first.age_days}d inactive at ${first.path}) \u2192 ${first.archive_path}`;
3167
+ return issueCheck(
3168
+ "Knowledge stale archive",
3169
+ "warn",
3170
+ "warning",
3171
+ "knowledge_stale_archive_required",
3172
+ `${inspection.candidates.length} draft knowledge entr${inspection.candidates.length === 1 ? "y is" : "ies are"} stale beyond the demote+${STALE_ARCHIVE_ADDITIONAL_DAYS}d additional quiet window. First: ${detail}.`,
3173
+ "Run `fab doctor --apply-lint` (rc.4 TASK-003) to move stale entries into `.fabric/.archive/<type>/`."
3174
+ );
3175
+ }
3176
+ function createPendingOverdueCheck(inspection) {
3177
+ if (inspection.candidates.length === 0) {
3178
+ return okCheck(
3179
+ "Knowledge pending overdue",
3180
+ "No pending knowledge entries exceed the 14-day review threshold."
3181
+ );
3182
+ }
3183
+ const first = inspection.candidates[0];
3184
+ const detail = `${first.path} (${first.age_days}d old)`;
3185
+ return issueCheck(
3186
+ "Knowledge pending overdue",
3187
+ "warn",
3188
+ "warning",
3189
+ "knowledge_pending_overdue",
3190
+ `${inspection.candidates.length} pending knowledge entr${inspection.candidates.length === 1 ? "y has" : "ies have"} been awaiting review for more than ${PENDING_OVERDUE_THRESHOLD_DAYS} days. First: ${detail}.`,
3191
+ "Review pending entries via the fabric-review Skill (`/fabric-review`) and approve, reject, defer, or modify."
3192
+ );
3193
+ }
3194
+ function createUnderseededCheck(inspection) {
3195
+ if (!inspection.underseeded) {
3196
+ return okCheck(
3197
+ "Knowledge underseeded",
3198
+ `Knowledge corpus has ${inspection.node_count} canonical entries (>= ${inspection.threshold}).`
3199
+ );
3200
+ }
3201
+ return issueCheck(
3202
+ "Knowledge underseeded",
3203
+ "ok",
3204
+ "info",
3205
+ "knowledge_underseeded",
3206
+ `Knowledge corpus has only ${inspection.node_count} canonical entr${inspection.node_count === 1 ? "y" : "ies"} (< ${inspection.threshold} threshold). The plan_context retrieval surface is below its useful floor.`,
3207
+ "Run the fabric-import Skill (`/fabric-import`) to backfill knowledge from git history and existing docs."
3208
+ );
3209
+ }
3210
+ function createSessionHintsStaleCheck(inspection) {
3211
+ if (inspection.candidates.length === 0) {
3212
+ return okCheck(
3213
+ "Knowledge session-hints stale",
3214
+ `No session-hints cache files older than ${SESSION_HINTS_STALE_DAYS} days under .fabric/.cache/.`
3215
+ );
3216
+ }
3217
+ const first = inspection.candidates[0];
3218
+ const detail = `${first.path} (${first.age_days}d old)`;
3219
+ return issueCheck(
3220
+ "Knowledge session-hints stale",
3221
+ "ok",
3222
+ "info",
3223
+ "knowledge_session_hints_stale",
3224
+ `${inspection.candidates.length} session-hints cache file${inspection.candidates.length === 1 ? "" : "s"} under .fabric/.cache/ ${inspection.candidates.length === 1 ? "is" : "are"} older than ${SESSION_HINTS_STALE_DAYS} days. First: ${detail}.`,
3225
+ "Run `fab doctor --apply-lint` to delete stale session-hints cache files."
3226
+ );
3227
+ }
3228
+ function extractKnowledgeFrontmatterRelevanceScope(source) {
3229
+ const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
3230
+ const fm = FM_PATTERN.exec(source);
3231
+ if (fm === null) {
3232
+ return "broad";
3233
+ }
3234
+ const match = RELEVANCE_SCOPE_LINE_PATTERN.exec(fm[1]);
3235
+ if (match === null) {
3236
+ return "broad";
3237
+ }
3238
+ return match[2];
3239
+ }
3240
+ function extractKnowledgeFrontmatterRelevancePaths(source) {
3241
+ const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
3242
+ const fm = FM_PATTERN.exec(source);
3243
+ if (fm === null) {
3244
+ return [];
3245
+ }
3246
+ const match = RELEVANCE_PATHS_LINE_PATTERN.exec(fm[1]);
3247
+ if (match === null) {
3248
+ return [];
3249
+ }
3250
+ const inner = match[1].trim();
3251
+ if (inner.length === 0) {
3252
+ return [];
3253
+ }
3254
+ return inner.split(",").map((token) => token.trim().replace(/^"(.*)"$/u, "$1")).filter((token) => token.length > 0);
3255
+ }
3256
+ function* iterateRelevanceFrontmatter(projectRoot) {
3257
+ for (const visit of iterateCanonicalFilenames(projectRoot)) {
3258
+ const layerRoot = visit.layer === "team" ? join5(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
3259
+ const absPath = join5(layerRoot, visit.type, visit.filename);
3260
+ let source;
3261
+ try {
3262
+ source = readFileSync(absPath, "utf8");
3263
+ } catch {
3264
+ continue;
3265
+ }
3266
+ const scope = extractKnowledgeFrontmatterRelevanceScope(source);
3267
+ const paths = extractKnowledgeFrontmatterRelevancePaths(source);
3268
+ yield { visit, scope, paths, absPath };
3269
+ }
3270
+ }
3271
+ function inspectNarrowNoPaths(projectRoot) {
3272
+ const candidates = [];
3273
+ for (const { visit, scope, paths } of iterateRelevanceFrontmatter(projectRoot)) {
3274
+ if (scope !== "narrow") {
3275
+ continue;
3276
+ }
3277
+ if (paths.length > 0) {
3278
+ continue;
3279
+ }
3280
+ candidates.push({
3281
+ stable_id: visit.parsed.stable_id,
3282
+ path: visit.displayPath
3283
+ });
3284
+ }
3285
+ candidates.sort((a, b) => a.path.localeCompare(b.path));
3286
+ return { candidates };
3287
+ }
3288
+ function inspectRelevancePathsDangling(projectRoot) {
3289
+ const entries = [];
3290
+ const workspacePaths = collectWorkspacePathsForGlobMatch(projectRoot);
3291
+ if (workspacePaths.length === 0) {
3292
+ return { entries };
3293
+ }
3294
+ for (const { visit, paths } of iterateRelevanceFrontmatter(projectRoot)) {
3295
+ if (paths.length === 0) {
3296
+ continue;
3297
+ }
3298
+ for (const rawGlob of paths) {
3299
+ const glob = rawGlob.endsWith("/") ? `${rawGlob}**` : rawGlob;
3300
+ let matched = false;
3301
+ for (const target of workspacePaths) {
3302
+ if (minimatch(target, glob, { dot: true, matchBase: false })) {
3303
+ matched = true;
3304
+ break;
3305
+ }
3306
+ }
3307
+ if (matched) {
3308
+ continue;
3309
+ }
3310
+ entries.push({
3311
+ stable_id: visit.parsed.stable_id,
3312
+ path: visit.displayPath,
3313
+ dangling_glob: rawGlob
3314
+ });
3315
+ }
3316
+ }
3317
+ entries.sort((a, b) => {
3318
+ const byPath = a.path.localeCompare(b.path);
3319
+ return byPath !== 0 ? byPath : a.dangling_glob.localeCompare(b.dangling_glob);
3320
+ });
3321
+ return { entries };
3322
+ }
3323
+ function collectWorkspacePathsForGlobMatch(projectRoot) {
3324
+ if (!existsSync4(projectRoot)) {
3325
+ return [];
3326
+ }
3327
+ let rootStat;
3328
+ try {
3329
+ rootStat = statSync3(projectRoot);
3330
+ } catch {
3331
+ return [];
3332
+ }
3333
+ if (!rootStat.isDirectory()) {
3334
+ return [];
3335
+ }
3336
+ const paths = [];
3337
+ const stack = [projectRoot];
3338
+ while (stack.length > 0) {
3339
+ const current = stack.pop();
3340
+ if (current === void 0) continue;
3341
+ let entries;
3342
+ try {
3343
+ entries = readdirSync(current, { withFileTypes: true });
3344
+ } catch {
3345
+ continue;
3346
+ }
3347
+ for (const entry of entries) {
3348
+ const abs = join5(current, entry.name);
3349
+ const rel = normalizePath(abs.slice(projectRoot.length + 1));
3350
+ if (rel.length === 0) continue;
3351
+ if (entry.isDirectory()) {
3352
+ if (IGNORED_DIRECTORIES.has(entry.name)) {
3353
+ continue;
3354
+ }
3355
+ paths.push(rel);
3356
+ stack.push(abs);
3357
+ } else if (entry.isFile()) {
3358
+ paths.push(rel);
3359
+ }
3360
+ }
3361
+ }
3362
+ return paths;
3363
+ }
3364
+ function inspectRelevancePathsDrift(projectRoot) {
3365
+ let recentPaths = null;
3366
+ try {
3367
+ recentPaths = readRecentGitTouchedPaths(projectRoot, RELEVANCE_PATHS_DRIFT_WINDOW_DAYS);
3368
+ } catch {
3369
+ recentPaths = null;
3370
+ }
3371
+ if (recentPaths === null) {
3372
+ return { candidates: [], git_available: false };
3373
+ }
3374
+ const candidates = [];
3375
+ for (const { visit, scope, paths } of iterateRelevanceFrontmatter(projectRoot)) {
3376
+ if (scope !== "narrow") {
3377
+ continue;
3378
+ }
3379
+ if (paths.length === 0) {
3380
+ continue;
3381
+ }
3382
+ let anyMatch = false;
3383
+ for (const rawGlob of paths) {
3384
+ const glob = rawGlob.endsWith("/") ? `${rawGlob}**` : rawGlob;
3385
+ for (const target of recentPaths) {
3386
+ if (minimatch(target, glob, { dot: true, matchBase: false })) {
3387
+ anyMatch = true;
3388
+ break;
3389
+ }
3390
+ }
3391
+ if (anyMatch) break;
3392
+ }
3393
+ if (anyMatch) {
3394
+ continue;
3395
+ }
3396
+ candidates.push({
3397
+ stable_id: visit.parsed.stable_id,
3398
+ path: visit.displayPath,
3399
+ globs: paths.slice()
3400
+ });
3401
+ }
3402
+ candidates.sort((a, b) => a.path.localeCompare(b.path));
3403
+ return { candidates, git_available: true };
3404
+ }
3405
+ function readRecentGitTouchedPaths(projectRoot, windowDays) {
3406
+ const since = new Date(Date.now() - windowDays * MS_PER_DAY).toISOString();
3407
+ const stdout = execFileSync(
3408
+ "git",
3409
+ ["log", `--since=${since}`, "--name-only", "--pretty=format:"],
3410
+ {
3411
+ cwd: projectRoot,
3412
+ stdio: ["ignore", "pipe", "pipe"],
3413
+ encoding: "utf8"
3414
+ }
3415
+ );
3416
+ const set = /* @__PURE__ */ new Set();
3417
+ for (const line of stdout.split(/\r?\n/u)) {
3418
+ const trimmed = line.trim();
3419
+ if (trimmed.length === 0) continue;
3420
+ set.add(normalizePath(trimmed));
3421
+ }
3422
+ return Array.from(set);
3423
+ }
3424
+ function createNarrowNoPathsCheck(inspection) {
3425
+ if (inspection.candidates.length === 0) {
3426
+ return okCheck(
3427
+ "Knowledge narrow without paths",
3428
+ "No narrow-scope canonical entries have an empty relevance_paths array."
3429
+ );
3430
+ }
3431
+ const first = inspection.candidates[0];
3432
+ const detail = `${first.stable_id} (${first.path})`;
3433
+ return issueCheck(
3434
+ "Knowledge narrow without paths",
3435
+ "warn",
3436
+ "warning",
3437
+ "knowledge_narrow_no_paths",
3438
+ `${inspection.candidates.length} narrow-scope canonical entr${inspection.candidates.length === 1 ? "y has" : "ies have"} an empty relevance_paths array (silent recall risk \u2014 narrow without anchors can never match a target path). First: ${detail}.`,
3439
+ "Either add path anchors to relevance_paths or widen the entry's relevance_scope to broad."
3440
+ );
3441
+ }
3442
+ function createRelevancePathsDanglingCheck(inspection) {
3443
+ if (inspection.entries.length === 0) {
3444
+ return okCheck(
3445
+ "Knowledge relevance_paths dangling",
3446
+ "All relevance_paths globs resolve to at least one file under the workspace root."
3447
+ );
3448
+ }
3449
+ const first = inspection.entries[0];
3450
+ const detail = `${first.stable_id} at ${first.path} \u2192 \`${first.dangling_glob}\` (0 matches)`;
3451
+ return issueCheck(
3452
+ "Knowledge relevance_paths dangling",
3453
+ "warn",
3454
+ "warning",
3455
+ "knowledge_relevance_paths_dangling",
3456
+ `${inspection.entries.length} relevance_paths glob${inspection.entries.length === 1 ? " resolves" : "s resolve"} to zero files in the current workspace. First: ${detail}.`,
3457
+ "Update the entry's relevance_paths to remove globs that no longer match any files, or use `fab_review.modify` to rewrite the anchor set."
3458
+ );
3459
+ }
3460
+ function createRelevancePathsDriftCheck(inspection) {
3461
+ if (!inspection.git_available) {
3462
+ return okCheck(
3463
+ "Knowledge relevance_paths drift",
3464
+ `Skipped (git history unavailable; cannot evaluate ${RELEVANCE_PATHS_DRIFT_WINDOW_DAYS}d drift window).`
3465
+ );
3466
+ }
3467
+ if (inspection.candidates.length === 0) {
3468
+ return okCheck(
3469
+ "Knowledge relevance_paths drift",
3470
+ `All narrow-scope canonical entries have at least one relevance_path touched in the last ${RELEVANCE_PATHS_DRIFT_WINDOW_DAYS}d.`
3471
+ );
3472
+ }
3473
+ const first = inspection.candidates[0];
3474
+ const detail = `${first.stable_id} at ${first.path} (globs: ${first.globs.join(", ")})`;
3475
+ return issueCheck(
3476
+ "Knowledge relevance_paths drift",
3477
+ "ok",
3478
+ "info",
3479
+ "knowledge_relevance_paths_drift",
3480
+ `${inspection.candidates.length} narrow-scope canonical entr${inspection.candidates.length === 1 ? "y has" : "ies have"} relevance_paths whose globs match no file touched in the last ${RELEVANCE_PATHS_DRIFT_WINDOW_DAYS}d of git history. First: ${detail}.`,
3481
+ "Review whether the entry is still relevant \u2014 use `fab_review.modify` to refresh the anchors or `fab_review.reject` to archive."
3482
+ );
3483
+ }
3484
+ function inspectRelevanceFieldsMissing(projectRoot) {
3485
+ const candidates = [];
3486
+ let scannedCount = 0;
3487
+ const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
3488
+ const teamRoot = join5(projectRoot, ".fabric", "knowledge", "pending");
3489
+ const personalRoot = join5(
3490
+ resolvePersonalRootForPending(),
3491
+ ".fabric",
3492
+ "knowledge",
3493
+ "pending"
3494
+ );
3495
+ for (const [root, displayPrefix] of [
3496
+ [teamRoot, ".fabric/knowledge/pending"],
3497
+ [personalRoot, "~/.fabric/knowledge/pending"]
3498
+ ]) {
3499
+ if (!existsSync4(root)) {
3500
+ continue;
3501
+ }
3502
+ let typeDirs = [];
3503
+ try {
3504
+ typeDirs = readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
3505
+ } catch {
3506
+ continue;
3507
+ }
3508
+ for (const typeDir of typeDirs) {
3509
+ const dir = join5(root, typeDir);
3510
+ let entries;
3511
+ try {
3512
+ entries = readdirSync(dir, { withFileTypes: true });
3513
+ } catch {
3514
+ continue;
3515
+ }
3516
+ for (const entry of entries) {
3517
+ if (!entry.isFile() || !entry.name.endsWith(".md")) {
3518
+ continue;
3519
+ }
3520
+ const absPath = join5(dir, entry.name);
3521
+ let source;
3522
+ try {
3523
+ source = readFileSync(absPath, "utf8");
3524
+ } catch {
3525
+ continue;
3526
+ }
3527
+ const fm = FM_PATTERN.exec(source);
3528
+ if (fm === null) {
3529
+ continue;
3530
+ }
3531
+ scannedCount += 1;
3532
+ const block = fm[1];
3533
+ const missingScope = !RELEVANCE_SCOPE_LINE_PATTERN.test(block);
3534
+ const missingPaths = !RELEVANCE_PATHS_LINE_PATTERN.test(block);
3535
+ if (!missingScope && !missingPaths) {
3536
+ continue;
3537
+ }
3538
+ candidates.push({
3539
+ pending_path: posix.join(displayPrefix, typeDir, entry.name),
3540
+ pending_path_abs: absPath,
3541
+ missing_scope: missingScope,
3542
+ missing_paths: missingPaths
3543
+ });
3544
+ }
3545
+ }
3546
+ }
3547
+ candidates.sort((a, b) => a.pending_path.localeCompare(b.pending_path));
3548
+ return { candidates, scanned_count: scannedCount };
3549
+ }
3550
+ function appendRelevanceFieldsToFrontmatter(source, needsScope, needsPaths) {
3551
+ const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
3552
+ const fm = FM_PATTERN.exec(source);
3553
+ if (fm === null) {
3554
+ return null;
3555
+ }
3556
+ const block = fm[1];
3557
+ const actuallyNeedsScope = needsScope && !RELEVANCE_SCOPE_LINE_PATTERN.test(block);
3558
+ const actuallyNeedsPaths = needsPaths && !RELEVANCE_PATHS_LINE_PATTERN.test(block);
3559
+ if (!actuallyNeedsScope && !actuallyNeedsPaths) {
3560
+ return source;
3561
+ }
3562
+ const additions = [];
3563
+ if (actuallyNeedsScope) {
3564
+ additions.push("relevance_scope: broad");
3565
+ }
3566
+ if (actuallyNeedsPaths) {
3567
+ additions.push("relevance_paths: []");
3568
+ }
3569
+ const trailing = block.endsWith("\n") ? "" : "\n";
3570
+ const replacedBlock = `${block}${trailing}${additions.join("\n")}`;
3571
+ const blockStart = source.indexOf(block);
3572
+ if (blockStart < 0) {
3573
+ return null;
3574
+ }
3575
+ return source.slice(0, blockStart) + replacedBlock + source.slice(blockStart + block.length);
3576
+ }
3577
+ async function applyRelevanceFieldsMissing(candidate) {
3578
+ const parts = [];
3579
+ if (candidate.missing_scope) parts.push("relevance_scope: broad");
3580
+ if (candidate.missing_paths) parts.push("relevance_paths: []");
3581
+ const detail = `back-filled: ${parts.join(", ")}`;
3582
+ try {
3583
+ const source = await readFile5(candidate.pending_path_abs, "utf8");
3584
+ const rewritten = appendRelevanceFieldsToFrontmatter(
3585
+ source,
3586
+ candidate.missing_scope,
3587
+ candidate.missing_paths
3588
+ );
3589
+ if (rewritten === null) {
3590
+ return {
3591
+ kind: "knowledge_relevance_fields_missing",
3592
+ path: candidate.pending_path,
3593
+ detail,
3594
+ applied: false,
3595
+ error: "frontmatter not parseable; cannot back-fill"
3596
+ };
3597
+ }
3598
+ if (rewritten === source) {
3599
+ return {
3600
+ kind: "knowledge_relevance_fields_missing",
3601
+ path: candidate.pending_path,
3602
+ detail,
3603
+ applied: false,
3604
+ error: "fields already present at write time (no diff)"
3605
+ };
3606
+ }
3607
+ await atomicWriteText3(candidate.pending_path_abs, rewritten);
3608
+ return {
3609
+ kind: "knowledge_relevance_fields_missing",
3610
+ path: candidate.pending_path,
3611
+ detail,
3612
+ applied: true
3613
+ };
3614
+ } catch (error) {
3615
+ return {
3616
+ kind: "knowledge_relevance_fields_missing",
3617
+ path: candidate.pending_path,
3618
+ detail,
3619
+ applied: false,
3620
+ error: truncateErrorMessage(error)
3621
+ };
3622
+ }
3623
+ }
3624
+ function createRelevanceFieldsMissingCheck(inspection) {
3625
+ if (inspection.candidates.length === 0) {
3626
+ return okCheck(
3627
+ "Knowledge relevance fields missing",
3628
+ "All pending entries declare both relevance_scope and relevance_paths."
3629
+ );
3630
+ }
3631
+ const first = inspection.candidates[0];
3632
+ const missingParts = [];
3633
+ if (first.missing_scope) missingParts.push("relevance_scope");
3634
+ if (first.missing_paths) missingParts.push("relevance_paths");
3635
+ const detail = `${first.pending_path} (missing: ${missingParts.join(", ")})`;
3636
+ return issueCheck(
3637
+ "Knowledge relevance fields missing",
3638
+ "ok",
3639
+ "info",
3640
+ "knowledge_relevance_fields_missing",
3641
+ `${inspection.candidates.length} pending entr${inspection.candidates.length === 1 ? "y is" : "ies are"} missing relevance_scope and/or relevance_paths in frontmatter. First: ${detail}.`,
3642
+ "Run `fab doctor --apply-lint` to back-fill the schema defaults (relevance_scope: broad, relevance_paths: [])."
3643
+ );
3644
+ }
3645
+ function createNarrowTooFewCheck(inspection) {
3646
+ const { structural_flagged, telemetry_flagged } = inspection;
3647
+ if (!structural_flagged && !telemetry_flagged) {
3648
+ const ratioPct = (inspection.narrow_ratio * 100).toFixed(0);
3649
+ const teleNote = inspection.telemetry_skipped ? "telemetry skipped (no edit-counter fires in window)" : `silence rate ${(inspection.silence_rate * 100).toFixed(0)}% over ${SILENCE_WINDOW_DAYS}d`;
3650
+ return okCheck(
3651
+ "Knowledge narrow too few",
3652
+ `Narrow-with-paths ratio ${ratioPct}% (${inspection.narrow_with_paths_count}/${inspection.total_canonical_entries}); ${teleNote}.`
3653
+ );
3654
+ }
3655
+ const parts = [];
3656
+ if (structural_flagged) {
3657
+ const ratioPct = (inspection.narrow_ratio * 100).toFixed(0);
3658
+ parts.push(
3659
+ `narrow-with-paths share ${ratioPct}% (${inspection.narrow_with_paths_count}/${inspection.total_canonical_entries}) below ${(NARROW_RATIO_THRESHOLD * 100).toFixed(0)}% threshold`
3660
+ );
3661
+ }
3662
+ if (telemetry_flagged) {
3663
+ const silencePct = (inspection.silence_rate * 100).toFixed(0);
3664
+ parts.push(
3665
+ `narrow-hook silence rate ${silencePct}% (${inspection.silence_fires_in_window}/${inspection.total_edit_fires_in_window}) over ${SILENCE_WINDOW_DAYS}d above ${(SILENCE_RATE_THRESHOLD * 100).toFixed(0)}% threshold`
3666
+ );
3667
+ }
3668
+ return issueCheck(
3669
+ "Knowledge narrow too few",
3670
+ "ok",
3671
+ "info",
3672
+ "knowledge_narrow_too_few",
3673
+ `Narrow-scope KB coverage is below the useful floor: ${parts.join("; ")}.`,
3674
+ "Run the fabric-import Skill (`/fabric-import`) to re-seed narrow anchors against the current codebase."
3675
+ );
3676
+ }
3677
+ function resolvePersonalKnowledgeRoot() {
3678
+ const home = process.env.FABRIC_HOME ?? homedir2();
3679
+ return join5(home, ".fabric", "knowledge");
3680
+ }
3681
+ function parseStableIdFromCanonicalFilename(filename) {
3682
+ const match = CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(filename);
3683
+ if (match === null) {
3684
+ return null;
3685
+ }
3686
+ const stableId = match[1];
3687
+ const inner = /^(K[PT])-(MOD|DEC|GLD|PIT|PRO)-(\d{4,})$/u.exec(stableId);
3688
+ if (inner === null) {
3689
+ return null;
3690
+ }
3691
+ return {
3692
+ prefix: inner[1],
3693
+ typeCode: inner[2],
3694
+ counter: Number.parseInt(inner[3], 10),
3695
+ stable_id: stableId
3696
+ };
3697
+ }
3698
+ function* iterateCanonicalFilenames(projectRoot) {
3699
+ const teamRoot = join5(projectRoot, ".fabric", "knowledge");
3700
+ const personalRoot = resolvePersonalKnowledgeRoot();
3701
+ for (const [layer, root, displayPrefix] of [
3702
+ ["team", teamRoot, ".fabric/knowledge"],
3703
+ ["personal", personalRoot, "~/.fabric/knowledge"]
3704
+ ]) {
3705
+ if (!existsSync4(root)) {
3706
+ continue;
3707
+ }
3708
+ for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
3709
+ const dir = join5(root, typeDir);
3710
+ if (!existsSync4(dir)) {
3711
+ continue;
3712
+ }
3713
+ let entries;
3714
+ try {
3715
+ entries = readdirSync(dir, { withFileTypes: true });
3716
+ } catch {
3717
+ continue;
3718
+ }
3719
+ for (const entry of entries) {
3720
+ if (!entry.isFile()) {
3721
+ continue;
3722
+ }
3723
+ const parsed = parseStableIdFromCanonicalFilename(entry.name);
3724
+ if (parsed === null) {
3725
+ continue;
3726
+ }
3727
+ const displayPath = posix.join(displayPrefix, typeDir, entry.name);
3728
+ yield {
3729
+ layer,
3730
+ type: typeDir,
3731
+ filename: entry.name,
3732
+ displayPath,
3733
+ parsed
3734
+ };
3735
+ }
3736
+ }
3737
+ }
3738
+ }
3739
+ function inspectStableIdDuplicate(projectRoot) {
3740
+ const idToPaths = /* @__PURE__ */ new Map();
3741
+ for (const visit of iterateCanonicalFilenames(projectRoot)) {
3742
+ const existing = idToPaths.get(visit.parsed.stable_id) ?? [];
3743
+ existing.push(visit.displayPath);
3744
+ idToPaths.set(visit.parsed.stable_id, existing);
3745
+ }
3746
+ const duplicates = [];
3747
+ for (const [stable_id, paths] of idToPaths) {
3748
+ if (paths.length > 1) {
3749
+ duplicates.push({ stable_id, paths: paths.slice().sort() });
3750
+ }
3751
+ }
3752
+ duplicates.sort((a, b) => a.stable_id.localeCompare(b.stable_id));
3753
+ return { duplicates };
3754
+ }
3755
+ function inspectLayerMismatch(projectRoot) {
3756
+ const mismatches = [];
3757
+ for (const visit of iterateCanonicalFilenames(projectRoot)) {
3758
+ const expected_layer = visit.parsed.prefix === "KT" ? "team" : "personal";
3759
+ if (expected_layer === visit.layer) {
3760
+ continue;
3761
+ }
3762
+ mismatches.push({
3763
+ path: visit.displayPath,
3764
+ located_in: visit.layer,
3765
+ expected_layer,
3766
+ stable_id: visit.parsed.stable_id
3767
+ });
3768
+ }
3769
+ mismatches.sort((a, b) => a.path.localeCompare(b.path));
3770
+ return { mismatches };
3771
+ }
3772
+ function inspectIndexDrift(projectRoot, meta) {
3773
+ if (!meta.valid || meta.meta === null) {
3774
+ return { drifts: [] };
3775
+ }
3776
+ const counters = AgentsMetaCountersSchema.parse(meta.meta.counters ?? void 0);
3777
+ const observed = {
3778
+ KP: { MOD: 0, DEC: 0, GLD: 0, PIT: 0, PRO: 0 },
3779
+ KT: { MOD: 0, DEC: 0, GLD: 0, PIT: 0, PRO: 0 }
3780
+ };
3781
+ for (const visit of iterateCanonicalFilenames(projectRoot)) {
3782
+ const { prefix, typeCode, counter } = visit.parsed;
3783
+ if (counter > observed[prefix][typeCode]) {
3784
+ observed[prefix][typeCode] = counter;
3785
+ }
3786
+ }
3787
+ const drifts = [];
3788
+ for (const layer of ["KP", "KT"]) {
3789
+ for (const code of COUNTER_TYPE_CODES) {
3790
+ const max = observed[layer][code];
3791
+ if (max === 0) {
3792
+ continue;
3793
+ }
3794
+ const current = counters[layer][code];
3795
+ if (current < max) {
3796
+ drifts.push({
3797
+ layer,
3798
+ type: code,
3799
+ counter: current,
3800
+ max_observed: max,
3801
+ proposed_after: max + 1
3802
+ });
3803
+ }
3804
+ }
3805
+ }
3806
+ drifts.sort(
3807
+ (a, b) => a.layer === b.layer ? a.type.localeCompare(b.type) : a.layer.localeCompare(b.layer)
3808
+ );
3809
+ return { drifts };
3810
+ }
3811
+ function createStableIdDuplicateCheck(inspection) {
3812
+ if (inspection.duplicates.length === 0) {
3813
+ return okCheck(
3814
+ "Knowledge stable_id duplicate",
3815
+ "No canonical knowledge files share a stable_id across team / personal trees."
3816
+ );
3817
+ }
3818
+ const first = inspection.duplicates[0];
3819
+ const detail = `${first.stable_id} appears in ${first.paths.length} files: ${first.paths.join(", ")}`;
3820
+ return issueCheck(
3821
+ "Knowledge stable_id duplicate",
3822
+ "error",
3823
+ "manual_error",
3824
+ "knowledge_stable_id_duplicate",
3825
+ `${inspection.duplicates.length} stable_id${inspection.duplicates.length === 1 ? "" : "s"} duplicated across canonical knowledge files (path-decoupled identity invariant). First: ${detail}.`,
3826
+ "Manually rename one of the colliding files to a fresh `<prefix>-<type>-<counter>--<slug>.md` allocated via the canonical id allocator; do not edit by hand."
3827
+ );
3828
+ }
3829
+ function createLayerMismatchCheck(inspection) {
3830
+ if (inspection.mismatches.length === 0) {
3831
+ return okCheck(
3832
+ "Knowledge layer mismatch",
3833
+ "All canonical knowledge files are physically located under the layer their stable_id prefix declares."
3834
+ );
3835
+ }
3836
+ const first = inspection.mismatches[0];
3837
+ const detail = `${first.stable_id} at ${first.path} (located in ${first.located_in}, expected ${first.expected_layer})`;
3838
+ return issueCheck(
3839
+ "Knowledge layer mismatch",
3840
+ "error",
3841
+ "manual_error",
3842
+ "knowledge_layer_mismatch",
3843
+ `${inspection.mismatches.length} canonical knowledge file${inspection.mismatches.length === 1 ? "" : "s"} are physically misaligned with their stable_id layer prefix (KT-* must live under team/, KP-* under personal/). First: ${detail}.`,
3844
+ "Move the file to the correct layer root, or use the fabric-review modify flow to flip its layer (which renames the stable_id prefix accordingly)."
3845
+ );
3846
+ }
3847
+ function createIndexDriftCheck(inspection) {
3848
+ if (inspection.drifts.length === 0) {
3849
+ return okCheck(
3850
+ "Knowledge index drift",
3851
+ "agents.meta.json counters envelope is at or above the highest existing canonical counter for every (layer, type) pair."
3852
+ );
3853
+ }
3854
+ const first = inspection.drifts[0];
3855
+ const detail = `${first.layer}.${first.type} counter=${first.counter} but max_observed=${first.max_observed} (would propose counters.${first.layer}.${first.type}=${first.proposed_after})`;
3856
+ return issueCheck(
3857
+ "Knowledge index drift",
3858
+ "error",
3859
+ "fixable_error",
3860
+ "knowledge_index_drift",
3861
+ `${inspection.drifts.length} (layer, type) counter slot${inspection.drifts.length === 1 ? "" : "s"} have drifted below the observed canonical maximum (next allocate would collide). First: ${detail}.`,
3862
+ "Run `fab doctor --apply-lint` (rc.4 TASK-003) to bump agents.meta.json counters to max_observed + 1."
3863
+ );
3864
+ }
3865
+ async function fixMcpConfigInWrongFile(projectRoot) {
3866
+ const settingsPath = join5(projectRoot, ".claude", "settings.json");
3867
+ if (!existsSync4(settingsPath)) {
3868
+ return;
3869
+ }
3870
+ let settings;
3871
+ try {
3872
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
3873
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
3874
+ return;
3875
+ }
3876
+ settings = parsed;
3877
+ } catch {
3878
+ return;
3879
+ }
3880
+ const mcpServers = settings.mcpServers;
3881
+ if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
3882
+ return;
3883
+ }
3884
+ const { fabric: _removed, ...remainingServers } = mcpServers;
3885
+ const cleaned = { ...settings };
3886
+ if (Object.keys(remainingServers).length === 0) {
3887
+ delete cleaned.mcpServers;
3888
+ } else {
3889
+ cleaned.mcpServers = remainingServers;
3890
+ }
3891
+ await atomicWriteJson2(settingsPath, cleaned, { indent: 2 });
3892
+ await appendEventLedgerEvent(projectRoot, {
3893
+ event_type: "mcp_config_migrated",
3894
+ source: "doctor_fix",
3895
+ removed_from: ".claude/settings.json"
3896
+ });
3897
+ }
3898
+ async function ensureKnowledgeSubdirs(projectRoot) {
3899
+ for (const sub of KNOWLEDGE_SUBDIRS2) {
3900
+ await mkdir3(join5(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
3901
+ }
3902
+ }
3903
+ async function fixCounterDesync(projectRoot) {
3904
+ const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
3905
+ if (!existsSync4(metaPath)) {
3906
+ return;
3907
+ }
3908
+ let meta;
3909
+ try {
3910
+ meta = agentsMetaSchema4.parse(JSON.parse(await readFile5(metaPath, "utf8")));
3911
+ } catch {
3912
+ return;
3913
+ }
3914
+ const synthetic = {
3915
+ present: true,
3916
+ valid: true,
3917
+ meta,
3918
+ revision: meta.revision,
3919
+ computedRevision: null,
3920
+ ruleCount: 0,
3921
+ missingContentRefs: [],
3922
+ invalidContentRefs: [],
3923
+ stale: false,
3924
+ changed: false
3925
+ };
3926
+ const desync = inspectCounterDesync(synthetic);
3927
+ if (desync.desyncs.length === 0 || desync.correctedCounters === null) {
3928
+ return;
3929
+ }
3930
+ const updated = { ...meta, counters: desync.correctedCounters };
3931
+ await atomicWriteJson2(metaPath, updated, { indent: 2 });
3932
+ }
3933
+ async function ensureEventLedger(projectRoot) {
3934
+ const path = getEventLedgerPath(projectRoot);
3935
+ await ensureParentDirectory(path);
3936
+ await writeFile2(path, "", { encoding: "utf8", flag: "a" });
3937
+ }
3938
+ function createFixMessage(fixed, report) {
3939
+ const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
3940
+ const manualText = report.manual_errors.length === 0 ? "No manual errors remain." : `${report.manual_errors.length} manual error${report.manual_errors.length === 1 ? "" : "s"} remain.`;
2164
3941
  return `${fixedText} ${manualText}`;
2165
3942
  }
2166
3943
  function isValidJsonLine(line) {
@@ -2246,141 +4023,35 @@ function isMissingFileError(error) {
2246
4023
  return error instanceof Error && "code" in error && error.code === "ENOENT";
2247
4024
  }
2248
4025
 
2249
- // src/services/get-rules.ts
4026
+ // src/services/get-knowledge.ts
2250
4027
  import { readFile as readFile6 } from "fs/promises";
2251
- import { join as join7 } from "path";
2252
- import { minimatch } from "minimatch";
2253
-
2254
- // src/services/audit-log.ts
2255
- import { open, stat as stat2 } from "fs/promises";
2256
- import { isAbsolute as isAbsolute3, join as join6, posix as posix2, relative as relative3, resolve as resolve4 } from "path";
2257
- var AUDIT_LOG_FILE = `${FABRIC_DIR}/audit.jsonl`;
2258
- var DEFAULT_AUDIT_WINDOW_MS = 5 * 60 * 1e3;
2259
- async function appendGetRulesAuditEvent(projectRoot, input) {
2260
- const entry = {
2261
- kind: "audit-event",
2262
- event: "get_rules",
2263
- ts: input.ts ?? Date.now(),
2264
- path: normalizeAuditPath(projectRoot, input.path),
2265
- client_hash: input.client_hash
2266
- };
2267
- await appendAuditLogEventLedgerEvents(projectRoot, [entry], {
2268
- rule_context: {
2269
- required_stable_ids: input.required_stable_ids,
2270
- ai_selectable_stable_ids: input.ai_selectable_stable_ids,
2271
- final_stable_ids: input.final_stable_ids
2272
- },
2273
- correlation_id: input.correlation_id,
2274
- session_id: input.session_id
2275
- });
2276
- return entry;
2277
- }
2278
- async function appendRuleSelectionAuditEvent(projectRoot, input) {
2279
- const entry = {
2280
- kind: "audit-event",
2281
- event: "knowledge_selection",
2282
- ts: input.ts ?? Date.now(),
2283
- path: normalizeAuditPath(projectRoot, input.path),
2284
- selection_token: input.selection_token,
2285
- target_paths: input.target_paths.map((path) => normalizeAuditPath(projectRoot, path)),
2286
- required_stable_ids: input.required_stable_ids,
2287
- ai_selectable_stable_ids: input.ai_selectable_stable_ids,
2288
- ai_selected_stable_ids: input.ai_selected_stable_ids,
2289
- final_stable_ids: input.final_stable_ids,
2290
- ai_selection_reasons: input.ai_selection_reasons,
2291
- rejected_stable_ids: input.rejected_stable_ids,
2292
- ignored_stable_ids: input.ignored_stable_ids
2293
- };
2294
- await appendAuditLogEventLedgerEvents(projectRoot, [entry], {
2295
- correlation_id: input.correlation_id,
2296
- session_id: input.session_id
2297
- });
2298
- return entry;
2299
- }
2300
- function normalizeAuditPath(projectRoot, value) {
2301
- const normalizedProjectRoot = resolve4(projectRoot);
2302
- const candidate = isAbsolute3(value) ? resolve4(value) : resolve4(normalizedProjectRoot, value);
2303
- const relativePath = relative3(normalizedProjectRoot, candidate);
2304
- if (relativePath.length > 0 && relativePath !== "." && !relativePath.startsWith("..") && !isAbsolute3(relativePath)) {
2305
- return posix2.normalize(relativePath.split("\\").join("/"));
2306
- }
2307
- return posix2.normalize(value.replaceAll("\\", "/"));
2308
- }
2309
- async function appendAuditLogEventLedgerEvents(projectRoot, entries, metadata = {}) {
2310
- for (const entry of entries) {
2311
- if (entry.event === "get_rules") {
2312
- await appendEventLedgerEvent(projectRoot, {
2313
- event_type: "knowledge_context_planned",
2314
- ts: entry.ts,
2315
- target_paths: [entry.path],
2316
- required_stable_ids: metadata.rule_context?.required_stable_ids ?? [],
2317
- ai_selectable_stable_ids: metadata.rule_context?.ai_selectable_stable_ids ?? [],
2318
- final_stable_ids: metadata.rule_context?.final_stable_ids ?? [],
2319
- client_hash: entry.client_hash,
2320
- correlation_id: metadata.correlation_id,
2321
- session_id: metadata.session_id
2322
- });
2323
- continue;
2324
- }
2325
- if (entry.event === "knowledge_selection") {
2326
- await appendEventLedgerEvent(projectRoot, {
2327
- event_type: "knowledge_selection",
2328
- ts: entry.ts,
2329
- selection_token: entry.selection_token,
2330
- target_paths: entry.target_paths,
2331
- required_stable_ids: entry.required_stable_ids,
2332
- ai_selectable_stable_ids: entry.ai_selectable_stable_ids,
2333
- ai_selected_stable_ids: entry.ai_selected_stable_ids,
2334
- final_stable_ids: entry.final_stable_ids,
2335
- ai_selection_reasons: entry.ai_selection_reasons,
2336
- rejected_stable_ids: entry.rejected_stable_ids,
2337
- ignored_stable_ids: entry.ignored_stable_ids,
2338
- correlation_id: metadata.correlation_id,
2339
- session_id: metadata.session_id
2340
- });
2341
- continue;
2342
- }
2343
- await appendEventLedgerEvent(projectRoot, {
2344
- event_type: "edit_intent_checked",
2345
- ts: entry.ts,
2346
- path: entry.path,
2347
- compliant: entry.compliant,
2348
- intent: entry.intent,
2349
- ledger_entry_id: entry.ledger_entry_id,
2350
- ledger_source: "ai",
2351
- matched_rule_context_ts: entry.matched_get_rules_ts,
2352
- window_ms: entry.window_ms,
2353
- correlation_id: metadata.correlation_id,
2354
- session_id: metadata.session_id
2355
- });
2356
- }
2357
- }
2358
-
2359
- // src/services/get-rules.ts
4028
+ import { join as join6 } from "path";
4029
+ import { minimatch as minimatch2 } from "minimatch";
2360
4030
  var PRIORITY_ORDER = {
2361
4031
  high: 0,
2362
4032
  medium: 1,
2363
4033
  low: 2
2364
4034
  };
2365
- async function getRules(projectRoot, input) {
2366
- const context = await loadGetRulesContext(projectRoot);
4035
+ async function getKnowledge(projectRoot, input) {
4036
+ const context = await loadGetKnowledgeContext(projectRoot);
2367
4037
  const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
2368
4038
  const matchedNodes = matchRuleNodes(context.meta, input.path);
2369
4039
  const requiredStableIds = matchedNodes.filter((node) => node.level === "L2").map((node) => node.stable_id);
2370
4040
  const aiSelectableStableIds = matchedNodes.filter((node) => node.level === "L1").map((node) => node.stable_id);
2371
- const rules = await resolveRulesForPath(projectRoot, context, input.path);
4041
+ const rules = await resolveKnowledgeForPath(projectRoot, context, input.path);
2372
4042
  const result = {
2373
4043
  revision_hash: context.meta.revision,
2374
4044
  stale,
2375
4045
  rules
2376
4046
  };
2377
4047
  try {
2378
- await appendGetRulesAuditEvent(projectRoot, {
2379
- path: input.path,
2380
- client_hash: input.client_hash,
4048
+ await appendEventLedgerEvent(projectRoot, {
4049
+ event_type: "knowledge_context_planned",
4050
+ target_paths: [input.path],
2381
4051
  required_stable_ids: requiredStableIds,
2382
4052
  ai_selectable_stable_ids: aiSelectableStableIds,
2383
4053
  final_stable_ids: [...requiredStableIds, ...aiSelectableStableIds],
4054
+ client_hash: input.client_hash,
2384
4055
  correlation_id: input.correlation_id,
2385
4056
  session_id: input.session_id
2386
4057
  });
@@ -2388,13 +4059,13 @@ async function getRules(projectRoot, input) {
2388
4059
  }
2389
4060
  return result;
2390
4061
  }
2391
- async function loadGetRulesContext(projectRoot) {
4062
+ async function loadGetKnowledgeContext(projectRoot) {
2392
4063
  const cached = contextCache.get("context", projectRoot);
2393
4064
  if (cached !== void 0) {
2394
4065
  return cached;
2395
4066
  }
2396
4067
  const meta = await readAgentsMeta(projectRoot);
2397
- const l0Content = await readFile6(join7(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
4068
+ const l0Content = await readFile6(join6(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
2398
4069
  const context = {
2399
4070
  meta,
2400
4071
  l0Content,
@@ -2403,20 +4074,20 @@ async function loadGetRulesContext(projectRoot) {
2403
4074
  contextCache.set("context", projectRoot, context);
2404
4075
  return context;
2405
4076
  }
2406
- async function resolveRulesForPath(projectRoot, context, path, options = {}) {
4077
+ async function resolveKnowledgeForPath(projectRoot, context, path, options = {}) {
2407
4078
  const matchedNodes = matchRuleNodes(context.meta, path);
2408
4079
  const loaded = await loadMatchedRules(projectRoot, matchedNodes);
2409
- return buildRulesPayload(context, loaded, options);
4080
+ return buildKnowledgePayload(context, loaded, options);
2410
4081
  }
2411
- function normalizeRulesPath(value) {
4082
+ function normalizeKnowledgePath(value) {
2412
4083
  return value.replaceAll("\\", "/");
2413
4084
  }
2414
4085
  function matchRuleNodes(meta, path) {
2415
- const requestedPath = normalizeRulesPath(path);
4086
+ const requestedPath = normalizeKnowledgePath(path);
2416
4087
  return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
2417
4088
  const [leftId, leftNode] = left;
2418
4089
  const [rightId, rightNode] = right;
2419
- const priorityDelta = PRIORITY_ORDER[leftNode.priority] - PRIORITY_ORDER[rightNode.priority];
4090
+ const priorityDelta = PRIORITY_ORDER[leftNode.priority ?? "medium"] - PRIORITY_ORDER[rightNode.priority ?? "medium"];
2420
4091
  return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
2421
4092
  }).map(([nodeId, node]) => ({
2422
4093
  node_id: nodeId,
@@ -2455,7 +4126,7 @@ async function loadMatchedRules(projectRoot, matchedNodes, fileContentCache = /*
2455
4126
  }
2456
4127
  return { rules, stubs };
2457
4128
  }
2458
- function buildRulesPayload(context, loaded, options = {}) {
4129
+ function buildKnowledgePayload(context, loaded, options = {}) {
2459
4130
  const { L1, L2 } = partitionRulesByLevel(loaded.rules, options.dedupeByPath ?? false);
2460
4131
  return {
2461
4132
  L0: context.l0Content,
@@ -2472,7 +4143,7 @@ function classifyNode(nodeId, node) {
2472
4143
  if (nodeId.startsWith("L2/")) {
2473
4144
  return "L2";
2474
4145
  }
2475
- return node.layer === "L0" ? null : node.layer;
4146
+ return node.layer === "L0" ? null : node.layer ?? null;
2476
4147
  }
2477
4148
  function partitionRulesByLevel(loadedRules, dedupeByPath) {
2478
4149
  const l1 = [];
@@ -2509,7 +4180,7 @@ function shouldLoadNodeForPath(requestedPath, node) {
2509
4180
  return true;
2510
4181
  case "path":
2511
4182
  case void 0:
2512
- return minimatch(requestedPath, normalizeRulesPath(node.scope_glob), { dot: true });
4183
+ return minimatch2(requestedPath, normalizeKnowledgePath(node.scope_glob), { dot: true });
2513
4184
  }
2514
4185
  }
2515
4186
  function dedupeDescriptionStubsByPath(stubs) {
@@ -2533,7 +4204,7 @@ async function readRuleContent(projectRoot, file, fileContentCache) {
2533
4204
  if (cached !== void 0) {
2534
4205
  return await cached;
2535
4206
  }
2536
- const pending = readFile6(join7(projectRoot, file), "utf8");
4207
+ const pending = readFile6(join6(projectRoot, file), "utf8");
2537
4208
  fileContentCache.set(file, pending);
2538
4209
  return await pending;
2539
4210
  }
@@ -2548,25 +4219,27 @@ export {
2548
4219
  getLedgerPath,
2549
4220
  getLegacyLedgerPath,
2550
4221
  getEventLedgerPath,
4222
+ ensureParentDirectory,
2551
4223
  sha256,
2552
4224
  isNodeError,
4225
+ atomicWriteText,
2553
4226
  appendEventLedgerEvent,
2554
4227
  readEventLedger,
2555
4228
  flushAndSyncEventLedger,
2556
- buildRuleMeta,
2557
- writeRuleMeta,
2558
- computeRulesBasedAgentsMeta,
2559
- computeRuleTestIndex,
2560
- deriveRuleMetaLayer,
2561
- deriveRuleMetaTopologyType,
2562
- isSameRuleTestIndex,
4229
+ buildKnowledgeMeta,
4230
+ writeKnowledgeMeta,
4231
+ computeKnowledgeBasedAgentsMeta,
4232
+ computeKnowledgeTestIndex,
4233
+ deriveKnowledgeMetaLayer,
4234
+ deriveKnowledgeMetaTopologyType,
4235
+ isSameKnowledgeTestIndex,
2563
4236
  stableStringify,
2564
- invalidateRuleSyncCooldown,
2565
- ensureRulesFresh,
2566
- reconcileRules,
2567
- appendRuleSelectionAuditEvent,
2568
- getRules,
2569
- normalizeRulesPath,
4237
+ invalidateKnowledgeSyncCooldown,
4238
+ ensureKnowledgeFresh,
4239
+ reconcileKnowledge,
4240
+ getKnowledge,
4241
+ normalizeKnowledgePath,
2570
4242
  runDoctorReport,
2571
- runDoctorFix
4243
+ runDoctorFix,
4244
+ runDoctorApplyLint
2572
4245
  };