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

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,20 @@ 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);
1315
1384
  const checks = [
1316
1385
  createBootstrapAnchorCheck(bootstrapAnchor),
1317
1386
  createKnowledgeDirMissingCheck(knowledgeDirMissing),
@@ -1327,7 +1396,7 @@ async function runDoctorReport(target) {
1327
1396
  // [MANDATORY_INJECTION] sections out of legacy rule files, a structural
1328
1397
  // concept that has no v2 equivalent. rc.4 will introduce a dedicated v2
1329
1398
  // lint suite for the new knowledge frontmatter contract.
1330
- createRuleTestIndexCheck(ruleTestIndex),
1399
+ createKnowledgeTestIndexCheck(knowledgeTestIndex),
1331
1400
  createEventLedgerCheck(eventLedger),
1332
1401
  createEventLedgerPartialWriteCheck(eventLedger),
1333
1402
  createMcpConfigInWrongFileCheck(mcpConfigInWrongFile),
@@ -1335,6 +1404,38 @@ async function runDoctorReport(target) {
1335
1404
  createKnowledgeDirUnindexedCheck(knowledgeDirUnindexed),
1336
1405
  createStableIdCollisionCheck(stableIdCollision),
1337
1406
  createCounterDesyncCheck(counterDesync),
1407
+ createFilesystemEditFallbackCheck(filesystemEditFallback),
1408
+ // rc.4 TASK-001: read-side lint checks #16-18. Findings only — mutation
1409
+ // + event emission lands in TASK-003 behind --apply-lint.
1410
+ createOrphanDemoteCheck(orphanDemote),
1411
+ createStaleArchiveCheck(staleArchive),
1412
+ createPendingOverdueCheck(pendingOverdue),
1413
+ // rc.4 TASK-002: read-side integrity checks #19-21. Stable_id duplicate
1414
+ // runs first in this trio — it is the most critical integrity break and
1415
+ // surfaces ahead of layer-mismatch / index-drift in the report so a
1416
+ // human operator triages the collision before reasoning about counter
1417
+ // state. Index drift is the only fixable_error of the three; stable_id
1418
+ // duplicate and layer mismatch require manual triage (rename / move).
1419
+ createStableIdDuplicateCheck(stableIdDuplicate),
1420
+ createLayerMismatchCheck(layerMismatch),
1421
+ createIndexDriftCheck(indexDrift),
1422
+ // rc.5 TASK-010: read-side underseeded-corpus check (#22). Info kind —
1423
+ // does not bump report status. Recommends running the fabric-import skill
1424
+ // to backfill knowledge when the corpus is below the threshold floor.
1425
+ createUnderseededCheck(underseeded),
1426
+ // rc.5 TASK-013 (C4): relevance_paths hygiene checks #23/#24/#25.
1427
+ // All three are flag-only in rc.5 (no apply-lint mutations).
1428
+ // #23 narrow_no_paths — warning kind (silent recall risk)
1429
+ // #24 relevance_paths_dangling — warning kind (glob → zero matches)
1430
+ // #25 relevance_paths_drift — info kind (git-log heuristic; noisy)
1431
+ createNarrowNoPathsCheck(narrowNoPaths),
1432
+ createRelevancePathsDanglingCheck(relevancePathsDangling),
1433
+ createRelevancePathsDriftCheck(relevancePathsDrift),
1434
+ // rc.6 TASK-023 (E6): narrow_too_few (lint #26). Info kind; both arms
1435
+ // (structural + telemetry) recommend the same fabric-import action.
1436
+ createNarrowTooFewCheck(narrowTooFew),
1437
+ // rc.6 TASK-021 (E3): session-hints cache hygiene (lint #27). Info kind.
1438
+ createSessionHintsStaleCheck(sessionHintsStale),
1338
1439
  createPreexistingRootFilesCheck(preexistingRootFiles)
1339
1440
  // v2.0 / rc.2: `createLegacyClientPathCheck` removed. The schema now
1340
1441
  // rejects retired clientPaths keys (windsurf/rooCode/geminiCLI) at Zod
@@ -1401,19 +1502,19 @@ async function runDoctorFix(target) {
1401
1502
  (issue) => [
1402
1503
  "agents_meta_missing",
1403
1504
  "agents_meta_stale",
1404
- "rule_test_index_missing",
1405
- "rule_test_index_stale",
1505
+ "knowledge_test_index_missing",
1506
+ "knowledge_test_index_stale",
1406
1507
  "content_ref_missing",
1407
1508
  "knowledge_dir_unindexed"
1408
1509
  ].includes(issue.code)
1409
1510
  )) {
1410
- await reconcileRules(projectRoot, { trigger: "doctor" });
1511
+ await reconcileKnowledge(projectRoot, { trigger: "doctor" });
1411
1512
  for (const issue of before.fixable_errors.filter(
1412
1513
  (candidate) => [
1413
1514
  "agents_meta_missing",
1414
1515
  "agents_meta_stale",
1415
- "rule_test_index_missing",
1416
- "rule_test_index_stale",
1516
+ "knowledge_test_index_missing",
1517
+ "knowledge_test_index_stale",
1417
1518
  "content_ref_missing",
1418
1519
  "knowledge_dir_unindexed"
1419
1520
  ].includes(candidate.code)
@@ -1449,6 +1550,376 @@ async function runDoctorFix(target) {
1449
1550
  report
1450
1551
  };
1451
1552
  }
1553
+ var MANUAL_LINT_ERROR_CODES = /* @__PURE__ */ new Set([
1554
+ "knowledge_stable_id_duplicate",
1555
+ "knowledge_layer_mismatch"
1556
+ ]);
1557
+ async function runDoctorApplyLint(target) {
1558
+ const projectRoot = normalizeTarget(target);
1559
+ const before = await runDoctorReport(projectRoot);
1560
+ const mutations = [];
1561
+ const blockingManual = before.manual_errors.find(
1562
+ (issue) => MANUAL_LINT_ERROR_CODES.has(issue.code)
1563
+ );
1564
+ if (blockingManual !== void 0) {
1565
+ return {
1566
+ changed: false,
1567
+ mutations: [],
1568
+ manual_errors: before.manual_errors,
1569
+ aborted: true,
1570
+ abort_reason: `Manual repair required for ${blockingManual.code}: ${blockingManual.message} - apply-lint cannot resolve this safely; triage by hand.`,
1571
+ message: `apply-lint aborted: ${blockingManual.code} requires manual repair.`,
1572
+ report: before
1573
+ };
1574
+ }
1575
+ const now = Date.now();
1576
+ const orphanDemote = await inspectOrphanDemote(projectRoot, now);
1577
+ for (const candidate of orphanDemote.candidates) {
1578
+ if (candidate.next_maturity === null) {
1579
+ continue;
1580
+ }
1581
+ mutations.push(await applyOrphanDemote(projectRoot, candidate, now));
1582
+ }
1583
+ const staleArchive = await inspectStaleArchive(projectRoot, now);
1584
+ for (const candidate of staleArchive.candidates) {
1585
+ mutations.push(await applyStaleArchive(projectRoot, candidate, now));
1586
+ }
1587
+ const pendingAutoArchive = inspectPendingAutoArchive(projectRoot, now);
1588
+ for (const candidate of pendingAutoArchive.candidates) {
1589
+ mutations.push(await applyPendingAutoArchive(projectRoot, candidate, now));
1590
+ }
1591
+ const sessionHintsStale = inspectSessionHintsStale(projectRoot, now);
1592
+ for (const candidate of sessionHintsStale.candidates) {
1593
+ mutations.push(await applySessionHintsStaleCleanup(projectRoot, candidate));
1594
+ }
1595
+ const meta = await inspectMeta(projectRoot);
1596
+ const indexDrift = inspectIndexDrift(projectRoot, meta);
1597
+ if (indexDrift.drifts.length > 0) {
1598
+ mutations.push(await applyIndexDriftFix(projectRoot, indexDrift));
1599
+ }
1600
+ contextCache.invalidate("meta_write", projectRoot);
1601
+ const after = await runDoctorReport(projectRoot);
1602
+ const successCount = mutations.filter((m) => m.applied).length;
1603
+ const failureCount = mutations.length - successCount;
1604
+ return {
1605
+ changed: successCount > 0,
1606
+ mutations,
1607
+ manual_errors: after.manual_errors,
1608
+ aborted: false,
1609
+ message: createApplyLintMessage(successCount, failureCount, after.manual_errors.length),
1610
+ report: after
1611
+ };
1612
+ }
1613
+ function createApplyLintMessage(succeeded, failed, manualErrorCount) {
1614
+ const parts = [];
1615
+ if (succeeded === 0 && failed === 0) {
1616
+ parts.push("No apply-lint mutations were needed.");
1617
+ } else {
1618
+ parts.push(`Applied ${succeeded} apply-lint mutation${succeeded === 1 ? "" : "s"}.`);
1619
+ if (failed > 0) {
1620
+ parts.push(`${failed} mutation${failed === 1 ? "" : "s"} failed.`);
1621
+ }
1622
+ }
1623
+ parts.push(
1624
+ manualErrorCount === 0 ? "No manual errors remain." : `${manualErrorCount} manual error${manualErrorCount === 1 ? "" : "s"} remain.`
1625
+ );
1626
+ return parts.join(" ");
1627
+ }
1628
+ function rewriteFrontmatterMaturity(source, newMaturity) {
1629
+ const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
1630
+ const fm = FM_PATTERN.exec(source);
1631
+ if (fm === null) {
1632
+ return null;
1633
+ }
1634
+ const block = fm[1];
1635
+ if (!MATURITY_LINE_PATTERN.test(block)) {
1636
+ return null;
1637
+ }
1638
+ const replacedBlock = block.replace(
1639
+ MATURITY_LINE_PATTERN,
1640
+ (line) => line.replace(/(stable|endorsed|draft)/u, newMaturity)
1641
+ );
1642
+ const blockStart = source.indexOf(block);
1643
+ if (blockStart < 0) {
1644
+ return null;
1645
+ }
1646
+ return source.slice(0, blockStart) + replacedBlock + source.slice(blockStart + block.length);
1647
+ }
1648
+ async function applyOrphanDemote(projectRoot, candidate, now) {
1649
+ const next = candidate.next_maturity;
1650
+ if (next === null) {
1651
+ return {
1652
+ kind: "knowledge_orphan_demote_required",
1653
+ path: candidate.path,
1654
+ detail: `${candidate.maturity} -> (none, already at terminal tier)`,
1655
+ applied: false,
1656
+ error: "next_maturity is null; orphan-demote not applicable"
1657
+ };
1658
+ }
1659
+ const detail = `${candidate.maturity} -> ${next}`;
1660
+ const absPath = join5(projectRoot, candidate.path);
1661
+ try {
1662
+ const source = await readFile5(absPath, "utf8");
1663
+ const rewritten = rewriteFrontmatterMaturity(source, next);
1664
+ if (rewritten === null) {
1665
+ return {
1666
+ kind: "knowledge_orphan_demote_required",
1667
+ path: candidate.path,
1668
+ detail,
1669
+ applied: false,
1670
+ error: "frontmatter missing maturity field; cannot rewrite"
1671
+ };
1672
+ }
1673
+ if (rewritten === source) {
1674
+ return {
1675
+ kind: "knowledge_orphan_demote_required",
1676
+ path: candidate.path,
1677
+ detail,
1678
+ applied: false,
1679
+ error: "rewrite produced byte-identical output"
1680
+ };
1681
+ }
1682
+ await atomicWriteText3(absPath, rewritten);
1683
+ try {
1684
+ await appendEventLedgerEvent(projectRoot, {
1685
+ event_type: "knowledge_demoted",
1686
+ stable_id: candidate.stable_id,
1687
+ timestamp: new Date(now).toISOString(),
1688
+ reason: `lint:orphan_demote ${candidate.maturity}->${next} after ${candidate.age_days}d inactive`
1689
+ });
1690
+ } catch (ledgerError) {
1691
+ try {
1692
+ await atomicWriteText3(absPath, source);
1693
+ } catch (rollbackError) {
1694
+ return {
1695
+ kind: "knowledge_orphan_demote_required",
1696
+ path: candidate.path,
1697
+ detail,
1698
+ applied: false,
1699
+ error: `ledger append failed (${truncateErrorMessage(ledgerError)}); rollback also failed (${truncateErrorMessage(rollbackError)}); disk may be in inconsistent state`
1700
+ };
1701
+ }
1702
+ return {
1703
+ kind: "knowledge_orphan_demote_required",
1704
+ path: candidate.path,
1705
+ detail,
1706
+ applied: false,
1707
+ error: `ledger append failed (${truncateErrorMessage(ledgerError)}); frontmatter rolled back`
1708
+ };
1709
+ }
1710
+ return {
1711
+ kind: "knowledge_orphan_demote_required",
1712
+ path: candidate.path,
1713
+ detail,
1714
+ applied: true
1715
+ };
1716
+ } catch (error) {
1717
+ return {
1718
+ kind: "knowledge_orphan_demote_required",
1719
+ path: candidate.path,
1720
+ detail,
1721
+ applied: false,
1722
+ error: truncateErrorMessage(error)
1723
+ };
1724
+ }
1725
+ }
1726
+ async function applyStaleArchive(projectRoot, candidate, now) {
1727
+ const sourceAbs = join5(projectRoot, candidate.path);
1728
+ const destAbs = join5(projectRoot, candidate.archive_path);
1729
+ const detail = `${candidate.path} -> ${candidate.archive_path}`;
1730
+ try {
1731
+ await mkdir3(join5(destAbs, ".."), { recursive: true });
1732
+ try {
1733
+ await rename(sourceAbs, destAbs);
1734
+ } catch (renameError) {
1735
+ if (renameError instanceof Error && "code" in renameError && renameError.code === "EXDEV") {
1736
+ const data = await readFile5(sourceAbs);
1737
+ await writeFile2(destAbs, data);
1738
+ const { unlink } = await import("fs/promises");
1739
+ await unlink(sourceAbs);
1740
+ } else {
1741
+ throw renameError;
1742
+ }
1743
+ }
1744
+ try {
1745
+ await appendEventLedgerEvent(projectRoot, {
1746
+ event_type: "knowledge_archived",
1747
+ stable_id: candidate.stable_id,
1748
+ timestamp: new Date(now).toISOString(),
1749
+ reason: `lint:stale_archive ${candidate.path} -> ${candidate.archive_path} after ${candidate.age_days}d inactive`
1750
+ });
1751
+ } catch (ledgerError) {
1752
+ try {
1753
+ await rename(destAbs, sourceAbs);
1754
+ } catch (rollbackError) {
1755
+ return {
1756
+ kind: "knowledge_stale_archive_required",
1757
+ path: candidate.path,
1758
+ detail,
1759
+ applied: false,
1760
+ error: `ledger append failed (${truncateErrorMessage(ledgerError)}); rollback also failed (${truncateErrorMessage(rollbackError)}); file may be stranded at ${candidate.archive_path}`
1761
+ };
1762
+ }
1763
+ return {
1764
+ kind: "knowledge_stale_archive_required",
1765
+ path: candidate.path,
1766
+ detail,
1767
+ applied: false,
1768
+ error: `ledger append failed (${truncateErrorMessage(ledgerError)}); archive rolled back`
1769
+ };
1770
+ }
1771
+ return {
1772
+ kind: "knowledge_stale_archive_required",
1773
+ path: candidate.path,
1774
+ detail,
1775
+ applied: true
1776
+ };
1777
+ } catch (error) {
1778
+ return {
1779
+ kind: "knowledge_stale_archive_required",
1780
+ path: candidate.path,
1781
+ detail,
1782
+ applied: false,
1783
+ error: truncateErrorMessage(error)
1784
+ };
1785
+ }
1786
+ }
1787
+ async function applyPendingAutoArchive(projectRoot, candidate, now) {
1788
+ const detail = `${candidate.pending_path} -> ${candidate.archived_to}`;
1789
+ try {
1790
+ await mkdir3(join5(candidate.archived_to_abs, ".."), { recursive: true });
1791
+ let moved = false;
1792
+ if (candidate.layer === "team") {
1793
+ try {
1794
+ const relSource = relativePosix(projectRoot, candidate.pending_path_abs);
1795
+ const relDest = relativePosix(projectRoot, candidate.archived_to_abs);
1796
+ execFileSync("git", ["mv", "-f", relSource, relDest], {
1797
+ cwd: projectRoot,
1798
+ stdio: ["ignore", "pipe", "pipe"]
1799
+ });
1800
+ moved = true;
1801
+ } catch {
1802
+ }
1803
+ }
1804
+ if (!moved) {
1805
+ try {
1806
+ await rename(candidate.pending_path_abs, candidate.archived_to_abs);
1807
+ } catch (renameError) {
1808
+ if (renameError instanceof Error && "code" in renameError && renameError.code === "EXDEV") {
1809
+ const data = await readFile5(candidate.pending_path_abs);
1810
+ await writeFile2(candidate.archived_to_abs, data);
1811
+ const { unlink } = await import("fs/promises");
1812
+ await unlink(candidate.pending_path_abs);
1813
+ } else {
1814
+ throw renameError;
1815
+ }
1816
+ }
1817
+ }
1818
+ try {
1819
+ await appendEventLedgerEvent(projectRoot, {
1820
+ event_type: "pending_auto_archived",
1821
+ pending_path: candidate.pending_path,
1822
+ archived_to: candidate.archived_to,
1823
+ reason: "auto_archive_30d"
1824
+ });
1825
+ } catch (ledgerError) {
1826
+ try {
1827
+ await rename(candidate.archived_to_abs, candidate.pending_path_abs);
1828
+ } catch (rollbackError) {
1829
+ return {
1830
+ kind: "knowledge_pending_auto_archive",
1831
+ path: candidate.pending_path,
1832
+ detail,
1833
+ applied: false,
1834
+ error: `ledger append failed (${truncateErrorMessage(ledgerError)}); rollback also failed (${truncateErrorMessage(rollbackError)}); file may be stranded at ${candidate.archived_to}`
1835
+ };
1836
+ }
1837
+ return {
1838
+ kind: "knowledge_pending_auto_archive",
1839
+ path: candidate.pending_path,
1840
+ detail,
1841
+ applied: false,
1842
+ error: `ledger append failed (${truncateErrorMessage(ledgerError)}); archive rolled back`
1843
+ };
1844
+ }
1845
+ return {
1846
+ kind: "knowledge_pending_auto_archive",
1847
+ path: candidate.pending_path,
1848
+ detail,
1849
+ applied: true
1850
+ };
1851
+ } catch (error) {
1852
+ return {
1853
+ kind: "knowledge_pending_auto_archive",
1854
+ path: candidate.pending_path,
1855
+ detail,
1856
+ applied: false,
1857
+ error: truncateErrorMessage(error)
1858
+ };
1859
+ }
1860
+ }
1861
+ function relativePosix(projectRoot, absolutePath) {
1862
+ const rel = nodeRelative(projectRoot, absolutePath);
1863
+ return rel.split(sep4).join("/");
1864
+ }
1865
+ async function applySessionHintsStaleCleanup(projectRoot, candidate) {
1866
+ const detail = `deleted (${candidate.age_days}d old)`;
1867
+ const absPath = join5(projectRoot, candidate.path);
1868
+ try {
1869
+ const { unlink } = await import("fs/promises");
1870
+ await unlink(absPath);
1871
+ return {
1872
+ kind: "knowledge_session_hints_stale_cleanup",
1873
+ path: candidate.path,
1874
+ detail,
1875
+ applied: true
1876
+ };
1877
+ } catch (error) {
1878
+ return {
1879
+ kind: "knowledge_session_hints_stale_cleanup",
1880
+ path: candidate.path,
1881
+ detail,
1882
+ applied: false,
1883
+ error: truncateErrorMessage(error)
1884
+ };
1885
+ }
1886
+ }
1887
+ async function applyIndexDriftFix(projectRoot, inspection) {
1888
+ const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
1889
+ const detailParts = [];
1890
+ try {
1891
+ const meta = agentsMetaSchema4.parse(JSON.parse(await readFile5(metaPath, "utf8")));
1892
+ const baseCounters = AgentsMetaCountersSchema.parse(meta.counters ?? void 0);
1893
+ const updatedCounters = {
1894
+ KP: { ...baseCounters.KP },
1895
+ KT: { ...baseCounters.KT }
1896
+ };
1897
+ for (const drift of inspection.drifts) {
1898
+ updatedCounters[drift.layer][drift.type] = drift.proposed_after;
1899
+ detailParts.push(`${drift.layer}.${drift.type}: ${drift.counter} -> ${drift.proposed_after}`);
1900
+ }
1901
+ const updated = { ...meta, counters: updatedCounters };
1902
+ await atomicWriteJson2(metaPath, updated, { indent: 2 });
1903
+ return {
1904
+ kind: "knowledge_index_drift",
1905
+ path: "agents.meta.json#counters",
1906
+ detail: detailParts.join("; "),
1907
+ applied: true
1908
+ };
1909
+ } catch (error) {
1910
+ return {
1911
+ kind: "knowledge_index_drift",
1912
+ path: "agents.meta.json#counters",
1913
+ detail: detailParts.join("; ") || "(no counters processed)",
1914
+ applied: false,
1915
+ error: truncateErrorMessage(error)
1916
+ };
1917
+ }
1918
+ }
1919
+ function truncateErrorMessage(error) {
1920
+ const raw = error instanceof Error ? error.message : String(error);
1921
+ return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
1922
+ }
1452
1923
  async function inspectForensic(projectRoot) {
1453
1924
  const path = join5(projectRoot, ".fabric", "forensic.json");
1454
1925
  try {
@@ -1537,7 +2008,7 @@ async function inspectMeta(projectRoot) {
1537
2008
  }
1538
2009
  async function tryBuildRuleMeta(projectRoot) {
1539
2010
  try {
1540
- return await buildRuleMeta(projectRoot);
2011
+ return await buildKnowledgeMeta(projectRoot);
1541
2012
  } catch {
1542
2013
  return null;
1543
2014
  }
@@ -1597,15 +2068,15 @@ async function inspectEventLedger(projectRoot) {
1597
2068
  };
1598
2069
  }
1599
2070
  }
1600
- async function inspectRuleTestIndex(projectRoot) {
1601
- const path = join5(projectRoot, ".fabric", "rule-test.index.json");
2071
+ async function inspectKnowledgeTestIndex(projectRoot) {
2072
+ const path = join5(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
1602
2073
  const built = await tryBuildRuleMeta(projectRoot);
1603
2074
  try {
1604
- const index = ruleTestIndexSchema2.parse(JSON.parse(await readFile5(path, "utf8")));
2075
+ const index = knowledgeTestIndexSchema2.parse(JSON.parse(await readFile5(path, "utf8")));
1605
2076
  return {
1606
2077
  present: true,
1607
2078
  valid: true,
1608
- stale: built === null ? false : !isSameRuleTestIndex(index, built.ruleTestIndex),
2079
+ stale: built === null ? false : !isSameKnowledgeTestIndex(index, built.knowledgeTestIndex),
1609
2080
  linkCount: index.links.length,
1610
2081
  orphanCount: index.orphan_annotations.length
1611
2082
  };
@@ -1616,7 +2087,7 @@ async function inspectRuleTestIndex(projectRoot) {
1616
2087
  stale: true,
1617
2088
  linkCount: 0,
1618
2089
  orphanCount: 0,
1619
- error: isMissingFileError(error) ? ".fabric/rule-test.index.json is missing." : error instanceof Error ? error.message : String(error)
2090
+ error: isMissingFileError(error) ? ".fabric/.cache/knowledge-test.index.json is missing." : error instanceof Error ? error.message : String(error)
1620
2091
  };
1621
2092
  }
1622
2093
  }
@@ -1732,17 +2203,17 @@ function createRuleContentRefCheck(meta) {
1732
2203
  }
1733
2204
  return okCheck("Rule content refs", "All content_ref entries resolve to .fabric/knowledge files.");
1734
2205
  }
1735
- function createRuleTestIndexCheck(index) {
2206
+ function createKnowledgeTestIndexCheck(index) {
1736
2207
  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.");
2208
+ 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
2209
  }
1739
2210
  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.");
2211
+ 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
2212
  }
1742
2213
  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.");
2214
+ 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
2215
  }
1745
- return okCheck("Rule-test index", `${index.linkCount} link${index.linkCount === 1 ? "" : "s"} and ${index.orphanCount} orphan annotation${index.orphanCount === 1 ? "" : "s"} indexed.`);
2216
+ return okCheck("Knowledge-test index", `${index.linkCount} link${index.linkCount === 1 ? "" : "s"} and ${index.orphanCount} orphan annotation${index.orphanCount === 1 ? "" : "s"} indexed.`);
1746
2217
  }
1747
2218
  function createEventLedgerCheck(ledger) {
1748
2219
  if (!ledger.exists) {
@@ -2071,6 +2542,83 @@ function inspectPreexistingRootFiles(projectRoot) {
2071
2542
  const detected = candidates.filter((name) => existsSync4(join5(projectRoot, name)));
2072
2543
  return { detected };
2073
2544
  }
2545
+ async function inspectFilesystemEditFallback(projectRoot) {
2546
+ const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
2547
+ if (!existsSync4(knowledgeRoot)) {
2548
+ return { synthesized: 0, synthesizedStableIds: [] };
2549
+ }
2550
+ const canonicalIds = /* @__PURE__ */ new Set();
2551
+ for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
2552
+ const dir = join5(knowledgeRoot, typeDir);
2553
+ if (!existsSync4(dir)) {
2554
+ continue;
2555
+ }
2556
+ let entries;
2557
+ try {
2558
+ entries = readdirSync(dir, { withFileTypes: true });
2559
+ } catch {
2560
+ continue;
2561
+ }
2562
+ for (const entry of entries) {
2563
+ if (!entry.isFile()) {
2564
+ continue;
2565
+ }
2566
+ const match = CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(entry.name);
2567
+ if (match === null) {
2568
+ continue;
2569
+ }
2570
+ canonicalIds.add(match[1]);
2571
+ }
2572
+ }
2573
+ if (canonicalIds.size === 0) {
2574
+ return { synthesized: 0, synthesizedStableIds: [] };
2575
+ }
2576
+ let promotedIds = /* @__PURE__ */ new Set();
2577
+ try {
2578
+ const { events } = await readEventLedger(projectRoot, { event_type: "knowledge_promoted" });
2579
+ promotedIds = new Set(
2580
+ events.map((event) => event.event_type === "knowledge_promoted" ? event.stable_id : void 0).filter((id) => typeof id === "string")
2581
+ );
2582
+ } catch {
2583
+ promotedIds = /* @__PURE__ */ new Set();
2584
+ }
2585
+ const orphanIds = [];
2586
+ for (const id of canonicalIds) {
2587
+ if (!promotedIds.has(id)) {
2588
+ orphanIds.push(id);
2589
+ }
2590
+ }
2591
+ orphanIds.sort();
2592
+ for (const stable_id of orphanIds) {
2593
+ await appendEventLedgerEvent(projectRoot, {
2594
+ event_type: "knowledge_promoted",
2595
+ stable_id,
2596
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2597
+ reason: SYNTHESIZED_PROMOTED_REASON,
2598
+ correlation_id: "doctor-synthesized",
2599
+ session_id: "doctor-synthesized"
2600
+ });
2601
+ }
2602
+ return { synthesized: orphanIds.length, synthesizedStableIds: orphanIds };
2603
+ }
2604
+ function createFilesystemEditFallbackCheck(inspection) {
2605
+ if (inspection.synthesized === 0) {
2606
+ return okCheck(
2607
+ "Filesystem-edit fallback",
2608
+ "No orphan canonical knowledge entries detected; events.jsonl promotion trail is complete."
2609
+ );
2610
+ }
2611
+ const sample = inspection.synthesizedStableIds.slice(0, 3).join(", ");
2612
+ return {
2613
+ name: "Filesystem-edit fallback",
2614
+ status: "ok",
2615
+ kind: "info",
2616
+ code: "knowledge_promoted_synthesized",
2617
+ fixable: false,
2618
+ message: `Synthesized ${inspection.synthesized} knowledge_promoted event${inspection.synthesized === 1 ? "" : "s"} for orphan canonical entries (${sample}${inspection.synthesizedStableIds.length > 3 ? ", ..." : ""}). Reason='${SYNTHESIZED_PROMOTED_REASON}'.`,
2619
+ actionHint: "These entries were moved into .fabric/knowledge/<type>/ outside fab_review.approve. The synthesized events restore audit-trail completeness."
2620
+ };
2621
+ }
2074
2622
  function createPreexistingRootFilesCheck(inspection) {
2075
2623
  if (inspection.detected.length === 0) {
2076
2624
  return okCheck("Preexisting root markdown", "No CLAUDE.md or AGENTS.md detected at project root.");
@@ -2085,85 +2633,1130 @@ function createPreexistingRootFilesCheck(inspection) {
2085
2633
  actionHint: "Move knowledge content to `.fabric/knowledge/{type}/` if you want it available in MCP responses."
2086
2634
  };
2087
2635
  }
2088
- async function fixMcpConfigInWrongFile(projectRoot) {
2089
- const settingsPath = join5(projectRoot, ".claude", "settings.json");
2090
- if (!existsSync4(settingsPath)) {
2091
- return;
2092
- }
2093
- let settings;
2636
+ async function buildLastConsumedIndex(projectRoot) {
2637
+ const map = /* @__PURE__ */ new Map();
2638
+ let events;
2094
2639
  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;
2640
+ ({ events } = await readEventLedger(projectRoot));
2100
2641
  } 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;
2642
+ return map;
2113
2643
  }
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 });
2644
+ for (const event of events) {
2645
+ if (event.event_type !== "knowledge_consumed" && event.event_type !== "knowledge_demoted" && event.event_type !== "knowledge_archived") {
2646
+ continue;
2647
+ }
2648
+ const ts = event.ts;
2649
+ if (typeof ts !== "number" || !Number.isFinite(ts)) {
2650
+ continue;
2651
+ }
2652
+ const stableId = event.stable_id;
2653
+ if (typeof stableId !== "string" || stableId.length === 0) {
2654
+ continue;
2655
+ }
2656
+ const prev = map.get(stableId);
2657
+ if (prev === void 0 || ts > prev) {
2658
+ map.set(stableId, ts);
2659
+ }
2124
2660
  }
2661
+ return map;
2125
2662
  }
2126
- async function fixCounterDesync(projectRoot) {
2127
- const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
2128
- if (!existsSync4(metaPath)) {
2129
- return;
2130
- }
2131
- let meta;
2663
+ async function buildLastActiveIndex(projectRoot) {
2664
+ const map = /* @__PURE__ */ new Map();
2665
+ let events;
2132
2666
  try {
2133
- meta = agentsMetaSchema4.parse(JSON.parse(await readFile5(metaPath, "utf8")));
2667
+ ({ events } = await readEventLedger(projectRoot));
2134
2668
  } catch {
2135
- return;
2669
+ return map;
2136
2670
  }
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;
2671
+ for (const event of events) {
2672
+ const ts = event.ts;
2673
+ if (typeof ts !== "number" || !Number.isFinite(ts)) {
2674
+ continue;
2675
+ }
2676
+ const ids = [];
2677
+ switch (event.event_type) {
2678
+ case "knowledge_proposed":
2679
+ case "knowledge_promote_started":
2680
+ case "knowledge_promoted":
2681
+ case "knowledge_promote_failed":
2682
+ case "knowledge_layer_changed":
2683
+ case "knowledge_slug_renamed":
2684
+ case "knowledge_demoted":
2685
+ case "knowledge_archived":
2686
+ case "knowledge_archive_attempted":
2687
+ case "knowledge_deferred":
2688
+ case "knowledge_rejected": {
2689
+ if (typeof event.stable_id === "string" && event.stable_id.length > 0) {
2690
+ ids.push(event.stable_id);
2691
+ }
2692
+ break;
2693
+ }
2694
+ case "knowledge_context_planned": {
2695
+ ids.push(...event.required_stable_ids, ...event.ai_selectable_stable_ids, ...event.final_stable_ids);
2696
+ break;
2697
+ }
2698
+ case "knowledge_selection": {
2699
+ ids.push(
2700
+ ...event.required_stable_ids,
2701
+ ...event.ai_selectable_stable_ids,
2702
+ ...event.ai_selected_stable_ids,
2703
+ ...event.final_stable_ids
2704
+ );
2705
+ break;
2706
+ }
2707
+ case "knowledge_sections_fetched": {
2708
+ ids.push(...event.final_stable_ids, ...event.ai_selected_stable_ids);
2709
+ break;
2710
+ }
2711
+ default:
2712
+ break;
2713
+ }
2714
+ for (const id of ids) {
2715
+ const prev = map.get(id);
2716
+ if (prev === void 0 || ts > prev) {
2717
+ map.set(id, ts);
2718
+ }
2719
+ }
2152
2720
  }
2153
- const updated = { ...meta, counters: desync.correctedCounters };
2154
- await atomicWriteJson2(metaPath, updated, { indent: 2 });
2721
+ return map;
2155
2722
  }
2156
- async function ensureEventLedger(projectRoot) {
2157
- const path = getEventLedgerPath(projectRoot);
2158
- await ensureParentDirectory(path);
2159
- await writeFile2(path, "", { encoding: "utf8", flag: "a" });
2723
+ function maturityThresholdDays(maturity) {
2724
+ return ORPHAN_DEMOTE_THRESHOLD_DAYS[maturity];
2160
2725
  }
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.`;
2164
- return `${fixedText} ${manualText}`;
2726
+ function nextLowerMaturity(current) {
2727
+ if (current === "stable") return "endorsed";
2728
+ if (current === "endorsed") return "draft";
2729
+ return null;
2165
2730
  }
2166
- function isValidJsonLine(line) {
2731
+ function extractKnowledgeFrontmatterMaturity(source) {
2732
+ const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
2733
+ const fm = FM_PATTERN.exec(source);
2734
+ if (fm === null) {
2735
+ return null;
2736
+ }
2737
+ const match = MATURITY_LINE_PATTERN.exec(fm[1]);
2738
+ return match === null ? null : match[2];
2739
+ }
2740
+ function extractKnowledgeFrontmatterCreatedAt(source) {
2741
+ const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
2742
+ const fm = FM_PATTERN.exec(source);
2743
+ if (fm === null) {
2744
+ return null;
2745
+ }
2746
+ const match = CREATED_AT_LINE_PATTERN.exec(fm[1]);
2747
+ if (match === null) {
2748
+ return null;
2749
+ }
2750
+ const parsed = Date.parse(match[2]);
2751
+ return Number.isFinite(parsed) ? parsed : null;
2752
+ }
2753
+ function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
2754
+ const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
2755
+ if (!existsSync4(knowledgeRoot)) {
2756
+ return;
2757
+ }
2758
+ for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
2759
+ const dir = join5(knowledgeRoot, typeDir);
2760
+ if (!existsSync4(dir)) {
2761
+ continue;
2762
+ }
2763
+ let entries;
2764
+ try {
2765
+ entries = readdirSync(dir, { withFileTypes: true });
2766
+ } catch {
2767
+ continue;
2768
+ }
2769
+ for (const entry of entries) {
2770
+ if (!entry.isFile()) {
2771
+ continue;
2772
+ }
2773
+ const match = CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(entry.name);
2774
+ if (match === null) {
2775
+ continue;
2776
+ }
2777
+ const stableId = match[1];
2778
+ const absPath = join5(dir, entry.name);
2779
+ let source;
2780
+ try {
2781
+ source = readFileSync(absPath, "utf8");
2782
+ } catch {
2783
+ continue;
2784
+ }
2785
+ const maturity = extractKnowledgeFrontmatterMaturity(source);
2786
+ if (maturity === null) {
2787
+ continue;
2788
+ }
2789
+ const createdAt = extractKnowledgeFrontmatterCreatedAt(source);
2790
+ const eventTs = lastActiveIndex.get(stableId) ?? 0;
2791
+ let lastReferenceMs = Math.max(createdAt ?? 0, eventTs);
2792
+ if (lastReferenceMs === 0) {
2793
+ try {
2794
+ lastReferenceMs = statSync3(absPath).mtimeMs;
2795
+ } catch {
2796
+ lastReferenceMs = 0;
2797
+ }
2798
+ }
2799
+ const relPath = posix.join(
2800
+ ".fabric/knowledge",
2801
+ typeDir,
2802
+ entry.name
2803
+ );
2804
+ yield { stable_id: stableId, maturity, type: typeDir, absPath, relPath, lastReferenceMs };
2805
+ }
2806
+ }
2807
+ }
2808
+ async function inspectOrphanDemote(projectRoot, now) {
2809
+ const lastConsumedIndex = await buildLastConsumedIndex(projectRoot);
2810
+ const candidates = [];
2811
+ for (const entry of iterateCanonicalEntries(projectRoot, lastConsumedIndex)) {
2812
+ const ageMs = entry.lastReferenceMs > 0 ? now - entry.lastReferenceMs : now;
2813
+ const ageDays = Math.floor(ageMs / MS_PER_DAY);
2814
+ const threshold = maturityThresholdDays(entry.maturity);
2815
+ if (ageDays <= threshold) {
2816
+ continue;
2817
+ }
2818
+ candidates.push({
2819
+ stable_id: entry.stable_id,
2820
+ path: entry.relPath,
2821
+ age_days: ageDays,
2822
+ maturity: entry.maturity,
2823
+ next_maturity: nextLowerMaturity(entry.maturity)
2824
+ });
2825
+ }
2826
+ candidates.sort((a, b) => a.path.localeCompare(b.path));
2827
+ return { candidates };
2828
+ }
2829
+ async function inspectStaleArchive(projectRoot, now) {
2830
+ const lastActiveIndex = await buildLastActiveIndex(projectRoot);
2831
+ const candidates = [];
2832
+ for (const entry of iterateCanonicalEntries(projectRoot, lastActiveIndex)) {
2833
+ if (entry.maturity !== "draft") {
2834
+ continue;
2835
+ }
2836
+ const ageMs = entry.lastReferenceMs > 0 ? now - entry.lastReferenceMs : now;
2837
+ const ageDays = Math.floor(ageMs / MS_PER_DAY);
2838
+ const requiredQuiet = ORPHAN_DEMOTE_THRESHOLD_DAYS.draft + STALE_ARCHIVE_ADDITIONAL_DAYS;
2839
+ if (ageDays <= requiredQuiet) {
2840
+ continue;
2841
+ }
2842
+ const filename = posix.basename(entry.relPath);
2843
+ candidates.push({
2844
+ stable_id: entry.stable_id,
2845
+ path: entry.relPath,
2846
+ age_days: ageDays,
2847
+ archive_path: posix.join(".fabric/.archive", entry.type, filename)
2848
+ });
2849
+ }
2850
+ candidates.sort((a, b) => a.path.localeCompare(b.path));
2851
+ return { candidates };
2852
+ }
2853
+ function* iteratePendingFiles(projectRoot, now) {
2854
+ const teamRoot = join5(projectRoot, ".fabric", "knowledge", "pending");
2855
+ const personalRoot = join5(resolvePersonalRootForPending(), ".fabric", "knowledge", "pending");
2856
+ for (const [layer, root, displayPrefix] of [
2857
+ ["team", teamRoot, ".fabric/knowledge/pending"],
2858
+ ["personal", personalRoot, "~/.fabric/knowledge/pending"]
2859
+ ]) {
2860
+ if (!existsSync4(root)) {
2861
+ continue;
2862
+ }
2863
+ let typeDirs = [];
2864
+ try {
2865
+ typeDirs = readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
2866
+ } catch {
2867
+ continue;
2868
+ }
2869
+ for (const typeDir of typeDirs) {
2870
+ const dir = join5(root, typeDir);
2871
+ let entries;
2872
+ try {
2873
+ entries = readdirSync(dir, { withFileTypes: true });
2874
+ } catch {
2875
+ continue;
2876
+ }
2877
+ for (const entry of entries) {
2878
+ if (!entry.isFile() || !entry.name.endsWith(".md")) {
2879
+ continue;
2880
+ }
2881
+ const absPath = join5(dir, entry.name);
2882
+ let source = "";
2883
+ try {
2884
+ source = readFileSync(absPath, "utf8");
2885
+ } catch {
2886
+ continue;
2887
+ }
2888
+ const createdAt = extractKnowledgeFrontmatterCreatedAt(source);
2889
+ let mtimeMs = 0;
2890
+ try {
2891
+ mtimeMs = statSync3(absPath).mtimeMs;
2892
+ } catch {
2893
+ mtimeMs = 0;
2894
+ }
2895
+ const referenceMs = createdAt ?? mtimeMs;
2896
+ const displayPath = posix.join(displayPrefix, typeDir, entry.name);
2897
+ if (referenceMs === 0) {
2898
+ yield {
2899
+ layer,
2900
+ type: typeDir,
2901
+ filename: entry.name,
2902
+ pending_path: displayPath,
2903
+ pending_path_abs: absPath,
2904
+ stable_id: void 0,
2905
+ age_days: PENDING_OVERDUE_THRESHOLD_DAYS + 1
2906
+ };
2907
+ continue;
2908
+ }
2909
+ const ageDays = Math.floor((now - referenceMs) / MS_PER_DAY);
2910
+ const stableId = extractKnowledgeFrontmatterId(source) ?? void 0;
2911
+ yield {
2912
+ layer,
2913
+ type: typeDir,
2914
+ filename: entry.name,
2915
+ pending_path: displayPath,
2916
+ pending_path_abs: absPath,
2917
+ stable_id: stableId,
2918
+ age_days: ageDays
2919
+ };
2920
+ }
2921
+ }
2922
+ }
2923
+ }
2924
+ function resolvePersonalRootForPending() {
2925
+ return process.env.FABRIC_HOME ?? homedir2();
2926
+ }
2927
+ function inspectPendingOverdue(projectRoot, now) {
2928
+ const candidates = [];
2929
+ for (const visit of iteratePendingFiles(projectRoot, now)) {
2930
+ if (visit.age_days <= PENDING_OVERDUE_THRESHOLD_DAYS) {
2931
+ continue;
2932
+ }
2933
+ candidates.push({
2934
+ stable_id: visit.stable_id,
2935
+ path: visit.pending_path,
2936
+ age_days: visit.age_days
2937
+ });
2938
+ }
2939
+ candidates.sort((a, b) => a.path.localeCompare(b.path));
2940
+ return { candidates };
2941
+ }
2942
+ function inspectPendingAutoArchive(projectRoot, now) {
2943
+ const candidates = [];
2944
+ for (const visit of iteratePendingFiles(projectRoot, now)) {
2945
+ if (visit.age_days <= PENDING_AUTO_ARCHIVE_THRESHOLD_DAYS) {
2946
+ continue;
2947
+ }
2948
+ if (visit.layer === "team") {
2949
+ const archivedToRel = posix.join(".fabric/.archive/pending", visit.type, visit.filename);
2950
+ candidates.push({
2951
+ layer: "team",
2952
+ type: visit.type,
2953
+ pending_path: visit.pending_path,
2954
+ pending_path_abs: visit.pending_path_abs,
2955
+ archived_to: archivedToRel,
2956
+ archived_to_abs: join5(projectRoot, archivedToRel),
2957
+ age_days: visit.age_days
2958
+ });
2959
+ } else {
2960
+ const archivedToDisplay = posix.join(
2961
+ "~/.fabric/.archive/pending",
2962
+ visit.type,
2963
+ visit.filename
2964
+ );
2965
+ const archivedToAbs = join5(
2966
+ resolvePersonalRootForPending(),
2967
+ ".fabric",
2968
+ ".archive",
2969
+ "pending",
2970
+ visit.type,
2971
+ visit.filename
2972
+ );
2973
+ candidates.push({
2974
+ layer: "personal",
2975
+ type: visit.type,
2976
+ pending_path: visit.pending_path,
2977
+ pending_path_abs: visit.pending_path_abs,
2978
+ archived_to: archivedToDisplay,
2979
+ archived_to_abs: archivedToAbs,
2980
+ age_days: visit.age_days
2981
+ });
2982
+ }
2983
+ }
2984
+ candidates.sort((a, b) => a.pending_path.localeCompare(b.pending_path));
2985
+ return { candidates };
2986
+ }
2987
+ function inspectUnderseeded(projectRoot) {
2988
+ const threshold = readUnderseedThresholdFromConfig(projectRoot);
2989
+ const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
2990
+ let nodeCount = 0;
2991
+ if (existsSync4(knowledgeRoot)) {
2992
+ for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
2993
+ const dir = join5(knowledgeRoot, typeDir);
2994
+ if (!existsSync4(dir)) continue;
2995
+ let entries;
2996
+ try {
2997
+ entries = readdirSync(dir, { withFileTypes: true });
2998
+ } catch {
2999
+ continue;
3000
+ }
3001
+ for (const entry of entries) {
3002
+ if (entry.isFile() && entry.name.endsWith(".md")) {
3003
+ nodeCount += 1;
3004
+ }
3005
+ }
3006
+ }
3007
+ }
3008
+ return {
3009
+ node_count: nodeCount,
3010
+ threshold,
3011
+ underseeded: nodeCount < threshold
3012
+ };
3013
+ }
3014
+ function inspectSessionHintsStale(projectRoot, now) {
3015
+ const cacheDir = join5(projectRoot, ".fabric", ".cache");
3016
+ if (!existsSync4(cacheDir)) {
3017
+ return { candidates: [] };
3018
+ }
3019
+ let entries;
3020
+ try {
3021
+ entries = readdirSync(cacheDir, { withFileTypes: true });
3022
+ } catch {
3023
+ return { candidates: [] };
3024
+ }
3025
+ const candidates = [];
3026
+ for (const entry of entries) {
3027
+ if (!entry.isFile()) continue;
3028
+ if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
3029
+ if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
3030
+ const absPath = join5(cacheDir, entry.name);
3031
+ let mtimeMs = 0;
3032
+ try {
3033
+ mtimeMs = statSync3(absPath).mtimeMs;
3034
+ } catch {
3035
+ continue;
3036
+ }
3037
+ const ageDays = Math.floor((now - mtimeMs) / MS_PER_DAY);
3038
+ if (ageDays < SESSION_HINTS_STALE_DAYS) continue;
3039
+ candidates.push({
3040
+ path: posix.join(".fabric", ".cache", entry.name),
3041
+ age_days: ageDays
3042
+ });
3043
+ }
3044
+ candidates.sort((a, b) => a.path.localeCompare(b.path));
3045
+ return { candidates };
3046
+ }
3047
+ function inspectNarrowTooFew(projectRoot, now) {
3048
+ let total = 0;
3049
+ let narrowWithPaths = 0;
3050
+ for (const { scope, paths } of iterateRelevanceFrontmatter(projectRoot)) {
3051
+ total += 1;
3052
+ if (scope === "narrow" && paths.length > 0) {
3053
+ narrowWithPaths += 1;
3054
+ }
3055
+ }
3056
+ const narrowRatio = total === 0 ? 0 : narrowWithPaths / total;
3057
+ const structuralFlagged = total >= NARROW_MIN_TOTAL && narrowRatio < NARROW_RATIO_THRESHOLD;
3058
+ const windowStartMs = now - SILENCE_WINDOW_DAYS * MS_PER_DAY;
3059
+ const editFires = readCounterTimestamps(
3060
+ join5(projectRoot, EDIT_COUNTER_FILE_REL),
3061
+ windowStartMs
3062
+ );
3063
+ const silenceFires = readCounterTimestamps(
3064
+ join5(projectRoot, HINT_SILENCE_COUNTER_FILE_REL),
3065
+ windowStartMs
3066
+ );
3067
+ const telemetrySkipped = editFires === 0;
3068
+ const silenceRate = editFires === 0 ? 0 : silenceFires / editFires;
3069
+ const telemetryFlagged = !telemetrySkipped && silenceRate > SILENCE_RATE_THRESHOLD;
3070
+ return {
3071
+ total_canonical_entries: total,
3072
+ narrow_with_paths_count: narrowWithPaths,
3073
+ narrow_ratio: narrowRatio,
3074
+ structural_flagged: structuralFlagged,
3075
+ total_edit_fires_in_window: editFires,
3076
+ silence_fires_in_window: silenceFires,
3077
+ silence_rate: silenceRate,
3078
+ telemetry_skipped: telemetrySkipped,
3079
+ telemetry_flagged: telemetryFlagged
3080
+ };
3081
+ }
3082
+ function readCounterTimestamps(absPath, windowStartMs) {
3083
+ if (!existsSync4(absPath)) return 0;
3084
+ let raw;
3085
+ try {
3086
+ raw = readFileSync(absPath, "utf8");
3087
+ } catch {
3088
+ return 0;
3089
+ }
3090
+ let count = 0;
3091
+ for (const line of raw.split(/\r?\n/u)) {
3092
+ const trimmed = line.trim();
3093
+ if (trimmed.length === 0) continue;
3094
+ const ts = Date.parse(trimmed);
3095
+ if (!Number.isFinite(ts)) continue;
3096
+ if (ts < windowStartMs) continue;
3097
+ count += 1;
3098
+ }
3099
+ return count;
3100
+ }
3101
+ function readUnderseedThresholdFromConfig(projectRoot) {
3102
+ const configPath = join5(projectRoot, ".fabric", "fabric-config.json");
3103
+ if (!existsSync4(configPath)) return DEFAULT_UNDERSEED_NODE_THRESHOLD;
3104
+ try {
3105
+ const raw = readFileSync(configPath, "utf8");
3106
+ const parsed = JSON.parse(raw);
3107
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3108
+ const v = parsed.underseed_node_threshold;
3109
+ if (typeof v === "number" && Number.isFinite(v) && v > 0) {
3110
+ return v;
3111
+ }
3112
+ }
3113
+ } catch {
3114
+ }
3115
+ return DEFAULT_UNDERSEED_NODE_THRESHOLD;
3116
+ }
3117
+ function createOrphanDemoteCheck(inspection) {
3118
+ if (inspection.candidates.length === 0) {
3119
+ return okCheck(
3120
+ "Knowledge orphan demote",
3121
+ "No canonical knowledge entries exceed their maturity-keyed inactivity threshold."
3122
+ );
3123
+ }
3124
+ const first = inspection.candidates[0];
3125
+ const detail = `${first.stable_id} (${first.maturity}, ${first.age_days}d inactive at ${first.path})`;
3126
+ return issueCheck(
3127
+ "Knowledge orphan demote",
3128
+ "warn",
3129
+ "warning",
3130
+ "knowledge_orphan_demote_required",
3131
+ `${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}.`,
3132
+ "Run `fab doctor --apply-lint` (rc.4 TASK-003) to demote orphan entries one maturity tier."
3133
+ );
3134
+ }
3135
+ function createStaleArchiveCheck(inspection) {
3136
+ if (inspection.candidates.length === 0) {
3137
+ return okCheck(
3138
+ "Knowledge stale archive",
3139
+ "No draft knowledge entries exceed the additional stale-archive quiet window."
3140
+ );
3141
+ }
3142
+ const first = inspection.candidates[0];
3143
+ const detail = `${first.stable_id} (${first.age_days}d inactive at ${first.path}) \u2192 ${first.archive_path}`;
3144
+ return issueCheck(
3145
+ "Knowledge stale archive",
3146
+ "warn",
3147
+ "warning",
3148
+ "knowledge_stale_archive_required",
3149
+ `${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}.`,
3150
+ "Run `fab doctor --apply-lint` (rc.4 TASK-003) to move stale entries into `.fabric/.archive/<type>/`."
3151
+ );
3152
+ }
3153
+ function createPendingOverdueCheck(inspection) {
3154
+ if (inspection.candidates.length === 0) {
3155
+ return okCheck(
3156
+ "Knowledge pending overdue",
3157
+ "No pending knowledge entries exceed the 14-day review threshold."
3158
+ );
3159
+ }
3160
+ const first = inspection.candidates[0];
3161
+ const detail = `${first.path} (${first.age_days}d old)`;
3162
+ return issueCheck(
3163
+ "Knowledge pending overdue",
3164
+ "warn",
3165
+ "warning",
3166
+ "knowledge_pending_overdue",
3167
+ `${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}.`,
3168
+ "Review pending entries via the fabric-review Skill (`/fabric-review`) and approve, reject, defer, or modify."
3169
+ );
3170
+ }
3171
+ function createUnderseededCheck(inspection) {
3172
+ if (!inspection.underseeded) {
3173
+ return okCheck(
3174
+ "Knowledge underseeded",
3175
+ `Knowledge corpus has ${inspection.node_count} canonical entries (>= ${inspection.threshold}).`
3176
+ );
3177
+ }
3178
+ return issueCheck(
3179
+ "Knowledge underseeded",
3180
+ "ok",
3181
+ "info",
3182
+ "knowledge_underseeded",
3183
+ `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.`,
3184
+ "Run the fabric-import Skill (`/fabric-import`) to backfill knowledge from git history and existing docs."
3185
+ );
3186
+ }
3187
+ function createSessionHintsStaleCheck(inspection) {
3188
+ if (inspection.candidates.length === 0) {
3189
+ return okCheck(
3190
+ "Knowledge session-hints stale",
3191
+ `No session-hints cache files older than ${SESSION_HINTS_STALE_DAYS} days under .fabric/.cache/.`
3192
+ );
3193
+ }
3194
+ const first = inspection.candidates[0];
3195
+ const detail = `${first.path} (${first.age_days}d old)`;
3196
+ return issueCheck(
3197
+ "Knowledge session-hints stale",
3198
+ "ok",
3199
+ "info",
3200
+ "knowledge_session_hints_stale",
3201
+ `${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}.`,
3202
+ "Run `fab doctor --apply-lint` to delete stale session-hints cache files."
3203
+ );
3204
+ }
3205
+ function extractKnowledgeFrontmatterRelevanceScope(source) {
3206
+ const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
3207
+ const fm = FM_PATTERN.exec(source);
3208
+ if (fm === null) {
3209
+ return "broad";
3210
+ }
3211
+ const match = RELEVANCE_SCOPE_LINE_PATTERN.exec(fm[1]);
3212
+ if (match === null) {
3213
+ return "broad";
3214
+ }
3215
+ return match[2];
3216
+ }
3217
+ function extractKnowledgeFrontmatterRelevancePaths(source) {
3218
+ const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
3219
+ const fm = FM_PATTERN.exec(source);
3220
+ if (fm === null) {
3221
+ return [];
3222
+ }
3223
+ const match = RELEVANCE_PATHS_LINE_PATTERN.exec(fm[1]);
3224
+ if (match === null) {
3225
+ return [];
3226
+ }
3227
+ const inner = match[1].trim();
3228
+ if (inner.length === 0) {
3229
+ return [];
3230
+ }
3231
+ return inner.split(",").map((token) => token.trim().replace(/^"(.*)"$/u, "$1")).filter((token) => token.length > 0);
3232
+ }
3233
+ function* iterateRelevanceFrontmatter(projectRoot) {
3234
+ for (const visit of iterateCanonicalFilenames(projectRoot)) {
3235
+ const layerRoot = visit.layer === "team" ? join5(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
3236
+ const absPath = join5(layerRoot, visit.type, visit.filename);
3237
+ let source;
3238
+ try {
3239
+ source = readFileSync(absPath, "utf8");
3240
+ } catch {
3241
+ continue;
3242
+ }
3243
+ const scope = extractKnowledgeFrontmatterRelevanceScope(source);
3244
+ const paths = extractKnowledgeFrontmatterRelevancePaths(source);
3245
+ yield { visit, scope, paths, absPath };
3246
+ }
3247
+ }
3248
+ function inspectNarrowNoPaths(projectRoot) {
3249
+ const candidates = [];
3250
+ for (const { visit, scope, paths } of iterateRelevanceFrontmatter(projectRoot)) {
3251
+ if (scope !== "narrow") {
3252
+ continue;
3253
+ }
3254
+ if (paths.length > 0) {
3255
+ continue;
3256
+ }
3257
+ candidates.push({
3258
+ stable_id: visit.parsed.stable_id,
3259
+ path: visit.displayPath
3260
+ });
3261
+ }
3262
+ candidates.sort((a, b) => a.path.localeCompare(b.path));
3263
+ return { candidates };
3264
+ }
3265
+ function inspectRelevancePathsDangling(projectRoot) {
3266
+ const entries = [];
3267
+ const workspacePaths = collectWorkspacePathsForGlobMatch(projectRoot);
3268
+ if (workspacePaths.length === 0) {
3269
+ return { entries };
3270
+ }
3271
+ for (const { visit, paths } of iterateRelevanceFrontmatter(projectRoot)) {
3272
+ if (paths.length === 0) {
3273
+ continue;
3274
+ }
3275
+ for (const rawGlob of paths) {
3276
+ const glob = rawGlob.endsWith("/") ? `${rawGlob}**` : rawGlob;
3277
+ let matched = false;
3278
+ for (const target of workspacePaths) {
3279
+ if (minimatch(target, glob, { dot: true, matchBase: false })) {
3280
+ matched = true;
3281
+ break;
3282
+ }
3283
+ }
3284
+ if (matched) {
3285
+ continue;
3286
+ }
3287
+ entries.push({
3288
+ stable_id: visit.parsed.stable_id,
3289
+ path: visit.displayPath,
3290
+ dangling_glob: rawGlob
3291
+ });
3292
+ }
3293
+ }
3294
+ entries.sort((a, b) => {
3295
+ const byPath = a.path.localeCompare(b.path);
3296
+ return byPath !== 0 ? byPath : a.dangling_glob.localeCompare(b.dangling_glob);
3297
+ });
3298
+ return { entries };
3299
+ }
3300
+ function collectWorkspacePathsForGlobMatch(projectRoot) {
3301
+ if (!existsSync4(projectRoot)) {
3302
+ return [];
3303
+ }
3304
+ let rootStat;
3305
+ try {
3306
+ rootStat = statSync3(projectRoot);
3307
+ } catch {
3308
+ return [];
3309
+ }
3310
+ if (!rootStat.isDirectory()) {
3311
+ return [];
3312
+ }
3313
+ const paths = [];
3314
+ const stack = [projectRoot];
3315
+ while (stack.length > 0) {
3316
+ const current = stack.pop();
3317
+ if (current === void 0) continue;
3318
+ let entries;
3319
+ try {
3320
+ entries = readdirSync(current, { withFileTypes: true });
3321
+ } catch {
3322
+ continue;
3323
+ }
3324
+ for (const entry of entries) {
3325
+ const abs = join5(current, entry.name);
3326
+ const rel = normalizePath(abs.slice(projectRoot.length + 1));
3327
+ if (rel.length === 0) continue;
3328
+ if (entry.isDirectory()) {
3329
+ if (IGNORED_DIRECTORIES.has(entry.name)) {
3330
+ continue;
3331
+ }
3332
+ paths.push(rel);
3333
+ stack.push(abs);
3334
+ } else if (entry.isFile()) {
3335
+ paths.push(rel);
3336
+ }
3337
+ }
3338
+ }
3339
+ return paths;
3340
+ }
3341
+ function inspectRelevancePathsDrift(projectRoot) {
3342
+ let recentPaths = null;
3343
+ try {
3344
+ recentPaths = readRecentGitTouchedPaths(projectRoot, RELEVANCE_PATHS_DRIFT_WINDOW_DAYS);
3345
+ } catch {
3346
+ recentPaths = null;
3347
+ }
3348
+ if (recentPaths === null) {
3349
+ return { candidates: [], git_available: false };
3350
+ }
3351
+ const candidates = [];
3352
+ for (const { visit, scope, paths } of iterateRelevanceFrontmatter(projectRoot)) {
3353
+ if (scope !== "narrow") {
3354
+ continue;
3355
+ }
3356
+ if (paths.length === 0) {
3357
+ continue;
3358
+ }
3359
+ let anyMatch = false;
3360
+ for (const rawGlob of paths) {
3361
+ const glob = rawGlob.endsWith("/") ? `${rawGlob}**` : rawGlob;
3362
+ for (const target of recentPaths) {
3363
+ if (minimatch(target, glob, { dot: true, matchBase: false })) {
3364
+ anyMatch = true;
3365
+ break;
3366
+ }
3367
+ }
3368
+ if (anyMatch) break;
3369
+ }
3370
+ if (anyMatch) {
3371
+ continue;
3372
+ }
3373
+ candidates.push({
3374
+ stable_id: visit.parsed.stable_id,
3375
+ path: visit.displayPath,
3376
+ globs: paths.slice()
3377
+ });
3378
+ }
3379
+ candidates.sort((a, b) => a.path.localeCompare(b.path));
3380
+ return { candidates, git_available: true };
3381
+ }
3382
+ function readRecentGitTouchedPaths(projectRoot, windowDays) {
3383
+ const since = new Date(Date.now() - windowDays * MS_PER_DAY).toISOString();
3384
+ const stdout = execFileSync(
3385
+ "git",
3386
+ ["log", `--since=${since}`, "--name-only", "--pretty=format:"],
3387
+ {
3388
+ cwd: projectRoot,
3389
+ stdio: ["ignore", "pipe", "pipe"],
3390
+ encoding: "utf8"
3391
+ }
3392
+ );
3393
+ const set = /* @__PURE__ */ new Set();
3394
+ for (const line of stdout.split(/\r?\n/u)) {
3395
+ const trimmed = line.trim();
3396
+ if (trimmed.length === 0) continue;
3397
+ set.add(normalizePath(trimmed));
3398
+ }
3399
+ return Array.from(set);
3400
+ }
3401
+ function createNarrowNoPathsCheck(inspection) {
3402
+ if (inspection.candidates.length === 0) {
3403
+ return okCheck(
3404
+ "Knowledge narrow without paths",
3405
+ "No narrow-scope canonical entries have an empty relevance_paths array."
3406
+ );
3407
+ }
3408
+ const first = inspection.candidates[0];
3409
+ const detail = `${first.stable_id} (${first.path})`;
3410
+ return issueCheck(
3411
+ "Knowledge narrow without paths",
3412
+ "warn",
3413
+ "warning",
3414
+ "knowledge_narrow_no_paths",
3415
+ `${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}.`,
3416
+ "Either add path anchors to relevance_paths or widen the entry's relevance_scope to broad."
3417
+ );
3418
+ }
3419
+ function createRelevancePathsDanglingCheck(inspection) {
3420
+ if (inspection.entries.length === 0) {
3421
+ return okCheck(
3422
+ "Knowledge relevance_paths dangling",
3423
+ "All relevance_paths globs resolve to at least one file under the workspace root."
3424
+ );
3425
+ }
3426
+ const first = inspection.entries[0];
3427
+ const detail = `${first.stable_id} at ${first.path} \u2192 \`${first.dangling_glob}\` (0 matches)`;
3428
+ return issueCheck(
3429
+ "Knowledge relevance_paths dangling",
3430
+ "warn",
3431
+ "warning",
3432
+ "knowledge_relevance_paths_dangling",
3433
+ `${inspection.entries.length} relevance_paths glob${inspection.entries.length === 1 ? " resolves" : "s resolve"} to zero files in the current workspace. First: ${detail}.`,
3434
+ "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."
3435
+ );
3436
+ }
3437
+ function createRelevancePathsDriftCheck(inspection) {
3438
+ if (!inspection.git_available) {
3439
+ return okCheck(
3440
+ "Knowledge relevance_paths drift",
3441
+ `Skipped (git history unavailable; cannot evaluate ${RELEVANCE_PATHS_DRIFT_WINDOW_DAYS}d drift window).`
3442
+ );
3443
+ }
3444
+ if (inspection.candidates.length === 0) {
3445
+ return okCheck(
3446
+ "Knowledge relevance_paths drift",
3447
+ `All narrow-scope canonical entries have at least one relevance_path touched in the last ${RELEVANCE_PATHS_DRIFT_WINDOW_DAYS}d.`
3448
+ );
3449
+ }
3450
+ const first = inspection.candidates[0];
3451
+ const detail = `${first.stable_id} at ${first.path} (globs: ${first.globs.join(", ")})`;
3452
+ return issueCheck(
3453
+ "Knowledge relevance_paths drift",
3454
+ "ok",
3455
+ "info",
3456
+ "knowledge_relevance_paths_drift",
3457
+ `${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}.`,
3458
+ "Review whether the entry is still relevant \u2014 use `fab_review.modify` to refresh the anchors or `fab_review.reject` to archive."
3459
+ );
3460
+ }
3461
+ function createNarrowTooFewCheck(inspection) {
3462
+ const { structural_flagged, telemetry_flagged } = inspection;
3463
+ if (!structural_flagged && !telemetry_flagged) {
3464
+ const ratioPct = (inspection.narrow_ratio * 100).toFixed(0);
3465
+ 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`;
3466
+ return okCheck(
3467
+ "Knowledge narrow too few",
3468
+ `Narrow-with-paths ratio ${ratioPct}% (${inspection.narrow_with_paths_count}/${inspection.total_canonical_entries}); ${teleNote}.`
3469
+ );
3470
+ }
3471
+ const parts = [];
3472
+ if (structural_flagged) {
3473
+ const ratioPct = (inspection.narrow_ratio * 100).toFixed(0);
3474
+ parts.push(
3475
+ `narrow-with-paths share ${ratioPct}% (${inspection.narrow_with_paths_count}/${inspection.total_canonical_entries}) below ${(NARROW_RATIO_THRESHOLD * 100).toFixed(0)}% threshold`
3476
+ );
3477
+ }
3478
+ if (telemetry_flagged) {
3479
+ const silencePct = (inspection.silence_rate * 100).toFixed(0);
3480
+ parts.push(
3481
+ `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`
3482
+ );
3483
+ }
3484
+ return issueCheck(
3485
+ "Knowledge narrow too few",
3486
+ "ok",
3487
+ "info",
3488
+ "knowledge_narrow_too_few",
3489
+ `Narrow-scope KB coverage is below the useful floor: ${parts.join("; ")}.`,
3490
+ "Run the fabric-import Skill (`/fabric-import`) to re-seed narrow anchors against the current codebase."
3491
+ );
3492
+ }
3493
+ function resolvePersonalKnowledgeRoot() {
3494
+ const home = process.env.FABRIC_HOME ?? homedir2();
3495
+ return join5(home, ".fabric", "knowledge");
3496
+ }
3497
+ function parseStableIdFromCanonicalFilename(filename) {
3498
+ const match = CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(filename);
3499
+ if (match === null) {
3500
+ return null;
3501
+ }
3502
+ const stableId = match[1];
3503
+ const inner = /^(K[PT])-(MOD|DEC|GLD|PIT|PRO)-(\d{4,})$/u.exec(stableId);
3504
+ if (inner === null) {
3505
+ return null;
3506
+ }
3507
+ return {
3508
+ prefix: inner[1],
3509
+ typeCode: inner[2],
3510
+ counter: Number.parseInt(inner[3], 10),
3511
+ stable_id: stableId
3512
+ };
3513
+ }
3514
+ function* iterateCanonicalFilenames(projectRoot) {
3515
+ const teamRoot = join5(projectRoot, ".fabric", "knowledge");
3516
+ const personalRoot = resolvePersonalKnowledgeRoot();
3517
+ for (const [layer, root, displayPrefix] of [
3518
+ ["team", teamRoot, ".fabric/knowledge"],
3519
+ ["personal", personalRoot, "~/.fabric/knowledge"]
3520
+ ]) {
3521
+ if (!existsSync4(root)) {
3522
+ continue;
3523
+ }
3524
+ for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
3525
+ const dir = join5(root, typeDir);
3526
+ if (!existsSync4(dir)) {
3527
+ continue;
3528
+ }
3529
+ let entries;
3530
+ try {
3531
+ entries = readdirSync(dir, { withFileTypes: true });
3532
+ } catch {
3533
+ continue;
3534
+ }
3535
+ for (const entry of entries) {
3536
+ if (!entry.isFile()) {
3537
+ continue;
3538
+ }
3539
+ const parsed = parseStableIdFromCanonicalFilename(entry.name);
3540
+ if (parsed === null) {
3541
+ continue;
3542
+ }
3543
+ const displayPath = posix.join(displayPrefix, typeDir, entry.name);
3544
+ yield {
3545
+ layer,
3546
+ type: typeDir,
3547
+ filename: entry.name,
3548
+ displayPath,
3549
+ parsed
3550
+ };
3551
+ }
3552
+ }
3553
+ }
3554
+ }
3555
+ function inspectStableIdDuplicate(projectRoot) {
3556
+ const idToPaths = /* @__PURE__ */ new Map();
3557
+ for (const visit of iterateCanonicalFilenames(projectRoot)) {
3558
+ const existing = idToPaths.get(visit.parsed.stable_id) ?? [];
3559
+ existing.push(visit.displayPath);
3560
+ idToPaths.set(visit.parsed.stable_id, existing);
3561
+ }
3562
+ const duplicates = [];
3563
+ for (const [stable_id, paths] of idToPaths) {
3564
+ if (paths.length > 1) {
3565
+ duplicates.push({ stable_id, paths: paths.slice().sort() });
3566
+ }
3567
+ }
3568
+ duplicates.sort((a, b) => a.stable_id.localeCompare(b.stable_id));
3569
+ return { duplicates };
3570
+ }
3571
+ function inspectLayerMismatch(projectRoot) {
3572
+ const mismatches = [];
3573
+ for (const visit of iterateCanonicalFilenames(projectRoot)) {
3574
+ const expected_layer = visit.parsed.prefix === "KT" ? "team" : "personal";
3575
+ if (expected_layer === visit.layer) {
3576
+ continue;
3577
+ }
3578
+ mismatches.push({
3579
+ path: visit.displayPath,
3580
+ located_in: visit.layer,
3581
+ expected_layer,
3582
+ stable_id: visit.parsed.stable_id
3583
+ });
3584
+ }
3585
+ mismatches.sort((a, b) => a.path.localeCompare(b.path));
3586
+ return { mismatches };
3587
+ }
3588
+ function inspectIndexDrift(projectRoot, meta) {
3589
+ if (!meta.valid || meta.meta === null) {
3590
+ return { drifts: [] };
3591
+ }
3592
+ const counters = AgentsMetaCountersSchema.parse(meta.meta.counters ?? void 0);
3593
+ const observed = {
3594
+ KP: { MOD: 0, DEC: 0, GLD: 0, PIT: 0, PRO: 0 },
3595
+ KT: { MOD: 0, DEC: 0, GLD: 0, PIT: 0, PRO: 0 }
3596
+ };
3597
+ for (const visit of iterateCanonicalFilenames(projectRoot)) {
3598
+ const { prefix, typeCode, counter } = visit.parsed;
3599
+ if (counter > observed[prefix][typeCode]) {
3600
+ observed[prefix][typeCode] = counter;
3601
+ }
3602
+ }
3603
+ const drifts = [];
3604
+ for (const layer of ["KP", "KT"]) {
3605
+ for (const code of COUNTER_TYPE_CODES) {
3606
+ const max = observed[layer][code];
3607
+ if (max === 0) {
3608
+ continue;
3609
+ }
3610
+ const current = counters[layer][code];
3611
+ if (current < max) {
3612
+ drifts.push({
3613
+ layer,
3614
+ type: code,
3615
+ counter: current,
3616
+ max_observed: max,
3617
+ proposed_after: max + 1
3618
+ });
3619
+ }
3620
+ }
3621
+ }
3622
+ drifts.sort(
3623
+ (a, b) => a.layer === b.layer ? a.type.localeCompare(b.type) : a.layer.localeCompare(b.layer)
3624
+ );
3625
+ return { drifts };
3626
+ }
3627
+ function createStableIdDuplicateCheck(inspection) {
3628
+ if (inspection.duplicates.length === 0) {
3629
+ return okCheck(
3630
+ "Knowledge stable_id duplicate",
3631
+ "No canonical knowledge files share a stable_id across team / personal trees."
3632
+ );
3633
+ }
3634
+ const first = inspection.duplicates[0];
3635
+ const detail = `${first.stable_id} appears in ${first.paths.length} files: ${first.paths.join(", ")}`;
3636
+ return issueCheck(
3637
+ "Knowledge stable_id duplicate",
3638
+ "error",
3639
+ "manual_error",
3640
+ "knowledge_stable_id_duplicate",
3641
+ `${inspection.duplicates.length} stable_id${inspection.duplicates.length === 1 ? "" : "s"} duplicated across canonical knowledge files (path-decoupled identity invariant). First: ${detail}.`,
3642
+ "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."
3643
+ );
3644
+ }
3645
+ function createLayerMismatchCheck(inspection) {
3646
+ if (inspection.mismatches.length === 0) {
3647
+ return okCheck(
3648
+ "Knowledge layer mismatch",
3649
+ "All canonical knowledge files are physically located under the layer their stable_id prefix declares."
3650
+ );
3651
+ }
3652
+ const first = inspection.mismatches[0];
3653
+ const detail = `${first.stable_id} at ${first.path} (located in ${first.located_in}, expected ${first.expected_layer})`;
3654
+ return issueCheck(
3655
+ "Knowledge layer mismatch",
3656
+ "error",
3657
+ "manual_error",
3658
+ "knowledge_layer_mismatch",
3659
+ `${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}.`,
3660
+ "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)."
3661
+ );
3662
+ }
3663
+ function createIndexDriftCheck(inspection) {
3664
+ if (inspection.drifts.length === 0) {
3665
+ return okCheck(
3666
+ "Knowledge index drift",
3667
+ "agents.meta.json counters envelope is at or above the highest existing canonical counter for every (layer, type) pair."
3668
+ );
3669
+ }
3670
+ const first = inspection.drifts[0];
3671
+ 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})`;
3672
+ return issueCheck(
3673
+ "Knowledge index drift",
3674
+ "error",
3675
+ "fixable_error",
3676
+ "knowledge_index_drift",
3677
+ `${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}.`,
3678
+ "Run `fab doctor --apply-lint` (rc.4 TASK-003) to bump agents.meta.json counters to max_observed + 1."
3679
+ );
3680
+ }
3681
+ async function fixMcpConfigInWrongFile(projectRoot) {
3682
+ const settingsPath = join5(projectRoot, ".claude", "settings.json");
3683
+ if (!existsSync4(settingsPath)) {
3684
+ return;
3685
+ }
3686
+ let settings;
3687
+ try {
3688
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
3689
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
3690
+ return;
3691
+ }
3692
+ settings = parsed;
3693
+ } catch {
3694
+ return;
3695
+ }
3696
+ const mcpServers = settings.mcpServers;
3697
+ if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
3698
+ return;
3699
+ }
3700
+ const { fabric: _removed, ...remainingServers } = mcpServers;
3701
+ const cleaned = { ...settings };
3702
+ if (Object.keys(remainingServers).length === 0) {
3703
+ delete cleaned.mcpServers;
3704
+ } else {
3705
+ cleaned.mcpServers = remainingServers;
3706
+ }
3707
+ await atomicWriteJson2(settingsPath, cleaned, { indent: 2 });
3708
+ await appendEventLedgerEvent(projectRoot, {
3709
+ event_type: "mcp_config_migrated",
3710
+ source: "doctor_fix",
3711
+ removed_from: ".claude/settings.json"
3712
+ });
3713
+ }
3714
+ async function ensureKnowledgeSubdirs(projectRoot) {
3715
+ for (const sub of KNOWLEDGE_SUBDIRS2) {
3716
+ await mkdir3(join5(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
3717
+ }
3718
+ }
3719
+ async function fixCounterDesync(projectRoot) {
3720
+ const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
3721
+ if (!existsSync4(metaPath)) {
3722
+ return;
3723
+ }
3724
+ let meta;
3725
+ try {
3726
+ meta = agentsMetaSchema4.parse(JSON.parse(await readFile5(metaPath, "utf8")));
3727
+ } catch {
3728
+ return;
3729
+ }
3730
+ const synthetic = {
3731
+ present: true,
3732
+ valid: true,
3733
+ meta,
3734
+ revision: meta.revision,
3735
+ computedRevision: null,
3736
+ ruleCount: 0,
3737
+ missingContentRefs: [],
3738
+ invalidContentRefs: [],
3739
+ stale: false,
3740
+ changed: false
3741
+ };
3742
+ const desync = inspectCounterDesync(synthetic);
3743
+ if (desync.desyncs.length === 0 || desync.correctedCounters === null) {
3744
+ return;
3745
+ }
3746
+ const updated = { ...meta, counters: desync.correctedCounters };
3747
+ await atomicWriteJson2(metaPath, updated, { indent: 2 });
3748
+ }
3749
+ async function ensureEventLedger(projectRoot) {
3750
+ const path = getEventLedgerPath(projectRoot);
3751
+ await ensureParentDirectory(path);
3752
+ await writeFile2(path, "", { encoding: "utf8", flag: "a" });
3753
+ }
3754
+ function createFixMessage(fixed, report) {
3755
+ const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
3756
+ const manualText = report.manual_errors.length === 0 ? "No manual errors remain." : `${report.manual_errors.length} manual error${report.manual_errors.length === 1 ? "" : "s"} remain.`;
3757
+ return `${fixedText} ${manualText}`;
3758
+ }
3759
+ function isValidJsonLine(line) {
2167
3760
  try {
2168
3761
  JSON.parse(line);
2169
3762
  return true;
@@ -2246,141 +3839,35 @@ function isMissingFileError(error) {
2246
3839
  return error instanceof Error && "code" in error && error.code === "ENOENT";
2247
3840
  }
2248
3841
 
2249
- // src/services/get-rules.ts
3842
+ // src/services/get-knowledge.ts
2250
3843
  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
3844
+ import { join as join6 } from "path";
3845
+ import { minimatch as minimatch2 } from "minimatch";
2360
3846
  var PRIORITY_ORDER = {
2361
3847
  high: 0,
2362
3848
  medium: 1,
2363
3849
  low: 2
2364
3850
  };
2365
- async function getRules(projectRoot, input) {
2366
- const context = await loadGetRulesContext(projectRoot);
3851
+ async function getKnowledge(projectRoot, input) {
3852
+ const context = await loadGetKnowledgeContext(projectRoot);
2367
3853
  const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
2368
3854
  const matchedNodes = matchRuleNodes(context.meta, input.path);
2369
3855
  const requiredStableIds = matchedNodes.filter((node) => node.level === "L2").map((node) => node.stable_id);
2370
3856
  const aiSelectableStableIds = matchedNodes.filter((node) => node.level === "L1").map((node) => node.stable_id);
2371
- const rules = await resolveRulesForPath(projectRoot, context, input.path);
3857
+ const rules = await resolveKnowledgeForPath(projectRoot, context, input.path);
2372
3858
  const result = {
2373
3859
  revision_hash: context.meta.revision,
2374
3860
  stale,
2375
3861
  rules
2376
3862
  };
2377
3863
  try {
2378
- await appendGetRulesAuditEvent(projectRoot, {
2379
- path: input.path,
2380
- client_hash: input.client_hash,
3864
+ await appendEventLedgerEvent(projectRoot, {
3865
+ event_type: "knowledge_context_planned",
3866
+ target_paths: [input.path],
2381
3867
  required_stable_ids: requiredStableIds,
2382
3868
  ai_selectable_stable_ids: aiSelectableStableIds,
2383
3869
  final_stable_ids: [...requiredStableIds, ...aiSelectableStableIds],
3870
+ client_hash: input.client_hash,
2384
3871
  correlation_id: input.correlation_id,
2385
3872
  session_id: input.session_id
2386
3873
  });
@@ -2388,13 +3875,13 @@ async function getRules(projectRoot, input) {
2388
3875
  }
2389
3876
  return result;
2390
3877
  }
2391
- async function loadGetRulesContext(projectRoot) {
3878
+ async function loadGetKnowledgeContext(projectRoot) {
2392
3879
  const cached = contextCache.get("context", projectRoot);
2393
3880
  if (cached !== void 0) {
2394
3881
  return cached;
2395
3882
  }
2396
3883
  const meta = await readAgentsMeta(projectRoot);
2397
- const l0Content = await readFile6(join7(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
3884
+ const l0Content = await readFile6(join6(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
2398
3885
  const context = {
2399
3886
  meta,
2400
3887
  l0Content,
@@ -2403,20 +3890,20 @@ async function loadGetRulesContext(projectRoot) {
2403
3890
  contextCache.set("context", projectRoot, context);
2404
3891
  return context;
2405
3892
  }
2406
- async function resolveRulesForPath(projectRoot, context, path, options = {}) {
3893
+ async function resolveKnowledgeForPath(projectRoot, context, path, options = {}) {
2407
3894
  const matchedNodes = matchRuleNodes(context.meta, path);
2408
3895
  const loaded = await loadMatchedRules(projectRoot, matchedNodes);
2409
- return buildRulesPayload(context, loaded, options);
3896
+ return buildKnowledgePayload(context, loaded, options);
2410
3897
  }
2411
- function normalizeRulesPath(value) {
3898
+ function normalizeKnowledgePath(value) {
2412
3899
  return value.replaceAll("\\", "/");
2413
3900
  }
2414
3901
  function matchRuleNodes(meta, path) {
2415
- const requestedPath = normalizeRulesPath(path);
3902
+ const requestedPath = normalizeKnowledgePath(path);
2416
3903
  return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
2417
3904
  const [leftId, leftNode] = left;
2418
3905
  const [rightId, rightNode] = right;
2419
- const priorityDelta = PRIORITY_ORDER[leftNode.priority] - PRIORITY_ORDER[rightNode.priority];
3906
+ const priorityDelta = PRIORITY_ORDER[leftNode.priority ?? "medium"] - PRIORITY_ORDER[rightNode.priority ?? "medium"];
2420
3907
  return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
2421
3908
  }).map(([nodeId, node]) => ({
2422
3909
  node_id: nodeId,
@@ -2455,7 +3942,7 @@ async function loadMatchedRules(projectRoot, matchedNodes, fileContentCache = /*
2455
3942
  }
2456
3943
  return { rules, stubs };
2457
3944
  }
2458
- function buildRulesPayload(context, loaded, options = {}) {
3945
+ function buildKnowledgePayload(context, loaded, options = {}) {
2459
3946
  const { L1, L2 } = partitionRulesByLevel(loaded.rules, options.dedupeByPath ?? false);
2460
3947
  return {
2461
3948
  L0: context.l0Content,
@@ -2472,7 +3959,7 @@ function classifyNode(nodeId, node) {
2472
3959
  if (nodeId.startsWith("L2/")) {
2473
3960
  return "L2";
2474
3961
  }
2475
- return node.layer === "L0" ? null : node.layer;
3962
+ return node.layer === "L0" ? null : node.layer ?? null;
2476
3963
  }
2477
3964
  function partitionRulesByLevel(loadedRules, dedupeByPath) {
2478
3965
  const l1 = [];
@@ -2509,7 +3996,7 @@ function shouldLoadNodeForPath(requestedPath, node) {
2509
3996
  return true;
2510
3997
  case "path":
2511
3998
  case void 0:
2512
- return minimatch(requestedPath, normalizeRulesPath(node.scope_glob), { dot: true });
3999
+ return minimatch2(requestedPath, normalizeKnowledgePath(node.scope_glob), { dot: true });
2513
4000
  }
2514
4001
  }
2515
4002
  function dedupeDescriptionStubsByPath(stubs) {
@@ -2533,7 +4020,7 @@ async function readRuleContent(projectRoot, file, fileContentCache) {
2533
4020
  if (cached !== void 0) {
2534
4021
  return await cached;
2535
4022
  }
2536
- const pending = readFile6(join7(projectRoot, file), "utf8");
4023
+ const pending = readFile6(join6(projectRoot, file), "utf8");
2537
4024
  fileContentCache.set(file, pending);
2538
4025
  return await pending;
2539
4026
  }
@@ -2548,25 +4035,27 @@ export {
2548
4035
  getLedgerPath,
2549
4036
  getLegacyLedgerPath,
2550
4037
  getEventLedgerPath,
4038
+ ensureParentDirectory,
2551
4039
  sha256,
2552
4040
  isNodeError,
4041
+ atomicWriteText,
2553
4042
  appendEventLedgerEvent,
2554
4043
  readEventLedger,
2555
4044
  flushAndSyncEventLedger,
2556
- buildRuleMeta,
2557
- writeRuleMeta,
2558
- computeRulesBasedAgentsMeta,
2559
- computeRuleTestIndex,
2560
- deriveRuleMetaLayer,
2561
- deriveRuleMetaTopologyType,
2562
- isSameRuleTestIndex,
4045
+ buildKnowledgeMeta,
4046
+ writeKnowledgeMeta,
4047
+ computeKnowledgeBasedAgentsMeta,
4048
+ computeKnowledgeTestIndex,
4049
+ deriveKnowledgeMetaLayer,
4050
+ deriveKnowledgeMetaTopologyType,
4051
+ isSameKnowledgeTestIndex,
2563
4052
  stableStringify,
2564
- invalidateRuleSyncCooldown,
2565
- ensureRulesFresh,
2566
- reconcileRules,
2567
- appendRuleSelectionAuditEvent,
2568
- getRules,
2569
- normalizeRulesPath,
4053
+ invalidateKnowledgeSyncCooldown,
4054
+ ensureKnowledgeFresh,
4055
+ reconcileKnowledge,
4056
+ getKnowledge,
4057
+ normalizeKnowledgePath,
2570
4058
  runDoctorReport,
2571
- runDoctorFix
4059
+ runDoctorFix,
4060
+ runDoctorApplyLint
2572
4061
  };