@fenglimg/fabric-server 2.0.0-rc.15 → 2.0.0-rc.22

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.
@@ -172,13 +172,18 @@ function isNodeError(error) {
172
172
 
173
173
  // src/services/event-ledger.ts
174
174
  import { randomUUID } from "crypto";
175
- import { existsSync, fsyncSync, openSync, closeSync } from "fs";
176
- import { readFile as readFile2, truncate, writeFile } from "fs/promises";
175
+ import { existsSync, fsyncSync, openSync, closeSync, readFileSync, statSync } from "fs";
176
+ import { appendFile, mkdir as mkdir2, readFile as readFile2, truncate, writeFile } from "fs/promises";
177
+ import { join as join3 } from "path";
177
178
  import {
178
179
  eventLedgerEventSchema
179
180
  } from "@fenglimg/fabric-shared";
180
- import { createLedgerWriteQueue } from "@fenglimg/fabric-shared/node/atomic-write";
181
+ import { atomicWriteText as atomicWriteText2, createLedgerWriteQueue } from "@fenglimg/fabric-shared/node/atomic-write";
181
182
  var ledgerQueue = createLedgerWriteQueue();
183
+ var EVENT_LEDGER_DEFAULT_RETENTION_DAYS = 30;
184
+ var EVENT_LEDGER_SIZE_WARN_BYTES = 50 * 1024 * 1024;
185
+ var EVENT_LEDGER_ARCHIVE_DIR = ".fabric/events.archive";
186
+ var warnedOversize = false;
182
187
  async function appendEventLedgerEvent(projectRoot, event) {
183
188
  const eventPath = getEventLedgerPath(projectRoot);
184
189
  const nextEvent = eventLedgerEventSchema.parse({
@@ -190,6 +195,18 @@ async function appendEventLedgerEvent(projectRoot, event) {
190
195
  });
191
196
  await ensureParentDirectory(eventPath);
192
197
  await ledgerQueue.append(eventPath, JSON.stringify(nextEvent));
198
+ if (!warnedOversize) {
199
+ try {
200
+ const size = statSync(eventPath).size;
201
+ if (size > EVENT_LEDGER_SIZE_WARN_BYTES) {
202
+ warnedOversize = true;
203
+ process.stderr.write(
204
+ 'fabric: events.jsonl > 50MB, run "fab doctor --fix" to rotate\n'
205
+ );
206
+ }
207
+ } catch {
208
+ }
209
+ }
193
210
  return nextEvent;
194
211
  }
195
212
  async function readEventLedger(projectRoot, options = {}) {
@@ -265,6 +282,120 @@ function createDerivedId(index, line) {
265
282
  function isNodeError2(error) {
266
283
  return error instanceof Error;
267
284
  }
285
+ async function rotateEventLedgerIfNeeded(projectRoot, opts = {}) {
286
+ const eventPath = getEventLedgerPath(projectRoot);
287
+ return ledgerQueue.runExclusive(eventPath, async () => {
288
+ const now = opts.now ?? /* @__PURE__ */ new Date();
289
+ const retentionDays = resolveRetentionDays(projectRoot, opts.retentionDays);
290
+ const cutoffMs = now.getTime() - retentionDays * 864e5;
291
+ let raw;
292
+ try {
293
+ raw = await readFile2(eventPath, "utf8");
294
+ } catch (error) {
295
+ if (isNodeError2(error) && error.code === "ENOENT") {
296
+ return { rotated: false, archivedCount: 0, keptCount: 0 };
297
+ }
298
+ throw error;
299
+ }
300
+ if (raw.length === 0) {
301
+ return { rotated: false, archivedCount: 0, keptCount: 0 };
302
+ }
303
+ const hasTrailingNewline = raw.endsWith("\n");
304
+ const segments = raw.split(/\r?\n/);
305
+ let keptTail = "";
306
+ if (!hasTrailingNewline && segments.length > 0) {
307
+ keptTail = segments.pop() ?? "";
308
+ }
309
+ const archived = [];
310
+ const kept = [];
311
+ for (const line of segments) {
312
+ const trimmed = line.trim();
313
+ if (trimmed.length === 0) continue;
314
+ let ts;
315
+ try {
316
+ const parsed = JSON.parse(trimmed);
317
+ const candidate = parsed["ts"];
318
+ if (typeof candidate === "number" && Number.isFinite(candidate)) {
319
+ ts = candidate;
320
+ }
321
+ } catch {
322
+ }
323
+ if (ts !== void 0 && ts < cutoffMs) {
324
+ archived.push(trimmed);
325
+ } else {
326
+ kept.push(trimmed);
327
+ }
328
+ }
329
+ if (archived.length === 0) {
330
+ return {
331
+ rotated: false,
332
+ archivedCount: 0,
333
+ keptCount: kept.length
334
+ };
335
+ }
336
+ const yyyymmdd = formatUtcDate(now);
337
+ const archiveDirAbsolute = join3(projectRoot, EVENT_LEDGER_ARCHIVE_DIR);
338
+ const archiveFilename = `events-rotated-${yyyymmdd}.jsonl`;
339
+ const archiveAbsolutePath = join3(archiveDirAbsolute, archiveFilename);
340
+ const archiveRelativePath = `${EVENT_LEDGER_ARCHIVE_DIR}/${archiveFilename}`;
341
+ await mkdir2(archiveDirAbsolute, { recursive: true });
342
+ await appendFile(
343
+ archiveAbsolutePath,
344
+ archived.map((line) => `${line}
345
+ `).join(""),
346
+ "utf8"
347
+ );
348
+ const auditEvent = eventLedgerEventSchema.parse({
349
+ kind: "fabric-event",
350
+ id: `event:${randomUUID()}`,
351
+ ts: now.getTime(),
352
+ schema_version: 1,
353
+ event_type: "events_rotated",
354
+ cutoff_ts: new Date(cutoffMs).toISOString(),
355
+ archived_count: archived.length,
356
+ kept_count: kept.length,
357
+ archive_path: archiveRelativePath
358
+ });
359
+ const newMainLines = [JSON.stringify(auditEvent), ...kept];
360
+ let newMainContent = newMainLines.join("\n") + "\n";
361
+ if (keptTail.length > 0) {
362
+ newMainContent += keptTail;
363
+ }
364
+ await atomicWriteText2(eventPath, newMainContent);
365
+ return {
366
+ rotated: true,
367
+ archivedCount: archived.length,
368
+ keptCount: kept.length,
369
+ archivePath: archiveRelativePath
370
+ };
371
+ });
372
+ }
373
+ function resolveRetentionDays(projectRoot, override) {
374
+ if (typeof override === "number" && Number.isFinite(override) && override >= 0) {
375
+ return override;
376
+ }
377
+ const configPath = join3(projectRoot, ".fabric", "fabric-config.json");
378
+ try {
379
+ if (existsSync(configPath)) {
380
+ const raw = readFileSync(configPath, "utf8");
381
+ const parsed = JSON.parse(raw);
382
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
383
+ const v = parsed.fabric_event_retention_days;
384
+ if (v === 7 || v === 30 || v === 90) {
385
+ return v;
386
+ }
387
+ }
388
+ }
389
+ } catch {
390
+ }
391
+ return EVENT_LEDGER_DEFAULT_RETENTION_DAYS;
392
+ }
393
+ function formatUtcDate(date) {
394
+ const yyyy = date.getUTCFullYear().toString().padStart(4, "0");
395
+ const mm = (date.getUTCMonth() + 1).toString().padStart(2, "0");
396
+ const dd = date.getUTCDate().toString().padStart(2, "0");
397
+ return `${yyyy}-${mm}-${dd}`;
398
+ }
268
399
  function flushAndSyncEventLedger(projectRoot) {
269
400
  const ledgerPath = getEventLedgerPath(projectRoot);
270
401
  if (!existsSync(ledgerPath)) return;
@@ -277,10 +408,10 @@ function flushAndSyncEventLedger(projectRoot) {
277
408
  }
278
409
 
279
410
  // src/services/knowledge-meta-builder.ts
280
- import { mkdir as mkdir2, readdir, readFile as readFile3 } from "fs/promises";
281
- import { existsSync as existsSync2, statSync } from "fs";
411
+ import { mkdir as mkdir3, readdir, readFile as readFile3 } from "fs/promises";
412
+ import { existsSync as existsSync2, statSync as statSync2 } from "fs";
282
413
  import { homedir } from "os";
283
- import { isAbsolute, join as join3, relative, resolve as resolve2, sep as sep2 } from "path";
414
+ import { isAbsolute, join as join4, relative, resolve as resolve2, sep as sep2 } from "path";
284
415
  import {
285
416
  KNOWLEDGE_TEST_INDEX_SCHEMA_VERSION,
286
417
  agentsMetaSchema as agentsMetaSchema3,
@@ -296,12 +427,12 @@ import {
296
427
  StableIdSchema,
297
428
  parseKnowledgeId
298
429
  } from "@fenglimg/fabric-shared";
299
- import { atomicWriteText as atomicWriteText2 } from "@fenglimg/fabric-shared/node/atomic-write";
430
+ import { atomicWriteText as atomicWriteText3 } from "@fenglimg/fabric-shared/node/atomic-write";
300
431
  async function buildKnowledgeMeta(projectRootInput) {
301
432
  const projectRoot = normalizeProjectRoot(projectRootInput);
302
433
  assertExistingDirectory(projectRoot);
303
- const metaPath = join3(projectRoot, ".fabric", "agents.meta.json");
304
- const knowledgeTestIndexPath = join3(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
434
+ const metaPath = join4(projectRoot, ".fabric", "agents.meta.json");
435
+ const knowledgeTestIndexPath = join4(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
305
436
  const existingMeta = await readExistingMeta(metaPath);
306
437
  const existingKnowledgeTestIndex = await readExistingKnowledgeTestIndex(knowledgeTestIndexPath);
307
438
  const meta = await computeKnowledgeBasedAgentsMeta(projectRoot, existingMeta);
@@ -314,18 +445,18 @@ async function buildKnowledgeMeta(projectRootInput) {
314
445
  }
315
446
  async function writeKnowledgeMeta(projectRootInput, options) {
316
447
  const projectRoot = normalizeProjectRoot(projectRootInput);
317
- const metaPath = join3(projectRoot, ".fabric", "agents.meta.json");
318
- const knowledgeTestIndexPath = join3(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
448
+ const metaPath = join4(projectRoot, ".fabric", "agents.meta.json");
449
+ const knowledgeTestIndexPath = join4(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
319
450
  const existingMeta = await readExistingMeta(metaPath);
320
451
  const result = await buildKnowledgeMeta(projectRoot);
321
452
  if (!result.changed) {
322
453
  return result;
323
454
  }
324
455
  await ensureParentDirectory(metaPath);
325
- await atomicWriteText2(metaPath, `${JSON.stringify(result.meta, null, 2)}
456
+ await atomicWriteText3(metaPath, `${JSON.stringify(result.meta, null, 2)}
326
457
  `);
327
458
  await ensureParentDirectory(knowledgeTestIndexPath);
328
- await atomicWriteText2(knowledgeTestIndexPath, `${JSON.stringify(result.knowledgeTestIndex, null, 2)}
459
+ await atomicWriteText3(knowledgeTestIndexPath, `${JSON.stringify(result.knowledgeTestIndex, null, 2)}
329
460
  `);
330
461
  if (existingMeta === void 0 || stableStringify(existingMeta) !== stableStringify(result.meta)) {
331
462
  await recordBaselineSynced(projectRoot, {
@@ -342,7 +473,7 @@ async function writeKnowledgeMeta(projectRootInput, options) {
342
473
  async function computeKnowledgeBasedAgentsMeta(projectRootInput, existingMeta) {
343
474
  const projectRoot = normalizeProjectRoot(projectRootInput);
344
475
  assertExistingDirectory(projectRoot);
345
- const previousMeta = existingMeta ?? await readExistingMeta(join3(projectRoot, ".fabric", "agents.meta.json"));
476
+ const previousMeta = existingMeta ?? await readExistingMeta(join4(projectRoot, ".fabric", "agents.meta.json"));
346
477
  const existingByContentRef = indexExistingNodesByContentRef(previousMeta);
347
478
  const ruleFiles = await findKnowledgeFiles(projectRoot);
348
479
  const nodes = {};
@@ -433,7 +564,7 @@ function normalizeProjectRoot(projectRoot) {
433
564
  return isAbsolute(projectRoot) ? projectRoot : resolve2(process.cwd(), projectRoot);
434
565
  }
435
566
  function assertExistingDirectory(projectRoot) {
436
- if (!existsSync2(projectRoot) || !statSync(projectRoot).isDirectory()) {
567
+ if (!existsSync2(projectRoot) || !statSync2(projectRoot).isDirectory()) {
437
568
  throw new Error(`Target directory does not exist: ${projectRoot}`);
438
569
  }
439
570
  }
@@ -477,17 +608,17 @@ function resolvePersonalRoot() {
477
608
  }
478
609
  function resolveContentRefPath(projectRoot, contentRef) {
479
610
  if (contentRef.startsWith(PERSONAL_CONTENT_REF_PREFIX)) {
480
- return join3(resolvePersonalRoot(), ".fabric", "knowledge", contentRef.slice(PERSONAL_CONTENT_REF_PREFIX.length));
611
+ return join4(resolvePersonalRoot(), ".fabric", "knowledge", contentRef.slice(PERSONAL_CONTENT_REF_PREFIX.length));
481
612
  }
482
- return join3(projectRoot, contentRef);
613
+ return join4(projectRoot, contentRef);
483
614
  }
484
615
  async function findKnowledgeFiles(projectRoot) {
485
- const teamRoot = join3(projectRoot, ".fabric", "knowledge");
486
- const personalRoot = join3(resolvePersonalRoot(), ".fabric", "knowledge");
616
+ const teamRoot = join4(projectRoot, ".fabric", "knowledge");
617
+ const personalRoot = join4(resolvePersonalRoot(), ".fabric", "knowledge");
487
618
  try {
488
- await mkdir2(personalRoot, { recursive: true });
619
+ await mkdir3(personalRoot, { recursive: true });
489
620
  for (const sub of KNOWLEDGE_SUBDIRS) {
490
- await mkdir2(join3(personalRoot, sub), { recursive: true });
621
+ await mkdir3(join4(personalRoot, sub), { recursive: true });
491
622
  }
492
623
  } catch {
493
624
  }
@@ -496,11 +627,11 @@ async function findKnowledgeFiles(projectRoot) {
496
627
  [teamRoot, TEAM_CONTENT_REF_PREFIX],
497
628
  [personalRoot, PERSONAL_CONTENT_REF_PREFIX]
498
629
  ]) {
499
- if (!existsSync2(root) || !statSync(root).isDirectory()) {
630
+ if (!existsSync2(root) || !statSync2(root).isDirectory()) {
500
631
  continue;
501
632
  }
502
633
  for (const subdir of KNOWLEDGE_SUBDIRS) {
503
- const dir = join3(root, subdir);
634
+ const dir = join4(root, subdir);
504
635
  let entries;
505
636
  try {
506
637
  entries = await readdir(dir, { withFileTypes: true });
@@ -524,7 +655,7 @@ async function findFabricVerifyAnnotations(projectRoot) {
524
655
  const annotations = [];
525
656
  const annotationPattern = /^\s*\/\/\s*@fabric-verify\s+([A-Za-z0-9][A-Za-z0-9/_-]*)\s*$/u;
526
657
  for (const testFile of files) {
527
- const source = await readFile3(join3(projectRoot, testFile), "utf8");
658
+ const source = await readFile3(join4(projectRoot, testFile), "utf8");
528
659
  const testHash = sha256(source);
529
660
  const lines = source.split(/\r?\n/u);
530
661
  for (const [index, line] of lines.entries()) {
@@ -552,7 +683,7 @@ async function findTestFiles(projectRoot) {
552
683
  continue;
553
684
  }
554
685
  for (const entry of await readdir(current, { withFileTypes: true })) {
555
- const absolutePath = join3(current, entry.name);
686
+ const absolutePath = join4(current, entry.name);
556
687
  const relativePath = toPosixPath(relative(projectRoot, absolutePath));
557
688
  const [rootSegment] = relativePath.split("/");
558
689
  if (entry.isDirectory()) {
@@ -802,23 +933,29 @@ function extractRuleDescription(source) {
802
933
  if (summary === void 0 || summary.length === 0) {
803
934
  return void 0;
804
935
  }
936
+ const knowledge = frontmatter !== null ? extractKnowledgeFieldsFromFrontmatter(frontmatter[1]) : void 0;
805
937
  return {
806
938
  summary,
807
939
  intent_clues: [],
808
940
  tech_stack: [],
809
941
  impact: [],
810
942
  must_read_if: summary,
811
- // v2.0 knowledge fields are absent in heading-only fallback.
812
- id: void 0,
813
- knowledge_type: void 0,
814
- maturity: void 0,
815
- knowledge_layer: void 0,
816
- layer_reason: void 0,
817
- created_at: 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: []
943
+ // v2.0-rc.22: when frontmatter is present, merge its knowledge fields;
944
+ // when fully absent (no `---` block), all knowledge fields stay
945
+ // undefined, matching the original heading-only fallback contract.
946
+ id: knowledge?.id,
947
+ knowledge_type: knowledge?.knowledge_type,
948
+ maturity: knowledge?.maturity,
949
+ knowledge_layer: knowledge?.knowledge_layer,
950
+ layer_reason: knowledge?.layer_reason,
951
+ created_at: knowledge?.created_at,
952
+ tags: knowledge?.tags,
953
+ // v2.0-rc.5 (C1): default-safe values when there is no frontmatter at all;
954
+ // when frontmatter exists, honor its declared values (extractKnowledge
955
+ // FieldsFromFrontmatter already applies the broad-default for missing
956
+ // or malformed scopes).
957
+ relevance_scope: knowledge?.relevance_scope ?? "broad",
958
+ relevance_paths: knowledge?.relevance_paths ?? []
822
959
  };
823
960
  }
824
961
  function extractRuleSections(source) {
@@ -960,9 +1097,34 @@ function isNodeError3(error) {
960
1097
 
961
1098
  // src/services/knowledge-sync.ts
962
1099
  import { readdir as readdir2, readFile as readFile4, stat } from "fs/promises";
963
- import { existsSync as existsSync3, statSync as statSync2 } from "fs";
964
- import { join as join4, relative as relative2, sep as sep3 } from "path";
1100
+ import { existsSync as existsSync3, statSync as statSync3 } from "fs";
1101
+ import { homedir as homedir2 } from "os";
1102
+ import { join as join5 } from "path";
965
1103
  import { RuleValidationError } from "@fenglimg/fabric-shared/errors";
1104
+ var PERSONAL_CONTENT_REF_PREFIX2 = "~/.fabric/knowledge/";
1105
+ var TEAM_CONTENT_REF_PREFIX2 = ".fabric/knowledge/";
1106
+ var KNOWLEDGE_SUBDIRS2 = [
1107
+ "decisions",
1108
+ "pitfalls",
1109
+ "guidelines",
1110
+ "models",
1111
+ "processes",
1112
+ "pending"
1113
+ ];
1114
+ function resolvePersonalRoot2() {
1115
+ return process.env.FABRIC_HOME ?? homedir2();
1116
+ }
1117
+ function resolveContentRefPath2(projectRoot, relPath) {
1118
+ if (relPath.startsWith(PERSONAL_CONTENT_REF_PREFIX2)) {
1119
+ return join5(
1120
+ resolvePersonalRoot2(),
1121
+ ".fabric",
1122
+ "knowledge",
1123
+ relPath.slice(PERSONAL_CONTENT_REF_PREFIX2.length)
1124
+ );
1125
+ }
1126
+ return join5(projectRoot, relPath);
1127
+ }
966
1128
  var lastSyncState = /* @__PURE__ */ new Map();
967
1129
  var freshSyncCooldown = /* @__PURE__ */ new Map();
968
1130
  var SYNC_COOLDOWN_MS = 500;
@@ -970,7 +1132,7 @@ function invalidateKnowledgeSyncCooldown(projectRoot) {
970
1132
  freshSyncCooldown.delete(projectRoot);
971
1133
  }
972
1134
  async function readMetaEntries(projectRoot) {
973
- const metaPath = join4(projectRoot, ".fabric", "agents.meta.json");
1135
+ const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
974
1136
  const map = /* @__PURE__ */ new Map();
975
1137
  let raw;
976
1138
  try {
@@ -995,32 +1157,36 @@ async function readMetaEntries(projectRoot) {
995
1157
  return map;
996
1158
  }
997
1159
  async function findRuleFiles(projectRoot) {
998
- const knowledgeRoot = join4(projectRoot, ".fabric", "knowledge");
999
- if (!existsSync3(knowledgeRoot) || !statSync2(knowledgeRoot).isDirectory()) {
1000
- return [];
1001
- }
1160
+ const teamRoot = join5(projectRoot, ".fabric", "knowledge");
1161
+ const personalRoot = join5(resolvePersonalRoot2(), ".fabric", "knowledge");
1002
1162
  const files = [];
1003
- const stack = [knowledgeRoot];
1004
- while (stack.length > 0) {
1005
- const current = stack.pop();
1006
- if (current === void 0) {
1163
+ for (const [root, prefix] of [
1164
+ [teamRoot, TEAM_CONTENT_REF_PREFIX2],
1165
+ [personalRoot, PERSONAL_CONTENT_REF_PREFIX2]
1166
+ ]) {
1167
+ if (!existsSync3(root) || !statSync3(root).isDirectory()) {
1007
1168
  continue;
1008
1169
  }
1009
- for (const entry of await readdir2(current, { withFileTypes: true })) {
1010
- const absolutePath = join4(current, entry.name);
1011
- if (entry.isDirectory()) {
1012
- stack.push(absolutePath);
1013
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
1014
- const rel = toPosixPath2(relative2(projectRoot, absolutePath));
1015
- files.push(rel);
1170
+ for (const subdir of KNOWLEDGE_SUBDIRS2) {
1171
+ const dir = join5(root, subdir);
1172
+ if (!existsSync3(dir) || !statSync3(dir).isDirectory()) {
1173
+ continue;
1174
+ }
1175
+ let entries;
1176
+ try {
1177
+ entries = await readdir2(dir, { withFileTypes: true });
1178
+ } catch {
1179
+ continue;
1180
+ }
1181
+ for (const entry of entries) {
1182
+ if (entry.isFile() && entry.name.endsWith(".md")) {
1183
+ files.push(`${prefix}${subdir}/${entry.name}`);
1184
+ }
1016
1185
  }
1017
1186
  }
1018
1187
  }
1019
1188
  return files.sort();
1020
1189
  }
1021
- function toPosixPath2(p) {
1022
- return p.split(sep3).join("/");
1023
- }
1024
1190
  function validateFrontmatter(source, filePath, throwOnInvalid) {
1025
1191
  if (!source.startsWith("---")) {
1026
1192
  return null;
@@ -1066,7 +1232,7 @@ function validateFrontmatter(source, filePath, throwOnInvalid) {
1066
1232
  return null;
1067
1233
  }
1068
1234
  async function processSingleFile(projectRoot, relPath, metaEntry, source, throwOnInvalidFrontmatter) {
1069
- const absPath = join4(projectRoot, relPath);
1235
+ const absPath = resolveContentRefPath2(projectRoot, relPath);
1070
1236
  try {
1071
1237
  await stat(absPath);
1072
1238
  } catch {
@@ -1165,7 +1331,7 @@ async function ensureKnowledgeFresh(projectRoot, opts) {
1165
1331
  }
1166
1332
  for (const [relPath, entry] of metaEntries) {
1167
1333
  if (!ruleFiles.includes(relPath)) {
1168
- const absPath = join4(projectRoot, relPath);
1334
+ const absPath = resolveContentRefPath2(projectRoot, relPath);
1169
1335
  if (!existsSync3(absPath)) {
1170
1336
  events.push({
1171
1337
  type: "rule_removed",
@@ -1217,7 +1383,7 @@ async function reconcileKnowledge(projectRoot, opts) {
1217
1383
  }
1218
1384
  for (const [relPath, entry] of metaEntries) {
1219
1385
  if (!ruleFiles.includes(relPath)) {
1220
- const absPath = join4(projectRoot, relPath);
1386
+ const absPath = resolveContentRefPath2(projectRoot, relPath);
1221
1387
  if (!existsSync3(absPath)) {
1222
1388
  events.push({
1223
1389
  type: "rule_removed",
@@ -1231,14 +1397,27 @@ async function reconcileKnowledge(projectRoot, opts) {
1231
1397
  }
1232
1398
  }
1233
1399
  }
1234
- if (events.length > 0) {
1400
+ let revisionDrift = false;
1401
+ try {
1402
+ const derived = await buildKnowledgeMeta(projectRoot);
1403
+ const onDisk = await readAgentsMeta(projectRoot);
1404
+ revisionDrift = onDisk.revision !== derived.meta.revision;
1405
+ } catch (error) {
1406
+ if (!(error instanceof AgentsMetaFileMissingError) && !(error instanceof AgentsMetaInvalidError)) {
1407
+ throw error;
1408
+ }
1409
+ }
1410
+ if (events.length > 0 || revisionDrift) {
1235
1411
  await writeKnowledgeMeta(projectRoot, { source: "sync_meta" });
1236
- await appendRuleSyncEvents(projectRoot, events);
1412
+ if (events.length > 0) {
1413
+ await appendRuleSyncEvents(projectRoot, events);
1414
+ }
1237
1415
  contextCache.invalidate("file_watch", projectRoot);
1416
+ contextCache.invalidate("meta_write", projectRoot);
1238
1417
  }
1239
1418
  const duration_ms = Date.now() - startTime;
1240
1419
  const reconciledFiles = events.map((e) => e.path);
1241
- if (trigger !== void 0 && events.length > 0) {
1420
+ if (trigger !== void 0 && (events.length > 0 || revisionDrift)) {
1242
1421
  if (trigger === "startup") {
1243
1422
  await appendEventLedgerEvent(projectRoot, {
1244
1423
  event_type: "meta_reconciled_on_startup",
@@ -1252,11 +1431,12 @@ async function reconcileKnowledge(projectRoot, opts) {
1252
1431
  reconciled_files: reconciledFiles,
1253
1432
  duration_ms,
1254
1433
  trigger,
1255
- source: "reconcileKnowledge"
1434
+ source: "reconcileKnowledge",
1435
+ ...events.length === 0 && revisionDrift ? { force_write_reason: "revision_drift" } : {}
1256
1436
  });
1257
1437
  }
1258
1438
  }
1259
- if (events.length === 0 && warnings.length === 0) {
1439
+ if (events.length === 0 && warnings.length === 0 && !revisionDrift) {
1260
1440
  return { status: "fresh", events: [], warnings: [] };
1261
1441
  }
1262
1442
  const status = warnings.length > 0 ? "errors" : "reconciled";
@@ -1270,21 +1450,26 @@ async function reconcileKnowledge(projectRoot, opts) {
1270
1450
 
1271
1451
  // src/services/doctor.ts
1272
1452
  import { execFileSync } from "child_process";
1273
- import { existsSync as existsSync4, readdirSync, readFileSync, statSync as statSync3 } from "fs";
1274
- import { access, mkdir as mkdir3, readFile as readFile5, rename, writeFile as writeFile2 } from "fs/promises";
1453
+ import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync2, statSync as statSync4 } from "fs";
1454
+ import { access, mkdir as mkdir4, readFile as readFile5, rename, writeFile as writeFile2 } from "fs/promises";
1275
1455
  import { constants } from "fs";
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";
1456
+ import { homedir as homedir3 } from "os";
1457
+ import { isAbsolute as isAbsolute2, join as join6, posix, relative as nodeRelative, resolve as resolve3, sep as sep3 } from "path";
1278
1458
  import { minimatch } from "minimatch";
1279
1459
  import {
1280
1460
  agentsMetaSchema as agentsMetaSchema4,
1281
1461
  AgentsMetaCountersSchema,
1282
1462
  forensicReportSchema,
1283
1463
  parseKnowledgeId as parseKnowledgeId2,
1284
- knowledgeTestIndexSchema as knowledgeTestIndexSchema2
1464
+ knowledgeTestIndexSchema as knowledgeTestIndexSchema2,
1465
+ LEGACY_KB_REGEX,
1466
+ BOOTSTRAP_CANONICAL,
1467
+ BOOTSTRAP_MARKER_BEGIN,
1468
+ BOOTSTRAP_MARKER_END,
1469
+ BOOTSTRAP_REGEX
1285
1470
  } from "@fenglimg/fabric-shared";
1286
1471
  import { detectFramework } from "@fenglimg/fabric-shared/node";
1287
- import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText3 } from "@fenglimg/fabric-shared/node/atomic-write";
1472
+ import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText4 } from "@fenglimg/fabric-shared/node/atomic-write";
1288
1473
  var ORPHAN_DEMOTE_THRESHOLD_DAYS = {
1289
1474
  stable: 90,
1290
1475
  endorsed: 30,
@@ -1322,7 +1507,22 @@ var KNOWLEDGE_CANONICAL_TYPE_DIRS = [
1322
1507
  "processes"
1323
1508
  ];
1324
1509
  var CANONICAL_KNOWLEDGE_FILENAME_PATTERN = /^(K[PT]-(?:MOD|DEC|GLD|PIT|PRO)-\d{4,})--[a-z0-9][a-z0-9-]*\.md$/u;
1325
- var KNOWLEDGE_SUBDIRS2 = ["decisions", "pitfalls", "guidelines", "models", "processes", "pending"];
1510
+ var KNOWLEDGE_SUBDIRS3 = ["decisions", "pitfalls", "guidelines", "models", "processes", "pending"];
1511
+ var BASELINE_FILENAME_LINT_BASELINE_IDS = /* @__PURE__ */ new Set([
1512
+ "KT-MOD-0001",
1513
+ // tech-stack
1514
+ "KT-MOD-0002",
1515
+ // module-structure
1516
+ "KT-MOD-0003",
1517
+ // readme-first-paragraph
1518
+ "KT-PRO-0001",
1519
+ // build-config
1520
+ "KT-PRO-0002",
1521
+ // ci-config
1522
+ "KT-GLD-0001"
1523
+ // code-style
1524
+ ]);
1525
+ var BASELINE_ID_PREFIXED_FILENAME_PATTERN = /^KT-[A-Z]+-\d+--.+\.md$/u;
1326
1526
  var COUNTER_TYPE_CODES = ["MOD", "DEC", "GLD", "PIT", "PRO"];
1327
1527
  var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
1328
1528
  var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
@@ -1342,7 +1542,13 @@ var TARGET_FILE_PATHS = [
1342
1542
  ".fabric/agents.meta.json",
1343
1543
  ".fabric/.cache/knowledge-test.index.json",
1344
1544
  ".fabric/events.jsonl",
1345
- ".fabric/knowledge"
1545
+ ".fabric/knowledge",
1546
+ // v2.0.0-rc.19 bootstrap-consolidation TASK-005: L1 canonical snapshot
1547
+ // (.fabric/AGENTS.md) and optional project-rules concat source
1548
+ // (.fabric/project-rules.md). Surfaced in summary.targetFiles so --json
1549
+ // consumers can confirm L1 presence at a glance.
1550
+ ".fabric/AGENTS.md",
1551
+ ".fabric/project-rules.md"
1346
1552
  ];
1347
1553
  async function runDoctorReport(target) {
1348
1554
  const projectRoot = normalizeTarget(target);
@@ -1352,17 +1558,30 @@ async function runDoctorReport(target) {
1352
1558
  forensic,
1353
1559
  meta,
1354
1560
  eventLedger,
1355
- knowledgeTestIndex
1561
+ knowledgeTestIndex,
1562
+ bootstrapMarkerMigration,
1563
+ l1BootstrapSnapshotDrift,
1564
+ l2ManagedBlockDrift
1356
1565
  ] = await Promise.all([
1357
1566
  inspectForensic(projectRoot),
1358
1567
  inspectMeta(projectRoot),
1359
1568
  inspectEventLedger(projectRoot),
1360
- inspectKnowledgeTestIndex(projectRoot)
1569
+ inspectKnowledgeTestIndex(projectRoot),
1570
+ // v2.0.0-rc.19 TASK-004: one-time fabric:knowledge-base → fabric:bootstrap
1571
+ // marker migration scan. Inspect runs in this Promise.all block to keep
1572
+ // performance parity with the other I/O-bound inspections.
1573
+ inspectBootstrapMarkerMigration(projectRoot),
1574
+ // v2.0.0-rc.19 TASK-005: L1 + L2 byte-level drift detection. Both are
1575
+ // I/O-bound (small file reads + buffer compare) so they live in the same
1576
+ // Promise.all block as the other bootstrap inspections.
1577
+ inspectL1BootstrapSnapshotDrift(projectRoot),
1578
+ inspectL2ManagedBlockDrift(projectRoot)
1361
1579
  ]);
1362
1580
  const mcpConfigInWrongFile = inspectMcpConfigInWrongFile(projectRoot);
1363
1581
  const metaManuallyDiverged = await inspectMetaManuallyDiverged(projectRoot);
1364
1582
  const knowledgeDirUnindexed = inspectKnowledgeDirUnindexed(projectRoot, meta);
1365
1583
  const knowledgeDirMissing = inspectKnowledgeDirMissing(projectRoot);
1584
+ const baselineFilenameFormat = inspectBaselineFilenameFormat(projectRoot);
1366
1585
  const stableIdCollision = await inspectStableIdCollisions(projectRoot);
1367
1586
  const counterDesync = inspectCounterDesync(meta);
1368
1587
  const preexistingRootFiles = inspectPreexistingRootFiles(projectRoot);
@@ -1385,7 +1604,20 @@ async function runDoctorReport(target) {
1385
1604
  const skillMdYamlInvalid = inspectSkillMdYamlInvalid(projectRoot);
1386
1605
  const checks = [
1387
1606
  createBootstrapAnchorCheck(bootstrapAnchor),
1607
+ // v2.0.0-rc.19 TASK-004: bootstrap marker migration check sits adjacent to
1608
+ // the anchor check — both are bootstrap-file invariants. fixable_error
1609
+ // when any of the four target paths still carries the legacy marker.
1610
+ createBootstrapMarkerMigrationCheck(bootstrapMarkerMigration),
1611
+ // v2.0.0-rc.19 TASK-005: L1 + L2 byte-level drift detection sit immediately
1612
+ // after the marker migration check. Order: anchor existence → migration →
1613
+ // L1 (canonical ↔ snapshot) → L2 (snapshot+rules ↔ three-end blocks).
1614
+ createL1BootstrapSnapshotDriftCheck(l1BootstrapSnapshotDrift),
1615
+ createL2ManagedBlockDriftCheck(l2ManagedBlockDrift),
1388
1616
  createKnowledgeDirMissingCheck(knowledgeDirMissing),
1617
+ // v2.0.0-rc.22 TASK-006: baseline filename format. Sits adjacent to
1618
+ // knowledge_dir_missing — both are knowledge-layout invariants. manual_error
1619
+ // kind; resolution delegates to `fab scan` (no --fix path).
1620
+ createBaselineFilenameFormatCheck(baselineFilenameFormat),
1389
1621
  createForensicCheck(forensic, framework.kind, entryPoints.length),
1390
1622
  // v2.0: removed `createInitContextCheck` — `.fabric/init-context.json`
1391
1623
  // is owned by the AI-side client init skill, not by `fabric install` CLI.
@@ -1485,7 +1717,7 @@ async function runDoctorReport(target) {
1485
1717
  warningCount: warnings.length,
1486
1718
  infoCount: infos.length,
1487
1719
  targetFiles: Object.fromEntries(
1488
- TARGET_FILE_PATHS.map((path) => [path, existsSync4(join5(projectRoot, path))])
1720
+ TARGET_FILE_PATHS.map((path) => [path, existsSync4(join6(projectRoot, path))])
1489
1721
  )
1490
1722
  }
1491
1723
  };
@@ -1494,6 +1726,33 @@ async function runDoctorFix(target) {
1494
1726
  const projectRoot = normalizeTarget(target);
1495
1727
  const before = await runDoctorReport(projectRoot);
1496
1728
  const fixed = [];
1729
+ if (before.fixable_errors.some(
1730
+ (issue) => issue.code === "bootstrap_marker_migration_required"
1731
+ )) {
1732
+ const migrated = await migrateBootstrapMarkers(projectRoot);
1733
+ fixed.push(findIssue(before.fixable_errors, "bootstrap_marker_migration_required"));
1734
+ for (const path of migrated.paths) {
1735
+ await appendEventLedgerEvent(projectRoot, {
1736
+ event_type: "bootstrap_marker_migrated",
1737
+ path,
1738
+ migrated_count: migrated.countPerPath[path] ?? 1,
1739
+ legacy_marker: "fabric:knowledge-base",
1740
+ new_marker: "fabric:bootstrap",
1741
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1742
+ }).catch(() => {
1743
+ });
1744
+ }
1745
+ }
1746
+ if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
1747
+ const snapshotPath = join6(projectRoot, ".fabric", "AGENTS.md");
1748
+ await ensureParentDirectory(snapshotPath);
1749
+ await atomicWriteText4(snapshotPath, BOOTSTRAP_CANONICAL);
1750
+ fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
1751
+ }
1752
+ if (before.fixable_errors.some((issue) => issue.code === "managed_block_drift")) {
1753
+ await rewriteThreeEndManagedBlocks(projectRoot);
1754
+ fixed.push(findIssue(before.fixable_errors, "managed_block_drift"));
1755
+ }
1497
1756
  if (before.fixable_errors.some((issue) => issue.code === "knowledge_dir_missing")) {
1498
1757
  await ensureKnowledgeSubdirs(projectRoot);
1499
1758
  fixed.push(findIssue(before.fixable_errors, "knowledge_dir_missing"));
@@ -1507,26 +1766,23 @@ async function runDoctorFix(target) {
1507
1766
  fixed.push(findIssue(before.fixable_errors, "counter_desync"));
1508
1767
  contextCache.invalidate("meta_write", projectRoot);
1509
1768
  }
1510
- if (before.fixable_errors.some(
1511
- (issue) => [
1512
- "agents_meta_missing",
1513
- "agents_meta_stale",
1514
- "knowledge_test_index_missing",
1515
- "knowledge_test_index_stale",
1516
- "content_ref_missing",
1517
- "knowledge_dir_unindexed"
1518
- ].includes(issue.code)
1519
- )) {
1769
+ const reconcileCodes = [
1770
+ "agents_meta_missing",
1771
+ "agents_meta_stale",
1772
+ "knowledge_test_index_missing",
1773
+ "knowledge_test_index_stale",
1774
+ "content_ref_missing",
1775
+ "knowledge_dir_unindexed"
1776
+ ];
1777
+ if (before.fixable_errors.some((issue) => reconcileCodes.includes(issue.code)) || before.warnings.some((issue) => reconcileCodes.includes(issue.code))) {
1520
1778
  await reconcileKnowledge(projectRoot, { trigger: "doctor" });
1521
1779
  for (const issue of before.fixable_errors.filter(
1522
- (candidate) => [
1523
- "agents_meta_missing",
1524
- "agents_meta_stale",
1525
- "knowledge_test_index_missing",
1526
- "knowledge_test_index_stale",
1527
- "content_ref_missing",
1528
- "knowledge_dir_unindexed"
1529
- ].includes(candidate.code)
1780
+ (candidate) => reconcileCodes.includes(candidate.code)
1781
+ )) {
1782
+ fixed.push(issue);
1783
+ }
1784
+ for (const issue of before.warnings.filter(
1785
+ (candidate) => reconcileCodes.includes(candidate.code)
1530
1786
  )) {
1531
1787
  fixed.push(issue);
1532
1788
  }
@@ -1545,6 +1801,15 @@ async function runDoctorFix(target) {
1545
1801
  });
1546
1802
  fixed.push(findIssue(before.fixable_errors, "event_ledger_partial_write"));
1547
1803
  }
1804
+ const rotateResult = await rotateEventLedgerIfNeeded(projectRoot);
1805
+ if (rotateResult.rotated && rotateResult.archivedCount > 0) {
1806
+ fixed.push({
1807
+ code: "event_ledger_rotated",
1808
+ name: "Event ledger rotated",
1809
+ message: `Rotated ${rotateResult.archivedCount} event(s) older than retention window to ${rotateResult.archivePath ?? "archive"}`,
1810
+ path: rotateResult.archivePath
1811
+ });
1812
+ }
1548
1813
  if (before.fixable_errors.some((issue) => issue.code === "mcp_config_in_wrong_file")) {
1549
1814
  await fixMcpConfigInWrongFile(projectRoot);
1550
1815
  fixed.push(findIssue(before.fixable_errors, "mcp_config_in_wrong_file"));
@@ -1684,7 +1949,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
1684
1949
  };
1685
1950
  }
1686
1951
  const detail = `${candidate.maturity} -> ${next}`;
1687
- const absPath = join5(projectRoot, candidate.path);
1952
+ const absPath = join6(projectRoot, candidate.path);
1688
1953
  try {
1689
1954
  const source = await readFile5(absPath, "utf8");
1690
1955
  const rewritten = rewriteFrontmatterMaturity(source, next);
@@ -1706,7 +1971,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
1706
1971
  error: "rewrite produced byte-identical output"
1707
1972
  };
1708
1973
  }
1709
- await atomicWriteText3(absPath, rewritten);
1974
+ await atomicWriteText4(absPath, rewritten);
1710
1975
  try {
1711
1976
  await appendEventLedgerEvent(projectRoot, {
1712
1977
  event_type: "knowledge_demoted",
@@ -1716,7 +1981,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
1716
1981
  });
1717
1982
  } catch (ledgerError) {
1718
1983
  try {
1719
- await atomicWriteText3(absPath, source);
1984
+ await atomicWriteText4(absPath, source);
1720
1985
  } catch (rollbackError) {
1721
1986
  return {
1722
1987
  kind: "knowledge_orphan_demote_required",
@@ -1751,11 +2016,11 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
1751
2016
  }
1752
2017
  }
1753
2018
  async function applyStaleArchive(projectRoot, candidate, now) {
1754
- const sourceAbs = join5(projectRoot, candidate.path);
1755
- const destAbs = join5(projectRoot, candidate.archive_path);
2019
+ const sourceAbs = join6(projectRoot, candidate.path);
2020
+ const destAbs = join6(projectRoot, candidate.archive_path);
1756
2021
  const detail = `${candidate.path} -> ${candidate.archive_path}`;
1757
2022
  try {
1758
- await mkdir3(join5(destAbs, ".."), { recursive: true });
2023
+ await mkdir4(join6(destAbs, ".."), { recursive: true });
1759
2024
  try {
1760
2025
  await rename(sourceAbs, destAbs);
1761
2026
  } catch (renameError) {
@@ -1814,7 +2079,7 @@ async function applyStaleArchive(projectRoot, candidate, now) {
1814
2079
  async function applyPendingAutoArchive(projectRoot, candidate, now) {
1815
2080
  const detail = `${candidate.pending_path} -> ${candidate.archived_to}`;
1816
2081
  try {
1817
- await mkdir3(join5(candidate.archived_to_abs, ".."), { recursive: true });
2082
+ await mkdir4(join6(candidate.archived_to_abs, ".."), { recursive: true });
1818
2083
  let moved = false;
1819
2084
  if (candidate.layer === "team") {
1820
2085
  try {
@@ -1887,11 +2152,11 @@ async function applyPendingAutoArchive(projectRoot, candidate, now) {
1887
2152
  }
1888
2153
  function relativePosix(projectRoot, absolutePath) {
1889
2154
  const rel = nodeRelative(projectRoot, absolutePath);
1890
- return rel.split(sep4).join("/");
2155
+ return rel.split(sep3).join("/");
1891
2156
  }
1892
2157
  async function applySessionHintsStaleCleanup(projectRoot, candidate) {
1893
2158
  const detail = `deleted (${candidate.age_days}d old)`;
1894
- const absPath = join5(projectRoot, candidate.path);
2159
+ const absPath = join6(projectRoot, candidate.path);
1895
2160
  try {
1896
2161
  const { unlink } = await import("fs/promises");
1897
2162
  await unlink(absPath);
@@ -1912,7 +2177,7 @@ async function applySessionHintsStaleCleanup(projectRoot, candidate) {
1912
2177
  }
1913
2178
  }
1914
2179
  async function applyIndexDriftFix(projectRoot, inspection) {
1915
- const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
2180
+ const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
1916
2181
  const detailParts = [];
1917
2182
  try {
1918
2183
  const meta = agentsMetaSchema4.parse(JSON.parse(await readFile5(metaPath, "utf8")));
@@ -1948,7 +2213,7 @@ function truncateErrorMessage(error) {
1948
2213
  return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
1949
2214
  }
1950
2215
  async function inspectForensic(projectRoot) {
1951
- const path = join5(projectRoot, ".fabric", "forensic.json");
2216
+ const path = join6(projectRoot, ".fabric", "forensic.json");
1952
2217
  try {
1953
2218
  const parsed = forensicReportSchema.parse(JSON.parse(await readFile5(path, "utf8")));
1954
2219
  return { present: true, valid: true, report: parsed };
@@ -1960,12 +2225,12 @@ async function inspectForensic(projectRoot) {
1960
2225
  }
1961
2226
  }
1962
2227
  function inspectMcpConfigInWrongFile(projectRoot) {
1963
- const settingsPath = join5(projectRoot, ".claude", "settings.json");
2228
+ const settingsPath = join6(projectRoot, ".claude", "settings.json");
1964
2229
  if (!existsSync4(settingsPath)) {
1965
2230
  return { hasWrongEntry: false, settingsPath };
1966
2231
  }
1967
2232
  try {
1968
- const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
2233
+ const parsed = JSON.parse(readFileSync2(settingsPath, "utf8"));
1969
2234
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1970
2235
  return { hasWrongEntry: false, settingsPath };
1971
2236
  }
@@ -1981,7 +2246,7 @@ function inspectMcpConfigInWrongFile(projectRoot) {
1981
2246
  }
1982
2247
  }
1983
2248
  async function inspectMeta(projectRoot) {
1984
- const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
2249
+ const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
1985
2250
  const built = await tryBuildRuleMeta(projectRoot);
1986
2251
  try {
1987
2252
  const raw = await readFile5(metaPath, "utf8");
@@ -2054,7 +2319,7 @@ function inspectContentRefs(projectRoot, meta) {
2054
2319
  if (isPersonalKnowledge) {
2055
2320
  continue;
2056
2321
  }
2057
- if (!existsSync4(join5(projectRoot, contentRef))) {
2322
+ if (!existsSync4(join6(projectRoot, contentRef))) {
2058
2323
  missing.push(contentRef);
2059
2324
  }
2060
2325
  }
@@ -2096,7 +2361,7 @@ async function inspectEventLedger(projectRoot) {
2096
2361
  }
2097
2362
  }
2098
2363
  async function inspectKnowledgeTestIndex(projectRoot) {
2099
- const path = join5(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
2364
+ const path = join6(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
2100
2365
  const built = await tryBuildRuleMeta(projectRoot);
2101
2366
  try {
2102
2367
  const index = knowledgeTestIndexSchema2.parse(JSON.parse(await readFile5(path, "utf8")));
@@ -2120,10 +2385,190 @@ async function inspectKnowledgeTestIndex(projectRoot) {
2120
2385
  }
2121
2386
  function inspectBootstrapAnchor(projectRoot) {
2122
2387
  return {
2123
- hasAgentsMd: existsSync4(join5(projectRoot, "AGENTS.md")),
2124
- hasClaudeMd: existsSync4(join5(projectRoot, "CLAUDE.md"))
2388
+ hasAgentsMd: existsSync4(join6(projectRoot, "AGENTS.md")),
2389
+ hasClaudeMd: existsSync4(join6(projectRoot, "CLAUDE.md"))
2125
2390
  };
2126
2391
  }
2392
+ var BOOTSTRAP_MARKER_MIGRATION_TARGETS = [
2393
+ "CLAUDE.md",
2394
+ "AGENTS.md",
2395
+ ".cursor/rules",
2396
+ ".cursor/rules/fabric-bootstrap.mdc"
2397
+ ];
2398
+ async function inspectBootstrapMarkerMigration(target) {
2399
+ const filesNeedingMigration = [];
2400
+ for (const rel of BOOTSTRAP_MARKER_MIGRATION_TARGETS) {
2401
+ const abs = join6(target, rel);
2402
+ if (!existsSync4(abs)) {
2403
+ continue;
2404
+ }
2405
+ let content;
2406
+ try {
2407
+ content = await readFile5(abs, "utf8");
2408
+ } catch {
2409
+ continue;
2410
+ }
2411
+ if (LEGACY_KB_REGEX.test(content)) {
2412
+ filesNeedingMigration.push(abs);
2413
+ }
2414
+ }
2415
+ return { filesNeedingMigration };
2416
+ }
2417
+ function createBootstrapMarkerMigrationCheck(inspection) {
2418
+ if (inspection.filesNeedingMigration.length === 0) {
2419
+ return okCheck(
2420
+ "Bootstrap marker migration",
2421
+ "No legacy fabric:knowledge-base markers detected in bootstrap target files."
2422
+ );
2423
+ }
2424
+ const list = inspection.filesNeedingMigration.join(", ");
2425
+ return issueCheck(
2426
+ "Bootstrap marker migration",
2427
+ "error",
2428
+ "fixable_error",
2429
+ "bootstrap_marker_migration_required",
2430
+ `${inspection.filesNeedingMigration.length} file${inspection.filesNeedingMigration.length === 1 ? "" : "s"} still carry the legacy fabric:knowledge-base bootstrap marker: ${list}.`,
2431
+ "Run `fab doctor --fix` to migrate to fabric:bootstrap marker"
2432
+ );
2433
+ }
2434
+ async function inspectL1BootstrapSnapshotDrift(target) {
2435
+ const abs = join6(target, ".fabric", "AGENTS.md");
2436
+ if (!existsSync4(abs)) {
2437
+ return { status: "missing", canonical: BOOTSTRAP_CANONICAL, onDisk: null };
2438
+ }
2439
+ let onDisk;
2440
+ try {
2441
+ onDisk = await readFile5(abs, "utf8");
2442
+ } catch {
2443
+ return { status: "missing", canonical: BOOTSTRAP_CANONICAL, onDisk: null };
2444
+ }
2445
+ if (onDisk === BOOTSTRAP_CANONICAL) {
2446
+ return { status: "ok", canonical: BOOTSTRAP_CANONICAL, onDisk };
2447
+ }
2448
+ return { status: "drift", canonical: BOOTSTRAP_CANONICAL, onDisk };
2449
+ }
2450
+ function createL1BootstrapSnapshotDriftCheck(inspection) {
2451
+ if (inspection.status === "drift") {
2452
+ return issueCheck(
2453
+ "Bootstrap snapshot drift",
2454
+ "error",
2455
+ "fixable_error",
2456
+ "bootstrap_snapshot_drift",
2457
+ ".fabric/AGENTS.md content diverges byte-for-byte from BOOTSTRAP_CANONICAL.",
2458
+ "Run `fab doctor --fix` to restore canonical bootstrap snapshot"
2459
+ );
2460
+ }
2461
+ return okCheck(
2462
+ "Bootstrap snapshot drift",
2463
+ inspection.status === "ok" ? ".fabric/AGENTS.md byte-equals BOOTSTRAP_CANONICAL." : ".fabric/AGENTS.md absent \u2014 delegated to bootstrap_anchor_missing."
2464
+ );
2465
+ }
2466
+ async function inspectL2ManagedBlockDrift(target) {
2467
+ const snapshotPath = join6(target, ".fabric", "AGENTS.md");
2468
+ if (!existsSync4(snapshotPath)) {
2469
+ return { status: "ok", drifted: [] };
2470
+ }
2471
+ let snapshot;
2472
+ try {
2473
+ snapshot = await readFile5(snapshotPath, "utf8");
2474
+ } catch {
2475
+ return { status: "ok", drifted: [] };
2476
+ }
2477
+ const projectRulesPath = join6(target, ".fabric", "project-rules.md");
2478
+ let expectedBody = snapshot;
2479
+ if (existsSync4(projectRulesPath)) {
2480
+ try {
2481
+ const projectRules = await readFile5(projectRulesPath, "utf8");
2482
+ expectedBody = `${snapshot}
2483
+ ---
2484
+ ${projectRules}`;
2485
+ } catch {
2486
+ }
2487
+ }
2488
+ const drifted = [];
2489
+ let anyManagedBlockFound = false;
2490
+ const blockTargets = [
2491
+ join6(target, "AGENTS.md"),
2492
+ join6(target, ".cursor", "rules", "fabric-bootstrap.mdc")
2493
+ ];
2494
+ for (const abs of blockTargets) {
2495
+ if (!existsSync4(abs)) {
2496
+ continue;
2497
+ }
2498
+ let content;
2499
+ try {
2500
+ content = await readFile5(abs, "utf8");
2501
+ } catch {
2502
+ continue;
2503
+ }
2504
+ if (!BOOTSTRAP_REGEX.test(content) && LEGACY_KB_REGEX.test(content)) {
2505
+ continue;
2506
+ }
2507
+ const match = content.match(BOOTSTRAP_REGEX);
2508
+ if (match === null) {
2509
+ continue;
2510
+ }
2511
+ anyManagedBlockFound = true;
2512
+ const region = match[0];
2513
+ const beginIdx = region.indexOf(BOOTSTRAP_MARKER_BEGIN);
2514
+ const bodyStart = beginIdx + BOOTSTRAP_MARKER_BEGIN.length;
2515
+ const endIdx = region.indexOf(BOOTSTRAP_MARKER_END, bodyStart);
2516
+ if (bodyStart < 0 || endIdx < 0) {
2517
+ continue;
2518
+ }
2519
+ let body = region.slice(bodyStart, endIdx);
2520
+ if (body.startsWith("\n")) body = body.slice(1);
2521
+ if (body.endsWith("\n")) body = body.slice(0, -1);
2522
+ if (body !== expectedBody) {
2523
+ drifted.push({ path: abs, expected: expectedBody, actual: body });
2524
+ }
2525
+ }
2526
+ const claudeMdPath = join6(target, "CLAUDE.md");
2527
+ if (existsSync4(claudeMdPath)) {
2528
+ let claudeContent;
2529
+ try {
2530
+ claudeContent = await readFile5(claudeMdPath, "utf8");
2531
+ if (!BOOTSTRAP_REGEX.test(claudeContent) && LEGACY_KB_REGEX.test(claudeContent)) {
2532
+ } else {
2533
+ anyManagedBlockFound = true;
2534
+ const lines = claudeContent.split(/\r?\n/u);
2535
+ const hasAtImport = lines.some((line) => line.trim() === "@.fabric/AGENTS.md");
2536
+ if (!hasAtImport) {
2537
+ drifted.push({
2538
+ path: claudeMdPath,
2539
+ expected: "@.fabric/AGENTS.md",
2540
+ actual: "(line missing)"
2541
+ });
2542
+ }
2543
+ }
2544
+ } catch {
2545
+ }
2546
+ }
2547
+ if (!anyManagedBlockFound) {
2548
+ return { status: "no-managed-block", drifted: [] };
2549
+ }
2550
+ if (drifted.length === 0) {
2551
+ return { status: "ok", drifted: [] };
2552
+ }
2553
+ return { status: "drift", drifted };
2554
+ }
2555
+ function createL2ManagedBlockDriftCheck(inspection) {
2556
+ if (inspection.status === "drift") {
2557
+ const list = inspection.drifted.map((d) => d.path).join(", ");
2558
+ return issueCheck(
2559
+ "Managed block drift",
2560
+ "error",
2561
+ "fixable_error",
2562
+ "managed_block_drift",
2563
+ `${inspection.drifted.length} three-end managed block${inspection.drifted.length === 1 ? "" : "s"} diverge from expected body (snapshot + optional project-rules concat): ${list}.`,
2564
+ "Run `fab doctor --fix` to restore three-end managed blocks from canonical"
2565
+ );
2566
+ }
2567
+ return okCheck(
2568
+ "Managed block drift",
2569
+ inspection.status === "ok" ? "Three-end managed blocks byte-equal expectedBody." : "No three-end managed blocks detected \u2014 propagation pending or legacy-marker state."
2570
+ );
2571
+ }
2127
2572
  function createBootstrapAnchorCheck(inspection) {
2128
2573
  if (!inspection.hasAgentsMd && !inspection.hasClaudeMd) {
2129
2574
  return issueCheck(
@@ -2142,16 +2587,82 @@ function createBootstrapAnchorCheck(inspection) {
2142
2587
  return okCheck("Bootstrap anchor", `Bootstrap anchor present at repo root: ${present}.`);
2143
2588
  }
2144
2589
  function inspectKnowledgeDirMissing(projectRoot) {
2145
- const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
2590
+ const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
2146
2591
  const missingSubdirs = [];
2147
- for (const sub of KNOWLEDGE_SUBDIRS2) {
2148
- const path = join5(knowledgeRoot, sub);
2592
+ for (const sub of KNOWLEDGE_SUBDIRS3) {
2593
+ const path = join6(knowledgeRoot, sub);
2149
2594
  if (!existsSync4(path)) {
2150
2595
  missingSubdirs.push(`.fabric/knowledge/${sub}`);
2151
2596
  }
2152
2597
  }
2153
2598
  return { missingSubdirs };
2154
2599
  }
2600
+ function inspectBaselineFilenameFormat(projectRoot) {
2601
+ const offenders = [];
2602
+ const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
2603
+ if (!existsSync4(knowledgeRoot)) {
2604
+ return { offenders };
2605
+ }
2606
+ for (const sub of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
2607
+ const dir = join6(knowledgeRoot, sub);
2608
+ if (!existsSync4(dir)) {
2609
+ continue;
2610
+ }
2611
+ let entries;
2612
+ try {
2613
+ entries = readdirSync(dir, { withFileTypes: true });
2614
+ } catch {
2615
+ continue;
2616
+ }
2617
+ for (const entry of entries) {
2618
+ const entryName = entry.name;
2619
+ if (!entry.isFile() || !entryName.endsWith(".md")) {
2620
+ continue;
2621
+ }
2622
+ if (BASELINE_ID_PREFIXED_FILENAME_PATTERN.test(entryName)) {
2623
+ continue;
2624
+ }
2625
+ const abs = join6(dir, entryName);
2626
+ let source;
2627
+ try {
2628
+ source = readFileSync2(abs, "utf8");
2629
+ } catch {
2630
+ continue;
2631
+ }
2632
+ const id = extractKnowledgeFrontmatterId(source);
2633
+ if (id === null) {
2634
+ continue;
2635
+ }
2636
+ if (!BASELINE_FILENAME_LINT_BASELINE_IDS.has(id)) {
2637
+ continue;
2638
+ }
2639
+ offenders.push({
2640
+ path: posix.join(".fabric/knowledge", sub, entryName),
2641
+ stable_id: id
2642
+ });
2643
+ }
2644
+ }
2645
+ offenders.sort((a, b) => a.path.localeCompare(b.path));
2646
+ return { offenders };
2647
+ }
2648
+ function createBaselineFilenameFormatCheck(inspection) {
2649
+ if (inspection.offenders.length === 0) {
2650
+ return okCheck(
2651
+ "Baseline filename format",
2652
+ "All baseline knowledge files use the canonical `${id}--${slug}.md` filename format."
2653
+ );
2654
+ }
2655
+ const first = inspection.offenders[0];
2656
+ const detail = `${first.stable_id} at ${first.path}`;
2657
+ return issueCheck(
2658
+ "Baseline filename format",
2659
+ "error",
2660
+ "manual_error",
2661
+ "lint-baseline-filename-format",
2662
+ `${inspection.offenders.length} baseline knowledge file${inspection.offenders.length === 1 ? "" : "s"} use${inspection.offenders.length === 1 ? "s" : ""} the deprecated bare-slug filename format and must be migrated to \`\${id}--\${slug}.md\`. First: ${detail}.`,
2663
+ "Run `fab scan` to auto-migrate baseline filenames to the canonical `${id}--${slug}.md` format."
2664
+ );
2665
+ }
2155
2666
  function createKnowledgeDirMissingCheck(inspection) {
2156
2667
  if (inspection.missingSubdirs.length > 0) {
2157
2668
  const list = inspection.missingSubdirs.join(", ");
@@ -2166,7 +2677,7 @@ function createKnowledgeDirMissingCheck(inspection) {
2166
2677
  }
2167
2678
  return okCheck(
2168
2679
  "Knowledge layout",
2169
- `All ${KNOWLEDGE_SUBDIRS2.length} required .fabric/knowledge/* subdirectories exist.`
2680
+ `All ${KNOWLEDGE_SUBDIRS3.length} required .fabric/knowledge/* subdirectories exist.`
2170
2681
  );
2171
2682
  }
2172
2683
  function createForensicCheck(forensic, frameworkKind, entryPointCount) {
@@ -2195,11 +2706,11 @@ function createMetaCheck(meta) {
2195
2706
  if (meta.stale) {
2196
2707
  return issueCheck(
2197
2708
  "Agents metadata",
2198
- "error",
2199
- "fixable_error",
2709
+ "warn",
2710
+ "warning",
2200
2711
  "agents_meta_stale",
2201
2712
  `.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/knowledge derived revision ${meta.computedRevision ?? "<unknown>"}.`,
2202
- "Run `fab doctor --fix` to reconcile agents.meta.json with the current knowledge files."
2713
+ "Benign \u2014 engine auto-heals on next plan-context/get-sections call. Run `fab doctor --fix` for explicit reconciliation."
2203
2714
  );
2204
2715
  }
2205
2716
  return okCheck("Agents metadata", `.fabric/agents.meta.json revision ${meta.revision} is aligned with .fabric/knowledge.`);
@@ -2312,7 +2823,7 @@ function findIssue(issues, code) {
2312
2823
  };
2313
2824
  }
2314
2825
  async function inspectMetaManuallyDiverged(projectRoot) {
2315
- const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
2826
+ const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
2316
2827
  if (!existsSync4(metaPath)) {
2317
2828
  return { extraMetaEntries: [], hashMismatchEntries: [], readable: false };
2318
2829
  }
@@ -2332,13 +2843,13 @@ async function inspectMetaManuallyDiverged(projectRoot) {
2332
2843
  const hashMismatchEntries = [];
2333
2844
  for (const node of Object.values(meta.nodes)) {
2334
2845
  const contentRef = node.content_ref ?? node.file;
2335
- const absPath = join5(projectRoot, contentRef);
2846
+ const absPath = join6(projectRoot, contentRef);
2336
2847
  if (!existsSync4(absPath)) {
2337
2848
  extraMetaEntries.push(contentRef);
2338
2849
  continue;
2339
2850
  }
2340
2851
  try {
2341
- const content = readFileSync(absPath, "utf8");
2852
+ const content = readFileSync2(absPath, "utf8");
2342
2853
  const diskHash = sha256(content);
2343
2854
  if (node.hash !== "" && node.hash !== diskHash) {
2344
2855
  hashMismatchEntries.push(contentRef);
@@ -2351,7 +2862,7 @@ async function inspectMetaManuallyDiverged(projectRoot) {
2351
2862
  }
2352
2863
  function inspectKnowledgeDirUnindexed(projectRoot, meta) {
2353
2864
  const physicalMdFiles = /* @__PURE__ */ new Set();
2354
- collectMdFilesUnder(physicalMdFiles, projectRoot, join5(projectRoot, ".fabric", "knowledge"), ".fabric/knowledge");
2865
+ collectMdFilesUnder(physicalMdFiles, projectRoot, join6(projectRoot, ".fabric", "knowledge"), ".fabric/knowledge");
2355
2866
  if (physicalMdFiles.size === 0) {
2356
2867
  return { unindexedFiles: [] };
2357
2868
  }
@@ -2376,7 +2887,7 @@ function collectMdFilesUnder(out, projectRoot, rootDir, relPrefix) {
2376
2887
  continue;
2377
2888
  }
2378
2889
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
2379
- const abs = join5(dir, entry.name);
2890
+ const abs = join6(dir, entry.name);
2380
2891
  if (entry.isDirectory()) {
2381
2892
  stack.push(abs);
2382
2893
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -2401,7 +2912,7 @@ function createKnowledgeDirUnindexedCheck(inspection) {
2401
2912
  }
2402
2913
  async function inspectStableIdCollisions(projectRoot) {
2403
2914
  const found = [];
2404
- const knowledgeDir = join5(projectRoot, ".fabric", "knowledge");
2915
+ const knowledgeDir = join6(projectRoot, ".fabric", "knowledge");
2405
2916
  if (existsSync4(knowledgeDir)) {
2406
2917
  const stack = [knowledgeDir];
2407
2918
  while (stack.length > 0) {
@@ -2410,7 +2921,7 @@ async function inspectStableIdCollisions(projectRoot) {
2410
2921
  continue;
2411
2922
  }
2412
2923
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
2413
- const abs = join5(dir, entry.name);
2924
+ const abs = join6(dir, entry.name);
2414
2925
  if (entry.isDirectory()) {
2415
2926
  stack.push(abs);
2416
2927
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -2566,17 +3077,17 @@ function createMetaManuallyDivergedCheck(inspection) {
2566
3077
  }
2567
3078
  function inspectPreexistingRootFiles(projectRoot) {
2568
3079
  const candidates = ["CLAUDE.md", "AGENTS.md"];
2569
- const detected = candidates.filter((name) => existsSync4(join5(projectRoot, name)));
3080
+ const detected = candidates.filter((name) => existsSync4(join6(projectRoot, name)));
2570
3081
  return { detected };
2571
3082
  }
2572
3083
  async function inspectFilesystemEditFallback(projectRoot) {
2573
- const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
3084
+ const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
2574
3085
  if (!existsSync4(knowledgeRoot)) {
2575
3086
  return { synthesized: 0, synthesizedStableIds: [] };
2576
3087
  }
2577
3088
  const canonicalIds = /* @__PURE__ */ new Set();
2578
3089
  for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
2579
- const dir = join5(knowledgeRoot, typeDir);
3090
+ const dir = join6(knowledgeRoot, typeDir);
2580
3091
  if (!existsSync4(dir)) {
2581
3092
  continue;
2582
3093
  }
@@ -2778,12 +3289,12 @@ function extractKnowledgeFrontmatterCreatedAt(source) {
2778
3289
  return Number.isFinite(parsed) ? parsed : null;
2779
3290
  }
2780
3291
  function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
2781
- const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
3292
+ const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
2782
3293
  if (!existsSync4(knowledgeRoot)) {
2783
3294
  return;
2784
3295
  }
2785
3296
  for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
2786
- const dir = join5(knowledgeRoot, typeDir);
3297
+ const dir = join6(knowledgeRoot, typeDir);
2787
3298
  if (!existsSync4(dir)) {
2788
3299
  continue;
2789
3300
  }
@@ -2802,10 +3313,10 @@ function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
2802
3313
  continue;
2803
3314
  }
2804
3315
  const stableId = match[1];
2805
- const absPath = join5(dir, entry.name);
3316
+ const absPath = join6(dir, entry.name);
2806
3317
  let source;
2807
3318
  try {
2808
- source = readFileSync(absPath, "utf8");
3319
+ source = readFileSync2(absPath, "utf8");
2809
3320
  } catch {
2810
3321
  continue;
2811
3322
  }
@@ -2818,7 +3329,7 @@ function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
2818
3329
  let lastReferenceMs = Math.max(createdAt ?? 0, eventTs);
2819
3330
  if (lastReferenceMs === 0) {
2820
3331
  try {
2821
- lastReferenceMs = statSync3(absPath).mtimeMs;
3332
+ lastReferenceMs = statSync4(absPath).mtimeMs;
2822
3333
  } catch {
2823
3334
  lastReferenceMs = 0;
2824
3335
  }
@@ -2878,8 +3389,8 @@ async function inspectStaleArchive(projectRoot, now) {
2878
3389
  return { candidates };
2879
3390
  }
2880
3391
  function* iteratePendingFiles(projectRoot, now) {
2881
- const teamRoot = join5(projectRoot, ".fabric", "knowledge", "pending");
2882
- const personalRoot = join5(resolvePersonalRootForPending(), ".fabric", "knowledge", "pending");
3392
+ const teamRoot = join6(projectRoot, ".fabric", "knowledge", "pending");
3393
+ const personalRoot = join6(resolvePersonalRootForPending(), ".fabric", "knowledge", "pending");
2883
3394
  for (const [layer, root, displayPrefix] of [
2884
3395
  ["team", teamRoot, ".fabric/knowledge/pending"],
2885
3396
  ["personal", personalRoot, "~/.fabric/knowledge/pending"]
@@ -2894,7 +3405,7 @@ function* iteratePendingFiles(projectRoot, now) {
2894
3405
  continue;
2895
3406
  }
2896
3407
  for (const typeDir of typeDirs) {
2897
- const dir = join5(root, typeDir);
3408
+ const dir = join6(root, typeDir);
2898
3409
  let entries;
2899
3410
  try {
2900
3411
  entries = readdirSync(dir, { withFileTypes: true });
@@ -2905,17 +3416,17 @@ function* iteratePendingFiles(projectRoot, now) {
2905
3416
  if (!entry.isFile() || !entry.name.endsWith(".md")) {
2906
3417
  continue;
2907
3418
  }
2908
- const absPath = join5(dir, entry.name);
3419
+ const absPath = join6(dir, entry.name);
2909
3420
  let source = "";
2910
3421
  try {
2911
- source = readFileSync(absPath, "utf8");
3422
+ source = readFileSync2(absPath, "utf8");
2912
3423
  } catch {
2913
3424
  continue;
2914
3425
  }
2915
3426
  const createdAt = extractKnowledgeFrontmatterCreatedAt(source);
2916
3427
  let mtimeMs = 0;
2917
3428
  try {
2918
- mtimeMs = statSync3(absPath).mtimeMs;
3429
+ mtimeMs = statSync4(absPath).mtimeMs;
2919
3430
  } catch {
2920
3431
  mtimeMs = 0;
2921
3432
  }
@@ -2949,7 +3460,7 @@ function* iteratePendingFiles(projectRoot, now) {
2949
3460
  }
2950
3461
  }
2951
3462
  function resolvePersonalRootForPending() {
2952
- return process.env.FABRIC_HOME ?? homedir2();
3463
+ return process.env.FABRIC_HOME ?? homedir3();
2953
3464
  }
2954
3465
  function inspectPendingOverdue(projectRoot, now) {
2955
3466
  const candidates = [];
@@ -2980,7 +3491,7 @@ function inspectPendingAutoArchive(projectRoot, now) {
2980
3491
  pending_path: visit.pending_path,
2981
3492
  pending_path_abs: visit.pending_path_abs,
2982
3493
  archived_to: archivedToRel,
2983
- archived_to_abs: join5(projectRoot, archivedToRel),
3494
+ archived_to_abs: join6(projectRoot, archivedToRel),
2984
3495
  age_days: visit.age_days
2985
3496
  });
2986
3497
  } else {
@@ -2989,7 +3500,7 @@ function inspectPendingAutoArchive(projectRoot, now) {
2989
3500
  visit.type,
2990
3501
  visit.filename
2991
3502
  );
2992
- const archivedToAbs = join5(
3503
+ const archivedToAbs = join6(
2993
3504
  resolvePersonalRootForPending(),
2994
3505
  ".fabric",
2995
3506
  ".archive",
@@ -3013,11 +3524,11 @@ function inspectPendingAutoArchive(projectRoot, now) {
3013
3524
  }
3014
3525
  function inspectUnderseeded(projectRoot) {
3015
3526
  const threshold = readUnderseedThresholdFromConfig(projectRoot);
3016
- const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
3527
+ const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
3017
3528
  let nodeCount = 0;
3018
3529
  if (existsSync4(knowledgeRoot)) {
3019
3530
  for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
3020
- const dir = join5(knowledgeRoot, typeDir);
3531
+ const dir = join6(knowledgeRoot, typeDir);
3021
3532
  if (!existsSync4(dir)) continue;
3022
3533
  let entries;
3023
3534
  try {
@@ -3039,7 +3550,7 @@ function inspectUnderseeded(projectRoot) {
3039
3550
  };
3040
3551
  }
3041
3552
  function inspectSessionHintsStale(projectRoot, now) {
3042
- const cacheDir = join5(projectRoot, ".fabric", ".cache");
3553
+ const cacheDir = join6(projectRoot, ".fabric", ".cache");
3043
3554
  if (!existsSync4(cacheDir)) {
3044
3555
  return { candidates: [] };
3045
3556
  }
@@ -3054,10 +3565,10 @@ function inspectSessionHintsStale(projectRoot, now) {
3054
3565
  if (!entry.isFile()) continue;
3055
3566
  if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
3056
3567
  if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
3057
- const absPath = join5(cacheDir, entry.name);
3568
+ const absPath = join6(cacheDir, entry.name);
3058
3569
  let mtimeMs = 0;
3059
3570
  try {
3060
- mtimeMs = statSync3(absPath).mtimeMs;
3571
+ mtimeMs = statSync4(absPath).mtimeMs;
3061
3572
  } catch {
3062
3573
  continue;
3063
3574
  }
@@ -3084,11 +3595,11 @@ function inspectNarrowTooFew(projectRoot, now) {
3084
3595
  const structuralFlagged = total >= NARROW_MIN_TOTAL && narrowRatio < NARROW_RATIO_THRESHOLD;
3085
3596
  const windowStartMs = now - SILENCE_WINDOW_DAYS * MS_PER_DAY;
3086
3597
  const editFires = readCounterTimestamps(
3087
- join5(projectRoot, EDIT_COUNTER_FILE_REL),
3598
+ join6(projectRoot, EDIT_COUNTER_FILE_REL),
3088
3599
  windowStartMs
3089
3600
  );
3090
3601
  const silenceFires = readCounterTimestamps(
3091
- join5(projectRoot, HINT_SILENCE_COUNTER_FILE_REL),
3602
+ join6(projectRoot, HINT_SILENCE_COUNTER_FILE_REL),
3092
3603
  windowStartMs
3093
3604
  );
3094
3605
  const telemetrySkipped = editFires === 0;
@@ -3110,7 +3621,7 @@ function readCounterTimestamps(absPath, windowStartMs) {
3110
3621
  if (!existsSync4(absPath)) return 0;
3111
3622
  let raw;
3112
3623
  try {
3113
- raw = readFileSync(absPath, "utf8");
3624
+ raw = readFileSync2(absPath, "utf8");
3114
3625
  } catch {
3115
3626
  return 0;
3116
3627
  }
@@ -3126,10 +3637,10 @@ function readCounterTimestamps(absPath, windowStartMs) {
3126
3637
  return count;
3127
3638
  }
3128
3639
  function readUnderseedThresholdFromConfig(projectRoot) {
3129
- const configPath = join5(projectRoot, ".fabric", "fabric-config.json");
3640
+ const configPath = join6(projectRoot, ".fabric", "fabric-config.json");
3130
3641
  if (!existsSync4(configPath)) return DEFAULT_UNDERSEED_NODE_THRESHOLD;
3131
3642
  try {
3132
- const raw = readFileSync(configPath, "utf8");
3643
+ const raw = readFileSync2(configPath, "utf8");
3133
3644
  const parsed = JSON.parse(raw);
3134
3645
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3135
3646
  const v = parsed.underseed_node_threshold;
@@ -3259,11 +3770,11 @@ function extractKnowledgeFrontmatterRelevancePaths(source) {
3259
3770
  }
3260
3771
  function* iterateRelevanceFrontmatter(projectRoot) {
3261
3772
  for (const visit of iterateCanonicalFilenames(projectRoot)) {
3262
- const layerRoot = visit.layer === "team" ? join5(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
3263
- const absPath = join5(layerRoot, visit.type, visit.filename);
3773
+ const layerRoot = visit.layer === "team" ? join6(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
3774
+ const absPath = join6(layerRoot, visit.type, visit.filename);
3264
3775
  let source;
3265
3776
  try {
3266
- source = readFileSync(absPath, "utf8");
3777
+ source = readFileSync2(absPath, "utf8");
3267
3778
  } catch {
3268
3779
  continue;
3269
3780
  }
@@ -3330,7 +3841,7 @@ function collectWorkspacePathsForGlobMatch(projectRoot) {
3330
3841
  }
3331
3842
  let rootStat;
3332
3843
  try {
3333
- rootStat = statSync3(projectRoot);
3844
+ rootStat = statSync4(projectRoot);
3334
3845
  } catch {
3335
3846
  return [];
3336
3847
  }
@@ -3349,7 +3860,7 @@ function collectWorkspacePathsForGlobMatch(projectRoot) {
3349
3860
  continue;
3350
3861
  }
3351
3862
  for (const entry of entries) {
3352
- const abs = join5(current, entry.name);
3863
+ const abs = join6(current, entry.name);
3353
3864
  const rel = normalizePath(abs.slice(projectRoot.length + 1));
3354
3865
  if (rel.length === 0) continue;
3355
3866
  if (entry.isDirectory()) {
@@ -3489,8 +4000,8 @@ function inspectRelevanceFieldsMissing(projectRoot) {
3489
4000
  const candidates = [];
3490
4001
  let scannedCount = 0;
3491
4002
  const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
3492
- const teamRoot = join5(projectRoot, ".fabric", "knowledge", "pending");
3493
- const personalRoot = join5(
4003
+ const teamRoot = join6(projectRoot, ".fabric", "knowledge", "pending");
4004
+ const personalRoot = join6(
3494
4005
  resolvePersonalRootForPending(),
3495
4006
  ".fabric",
3496
4007
  "knowledge",
@@ -3510,7 +4021,7 @@ function inspectRelevanceFieldsMissing(projectRoot) {
3510
4021
  continue;
3511
4022
  }
3512
4023
  for (const typeDir of typeDirs) {
3513
- const dir = join5(root, typeDir);
4024
+ const dir = join6(root, typeDir);
3514
4025
  let entries;
3515
4026
  try {
3516
4027
  entries = readdirSync(dir, { withFileTypes: true });
@@ -3521,10 +4032,10 @@ function inspectRelevanceFieldsMissing(projectRoot) {
3521
4032
  if (!entry.isFile() || !entry.name.endsWith(".md")) {
3522
4033
  continue;
3523
4034
  }
3524
- const absPath = join5(dir, entry.name);
4035
+ const absPath = join6(dir, entry.name);
3525
4036
  let source;
3526
4037
  try {
3527
- source = readFileSync(absPath, "utf8");
4038
+ source = readFileSync2(absPath, "utf8");
3528
4039
  } catch {
3529
4040
  continue;
3530
4041
  }
@@ -3608,7 +4119,7 @@ async function applyRelevanceFieldsMissing(candidate) {
3608
4119
  error: "fields already present at write time (no diff)"
3609
4120
  };
3610
4121
  }
3611
- await atomicWriteText3(candidate.pending_path_abs, rewritten);
4122
+ await atomicWriteText4(candidate.pending_path_abs, rewritten);
3612
4123
  return {
3613
4124
  kind: "knowledge_relevance_fields_missing",
3614
4125
  path: candidate.pending_path,
@@ -3652,7 +4163,7 @@ var SKILL_QUOTED_VALUE_LEADS = /* @__PURE__ */ new Set(['"', "'", "[", "{", ">",
3652
4163
  function inspectSkillMdYamlInvalid(projectRoot) {
3653
4164
  const candidates = [];
3654
4165
  for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
3655
- const rootAbs = join5(projectRoot, rootRel);
4166
+ const rootAbs = join6(projectRoot, rootRel);
3656
4167
  if (!existsSync4(rootAbs)) continue;
3657
4168
  let dirEntries;
3658
4169
  try {
@@ -3662,11 +4173,11 @@ function inspectSkillMdYamlInvalid(projectRoot) {
3662
4173
  }
3663
4174
  for (const dirEntry of dirEntries) {
3664
4175
  if (!dirEntry.isDirectory()) continue;
3665
- const skillFile = join5(rootAbs, dirEntry.name, "SKILL.md");
4176
+ const skillFile = join6(rootAbs, dirEntry.name, "SKILL.md");
3666
4177
  if (!existsSync4(skillFile)) continue;
3667
4178
  let raw;
3668
4179
  try {
3669
- raw = readFileSync(skillFile, "utf8");
4180
+ raw = readFileSync2(skillFile, "utf8");
3670
4181
  } catch {
3671
4182
  continue;
3672
4183
  }
@@ -3766,8 +4277,8 @@ function createNarrowTooFewCheck(inspection) {
3766
4277
  );
3767
4278
  }
3768
4279
  function resolvePersonalKnowledgeRoot() {
3769
- const home = process.env.FABRIC_HOME ?? homedir2();
3770
- return join5(home, ".fabric", "knowledge");
4280
+ const home = process.env.FABRIC_HOME ?? homedir3();
4281
+ return join6(home, ".fabric", "knowledge");
3771
4282
  }
3772
4283
  function parseStableIdFromCanonicalFilename(filename) {
3773
4284
  const match = CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(filename);
@@ -3787,7 +4298,7 @@ function parseStableIdFromCanonicalFilename(filename) {
3787
4298
  };
3788
4299
  }
3789
4300
  function* iterateCanonicalFilenames(projectRoot) {
3790
- const teamRoot = join5(projectRoot, ".fabric", "knowledge");
4301
+ const teamRoot = join6(projectRoot, ".fabric", "knowledge");
3791
4302
  const personalRoot = resolvePersonalKnowledgeRoot();
3792
4303
  for (const [layer, root, displayPrefix] of [
3793
4304
  ["team", teamRoot, ".fabric/knowledge"],
@@ -3797,7 +4308,7 @@ function* iterateCanonicalFilenames(projectRoot) {
3797
4308
  continue;
3798
4309
  }
3799
4310
  for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
3800
- const dir = join5(root, typeDir);
4311
+ const dir = join6(root, typeDir);
3801
4312
  if (!existsSync4(dir)) {
3802
4313
  continue;
3803
4314
  }
@@ -3953,14 +4464,133 @@ function createIndexDriftCheck(inspection) {
3953
4464
  "Run `fab doctor --apply-lint` (rc.4 TASK-003) to bump agents.meta.json counters to max_observed + 1."
3954
4465
  );
3955
4466
  }
4467
+ async function migrateBootstrapMarkers(projectRoot) {
4468
+ const paths = [];
4469
+ const countPerPath = {};
4470
+ for (const rel of BOOTSTRAP_MARKER_MIGRATION_TARGETS) {
4471
+ const abs = join6(projectRoot, rel);
4472
+ if (!existsSync4(abs)) {
4473
+ continue;
4474
+ }
4475
+ let original;
4476
+ try {
4477
+ original = await readFile5(abs, "utf8");
4478
+ } catch {
4479
+ continue;
4480
+ }
4481
+ const beginMatches = original.match(/<!-- fabric:knowledge-base:begin -->/g);
4482
+ const endMatches = original.match(/<!-- fabric:knowledge-base:end -->/g);
4483
+ const replacedCount = (beginMatches?.length ?? 0) + (endMatches?.length ?? 0);
4484
+ if (replacedCount === 0) {
4485
+ continue;
4486
+ }
4487
+ const rewritten = original.replace(/<!-- fabric:knowledge-base:begin -->/g, BOOTSTRAP_MARKER_BEGIN).replace(/<!-- fabric:knowledge-base:end -->/g, BOOTSTRAP_MARKER_END);
4488
+ if (rewritten === original) {
4489
+ continue;
4490
+ }
4491
+ await atomicWriteText4(abs, rewritten);
4492
+ paths.push(abs);
4493
+ countPerPath[abs] = replacedCount;
4494
+ }
4495
+ return { paths, countPerPath };
4496
+ }
4497
+ async function rewriteThreeEndManagedBlocks(projectRoot) {
4498
+ const snapshotPath = join6(projectRoot, ".fabric", "AGENTS.md");
4499
+ if (!existsSync4(snapshotPath)) {
4500
+ return;
4501
+ }
4502
+ let snapshot;
4503
+ try {
4504
+ snapshot = await readFile5(snapshotPath, "utf8");
4505
+ } catch {
4506
+ return;
4507
+ }
4508
+ const projectRulesPath = join6(projectRoot, ".fabric", "project-rules.md");
4509
+ const hasProjectRules = existsSync4(projectRulesPath);
4510
+ let expectedBody = snapshot;
4511
+ if (hasProjectRules) {
4512
+ try {
4513
+ const projectRules = await readFile5(projectRulesPath, "utf8");
4514
+ expectedBody = `${snapshot}
4515
+ ---
4516
+ ${projectRules}`;
4517
+ } catch {
4518
+ }
4519
+ }
4520
+ const managedBlock = `${BOOTSTRAP_MARKER_BEGIN}
4521
+ ${expectedBody}
4522
+ ${BOOTSTRAP_MARKER_END}`;
4523
+ const blockTargets = [
4524
+ join6(projectRoot, "AGENTS.md"),
4525
+ join6(projectRoot, ".cursor", "rules", "fabric-bootstrap.mdc")
4526
+ ];
4527
+ for (const abs of blockTargets) {
4528
+ if (!existsSync4(abs)) {
4529
+ continue;
4530
+ }
4531
+ let existing;
4532
+ try {
4533
+ existing = await readFile5(abs, "utf8");
4534
+ } catch {
4535
+ continue;
4536
+ }
4537
+ let next;
4538
+ const match = existing.match(BOOTSTRAP_REGEX);
4539
+ if (match !== null) {
4540
+ const before = existing.slice(0, match.index ?? 0);
4541
+ const after = existing.slice((match.index ?? 0) + match[0].length);
4542
+ const stripped = `${before}${after.replace(/^\r?\n/, "")}`;
4543
+ const trailingNewline = stripped.length === 0 || stripped.endsWith("\n") ? "" : "\n";
4544
+ next = `${stripped}${trailingNewline}
4545
+ ${managedBlock}
4546
+ `;
4547
+ } else {
4548
+ const trailingNewline = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
4549
+ next = `${existing}${trailingNewline}
4550
+ ${managedBlock}
4551
+ `;
4552
+ }
4553
+ if (next === existing) {
4554
+ continue;
4555
+ }
4556
+ await atomicWriteText4(abs, next);
4557
+ }
4558
+ const claudeMdPath = join6(projectRoot, "CLAUDE.md");
4559
+ if (existsSync4(claudeMdPath)) {
4560
+ let claudeContent;
4561
+ try {
4562
+ claudeContent = await readFile5(claudeMdPath, "utf8");
4563
+ } catch {
4564
+ return;
4565
+ }
4566
+ const lines = claudeContent.split(/\r?\n/u);
4567
+ let updated = claudeContent;
4568
+ const ensureLine = (line) => {
4569
+ if (lines.some((existingLine) => existingLine.trim() === line)) {
4570
+ return;
4571
+ }
4572
+ const trailingNewline = updated.length === 0 || updated.endsWith("\n") ? "" : "\n";
4573
+ updated = `${updated}${trailingNewline}${line}
4574
+ `;
4575
+ lines.push(line);
4576
+ };
4577
+ ensureLine("@.fabric/AGENTS.md");
4578
+ if (hasProjectRules) {
4579
+ ensureLine("@.fabric/project-rules.md");
4580
+ }
4581
+ if (updated !== claudeContent) {
4582
+ await atomicWriteText4(claudeMdPath, updated);
4583
+ }
4584
+ }
4585
+ }
3956
4586
  async function fixMcpConfigInWrongFile(projectRoot) {
3957
- const settingsPath = join5(projectRoot, ".claude", "settings.json");
4587
+ const settingsPath = join6(projectRoot, ".claude", "settings.json");
3958
4588
  if (!existsSync4(settingsPath)) {
3959
4589
  return;
3960
4590
  }
3961
4591
  let settings;
3962
4592
  try {
3963
- const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
4593
+ const parsed = JSON.parse(readFileSync2(settingsPath, "utf8"));
3964
4594
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
3965
4595
  return;
3966
4596
  }
@@ -3987,12 +4617,12 @@ async function fixMcpConfigInWrongFile(projectRoot) {
3987
4617
  });
3988
4618
  }
3989
4619
  async function ensureKnowledgeSubdirs(projectRoot) {
3990
- for (const sub of KNOWLEDGE_SUBDIRS2) {
3991
- await mkdir3(join5(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
4620
+ for (const sub of KNOWLEDGE_SUBDIRS3) {
4621
+ await mkdir4(join6(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
3992
4622
  }
3993
4623
  }
3994
4624
  async function fixCounterDesync(projectRoot) {
3995
- const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
4625
+ const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
3996
4626
  if (!existsSync4(metaPath)) {
3997
4627
  return;
3998
4628
  }
@@ -4026,6 +4656,275 @@ async function ensureEventLedger(projectRoot) {
4026
4656
  await ensureParentDirectory(path);
4027
4657
  await writeFile2(path, "", { encoding: "utf8", flag: "a" });
4028
4658
  }
4659
+ var CITE_POLICY_VERSION = "2.0.0-rc.20";
4660
+ async function ensureCitePolicyActivatedMarker(projectRoot) {
4661
+ let existing;
4662
+ try {
4663
+ const { events } = await readEventLedger(projectRoot, { event_type: "cite_policy_activated" });
4664
+ if (events.length > 0) {
4665
+ existing = events[0];
4666
+ }
4667
+ } catch {
4668
+ return { marker_ts: 0, emitted_now: false };
4669
+ }
4670
+ if (existing !== void 0) {
4671
+ return { marker_ts: existing.ts, emitted_now: false };
4672
+ }
4673
+ try {
4674
+ const stored = await appendEventLedgerEvent(projectRoot, {
4675
+ event_type: "cite_policy_activated",
4676
+ policy_version: CITE_POLICY_VERSION,
4677
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4678
+ });
4679
+ return { marker_ts: stored.ts, emitted_now: true };
4680
+ } catch {
4681
+ return { marker_ts: 0, emitted_now: false };
4682
+ }
4683
+ }
4684
+ function categorizeCiteTag(tag) {
4685
+ if (tag === "planned" || tag === "recalled" || tag === "chained-from" || tag === "none") {
4686
+ return { category: tag };
4687
+ }
4688
+ if (tag === "dismissed") {
4689
+ return { category: "dismissed", reason: "unspecified" };
4690
+ }
4691
+ if (tag.startsWith("dismissed:")) {
4692
+ const remainder = tag.slice("dismissed:".length);
4693
+ if (remainder.startsWith("other:")) {
4694
+ return { category: "dismissed", reason: remainder.slice("other:".length) || "other" };
4695
+ }
4696
+ return { category: "dismissed", reason: remainder || "unspecified" };
4697
+ }
4698
+ return { category: "none" };
4699
+ }
4700
+ function matchesRelevancePath(editPath, relevancePaths) {
4701
+ if (relevancePaths.length === 0) {
4702
+ return false;
4703
+ }
4704
+ const normalized = normalizePath(editPath);
4705
+ for (const glob of relevancePaths) {
4706
+ if (minimatch(normalized, glob, { dot: true, matchBase: false })) {
4707
+ return true;
4708
+ }
4709
+ }
4710
+ return false;
4711
+ }
4712
+ async function runDoctorCiteCoverage(projectRoot, options) {
4713
+ const marker = await ensureCitePolicyActivatedMarker(projectRoot);
4714
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
4715
+ const zeroMetrics = {
4716
+ edits_touched: 0,
4717
+ qualifying_cites: 0,
4718
+ recalled_unverified: 0,
4719
+ expected_but_missed: 0,
4720
+ total_turns: 0
4721
+ };
4722
+ if (marker.marker_ts === 0) {
4723
+ return {
4724
+ status: "skipped",
4725
+ marker_ts: 0,
4726
+ marker_emitted_now: false,
4727
+ since_ts: options.since,
4728
+ client_filter: options.client,
4729
+ metrics: zeroMetrics,
4730
+ generated_at: generatedAt
4731
+ };
4732
+ }
4733
+ const effectiveSince = Math.max(marker.marker_ts, options.since);
4734
+ let ledgerEvents = [];
4735
+ try {
4736
+ const result = await readEventLedger(projectRoot, { since: effectiveSince });
4737
+ ledgerEvents = result.events;
4738
+ } catch {
4739
+ return {
4740
+ status: "ok",
4741
+ marker_ts: marker.marker_ts,
4742
+ marker_emitted_now: marker.emitted_now,
4743
+ since_ts: effectiveSince,
4744
+ client_filter: options.client,
4745
+ metrics: zeroMetrics,
4746
+ generated_at: generatedAt
4747
+ };
4748
+ }
4749
+ const assistantTurns = [];
4750
+ const editEvents = [];
4751
+ const fetchEvents = [];
4752
+ for (const event of ledgerEvents) {
4753
+ switch (event.event_type) {
4754
+ case "assistant_turn_observed":
4755
+ assistantTurns.push(event);
4756
+ break;
4757
+ case "edit_intent_checked":
4758
+ editEvents.push(event);
4759
+ break;
4760
+ case "knowledge_sections_fetched":
4761
+ fetchEvents.push(event);
4762
+ break;
4763
+ default:
4764
+ break;
4765
+ }
4766
+ }
4767
+ const filteredTurns = options.client === "all" ? assistantTurns : assistantTurns.filter((t) => t.client === options.client);
4768
+ let clientSessionIds = null;
4769
+ if (options.client !== "all") {
4770
+ clientSessionIds = /* @__PURE__ */ new Set();
4771
+ for (const turn of assistantTurns) {
4772
+ if (turn.client === options.client) {
4773
+ const sid = turn.session_id;
4774
+ if (typeof sid === "string" && sid.length > 0) {
4775
+ clientSessionIds.add(sid);
4776
+ }
4777
+ }
4778
+ }
4779
+ }
4780
+ const kbIndex = /* @__PURE__ */ new Map();
4781
+ try {
4782
+ const meta = await readAgentsMeta(projectRoot);
4783
+ for (const node of Object.values(meta.nodes)) {
4784
+ const stableId = node.stable_id;
4785
+ if (typeof stableId !== "string" || stableId.length === 0) continue;
4786
+ const description = node.description;
4787
+ if (description === void 0) continue;
4788
+ const paths = description.relevance_paths ?? [];
4789
+ const scope = description.relevance_scope ?? "broad";
4790
+ kbIndex.set(stableId, {
4791
+ relevance_paths: paths,
4792
+ // A broad entry with no paths is the safe default. A narrow entry must
4793
+ // carry at least one path; an empty-paths narrow is treated as broad.
4794
+ relevance_scope: scope === "narrow" && paths.length > 0 ? "narrow" : "broad"
4795
+ });
4796
+ }
4797
+ } catch {
4798
+ }
4799
+ const fetchesBySession = /* @__PURE__ */ new Map();
4800
+ for (const fetch of fetchEvents) {
4801
+ const sid = fetch.session_id;
4802
+ if (typeof sid !== "string" || sid.length === 0) continue;
4803
+ const list = fetchesBySession.get(sid) ?? [];
4804
+ list.push(fetch.ts);
4805
+ fetchesBySession.set(sid, list);
4806
+ }
4807
+ for (const list of fetchesBySession.values()) {
4808
+ list.sort((a, b) => a - b);
4809
+ }
4810
+ const RECALL_WINDOW_MS = 6e4;
4811
+ const isRecallVerified = (turn) => {
4812
+ const sid = turn.session_id;
4813
+ if (typeof sid !== "string" || sid.length === 0) return false;
4814
+ const fetches = fetchesBySession.get(sid);
4815
+ if (fetches === void 0 || fetches.length === 0) return false;
4816
+ for (const ft of fetches) {
4817
+ if (Math.abs(ft - turn.ts) <= RECALL_WINDOW_MS) return true;
4818
+ }
4819
+ return false;
4820
+ };
4821
+ const dismissedHistogram = {};
4822
+ const perClientAccum = /* @__PURE__ */ new Map();
4823
+ const emptyMetrics = () => ({
4824
+ edits_touched: 0,
4825
+ qualifying_cites: 0,
4826
+ recalled_unverified: 0,
4827
+ expected_but_missed: 0,
4828
+ total_turns: 0
4829
+ });
4830
+ const bumpClient = (client, mut) => {
4831
+ if (typeof client !== "string" || client.length === 0) return;
4832
+ const existing = perClientAccum.get(client) ?? emptyMetrics();
4833
+ mut(existing);
4834
+ perClientAccum.set(client, existing);
4835
+ };
4836
+ const sessionCitedKbs = /* @__PURE__ */ new Map();
4837
+ let totalTurns = 0;
4838
+ let qualifyingCites = 0;
4839
+ let recalledUnverified = 0;
4840
+ for (const turn of filteredTurns) {
4841
+ totalTurns += 1;
4842
+ bumpClient(turn.client, (m) => {
4843
+ m.total_turns += 1;
4844
+ });
4845
+ const sid = turn.session_id;
4846
+ if (typeof sid === "string" && sid.length > 0) {
4847
+ const set = sessionCitedKbs.get(sid) ?? /* @__PURE__ */ new Set();
4848
+ for (const id of turn.cite_ids) {
4849
+ set.add(id);
4850
+ }
4851
+ sessionCitedKbs.set(sid, set);
4852
+ }
4853
+ let turnHadRecalled = false;
4854
+ for (const tag of turn.cite_tags) {
4855
+ const { category, reason } = categorizeCiteTag(tag);
4856
+ switch (category) {
4857
+ case "planned":
4858
+ case "recalled":
4859
+ case "chained-from":
4860
+ qualifyingCites += 1;
4861
+ bumpClient(turn.client, (m) => {
4862
+ m.qualifying_cites += 1;
4863
+ });
4864
+ if (category === "recalled") turnHadRecalled = true;
4865
+ break;
4866
+ case "dismissed": {
4867
+ const key = reason ?? "unspecified";
4868
+ dismissedHistogram[key] = (dismissedHistogram[key] ?? 0) + 1;
4869
+ break;
4870
+ }
4871
+ case "none":
4872
+ default:
4873
+ break;
4874
+ }
4875
+ }
4876
+ if (turnHadRecalled && !isRecallVerified(turn)) {
4877
+ recalledUnverified += 1;
4878
+ bumpClient(turn.client, (m) => {
4879
+ m.recalled_unverified += 1;
4880
+ });
4881
+ }
4882
+ }
4883
+ let editsTouched = 0;
4884
+ let expectedButMissed = 0;
4885
+ for (const edit of editEvents) {
4886
+ const sid = edit.session_id;
4887
+ if (clientSessionIds !== null) {
4888
+ if (typeof sid !== "string" || sid.length === 0) continue;
4889
+ if (!clientSessionIds.has(sid)) continue;
4890
+ }
4891
+ editsTouched += 1;
4892
+ if (typeof sid !== "string" || sid.length === 0) continue;
4893
+ const citedSet = sessionCitedKbs.get(sid) ?? /* @__PURE__ */ new Set();
4894
+ for (const [kbId, kb] of kbIndex) {
4895
+ if (kb.relevance_scope !== "narrow") continue;
4896
+ if (!matchesRelevancePath(edit.path, kb.relevance_paths)) continue;
4897
+ if (!citedSet.has(kbId)) {
4898
+ expectedButMissed += 1;
4899
+ }
4900
+ }
4901
+ }
4902
+ const metrics = {
4903
+ edits_touched: editsTouched,
4904
+ qualifying_cites: qualifyingCites,
4905
+ recalled_unverified: recalledUnverified,
4906
+ expected_but_missed: expectedButMissed,
4907
+ total_turns: totalTurns
4908
+ };
4909
+ let perClient;
4910
+ if (options.client === "all" && perClientAccum.size > 0) {
4911
+ perClient = {};
4912
+ for (const [client, m] of perClientAccum) {
4913
+ perClient[client] = m;
4914
+ }
4915
+ }
4916
+ return {
4917
+ status: "ok",
4918
+ marker_ts: marker.marker_ts,
4919
+ marker_emitted_now: marker.emitted_now,
4920
+ since_ts: effectiveSince,
4921
+ client_filter: options.client,
4922
+ metrics,
4923
+ ...perClient !== void 0 ? { per_client: perClient } : {},
4924
+ ...Object.keys(dismissedHistogram).length > 0 ? { dismissed_reason_histogram: dismissedHistogram } : {},
4925
+ generated_at: generatedAt
4926
+ };
4927
+ }
4029
4928
  function createFixMessage(fixed, report) {
4030
4929
  const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
4031
4930
  const manualText = report.manual_errors.length === 0 ? "No manual errors remain." : `${report.manual_errors.length} manual error${report.manual_errors.length === 1 ? "" : "s"} remain.`;
@@ -4046,7 +4945,7 @@ function normalizePath(path) {
4046
4945
  return posix.normalize(path.split("\\").join("/"));
4047
4946
  }
4048
4947
  function collectEntryPoints(root) {
4049
- if (!existsSync4(root) || !statSync3(root).isDirectory()) {
4948
+ if (!existsSync4(root) || !statSync4(root).isDirectory()) {
4050
4949
  return [];
4051
4950
  }
4052
4951
  const entries = [];
@@ -4057,7 +4956,7 @@ function collectEntryPoints(root) {
4057
4956
  continue;
4058
4957
  }
4059
4958
  for (const entry of readdirSync(current, { withFileTypes: true })) {
4060
- const absolutePath = join5(current, entry.name);
4959
+ const absolutePath = join6(current, entry.name);
4061
4960
  const relativePath = normalizePath(absolutePath.slice(root.length + 1));
4062
4961
  if (relativePath.length === 0) {
4063
4962
  continue;
@@ -4114,9 +5013,109 @@ function isMissingFileError(error) {
4114
5013
  return error instanceof Error && "code" in error && error.code === "ENOENT";
4115
5014
  }
4116
5015
 
5016
+ // src/services/load-active-meta.ts
5017
+ async function loadActiveMeta(projectRoot, opts = {}) {
5018
+ const onDisk = await readAgentsMeta(projectRoot);
5019
+ const previousRevisionHash = onDisk.revision;
5020
+ const derived = await buildKnowledgeMeta(projectRoot);
5021
+ if (derived.meta.revision === previousRevisionHash) {
5022
+ return {
5023
+ meta: onDisk,
5024
+ auto_healed: false,
5025
+ previous_revision_hash: previousRevisionHash,
5026
+ revision_hash: previousRevisionHash
5027
+ };
5028
+ }
5029
+ const written = await writeKnowledgeMeta(projectRoot, { source: "doctor_fix" });
5030
+ contextCache.invalidate("meta_write", projectRoot);
5031
+ await emitAutoHealEventBestEffort(projectRoot, {
5032
+ previous_revision_hash: previousRevisionHash,
5033
+ revision_hash: written.meta.revision,
5034
+ caller: opts.caller
5035
+ });
5036
+ return {
5037
+ meta: written.meta,
5038
+ auto_healed: true,
5039
+ previous_revision_hash: previousRevisionHash,
5040
+ revision_hash: written.meta.revision
5041
+ };
5042
+ }
5043
+ async function loadActiveMetaOrStale(projectRoot, opts = {}) {
5044
+ let onDisk;
5045
+ try {
5046
+ onDisk = await readAgentsMeta(projectRoot);
5047
+ } catch (error) {
5048
+ if (error instanceof AgentsMetaFileMissingError || error instanceof AgentsMetaInvalidError) {
5049
+ throw error;
5050
+ }
5051
+ throw error;
5052
+ }
5053
+ const previousRevisionHash = onDisk.revision;
5054
+ let derived;
5055
+ try {
5056
+ derived = await buildKnowledgeMeta(projectRoot);
5057
+ } catch (error) {
5058
+ return {
5059
+ meta: onDisk,
5060
+ auto_healed: false,
5061
+ previous_revision_hash: previousRevisionHash,
5062
+ revision_hash: previousRevisionHash,
5063
+ degraded: true,
5064
+ error: error instanceof Error ? error.message : String(error)
5065
+ };
5066
+ }
5067
+ if (derived.meta.revision === previousRevisionHash) {
5068
+ return {
5069
+ meta: onDisk,
5070
+ auto_healed: false,
5071
+ previous_revision_hash: previousRevisionHash,
5072
+ revision_hash: previousRevisionHash,
5073
+ degraded: false
5074
+ };
5075
+ }
5076
+ let written;
5077
+ try {
5078
+ written = await writeKnowledgeMeta(projectRoot, { source: "doctor_fix" });
5079
+ } catch (error) {
5080
+ return {
5081
+ meta: onDisk,
5082
+ auto_healed: false,
5083
+ previous_revision_hash: previousRevisionHash,
5084
+ revision_hash: previousRevisionHash,
5085
+ degraded: true,
5086
+ error: error instanceof Error ? error.message : String(error)
5087
+ };
5088
+ }
5089
+ contextCache.invalidate("meta_write", projectRoot);
5090
+ await emitAutoHealEventBestEffort(projectRoot, {
5091
+ previous_revision_hash: previousRevisionHash,
5092
+ revision_hash: written.meta.revision,
5093
+ caller: opts.caller
5094
+ });
5095
+ return {
5096
+ meta: written.meta,
5097
+ auto_healed: true,
5098
+ previous_revision_hash: previousRevisionHash,
5099
+ revision_hash: written.meta.revision,
5100
+ degraded: false
5101
+ };
5102
+ }
5103
+ async function emitAutoHealEventBestEffort(projectRoot, payload) {
5104
+ try {
5105
+ await appendEventLedgerEvent(projectRoot, {
5106
+ event_type: "knowledge_meta_auto_healed",
5107
+ previous_revision_hash: payload.previous_revision_hash,
5108
+ revision_hash: payload.revision_hash,
5109
+ trigger: "read",
5110
+ ...payload.caller !== void 0 ? { caller: payload.caller } : {}
5111
+ });
5112
+ } catch {
5113
+ }
5114
+ }
5115
+
4117
5116
  // src/services/get-knowledge.ts
4118
5117
  import { readFile as readFile6 } from "fs/promises";
4119
- import { join as join6 } from "path";
5118
+ import { join as join7 } from "path";
4120
5119
  import { minimatch as minimatch2 } from "minimatch";
4121
5120
  var PRIORITY_ORDER = {
4122
5121
  high: 0,
@@ -4124,6 +5123,10 @@ var PRIORITY_ORDER = {
4124
5123
  low: 2
4125
5124
  };
4126
5125
  async function getKnowledge(projectRoot, input) {
5126
+ const metaResult = await loadActiveMeta(projectRoot, { caller: "getKnowledge" });
5127
+ if (metaResult.auto_healed) {
5128
+ contextCache.invalidate("file_watch", projectRoot);
5129
+ }
4127
5130
  const context = await loadGetKnowledgeContext(projectRoot);
4128
5131
  const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
4129
5132
  const matchedNodes = matchRuleNodes(context.meta, input.path);
@@ -4156,7 +5159,7 @@ async function loadGetKnowledgeContext(projectRoot) {
4156
5159
  return cached;
4157
5160
  }
4158
5161
  const meta = await readAgentsMeta(projectRoot);
4159
- const l0Content = await readFile6(join6(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
5162
+ const l0Content = await readFile6(join7(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
4160
5163
  const context = {
4161
5164
  meta,
4162
5165
  l0Content,
@@ -4295,13 +5298,14 @@ async function readRuleContent(projectRoot, file, fileContentCache) {
4295
5298
  if (cached !== void 0) {
4296
5299
  return await cached;
4297
5300
  }
4298
- const pending = readFile6(join6(projectRoot, file), "utf8");
5301
+ const pending = readFile6(join7(projectRoot, file), "utf8");
4299
5302
  fileContentCache.set(file, pending);
4300
5303
  return await pending;
4301
5304
  }
4302
5305
 
4303
5306
  export {
4304
5307
  contextCache,
5308
+ AgentsMetaFileMissingError,
4305
5309
  resolveProjectRoot,
4306
5310
  readAgentsMeta,
4307
5311
  LEDGER_PATH,
@@ -4328,9 +5332,12 @@ export {
4328
5332
  invalidateKnowledgeSyncCooldown,
4329
5333
  ensureKnowledgeFresh,
4330
5334
  reconcileKnowledge,
5335
+ loadActiveMeta,
5336
+ loadActiveMetaOrStale,
4331
5337
  getKnowledge,
4332
5338
  normalizeKnowledgePath,
4333
5339
  runDoctorReport,
4334
5340
  runDoctorFix,
4335
- runDoctorApplyLint
5341
+ runDoctorApplyLint,
5342
+ runDoctorCiteCoverage
4336
5343
  };