@fenglimg/fabric-server 2.2.0-rc.1 → 2.2.0-rc.3

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +152 -2
  2. package/dist/index.js +1039 -340
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/index.ts
2
2
  import { existsSync as existsSync9 } from "fs";
3
- import { readFile as readFile15 } from "fs/promises";
4
- import { join as join12, resolve as resolve5 } from "path";
3
+ import { readFile as readFile16 } from "fs/promises";
4
+ import { join as join14, resolve as resolve5 } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -162,7 +162,11 @@ import {
162
162
  eventLedgerEventSchema,
163
163
  redactSecrets
164
164
  } from "@fenglimg/fabric-shared";
165
- import { atomicWriteText as atomicWriteText2, createLedgerWriteQueue } from "@fenglimg/fabric-shared/node/atomic-write";
165
+ import {
166
+ atomicWriteText as atomicWriteText2,
167
+ createLedgerWriteQueue,
168
+ withFileLock
169
+ } from "@fenglimg/fabric-shared/node/atomic-write";
166
170
 
167
171
  // src/services/_shared.ts
168
172
  import { dirname, join as join2, resolve, sep } from "path";
@@ -214,6 +218,15 @@ function isNodeError(error) {
214
218
 
215
219
  // src/services/event-ledger.ts
216
220
  var ledgerQueue = createLedgerWriteQueue();
221
+ function eventLedgerLockPath(eventPath) {
222
+ return `${eventPath}.lock`;
223
+ }
224
+ function exclusiveLedgerWrite(eventPath, fn) {
225
+ return withFileLock(
226
+ eventLedgerLockPath(eventPath),
227
+ () => ledgerQueue.runExclusive(eventPath, fn)
228
+ );
229
+ }
217
230
  var __eventLedgerParseStats = { lineParses: 0 };
218
231
  var EVENT_LEDGER_DEFAULT_RETENTION_DAYS = 30;
219
232
  var EVENT_LEDGER_SIZE_WARN_BYTES = 50 * 1024 * 1024;
@@ -317,7 +330,10 @@ async function appendEventLedgerEvent(projectRoot, event) {
317
330
  schema_version: 1
318
331
  });
319
332
  await ensureParentDirectory(eventPath);
320
- await ledgerQueue.append(eventPath, JSON.stringify(nextEvent));
333
+ await withFileLock(
334
+ eventLedgerLockPath(eventPath),
335
+ () => ledgerQueue.append(eventPath, JSON.stringify(nextEvent))
336
+ );
321
337
  try {
322
338
  const size = statSync(eventPath).size;
323
339
  if (size > EVENT_LEDGER_SIZE_WARN_BYTES) {
@@ -385,24 +401,26 @@ async function readEventLedger(projectRoot, options = {}) {
385
401
  return { events: filtered, warnings };
386
402
  }
387
403
  async function truncateLedgerToLastNewline(path2) {
388
- const raw = await readFile2(path2);
389
- const content = raw.toString("utf8");
390
- if (content.endsWith("\n") || content.length === 0) {
391
- return { truncated_bytes: 0, corrupted_path: "" };
392
- }
393
- const lastNewlineIndex = content.lastIndexOf("\n");
394
- if (lastNewlineIndex === -1) {
395
- const corruptedPath2 = `${path2}.corrupted.${Date.now()}`;
396
- await writeFile(corruptedPath2, raw);
397
- await truncate(path2, 0);
398
- return { truncated_bytes: raw.length, corrupted_path: corruptedPath2 };
399
- }
400
- const keepByteLength = Buffer.byteLength(content.slice(0, lastNewlineIndex + 1), "utf8");
401
- const corruptedBytes = raw.slice(keepByteLength);
402
- const corruptedPath = `${path2}.corrupted.${Date.now()}`;
403
- await writeFile(corruptedPath, corruptedBytes);
404
- await truncate(path2, keepByteLength);
405
- return { truncated_bytes: corruptedBytes.length, corrupted_path: corruptedPath };
404
+ return withFileLock(eventLedgerLockPath(path2), async () => {
405
+ const raw = await readFile2(path2);
406
+ const content = raw.toString("utf8");
407
+ if (content.endsWith("\n") || content.length === 0) {
408
+ return { truncated_bytes: 0, corrupted_path: "" };
409
+ }
410
+ const lastNewlineIndex = content.lastIndexOf("\n");
411
+ if (lastNewlineIndex === -1) {
412
+ const corruptedPath2 = `${path2}.corrupted.${Date.now()}`;
413
+ await writeFile(corruptedPath2, raw);
414
+ await truncate(path2, 0);
415
+ return { truncated_bytes: raw.length, corrupted_path: corruptedPath2 };
416
+ }
417
+ const keepByteLength = Buffer.byteLength(content.slice(0, lastNewlineIndex + 1), "utf8");
418
+ const corruptedBytes = raw.slice(keepByteLength);
419
+ const corruptedPath = `${path2}.corrupted.${Date.now()}`;
420
+ await writeFile(corruptedPath, corruptedBytes);
421
+ await truncate(path2, keepByteLength);
422
+ return { truncated_bytes: corruptedBytes.length, corrupted_path: corruptedPath };
423
+ });
406
424
  }
407
425
  function parseEventLedgerLine(line, index) {
408
426
  try {
@@ -427,7 +445,7 @@ function isNodeError2(error) {
427
445
  }
428
446
  async function rotateEventLedgerIfNeeded(projectRoot, opts = {}) {
429
447
  const eventPath = getEventLedgerPath(projectRoot);
430
- return ledgerQueue.runExclusive(eventPath, async () => {
448
+ return exclusiveLedgerWrite(eventPath, async () => {
431
449
  const now = opts.now ?? /* @__PURE__ */ new Date();
432
450
  const retentionDays = resolveRetentionDays(projectRoot, opts.retentionDays);
433
451
  const cutoffMs = now.getTime() - retentionDays * 864e5;
@@ -523,7 +541,7 @@ async function rotateEventLedgerIfNeeded(projectRoot, opts = {}) {
523
541
  }
524
542
  async function dropEventsFromLedger(projectRoot, opts) {
525
543
  const eventPath = getEventLedgerPath(projectRoot);
526
- return ledgerQueue.runExclusive(eventPath, async () => {
544
+ return exclusiveLedgerWrite(eventPath, async () => {
527
545
  const now = opts.now ?? /* @__PURE__ */ new Date();
528
546
  let raw;
529
547
  try {
@@ -741,7 +759,7 @@ import { join as join5 } from "path";
741
759
  import { RuleValidationError } from "@fenglimg/fabric-shared/errors";
742
760
 
743
761
  // src/services/knowledge-meta-builder.ts
744
- import { mkdir as mkdir3, readdir, readFile as readFile3, stat } from "fs/promises";
762
+ import { readdir, readFile as readFile3, stat } from "fs/promises";
745
763
  import { existsSync as existsSync2, statSync as statSync2 } from "fs";
746
764
  import { homedir } from "os";
747
765
  import { isAbsolute, join as join4, relative, resolve as resolve2, sep as sep2 } from "path";
@@ -760,7 +778,7 @@ import {
760
778
  StableIdSchema,
761
779
  parseKnowledgeId
762
780
  } from "@fenglimg/fabric-shared";
763
- import { atomicWriteText as atomicWriteText3 } from "@fenglimg/fabric-shared/node/atomic-write";
781
+ import { atomicWriteText as atomicWriteText3, withFileLock as withFileLock2 } from "@fenglimg/fabric-shared/node/atomic-write";
764
782
  async function loadKbIdTypeMap(projectRootInput) {
765
783
  const projectRoot = normalizeProjectRoot(projectRootInput);
766
784
  const metaPath = join4(projectRoot, ".fabric", "agents.meta.json");
@@ -801,28 +819,30 @@ async function writeKnowledgeMeta(projectRootInput, options) {
801
819
  const projectRoot = normalizeProjectRoot(projectRootInput);
802
820
  const metaPath = join4(projectRoot, ".fabric", "agents.meta.json");
803
821
  const knowledgeTestIndexPath = join4(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
804
- const existingMeta = await readExistingMeta(metaPath);
805
- const result = await buildKnowledgeMeta(projectRoot);
806
- if (!result.changed) {
807
- return result;
808
- }
809
- await ensureParentDirectory(metaPath);
810
- await atomicWriteText3(metaPath, `${JSON.stringify(result.meta, null, 2)}
822
+ return withFileLock2(`${metaPath}.lock`, async () => {
823
+ const existingMeta = await readExistingMeta(metaPath);
824
+ const result = await buildKnowledgeMeta(projectRoot);
825
+ if (!result.changed) {
826
+ return result;
827
+ }
828
+ await ensureParentDirectory(metaPath);
829
+ await atomicWriteText3(metaPath, `${JSON.stringify(result.meta, null, 2)}
811
830
  `);
812
- await ensureParentDirectory(knowledgeTestIndexPath);
813
- await atomicWriteText3(knowledgeTestIndexPath, `${JSON.stringify(result.knowledgeTestIndex, null, 2)}
831
+ await ensureParentDirectory(knowledgeTestIndexPath);
832
+ await atomicWriteText3(knowledgeTestIndexPath, `${JSON.stringify(result.knowledgeTestIndex, null, 2)}
814
833
  `);
815
- if (existingMeta === void 0 || stableStringify(existingMeta) !== stableStringify(result.meta)) {
816
- await recordBaselineSynced(projectRoot, {
817
- previousRevision: existingMeta?.revision,
818
- revision: result.meta.revision,
819
- syncedFiles: collectSyncedFiles(existingMeta, result.meta),
820
- acceptedStableIds: collectStableIds(result.meta),
821
- driftDetails: collectDriftDetails(existingMeta, result.meta),
822
- source: options.source
823
- });
824
- }
825
- return result;
834
+ if (existingMeta === void 0 || stableStringify(existingMeta) !== stableStringify(result.meta)) {
835
+ await recordBaselineSynced(projectRoot, {
836
+ previousRevision: existingMeta?.revision,
837
+ revision: result.meta.revision,
838
+ syncedFiles: collectSyncedFiles(existingMeta, result.meta),
839
+ acceptedStableIds: collectStableIds(result.meta),
840
+ driftDetails: collectDriftDetails(existingMeta, result.meta),
841
+ source: options.source
842
+ });
843
+ }
844
+ return result;
845
+ });
826
846
  }
827
847
  var knowledgeMetaCache = /* @__PURE__ */ new Map();
828
848
  var __knowledgeMetaCacheStats = { fileReads: 0 };
@@ -992,13 +1012,6 @@ function resolveContentRefPath(projectRoot, contentRef) {
992
1012
  async function findKnowledgeFiles(projectRoot) {
993
1013
  const teamRoot = join4(projectRoot, ".fabric", "knowledge");
994
1014
  const personalRoot = join4(resolvePersonalRoot(), ".fabric", "knowledge");
995
- try {
996
- await mkdir3(personalRoot, { recursive: true });
997
- for (const sub of KNOWLEDGE_SUBDIRS) {
998
- await mkdir3(join4(personalRoot, sub), { recursive: true });
999
- }
1000
- } catch {
1001
- }
1002
1015
  const files = [];
1003
1016
  for (const [root, prefix] of [
1004
1017
  [teamRoot, TEAM_CONTENT_REF_PREFIX],
@@ -1390,6 +1403,16 @@ function extractDescriptionFromFrontmatter(frontmatter) {
1390
1403
  related: knowledge.related
1391
1404
  };
1392
1405
  }
1406
+ function isForbiddenCrossLayerEdge(sourceLayer, targetId) {
1407
+ if (sourceLayer !== "team") {
1408
+ return false;
1409
+ }
1410
+ const decoded = parseKnowledgeId(targetId);
1411
+ if (decoded === null) {
1412
+ return false;
1413
+ }
1414
+ return decoded.layer === "personal";
1415
+ }
1393
1416
  function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
1394
1417
  const rawId = extractScalar(frontmatter, "id");
1395
1418
  const rawType = extractScalar(frontmatter, "type");
@@ -1469,7 +1492,18 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
1469
1492
  const rawRelevanceScope = extractScalar(frontmatter, "relevance_scope");
1470
1493
  const relevance_scope = rawRelevanceScope === "narrow" || rawRelevanceScope === "broad" ? rawRelevanceScope : "broad";
1471
1494
  const relevance_paths = extractInlineArray(frontmatter, "relevance_paths");
1472
- const related = extractInlineArray(frontmatter, "related");
1495
+ const rawRelated = extractInlineArray(frontmatter, "related");
1496
+ const sourceLayer = knowledge_layer ?? (id !== void 0 ? parseKnowledgeId(id)?.layer ?? "team" : "team");
1497
+ const related = rawRelated.filter((targetId) => {
1498
+ if (isForbiddenCrossLayerEdge(sourceLayer, targetId)) {
1499
+ process.stderr.write(
1500
+ `[fabric] frontmatter: stripping forbidden cross-layer related edge ${id ?? "(team entry)"} \u2192 ${targetId} (KT\u2192KP topology leak; \xA74 privacy iron law)
1501
+ `
1502
+ );
1503
+ return false;
1504
+ }
1505
+ return true;
1506
+ });
1473
1507
  return {
1474
1508
  id,
1475
1509
  knowledge_type,
@@ -1934,15 +1968,36 @@ function readSelectionTokenTtlMs(projectRoot) {
1934
1968
  return void 0;
1935
1969
  }
1936
1970
  }
1971
+ var DEFAULT_EMBED_MODEL = "fast-bge-small-zh-v1.5";
1972
+ var SUPPORTED_EMBED_MODELS = /* @__PURE__ */ new Set([
1973
+ "fast-bge-small-zh-v1.5",
1974
+ "fast-multilingual-e5-large",
1975
+ "fast-bge-small-en-v1.5",
1976
+ "fast-bge-small-en",
1977
+ "fast-bge-base-en-v1.5",
1978
+ "fast-bge-base-en",
1979
+ "fast-all-MiniLM-L6-v2"
1980
+ ]);
1937
1981
  function readEmbedConfig(projectRoot) {
1938
1982
  try {
1939
1983
  const config = readFabricConfig(projectRoot);
1940
1984
  const enabled = config.embed_enabled === true;
1941
1985
  const rawWeight = config.embed_weight;
1942
1986
  const weight = typeof rawWeight === "number" && Number.isInteger(rawWeight) && rawWeight >= 0 && rawWeight <= 49 ? rawWeight : 30;
1943
- return { enabled, weight };
1987
+ const rawModel = config.embed_model;
1988
+ const model = typeof rawModel === "string" && SUPPORTED_EMBED_MODELS.has(rawModel) ? rawModel : DEFAULT_EMBED_MODEL;
1989
+ return { enabled, weight, model };
1990
+ } catch {
1991
+ return { enabled: false, weight: 30, model: DEFAULT_EMBED_MODEL };
1992
+ }
1993
+ }
1994
+ function readDefaultLayerFilter(projectRoot) {
1995
+ try {
1996
+ const config = readFabricConfig(projectRoot);
1997
+ const raw = config.default_layer_filter;
1998
+ return raw === "team" || raw === "personal" ? raw : "both";
1944
1999
  } catch {
1945
- return { enabled: false, weight: 30 };
2000
+ return "both";
1946
2001
  }
1947
2002
  }
1948
2003
  function readPlanContextTopK(projectRoot) {
@@ -1968,10 +2023,10 @@ function readOrphanDemoteThresholdDays(projectRoot) {
1968
2023
  }
1969
2024
  return v;
1970
2025
  };
1971
- const s = validate(cfg.orphan_demote_stable_days);
1972
- if (s !== void 0) out.stable = s;
1973
- const e = validate(cfg.orphan_demote_endorsed_days);
1974
- if (e !== void 0) out.endorsed = e;
2026
+ const proven = validate(cfg.orphan_demote_proven_days) ?? validate(cfg.orphan_demote_stable_days);
2027
+ if (proven !== void 0) out.stable = proven;
2028
+ const verified = validate(cfg.orphan_demote_verified_days) ?? validate(cfg.orphan_demote_endorsed_days);
2029
+ if (verified !== void 0) out.endorsed = verified;
1975
2030
  const d = validate(cfg.orphan_demote_draft_days);
1976
2031
  if (d !== void 0) out.draft = d;
1977
2032
  return out;
@@ -1979,12 +2034,27 @@ function readOrphanDemoteThresholdDays(projectRoot) {
1979
2034
  return {};
1980
2035
  }
1981
2036
  }
2037
+ function readConflictLintThreshold(projectRoot) {
2038
+ try {
2039
+ const cfgPath = join6(projectRoot, ".fabric", "fabric-config.json");
2040
+ if (!existsSync4(cfgPath)) return void 0;
2041
+ const parsed = JSON.parse(readFileSync2(cfgPath, "utf8"));
2042
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
2043
+ const v = parsed.conflict_lint_similarity_threshold;
2044
+ if (typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 1) {
2045
+ return v;
2046
+ }
2047
+ return void 0;
2048
+ } catch {
2049
+ return void 0;
2050
+ }
2051
+ }
1982
2052
 
1983
2053
  // src/services/extract-knowledge.ts
1984
2054
  import { existsSync as existsSync5 } from "fs";
1985
2055
  import { readFile as readFile5 } from "fs/promises";
1986
2056
  import { homedir as homedir3 } from "os";
1987
- import { join as join7, relative as relative2 } from "path";
2057
+ import { join as join8, relative as relative2 } from "path";
1988
2058
  import {
1989
2059
  PROPOSED_REASON_DESCRIPTIONS
1990
2060
  } from "@fenglimg/fabric-shared/schemas/api-contracts";
@@ -2090,8 +2160,44 @@ async function emitAutoHealEventBestEffort(projectRoot, payload) {
2090
2160
  }
2091
2161
  }
2092
2162
 
2163
+ // src/services/cross-store-write.ts
2164
+ import { join as join7 } from "path";
2165
+ import {
2166
+ STORE_LAYOUT,
2167
+ STORE_PENDING_DIR,
2168
+ buildStoreResolveInput,
2169
+ createStoreResolver,
2170
+ resolveGlobalRoot,
2171
+ storeRelativePath
2172
+ } from "@fenglimg/fabric-shared";
2173
+ import { StoreWriteTargetUnresolvedError } from "@fenglimg/fabric-shared/errors";
2174
+ function writeTargetUnresolved(layer) {
2175
+ const actionHint = layer === "personal" ? "run `fabric install --global` to mint your personal store, then retry" : "mount + select a team store: `fabric install --global` then `fabric store bind <alias>` and `fabric store switch-write <alias>`, then retry";
2176
+ return new StoreWriteTargetUnresolvedError(
2177
+ `no ${layer} write-target store resolved \u2014 knowledge writes are store-only (dual-root co-location removed)`,
2178
+ { actionHint, fixable: true, details: { layer } }
2179
+ );
2180
+ }
2181
+ function resolveWriteTargetStoreDir(layer, projectRoot) {
2182
+ const input = buildStoreResolveInput(projectRoot);
2183
+ if (input === null) {
2184
+ throw writeTargetUnresolved(layer);
2185
+ }
2186
+ const scope = layer === "personal" ? "personal" : "team";
2187
+ const { target } = createStoreResolver().resolveWriteTarget(input, scope);
2188
+ if (target === null) {
2189
+ throw writeTargetUnresolved(layer);
2190
+ }
2191
+ return join7(resolveGlobalRoot(), storeRelativePath(target.store_uuid));
2192
+ }
2193
+ function resolveStorePendingBase(layer, projectRoot) {
2194
+ return join7(resolveWriteTargetStoreDir(layer, projectRoot), STORE_LAYOUT.knowledgeDir, STORE_PENDING_DIR);
2195
+ }
2196
+ function resolveStoreCanonicalBase(layer, projectRoot) {
2197
+ return join7(resolveWriteTargetStoreDir(layer, projectRoot), STORE_LAYOUT.knowledgeDir);
2198
+ }
2199
+
2093
2200
  // src/services/extract-knowledge.ts
2094
- var TEAM_PENDING_REL = ".fabric/knowledge/pending";
2095
2201
  var SLUG_MAX_LENGTH = 40;
2096
2202
  var INJECTION_PATTERNS = [
2097
2203
  {
@@ -2160,10 +2266,7 @@ function sanitizeInjectionFields(fields) {
2160
2266
  return { sanitized: out, allRedactions };
2161
2267
  }
2162
2268
  function pendingBase(layer, projectRoot) {
2163
- if (layer === "personal") {
2164
- return join7(resolvePersonalRoot3(), ".fabric", "knowledge", "pending");
2165
- }
2166
- return join7(projectRoot, TEAM_PENDING_REL);
2269
+ return resolveStorePendingBase(layer, projectRoot);
2167
2270
  }
2168
2271
  function resolvePersonalRoot3() {
2169
2272
  return process.env.FABRIC_HOME ?? homedir3();
@@ -2277,7 +2380,7 @@ async function extractKnowledge(projectRoot, input) {
2277
2380
  primarySession,
2278
2381
  baseIdempotencyKey: idempotencyKey
2279
2382
  });
2280
- const reportedPath = layer === "personal" ? `~/${relative2(resolvePersonalRoot3(), absolutePath)}` : relative2(projectRoot, absolutePath);
2383
+ const reportedPath = `~/${relative2(resolvePersonalRoot3(), absolutePath)}`;
2281
2384
  const effectiveSanitizedSlug = chosenSlug;
2282
2385
  const effectiveIdempotencyKey = chosenKey;
2283
2386
  await ensureParentDirectory(absolutePath);
@@ -2361,7 +2464,7 @@ var SLUG_DISAMBIGUATE_MAX_VARIANTS = 9;
2361
2464
  async function resolveDisambiguatedSlugPath(args) {
2362
2465
  for (let n = 1; n <= SLUG_DISAMBIGUATE_MAX_VARIANTS; n += 1) {
2363
2466
  const candidateSlug = n === 1 ? args.slug : `${args.slug}-${n}`;
2364
- const candidatePath = join7(args.baseDir, args.type, `${candidateSlug}.md`);
2467
+ const candidatePath = join8(args.baseDir, args.type, `${candidateSlug}.md`);
2365
2468
  const candidateKey = n === 1 ? args.baseIdempotencyKey : sha256(
2366
2469
  JSON.stringify({
2367
2470
  source_session: args.primarySession,
@@ -2599,11 +2702,11 @@ function readFrontmatterKey(content, key) {
2599
2702
  }
2600
2703
  for (const rawLine of block.split(/\r?\n/u)) {
2601
2704
  const line = rawLine.trim();
2602
- const sep4 = line.indexOf(":");
2603
- if (sep4 === -1) continue;
2604
- const k = line.slice(0, sep4).trim();
2705
+ const sep5 = line.indexOf(":");
2706
+ if (sep5 === -1) continue;
2707
+ const k = line.slice(0, sep5).trim();
2605
2708
  if (k === key) {
2606
- return line.slice(sep4 + 1).trim();
2709
+ return line.slice(sep5 + 1).trim();
2607
2710
  }
2608
2711
  }
2609
2712
  return void 0;
@@ -2674,7 +2777,7 @@ import { tokenize as tokenize2 } from "@fenglimg/fabric-shared";
2674
2777
 
2675
2778
  // src/services/get-knowledge.ts
2676
2779
  import { readFile as readFile6 } from "fs/promises";
2677
- import { join as join8 } from "path";
2780
+ import { join as join9 } from "path";
2678
2781
  import { deriveAgentsMetaLayer as deriveAgentsMetaLayer2 } from "@fenglimg/fabric-shared";
2679
2782
  import { minimatch } from "minimatch";
2680
2783
  async function getKnowledge(projectRoot, input) {
@@ -2714,7 +2817,7 @@ async function loadGetKnowledgeContext(projectRoot) {
2714
2817
  return cached;
2715
2818
  }
2716
2819
  const meta = await readAgentsMeta(projectRoot);
2717
- const l0Content = await readFile6(join8(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
2820
+ const l0Content = await readFile6(join9(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
2718
2821
  const context = {
2719
2822
  meta,
2720
2823
  l0Content,
@@ -2849,11 +2952,98 @@ async function readRuleContent(projectRoot, file, fileContentCache) {
2849
2952
  if (cached !== void 0) {
2850
2953
  return await cached;
2851
2954
  }
2852
- const pending = readFile6(join8(projectRoot, file), "utf8");
2955
+ const pending = readFile6(join9(projectRoot, file), "utf8");
2853
2956
  fileContentCache.set(file, pending);
2854
2957
  return await pending;
2855
2958
  }
2856
2959
 
2960
+ // src/services/cross-store-recall.ts
2961
+ import { readFileSync as readFileSync3 } from "fs";
2962
+ import { join as join10 } from "path";
2963
+ import {
2964
+ buildStoreResolveInput as buildStoreResolveInput2,
2965
+ createStoreResolver as createStoreResolver2,
2966
+ readKnowledgeAcrossStores,
2967
+ resolveGlobalRoot as resolveGlobalRoot2,
2968
+ storeRelativePath as storeRelativePath2
2969
+ } from "@fenglimg/fabric-shared";
2970
+ function walkReadSetStores(projectRoot) {
2971
+ const resolveInput = buildStoreResolveInput2(projectRoot);
2972
+ if (resolveInput === null) {
2973
+ return [];
2974
+ }
2975
+ const readSet = createStoreResolver2().resolveReadSet(resolveInput);
2976
+ if (readSet.stores.length === 0) {
2977
+ return [];
2978
+ }
2979
+ const personalUuids = new Set(
2980
+ resolveInput.mountedStores.filter((s) => s.personal).map((s) => s.store_uuid)
2981
+ );
2982
+ const globalRoot = resolveGlobalRoot2();
2983
+ const dirs = readSet.stores.map((entry) => ({
2984
+ store_uuid: entry.store_uuid,
2985
+ alias: entry.alias,
2986
+ dir: join10(globalRoot, storeRelativePath2(entry.store_uuid))
2987
+ }));
2988
+ const entries = [];
2989
+ for (const ref of readKnowledgeAcrossStores(dirs)) {
2990
+ let source;
2991
+ try {
2992
+ source = readFileSync3(ref.file, "utf8");
2993
+ } catch {
2994
+ continue;
2995
+ }
2996
+ const stableId = deriveRuleIdentity(ref.file, source, void 0).stableId;
2997
+ const layer = personalUuids.has(ref.store_uuid) ? "personal" : "team";
2998
+ entries.push({
2999
+ qualifiedId: `${ref.alias}:${stableId}`,
3000
+ file: ref.file,
3001
+ alias: ref.alias,
3002
+ layer,
3003
+ source
3004
+ });
3005
+ }
3006
+ return entries;
3007
+ }
3008
+ async function buildCrossStoreRawItems(projectRoot) {
3009
+ const items = [];
3010
+ for (const entry of walkReadSetStores(projectRoot)) {
3011
+ const baseDescription = extractRuleDescription(entry.source);
3012
+ if (baseDescription === void 0) {
3013
+ continue;
3014
+ }
3015
+ items.push({
3016
+ stable_id: entry.qualifiedId,
3017
+ description: { ...baseDescription, knowledge_layer: entry.layer }
3018
+ });
3019
+ }
3020
+ return items;
3021
+ }
3022
+ function buildCrossStoreBodyIndex(projectRoot) {
3023
+ const index = /* @__PURE__ */ new Map();
3024
+ for (const entry of walkReadSetStores(projectRoot)) {
3025
+ if (!index.has(entry.qualifiedId)) {
3026
+ index.set(entry.qualifiedId, { file: entry.file, layer: entry.layer });
3027
+ }
3028
+ }
3029
+ return index;
3030
+ }
3031
+ function collectStoreKnowledgeSummaries(projectRoot) {
3032
+ const out = [];
3033
+ for (const entry of walkReadSetStores(projectRoot)) {
3034
+ const description = extractRuleDescription(entry.source);
3035
+ if (description === void 0) {
3036
+ continue;
3037
+ }
3038
+ out.push({
3039
+ stableId: entry.qualifiedId,
3040
+ summary: description.summary ?? "",
3041
+ layer: entry.layer
3042
+ });
3043
+ }
3044
+ return out;
3045
+ }
3046
+
2857
3047
  // src/services/id-redirect.ts
2858
3048
  var DEFAULT_REDIRECT_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
2859
3049
  async function loadIdRedirectMap(projectRoot, options = {}) {
@@ -3066,7 +3256,14 @@ function buildQueryTerms(text) {
3066
3256
  // src/services/vector-retrieval.ts
3067
3257
  var embedderLoad;
3068
3258
  var OPTIONAL_EMBED_PACKAGE = "fastembed";
3069
- async function loadEmbedder() {
3259
+ function buildEmbedInitOptions(modelName) {
3260
+ return {
3261
+ maxLength: 512,
3262
+ cacheDir: process.env.FABRIC_EMBED_CACHE_DIR,
3263
+ ...typeof modelName === "string" && modelName.length > 0 ? { model: modelName } : {}
3264
+ };
3265
+ }
3266
+ async function loadEmbedder(modelName) {
3070
3267
  if (embedderLoad === void 0) {
3071
3268
  embedderLoad = (async () => {
3072
3269
  try {
@@ -3075,10 +3272,7 @@ async function loadEmbedder() {
3075
3272
  if (mod?.FlagEmbedding?.init === void 0) {
3076
3273
  return null;
3077
3274
  }
3078
- const model = await mod.FlagEmbedding.init({
3079
- maxLength: 512,
3080
- cacheDir: process.env.FABRIC_EMBED_CACHE_DIR
3081
- });
3275
+ const model = await mod.FlagEmbedding.init(buildEmbedInitOptions(modelName));
3082
3276
  return {
3083
3277
  async embed(texts) {
3084
3278
  const out = [];
@@ -3245,7 +3439,11 @@ async function planContext(projectRoot, input) {
3245
3439
  targetPaths: input.target_paths ?? dedupePaths(input.paths),
3246
3440
  queryTerms: buildQueryTerms(queryText)
3247
3441
  };
3248
- const { rawItems, suppressedStableIds } = buildRawDescriptionItems(meta);
3442
+ const { rawItems: projectRawItems, suppressedStableIds } = buildRawDescriptionItems(meta);
3443
+ const storeRawItems = await buildCrossStoreRawItems(projectRoot).catch(() => []);
3444
+ const allRawItems = [...projectRawItems, ...storeRawItems];
3445
+ const effectiveLayerFilter = input.layer_filter ?? readDefaultLayerFilter(projectRoot);
3446
+ const rawItems = effectiveLayerFilter === "both" ? allRawItems : allRawItems.filter((item) => item.description.knowledge_layer === effectiveLayerFilter);
3249
3447
  const docTexts = /* @__PURE__ */ new Map();
3250
3448
  for (const item of rawItems) {
3251
3449
  docTexts.set(item.stable_id, documentTextForItem(item.description));
@@ -3256,7 +3454,7 @@ async function planContext(projectRoot, input) {
3256
3454
  }
3257
3455
  const embedConfig = readEmbedConfig(projectRoot);
3258
3456
  if (embedConfig.enabled && queryText.trim().length > 0 && rawItems.length > 0) {
3259
- const embedder = await loadEmbedder();
3457
+ const embedder = await loadEmbedder(embedConfig.model);
3260
3458
  const vectorScores = await buildVectorScores(
3261
3459
  embedder,
3262
3460
  queryText,
@@ -3274,7 +3472,27 @@ async function planContext(projectRoot, input) {
3274
3472
  const rankedCandidates = dedupeDescriptionIndex(builtItems);
3275
3473
  const topK = readPlanContextTopK(projectRoot);
3276
3474
  const omittedCandidateCount = Math.max(0, rankedCandidates.length - topK);
3277
- const candidates = omittedCandidateCount > 0 ? rankedCandidates.slice(0, topK) : rankedCandidates;
3475
+ const topKCandidates = omittedCandidateCount > 0 ? rankedCandidates.slice(0, topK) : rankedCandidates;
3476
+ let candidates = topKCandidates;
3477
+ const relatedAppended = {};
3478
+ if (input.include_related === true) {
3479
+ const inTopK = new Set(topKCandidates.map((item) => item.stable_id));
3480
+ const rankedById = new Map(rankedCandidates.map((item) => [item.stable_id, item]));
3481
+ const appended = [];
3482
+ for (const surfaced of topKCandidates) {
3483
+ for (const rel of surfaced.description.related ?? []) {
3484
+ if (inTopK.has(rel)) continue;
3485
+ if (relatedAppended[rel] !== void 0) continue;
3486
+ const neighbour = rankedById.get(rel);
3487
+ if (neighbour === void 0) continue;
3488
+ relatedAppended[rel] = surfaced.stable_id;
3489
+ appended.push(neighbour);
3490
+ }
3491
+ }
3492
+ if (appended.length > 0) {
3493
+ candidates = [...topKCandidates, ...appended];
3494
+ }
3495
+ }
3278
3496
  const entries = uniquePaths.map((path2) => ({
3279
3497
  path: path2,
3280
3498
  requirement_profile: buildRequirementProfile(path2, input)
@@ -3308,7 +3526,11 @@ async function planContext(projectRoot, input) {
3308
3526
  auto_healed: true,
3309
3527
  previous_revision_hash: firstSeenPreviousRevision
3310
3528
  } : {},
3311
- ...redirects !== void 0 ? { redirects } : {}
3529
+ ...redirects !== void 0 ? { redirects } : {},
3530
+ // lifecycle-refactor W3-T2 (§7): surface the related-expansion provenance map
3531
+ // ONLY when at least one neighbour was actually appended. Empty (graph-empty
3532
+ // no-op) → field omitted, steady-state wire shape unchanged.
3533
+ ...Object.keys(relatedAppended).length > 0 ? { related_appended: relatedAppended } : {}
3312
3534
  };
3313
3535
  bumpCounter(projectRoot, METRIC_COUNTER_NAMES.knowledge_context_planned);
3314
3536
  try {
@@ -3594,7 +3816,7 @@ function registerPlanContext(server, tracker) {
3594
3816
  outputSchema: planContextOutputSchema,
3595
3817
  annotations: planContextAnnotations
3596
3818
  },
3597
- async ({ paths, intent, known_tech, detected_entities, client_hash, correlation_id, session_id }) => {
3819
+ async ({ paths, intent, known_tech, detected_entities, client_hash, correlation_id, session_id, target_paths, layer_filter }) => {
3598
3820
  const requestId = randomUUID3();
3599
3821
  tracker?.enter(requestId);
3600
3822
  try {
@@ -3609,7 +3831,12 @@ function registerPlanContext(server, tracker) {
3609
3831
  detected_entities,
3610
3832
  client_hash,
3611
3833
  correlation_id,
3612
- session_id
3834
+ session_id,
3835
+ // F54 (ISS-20260531-090): these were declared in
3836
+ // planContextInputSchema but never forwarded to the service, so any
3837
+ // client/LLM-supplied value was silently discarded.
3838
+ target_paths,
3839
+ layer_filter
3613
3840
  });
3614
3841
  let response = {
3615
3842
  ...result,
@@ -3687,7 +3914,7 @@ import { enforcePayloadLimit as enforcePayloadLimit3 } from "@fenglimg/fabric-sh
3687
3914
  // src/services/knowledge-sections.ts
3688
3915
  import { readFile as readFile8 } from "fs/promises";
3689
3916
  import { homedir as homedir4 } from "os";
3690
- import { join as join9 } from "path";
3917
+ import { join as join11 } from "path";
3691
3918
  import { deriveAgentsMetaLayer as deriveAgentsMetaLayer3 } from "@fenglimg/fabric-shared";
3692
3919
  import { McpToolError } from "@fenglimg/fabric-shared/errors";
3693
3920
  var PRIORITY_ORDER = {
@@ -3729,13 +3956,25 @@ async function getKnowledgeSections(projectRoot, input) {
3729
3956
  const { meta } = await loadActiveMeta(projectRoot, { caller: "getKnowledgeSections" });
3730
3957
  const selectedStableIds = [...token.required_stable_ids, ...rewrittenAiSelected];
3731
3958
  const ruleNodeIndex = buildRuleNodeIndex(meta);
3959
+ const storeBodyIndex = buildCrossStoreBodyIndex(projectRoot);
3960
+ const unresolvedSelectedIds = [];
3961
+ const storeSelected = [];
3732
3962
  const selectedRules = sortRuleNodes(
3733
- selectedStableIds.map((stableId) => {
3963
+ selectedStableIds.flatMap((stableId) => {
3734
3964
  const entry = ruleNodeIndex.get(stableId);
3735
3965
  if (entry === void 0) {
3966
+ if (stableId.includes(":")) {
3967
+ const ref = storeBodyIndex.get(stableId);
3968
+ if (ref !== void 0) {
3969
+ storeSelected.push({ stableId, ref });
3970
+ } else {
3971
+ unresolvedSelectedIds.push(stableId);
3972
+ }
3973
+ return [];
3974
+ }
3736
3975
  throw new Error(`Selected rule is not present in agents.meta.json: ${stableId}`);
3737
3976
  }
3738
- return entry;
3977
+ return [entry];
3739
3978
  })
3740
3979
  );
3741
3980
  const diagnostics = [];
@@ -3759,6 +3998,29 @@ async function getKnowledgeSections(projectRoot, input) {
3759
3998
  body
3760
3999
  });
3761
4000
  }
4001
+ for (const { stableId, ref } of storeSelected) {
4002
+ let content;
4003
+ try {
4004
+ content = await readFile8(ref.file, "utf8");
4005
+ } catch {
4006
+ unresolvedSelectedIds.push(stableId);
4007
+ continue;
4008
+ }
4009
+ rules.push({
4010
+ stable_id: stableId,
4011
+ level: "L1",
4012
+ path: ref.file,
4013
+ body: extractBody(content)
4014
+ });
4015
+ }
4016
+ for (const stableId of unresolvedSelectedIds) {
4017
+ diagnostics.push({
4018
+ code: "unresolved_selected_id",
4019
+ severity: "warn",
4020
+ stable_id: stableId,
4021
+ message: `Selected rule '${stableId}' is not present in the project's agents.meta.json or any read-set store \u2014 skipped (deleted, layer-flipped, or its store is not bound).`
4022
+ });
4023
+ }
3762
4024
  const result = {
3763
4025
  revision_hash: meta.revision,
3764
4026
  selected_stable_ids: rules.map((rule) => rule.stable_id),
@@ -3835,12 +4097,6 @@ function validateAiSelections(aiSelectableStableIds, aiSelectedStableIds, aiSele
3835
4097
  `Invalid rule selection "${stableId}": not in this token's plan-context candidates. Pass only stable_ids from fab_plan_context candidates[].stable_id.`
3836
4098
  );
3837
4099
  }
3838
- if (aiSelectionReasons[stableId]?.trim() === "") {
3839
- throw new Error(`Missing AI selection reason for ${stableId}`);
3840
- }
3841
- if (aiSelectionReasons[stableId] === void 0) {
3842
- throw new Error(`Missing AI selection reason for ${stableId}`);
3843
- }
3844
4100
  }
3845
4101
  }
3846
4102
  function buildRuleNodeIndex(meta) {
@@ -3887,9 +4143,9 @@ function outputLevelOrder(level) {
3887
4143
  function resolveRuleSourcePath(projectRoot, contentRef) {
3888
4144
  if (contentRef.startsWith("~/.fabric/knowledge/")) {
3889
4145
  const home = process.env.FABRIC_HOME ?? homedir4();
3890
- return join9(home, ".fabric", "knowledge", contentRef.slice("~/.fabric/knowledge/".length));
4146
+ return join11(home, ".fabric", "knowledge", contentRef.slice("~/.fabric/knowledge/".length));
3891
4147
  }
3892
- return join9(projectRoot, contentRef);
4148
+ return join11(projectRoot, contentRef);
3893
4149
  }
3894
4150
  function pickSelectionReasons(selectedStableIds, reasons) {
3895
4151
  return Object.fromEntries(selectedStableIds.map((stableId) => [stableId, reasons[stableId] ?? ""]));
@@ -3964,12 +4220,20 @@ async function recall(projectRoot, input) {
3964
4220
  });
3965
4221
  return {
3966
4222
  ...planResult,
3967
- rules: sectionsResult.rules,
4223
+ rules: sectionsResult.rules.map(attachStoreProvenance),
3968
4224
  selected_stable_ids: sectionsResult.selected_stable_ids,
3969
4225
  diagnostics: sectionsResult.diagnostics,
3970
4226
  ...packaging
3971
4227
  };
3972
4228
  }
4229
+ function attachStoreProvenance(rule) {
4230
+ const colon = rule.stable_id.indexOf(":");
4231
+ if (colon <= 0) {
4232
+ return rule;
4233
+ }
4234
+ const alias = rule.stable_id.slice(0, colon);
4235
+ return { ...rule, store: { alias } };
4236
+ }
3973
4237
  function buildRecallPackaging(planResult, relatedAvailableNotIncluded) {
3974
4238
  const omitted = planResult.omitted_candidate_count ?? 0;
3975
4239
  const nextSteps = [];
@@ -4009,6 +4273,7 @@ function registerRecall(server, tracker) {
4009
4273
  correlation_id,
4010
4274
  session_id,
4011
4275
  target_paths,
4276
+ layer_filter,
4012
4277
  ids,
4013
4278
  include_related
4014
4279
  }) => {
@@ -4031,6 +4296,9 @@ function registerRecall(server, tracker) {
4031
4296
  correlation_id,
4032
4297
  session_id,
4033
4298
  target_paths,
4299
+ // F54 (ISS-20260531-090): forwarded so recall→planContext honors the
4300
+ // declared layer restriction instead of silently discarding it.
4301
+ layer_filter,
4034
4302
  ids,
4035
4303
  include_related
4036
4304
  };
@@ -4286,19 +4554,19 @@ import { execFileSync } from "child_process";
4286
4554
  import { existsSync as existsSync6 } from "fs";
4287
4555
  import { readFile as readFile10, readdir as readdir3, unlink } from "fs/promises";
4288
4556
  import { homedir as homedir5 } from "os";
4289
- import { basename, join as join10, relative as relative3, resolve as resolve3 } from "path";
4557
+ import { basename, join as join12, relative as relative3, resolve as resolve3, sep as sep3 } from "path";
4290
4558
 
4291
4559
  // src/services/knowledge-id-allocator.ts
4292
4560
  import { readFile as readFile9, writeFile as writeFile2 } from "fs/promises";
4293
4561
  import { dirname as dirname2 } from "path";
4294
- import { mkdir as mkdir4 } from "fs/promises";
4562
+ import { mkdir as mkdir3 } from "fs/promises";
4295
4563
  import {
4296
4564
  AgentsMetaCountersSchema,
4297
4565
  agentsMetaSchema as agentsMetaSchema4,
4298
4566
  allocateKnowledgeId,
4299
4567
  defaultAgentsMetaCounters as defaultAgentsMetaCounters2
4300
4568
  } from "@fenglimg/fabric-shared";
4301
- import { atomicWriteJson as atomicWriteJson2, withFileLock } from "@fenglimg/fabric-shared/node/atomic-write";
4569
+ import { atomicWriteJson as atomicWriteJson2, withFileLock as withFileLock3 } from "@fenglimg/fabric-shared/node/atomic-write";
4302
4570
  import { GenericIOError } from "@fenglimg/fabric-shared/errors";
4303
4571
  var KnowledgeIdAllocator = class {
4304
4572
  constructor(metaPath) {
@@ -4310,7 +4578,7 @@ var KnowledgeIdAllocator = class {
4310
4578
  * the advanced counter to `agents.meta.json`.
4311
4579
  */
4312
4580
  async allocate(layer, type) {
4313
- return withFileLock(`${this.metaPath}.lock`, async () => {
4581
+ return withFileLock3(`${this.metaPath}.lock`, async () => {
4314
4582
  const meta = await this.readMeta();
4315
4583
  const counters2 = this.normalizeCounters(meta.counters);
4316
4584
  const { id, nextCounters } = allocateKnowledgeId(layer, type, counters2);
@@ -4373,20 +4641,13 @@ var KnowledgeIdAllocator = class {
4373
4641
  }
4374
4642
  };
4375
4643
  async function ensureParentDirectory2(filePath) {
4376
- await mkdir4(dirname2(filePath), { recursive: true });
4644
+ await mkdir3(dirname2(filePath), { recursive: true });
4377
4645
  }
4378
4646
  function isNodeError4(err) {
4379
4647
  return err instanceof Error && typeof err.code === "string";
4380
4648
  }
4381
4649
 
4382
4650
  // src/services/review.ts
4383
- var PENDING_BASE_TEAM_REL = ".fabric/knowledge/pending";
4384
- function pendingBaseAbs(layer, projectRoot) {
4385
- if (layer === "personal") {
4386
- return join10(resolvePersonalRoot4(), ".fabric", "knowledge", "pending");
4387
- }
4388
- return join10(projectRoot, PENDING_BASE_TEAM_REL);
4389
- }
4390
4651
  var PLURAL_TYPES = [
4391
4652
  "decisions",
4392
4653
  "pitfalls",
@@ -4444,29 +4705,50 @@ async function reviewKnowledge(projectRoot, input) {
4444
4705
  }
4445
4706
  }
4446
4707
  }
4708
+ function storeKnowledgeRoots(projectRoot) {
4709
+ const roots = [];
4710
+ for (const layer of ["team", "personal"]) {
4711
+ try {
4712
+ roots.push(resolve3(resolveStoreCanonicalBase(layer, projectRoot)));
4713
+ } catch {
4714
+ }
4715
+ }
4716
+ return roots;
4717
+ }
4718
+ function isUnder(abs, root) {
4719
+ return abs === root || abs.startsWith(root + "/");
4720
+ }
4447
4721
  function resolveSandboxedPath(projectRoot, candidate, options = {}) {
4448
4722
  if (candidate.length === 0) {
4449
4723
  throw new Error("path is empty");
4450
4724
  }
4451
4725
  const projectKnowledgeRoot = resolve3(projectRoot, ".fabric", "knowledge");
4452
4726
  const personalKnowledgeRoot = resolve3(resolvePersonalRoot4(), ".fabric", "knowledge");
4727
+ const storeRoots = storeKnowledgeRoots(projectRoot);
4453
4728
  if (candidate.startsWith("~/")) {
4454
4729
  if (options.allowPersonal !== true) {
4455
4730
  throw new Error(`personal-root path not allowed for this action: ${candidate}`);
4456
4731
  }
4457
4732
  const abs = resolve3(resolvePersonalRoot4(), candidate.slice(2));
4458
- if (abs !== personalKnowledgeRoot && !abs.startsWith(personalKnowledgeRoot + "/")) {
4733
+ if (!isUnder(abs, personalKnowledgeRoot)) {
4459
4734
  throw new Error(`path escapes personal knowledge root: ${candidate}`);
4460
4735
  }
4461
4736
  return { abs, isInProjectTree: false };
4462
4737
  }
4738
+ if (candidate.startsWith("/")) {
4739
+ const abs = resolve3(candidate);
4740
+ if (storeRoots.some((root) => isUnder(abs, root))) {
4741
+ return { abs, isInProjectTree: false };
4742
+ }
4743
+ throw new Error(`path escapes knowledge root: ${candidate}`);
4744
+ }
4463
4745
  const projectAbs = resolve3(projectRoot, candidate);
4464
- if (projectAbs === projectKnowledgeRoot || projectAbs.startsWith(projectKnowledgeRoot + "/")) {
4746
+ if (isUnder(projectAbs, projectKnowledgeRoot)) {
4465
4747
  return { abs: projectAbs, isInProjectTree: true };
4466
4748
  }
4467
4749
  if (options.allowPersonal === true) {
4468
4750
  const personalAbs = resolve3(resolvePersonalRoot4(), candidate);
4469
- if (personalAbs === personalKnowledgeRoot || personalAbs.startsWith(personalKnowledgeRoot + "/")) {
4751
+ if (isUnder(personalAbs, personalKnowledgeRoot)) {
4470
4752
  return { abs: personalAbs, isInProjectTree: false };
4471
4753
  }
4472
4754
  }
@@ -4488,13 +4770,24 @@ function isVisibleByLifecycle(fm, filters) {
4488
4770
  async function listPending(projectRoot, filters) {
4489
4771
  const items = [];
4490
4772
  const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
4491
- const sources = [
4492
- { origin: "team", root: pendingBaseAbs("team", projectRoot) },
4493
- { origin: "personal", root: pendingBaseAbs("personal", projectRoot) }
4494
- ];
4773
+ const sources = [];
4774
+ for (const origin of ["team", "personal"]) {
4775
+ try {
4776
+ const pendingRoot = resolveStorePendingBase(origin, projectRoot);
4777
+ sources.push({ origin, root: pendingRoot, isStore: true });
4778
+ if (filters?.include_rejected === true) {
4779
+ sources.push({
4780
+ origin,
4781
+ root: pendingRoot.replace(`${sep3}pending`, `${sep3}rejected`),
4782
+ isStore: true
4783
+ });
4784
+ }
4785
+ } catch {
4786
+ }
4787
+ }
4495
4788
  for (const source of sources) {
4496
4789
  for (const type of typesToScan) {
4497
- const dir = join10(source.root, type);
4790
+ const dir = join12(source.root, type);
4498
4791
  if (!existsSync6(dir)) {
4499
4792
  continue;
4500
4793
  }
@@ -4506,7 +4799,7 @@ async function listPending(projectRoot, filters) {
4506
4799
  }
4507
4800
  for (const name of entries) {
4508
4801
  if (!name.endsWith(".md")) continue;
4509
- const absolutePath = join10(dir, name);
4802
+ const absolutePath = join12(dir, name);
4510
4803
  let content;
4511
4804
  try {
4512
4805
  content = await readFile10(absolutePath, "utf8");
@@ -4536,13 +4829,15 @@ async function listPending(projectRoot, filters) {
4536
4829
  if (!isVisibleByLifecycle(fm, filters)) {
4537
4830
  continue;
4538
4831
  }
4539
- const reportedPath = source.origin === "personal" ? `~/${relative3(resolvePersonalRoot4(), absolutePath)}` : relative3(projectRoot, absolutePath);
4832
+ const reportedPath = source.isStore ? absolutePath : source.origin === "personal" ? `~/${relative3(resolvePersonalRoot4(), absolutePath)}` : relative3(projectRoot, absolutePath);
4540
4833
  items.push({
4541
4834
  pending_path: reportedPath,
4542
4835
  // v2.0.0-rc.27 TASK-001 (§2.12): absolute path companion for
4543
4836
  // personal entries so programmatic consumers (Read, fs.readFile)
4544
- // don't need to shell-expand the `~` themselves.
4545
- ...source.origin === "personal" ? { pending_path_absolute: absolutePath } : {},
4837
+ // don't need to shell-expand the `~` themselves. Store entries
4838
+ // already report an absolute pending_path, so the companion is
4839
+ // emitted for non-store personal entries only.
4840
+ ...source.origin === "personal" && !source.isStore ? { pending_path_absolute: absolutePath } : {},
4546
4841
  type,
4547
4842
  layer,
4548
4843
  maturity,
@@ -4562,7 +4857,7 @@ async function listPending(projectRoot, filters) {
4562
4857
  }
4563
4858
  async function approveAll(projectRoot, pendingPaths) {
4564
4859
  const allocator = new KnowledgeIdAllocator(
4565
- join10(projectRoot, ".fabric", "agents.meta.json")
4860
+ join12(projectRoot, ".fabric", "agents.meta.json")
4566
4861
  );
4567
4862
  const approved = [];
4568
4863
  for (const pendingPath of pendingPaths) {
@@ -4576,17 +4871,26 @@ async function approveAll(projectRoot, pendingPaths) {
4576
4871
  async function approveOne(projectRoot, pendingPath, allocator) {
4577
4872
  let sourceAbs;
4578
4873
  let sourceOrigin;
4874
+ let sourceIsStore = false;
4579
4875
  try {
4580
4876
  const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
4581
- const teamPendingAbs = pendingBaseAbs("team", projectRoot);
4582
- const personalPendingAbs = pendingBaseAbs("personal", projectRoot);
4583
- const inTeamPending = sandboxed.abs === teamPendingAbs || sandboxed.abs.startsWith(teamPendingAbs + "/");
4584
- const inPersonalPending = sandboxed.abs === personalPendingAbs || sandboxed.abs.startsWith(personalPendingAbs + "/");
4877
+ const resolvePendingBaseOrNull = (layer) => {
4878
+ try {
4879
+ return resolveStorePendingBase(layer, projectRoot);
4880
+ } catch {
4881
+ return null;
4882
+ }
4883
+ };
4884
+ const teamPendingAbs = resolvePendingBaseOrNull("team");
4885
+ const personalPendingAbs = resolvePendingBaseOrNull("personal");
4886
+ const inTeamPending = teamPendingAbs !== null && (sandboxed.abs === teamPendingAbs || sandboxed.abs.startsWith(teamPendingAbs + "/"));
4887
+ const inPersonalPending = personalPendingAbs !== null && (sandboxed.abs === personalPendingAbs || sandboxed.abs.startsWith(personalPendingAbs + "/"));
4585
4888
  if (!inTeamPending && !inPersonalPending) {
4586
4889
  throw new Error(`approve path is outside .fabric/knowledge/pending/: ${pendingPath}`);
4587
4890
  }
4588
4891
  sourceAbs = sandboxed.abs;
4589
4892
  sourceOrigin = inPersonalPending ? "personal" : "team";
4893
+ sourceIsStore = true;
4590
4894
  } catch (err) {
4591
4895
  const reason = err instanceof Error ? err.message : String(err);
4592
4896
  await emitEventBestEffort2(projectRoot, {
@@ -4621,13 +4925,16 @@ async function approveOne(projectRoot, pendingPath, allocator) {
4621
4925
  const stableId = await allocator.allocate(layer, pluralType);
4622
4926
  allocatedId = stableId;
4623
4927
  const newFilename = `${stableId}--${slug}.md`;
4624
- const layerRoot = layer === "personal" ? join10(resolvePersonalRoot4(), ".fabric") : join10(projectRoot, ".fabric");
4625
- targetAbs = join10(layerRoot, "knowledge", pluralType, newFilename);
4928
+ targetAbs = join12(resolveStoreCanonicalBase(layer, projectRoot), pluralType, newFilename);
4626
4929
  await ensureParentDirectory(targetAbs);
4627
4930
  const rewritten = rewriteFrontmatterForPromote(content, stableId);
4628
4931
  await atomicWriteText(targetAbs, rewritten);
4629
4932
  writtenTarget = true;
4630
- if (sourceOrigin === "team") {
4933
+ if (sourceIsStore) {
4934
+ if (existsSync6(sourceAbs)) {
4935
+ await unlink(sourceAbs);
4936
+ }
4937
+ } else if (sourceOrigin === "team") {
4631
4938
  try {
4632
4939
  execFileSync("git", ["rm", "--quiet", "-f", pendingPath], {
4633
4940
  cwd: projectRoot,
@@ -4679,7 +4986,12 @@ async function rejectAll(projectRoot, pendingPaths, reason) {
4679
4986
  if (existsSync6(sandboxed.abs)) {
4680
4987
  const content = await readFile10(sandboxed.abs, "utf8");
4681
4988
  const merged = rewriteFrontmatterMerge(content, { status: "rejected" });
4682
- if (merged !== content) {
4989
+ const rejectedAbs = sandboxed.abs.includes(`${sep3}pending${sep3}`) ? sandboxed.abs.replace(`${sep3}pending${sep3}`, `${sep3}rejected${sep3}`) : null;
4990
+ if (rejectedAbs !== null) {
4991
+ await ensureParentDirectory(rejectedAbs);
4992
+ await atomicWriteText(rejectedAbs, merged);
4993
+ await unlink(sandboxed.abs);
4994
+ } else if (merged !== content) {
4683
4995
  await atomicWriteText(sandboxed.abs, merged);
4684
4996
  }
4685
4997
  }
@@ -4759,11 +5071,14 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
4759
5071
  const fromScope = fm.relevance_scope ?? "broad";
4760
5072
  const shouldAutoDegrade = fromScope === "narrow" && fromLayer === "team" && toLayer === "personal";
4761
5073
  const allocator = new KnowledgeIdAllocator(
4762
- join10(projectRoot, ".fabric", "agents.meta.json")
5074
+ join12(projectRoot, ".fabric", "agents.meta.json")
4763
5075
  );
4764
5076
  const newStableId = await allocator.allocate(toLayer, pluralType);
4765
- const toRoot = toLayer === "personal" ? join10(resolvePersonalRoot4(), ".fabric") : join10(projectRoot, ".fabric");
4766
- const toAbs = join10(toRoot, "knowledge", pluralType, `${newStableId}--${slug}.md`);
5077
+ const toAbs = join12(
5078
+ resolveStoreCanonicalBase(toLayer, projectRoot),
5079
+ pluralType,
5080
+ `${newStableId}--${slug}.md`
5081
+ );
4767
5082
  await ensureParentDirectory(toAbs);
4768
5083
  await emitEventBestEffort2(projectRoot, {
4769
5084
  event_type: "knowledge_promote_started",
@@ -4842,16 +5157,22 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
4842
5157
  async function searchEntries(projectRoot, query, filters) {
4843
5158
  const lowerQuery = query.toLowerCase();
4844
5159
  const items = [];
4845
- const sources = [
4846
- { root: pendingBaseAbs("team", projectRoot), isPending: true, isPersonal: false },
4847
- { root: pendingBaseAbs("personal", projectRoot), isPending: true, isPersonal: true },
4848
- { root: join10(projectRoot, ".fabric", "knowledge"), isPending: false, isPersonal: false },
4849
- { root: join10(resolvePersonalRoot4(), ".fabric", "knowledge"), isPending: false, isPersonal: true }
4850
- ];
5160
+ const sources = [];
5161
+ for (const layer of ["team", "personal"]) {
5162
+ const isPersonal = layer === "personal";
5163
+ try {
5164
+ sources.push({ root: resolveStorePendingBase(layer, projectRoot), isPending: true, isPersonal, isStore: true });
5165
+ } catch {
5166
+ }
5167
+ try {
5168
+ sources.push({ root: resolveStoreCanonicalBase(layer, projectRoot), isPending: false, isPersonal, isStore: true });
5169
+ } catch {
5170
+ }
5171
+ }
4851
5172
  const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
4852
5173
  for (const source of sources) {
4853
5174
  for (const type of typesToScan) {
4854
- const dir = join10(source.root, type);
5175
+ const dir = join12(source.root, type);
4855
5176
  if (!existsSync6(dir)) continue;
4856
5177
  let entries;
4857
5178
  try {
@@ -4861,7 +5182,7 @@ async function searchEntries(projectRoot, query, filters) {
4861
5182
  }
4862
5183
  for (const name of entries) {
4863
5184
  if (!name.endsWith(".md")) continue;
4864
- const absolutePath = join10(dir, name);
5185
+ const absolutePath = join12(dir, name);
4865
5186
  let content;
4866
5187
  try {
4867
5188
  content = await readFile10(absolutePath, "utf8");
@@ -4901,7 +5222,7 @@ async function searchEntries(projectRoot, query, filters) {
4901
5222
  ].map((s) => s.toLowerCase());
4902
5223
  const matches = haystacks.some((h) => h.includes(lowerQuery));
4903
5224
  if (!matches) continue;
4904
- const reportedPath = source.isPersonal ? `~/${relative3(resolvePersonalRoot4(), absolutePath)}` : relative3(projectRoot, absolutePath);
5225
+ const reportedPath = source.isStore ? absolutePath : source.isPersonal ? `~/${relative3(resolvePersonalRoot4(), absolutePath)}` : relative3(projectRoot, absolutePath);
4905
5226
  items.push({
4906
5227
  area: source.isPending ? "pending" : "canonical",
4907
5228
  path: reportedPath,
@@ -4970,10 +5291,10 @@ function parseFrontmatter(content) {
4970
5291
  for (const rawLine of block.split(/\r?\n/u)) {
4971
5292
  const line = rawLine.trim();
4972
5293
  if (line.length === 0) continue;
4973
- const sep4 = line.indexOf(":");
4974
- if (sep4 === -1) continue;
4975
- const key = line.slice(0, sep4).trim();
4976
- const value = line.slice(sep4 + 1).trim();
5294
+ const sep5 = line.indexOf(":");
5295
+ if (sep5 === -1) continue;
5296
+ const key = line.slice(0, sep5).trim();
5297
+ const value = line.slice(sep5 + 1).trim();
4977
5298
  switch (key) {
4978
5299
  case "id":
4979
5300
  out.id = stripQuotes(value);
@@ -5082,17 +5403,17 @@ ${content}`;
5082
5403
  if (patch.summary !== void 0) updates.summary = `summary: ${quoteIfNeeded(patch.summary)}`;
5083
5404
  if (patch.layer !== void 0) updates.layer = `layer: ${patch.layer}`;
5084
5405
  if (patch.maturity !== void 0) updates.maturity = `maturity: ${patch.maturity}`;
5085
- if (patch.tags !== void 0) updates.tags = `tags: [${patch.tags.join(", ")}]`;
5406
+ if (patch.tags !== void 0) updates.tags = `tags: ${flowArray(patch.tags)}`;
5086
5407
  if (patch.relevance_scope !== void 0) updates.relevance_scope = `relevance_scope: ${patch.relevance_scope}`;
5087
- if (patch.relevance_paths !== void 0) updates.relevance_paths = `relevance_paths: [${patch.relevance_paths.join(", ")}]`;
5408
+ if (patch.relevance_paths !== void 0) updates.relevance_paths = `relevance_paths: ${flowArray(patch.relevance_paths)}`;
5088
5409
  if (patch.status !== void 0) updates.status = `status: ${patch.status}`;
5089
5410
  if (patch.deferred_until !== void 0) updates.deferred_until = `deferred_until: ${quoteIfNeeded(patch.deferred_until)}`;
5090
5411
  const lines = block.split(/\r?\n/u);
5091
5412
  const seen = /* @__PURE__ */ new Set();
5092
5413
  const newLines = [];
5093
5414
  for (const line of lines) {
5094
- const sep4 = line.indexOf(":");
5095
- const key = sep4 === -1 ? "" : line.slice(0, sep4).trim();
5415
+ const sep5 = line.indexOf(":");
5416
+ const key = sep5 === -1 ? "" : line.slice(0, sep5).trim();
5096
5417
  if (key in updates) {
5097
5418
  newLines.push(updates[key]);
5098
5419
  seen.add(key);
@@ -5117,18 +5438,27 @@ function appendPatchLines(lines, patch) {
5117
5438
  if (patch.summary !== void 0) lines.push(`summary: ${quoteIfNeeded(patch.summary)}`);
5118
5439
  if (patch.layer !== void 0) lines.push(`layer: ${patch.layer}`);
5119
5440
  if (patch.maturity !== void 0) lines.push(`maturity: ${patch.maturity}`);
5120
- if (patch.tags !== void 0) lines.push(`tags: [${patch.tags.join(", ")}]`);
5441
+ if (patch.tags !== void 0) lines.push(`tags: ${flowArray(patch.tags)}`);
5121
5442
  if (patch.relevance_scope !== void 0) lines.push(`relevance_scope: ${patch.relevance_scope}`);
5122
- if (patch.relevance_paths !== void 0) lines.push(`relevance_paths: [${patch.relevance_paths.join(", ")}]`);
5443
+ if (patch.relevance_paths !== void 0) lines.push(`relevance_paths: ${flowArray(patch.relevance_paths)}`);
5123
5444
  if (patch.status !== void 0) lines.push(`status: ${patch.status}`);
5124
5445
  if (patch.deferred_until !== void 0) lines.push(`deferred_until: ${quoteIfNeeded(patch.deferred_until)}`);
5125
5446
  }
5447
+ function flowArrayElement(value) {
5448
+ if (/[\n\r,\[\]{}"#:]/u.test(value) || /^\s|\s$/u.test(value)) {
5449
+ return JSON.stringify(value);
5450
+ }
5451
+ return value;
5452
+ }
5453
+ function flowArray(values) {
5454
+ return `[${values.map(flowArrayElement).join(", ")}]`;
5455
+ }
5126
5456
  function quoteIfNeeded(value) {
5127
5457
  if (/[\n\r]/u.test(value)) {
5128
5458
  return JSON.stringify(value);
5129
5459
  }
5130
- if (/[:#\[\]{}&*!|>'"%@`,]|^\s|\s$/u.test(value)) {
5131
- return `"${value.replace(/"/gu, '\\"')}"`;
5460
+ if (/[\\:#\[\]{}&*!|>'"%@`,]|^\s|\s$/u.test(value)) {
5461
+ return `"${value.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"')}"`;
5132
5462
  }
5133
5463
  return value;
5134
5464
  }
@@ -5242,11 +5572,11 @@ function registerKnowledgeSections(server, tracker) {
5242
5572
 
5243
5573
  // src/services/doctor.ts
5244
5574
  import { execFileSync as execFileSync2, spawnSync } from "child_process";
5245
- import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync3, statSync as statSync4 } from "fs";
5246
- import { access as access2, mkdir as mkdir6, readFile as readFile13, rename as rename2, unlink as unlink3, writeFile as writeFile4 } from "fs/promises";
5575
+ import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync4, statSync as statSync4 } from "fs";
5576
+ import { access as access2, mkdir as mkdir5, readFile as readFile13, rename as rename2, unlink as unlink3, writeFile as writeFile4 } from "fs/promises";
5247
5577
  import { constants } from "fs";
5248
5578
  import { homedir as homedir6 } from "os";
5249
- import { isAbsolute as isAbsolute2, join as join11, posix, relative as nodeRelative, resolve as resolve4, sep as sep3 } from "path";
5579
+ import { isAbsolute as isAbsolute2, join as join13, posix, relative as nodeRelative, resolve as resolve4, sep as sep4 } from "path";
5250
5580
  import { Script } from "vm";
5251
5581
  import { minimatch as minimatch3 } from "minimatch";
5252
5582
  import { ZodError } from "zod";
@@ -5271,7 +5601,7 @@ import {
5271
5601
  PAYLOAD_LIMIT_DEFAULT_HARD_BYTES,
5272
5602
  PAYLOAD_LIMIT_DEFAULT_WARN_BYTES
5273
5603
  } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
5274
- import { atomicWriteJson as atomicWriteJson3, atomicWriteText as atomicWriteText4 } from "@fenglimg/fabric-shared/node/atomic-write";
5604
+ import { atomicWriteJson as atomicWriteJson3, atomicWriteText as atomicWriteText4, withFileLock as withFileLock4 } from "@fenglimg/fabric-shared/node/atomic-write";
5275
5605
 
5276
5606
  // src/services/legacy-serve-lock-probe.ts
5277
5607
  import fs from "fs";
@@ -5502,6 +5832,54 @@ function matchesRelevancePath(editPath, relevancePaths) {
5502
5832
  }
5503
5833
  return false;
5504
5834
  }
5835
+ function recallPathOverlaps(editPath, recallPaths) {
5836
+ if (recallPaths.length === 0) return false;
5837
+ const e = normalizePath(editPath);
5838
+ if (e.length === 0) return false;
5839
+ for (const rp of recallPaths) {
5840
+ const r = normalizePath(rp);
5841
+ if (r.length === 0) continue;
5842
+ if (e === r) return true;
5843
+ if (e.endsWith("/" + r) || r.endsWith("/" + e)) return true;
5844
+ if (e.startsWith(r + "/") || r.startsWith(e + "/")) return true;
5845
+ }
5846
+ return false;
5847
+ }
5848
+ function isSpecificGlob(glob) {
5849
+ const g = glob.trim();
5850
+ if (g.length === 0) return false;
5851
+ return g !== "**/*" && g !== "**" && g !== "*";
5852
+ }
5853
+ function computeExposedAndMutated(args) {
5854
+ const { narrowSurfacedBySession, dismissedBySession, editPathsBySession, kbIndex, idTypeMap } = args;
5855
+ const qualifiedIds = /* @__PURE__ */ new Set();
5856
+ let count = 0;
5857
+ for (const [sessionId, surfacedIds] of narrowSurfacedBySession) {
5858
+ const editPaths = editPathsBySession.get(sessionId);
5859
+ if (editPaths === void 0 || editPaths.length === 0) continue;
5860
+ const dismissed = dismissedBySession.get(sessionId);
5861
+ for (const id of surfacedIds) {
5862
+ if (dismissed !== void 0 && dismissed.has(id)) continue;
5863
+ const kb = kbIndex.get(id);
5864
+ if (kb === void 0 || kb.relevance_scope !== "narrow") continue;
5865
+ const specificGlobs = kb.relevance_paths.filter(isSpecificGlob);
5866
+ if (specificGlobs.length === 0) continue;
5867
+ const type = idTypeMap.get(id);
5868
+ if (type === "guidelines" || type === "processes") continue;
5869
+ let mutated = false;
5870
+ for (const p of editPaths) {
5871
+ if (matchesRelevancePath(p, specificGlobs)) {
5872
+ mutated = true;
5873
+ break;
5874
+ }
5875
+ }
5876
+ if (!mutated) continue;
5877
+ count += 1;
5878
+ qualifiedIds.add(id);
5879
+ }
5880
+ }
5881
+ return { count, ids: [...qualifiedIds].sort() };
5882
+ }
5505
5883
  var ASSISTANT_TURN_COUNTER_PREFIX = "assistant_turn_observed";
5506
5884
  function sumFoldedTurnCounters(rows, options) {
5507
5885
  let sum = 0;
@@ -5525,6 +5903,7 @@ function sumFoldedTurnCounters(rows, options) {
5525
5903
  }
5526
5904
  async function runDoctorCiteCoverage(projectRoot, options) {
5527
5905
  const layerFilter = options.layer ?? "all";
5906
+ const recallWindowMs = typeof options.recallWindowMs === "number" && options.recallWindowMs >= 0 ? options.recallWindowMs : 30 * 6e4;
5528
5907
  const marker = await ensureCitePolicyActivatedMarker(projectRoot);
5529
5908
  const contractMarker = await ensureCiteContractPolicyActivatedMarker(projectRoot);
5530
5909
  const idTypeMap = await loadKbIdTypeMap(projectRoot);
@@ -5591,6 +5970,10 @@ async function runDoctorCiteCoverage(projectRoot, options) {
5591
5970
  const assistantTurns = [];
5592
5971
  const editEvents = [];
5593
5972
  const fetchEvents = [];
5973
+ const plannedEvents = [];
5974
+ const hookSurfaceEvents = [];
5975
+ const fileMutatedEvents = [];
5976
+ const sessionEndedEvents = [];
5594
5977
  for (const event of ledgerEvents) {
5595
5978
  switch (event.event_type) {
5596
5979
  case "assistant_turn_observed":
@@ -5602,10 +5985,33 @@ async function runDoctorCiteCoverage(projectRoot, options) {
5602
5985
  case "knowledge_sections_fetched":
5603
5986
  fetchEvents.push(event);
5604
5987
  break;
5988
+ case "knowledge_context_planned":
5989
+ plannedEvents.push(event);
5990
+ break;
5991
+ case "hook_surface_emitted":
5992
+ hookSurfaceEvents.push(event);
5993
+ break;
5994
+ case "file_mutated":
5995
+ fileMutatedEvents.push(event);
5996
+ break;
5997
+ case "session_ended":
5998
+ sessionEndedEvents.push(event);
5999
+ break;
5605
6000
  default:
5606
6001
  break;
5607
6002
  }
5608
6003
  }
6004
+ const plannedBySession = /* @__PURE__ */ new Map();
6005
+ for (const planned of plannedEvents) {
6006
+ const sid = planned.session_id;
6007
+ if (typeof sid !== "string" || sid.length === 0) continue;
6008
+ const list = plannedBySession.get(sid) ?? [];
6009
+ list.push({ ts: planned.ts, target_paths: planned.target_paths ?? [] });
6010
+ plannedBySession.set(sid, list);
6011
+ }
6012
+ for (const list of plannedBySession.values()) {
6013
+ list.sort((a, b) => a.ts - b.ts);
6014
+ }
5609
6015
  const filteredTurns = options.client === "all" ? assistantTurns : assistantTurns.filter((t) => t.client === options.client);
5610
6016
  let clientSessionIds = null;
5611
6017
  if (options.client !== "all") {
@@ -5677,6 +6083,7 @@ async function runDoctorCiteCoverage(projectRoot, options) {
5677
6083
  perClientAccum.set(client, existing);
5678
6084
  };
5679
6085
  const sessionCitedKbs = /* @__PURE__ */ new Map();
6086
+ const dismissedBySession = /* @__PURE__ */ new Map();
5680
6087
  const sessionEditPaths = /* @__PURE__ */ new Map();
5681
6088
  for (const edit of editEvents) {
5682
6089
  const sid = edit.session_id;
@@ -5754,6 +6161,8 @@ async function runDoctorCiteCoverage(projectRoot, options) {
5754
6161
  let totalTurns = 0;
5755
6162
  let qualifyingCites = 0;
5756
6163
  let recalledUnverified = 0;
6164
+ const STORE_LOCAL_KEY = "local";
6165
+ const byStoreQualifying = {};
5757
6166
  for (const turn of filteredTurns) {
5758
6167
  totalTurns += 1;
5759
6168
  bumpClient(turn.client, (m) => {
@@ -5766,6 +6175,15 @@ async function runDoctorCiteCoverage(projectRoot, options) {
5766
6175
  set.add(id);
5767
6176
  }
5768
6177
  sessionCitedKbs.set(sid, set);
6178
+ for (let i = 0; i < turn.cite_tags.length; i += 1) {
6179
+ const id = turn.cite_ids[i];
6180
+ if (typeof id !== "string" || id.length === 0) continue;
6181
+ if (categorizeCiteTag(turn.cite_tags[i]).category === "dismissed") {
6182
+ const dset = dismissedBySession.get(sid) ?? /* @__PURE__ */ new Set();
6183
+ dset.add(id);
6184
+ dismissedBySession.set(sid, dset);
6185
+ }
6186
+ }
5769
6187
  }
5770
6188
  let turnHadApplied = false;
5771
6189
  for (const tag of turn.cite_tags) {
@@ -5792,6 +6210,14 @@ async function runDoctorCiteCoverage(projectRoot, options) {
5792
6210
  break;
5793
6211
  }
5794
6212
  }
6213
+ const turnCiteStores = turn.cite_stores ?? [];
6214
+ for (let i = 0; i < turn.cite_ids.length; i += 1) {
6215
+ const tag = turn.cite_tags[i];
6216
+ if (categorizeCiteTag(typeof tag === "string" ? tag : "none").category !== "applied") continue;
6217
+ const rawStore = turnCiteStores[i];
6218
+ const storeKey = typeof rawStore === "string" && rawStore.length > 0 ? rawStore : STORE_LOCAL_KEY;
6219
+ byStoreQualifying[storeKey] = (byStoreQualifying[storeKey] ?? 0) + 1;
6220
+ }
5795
6221
  if (turnHadApplied && !isRecallVerified(turn)) {
5796
6222
  recalledUnverified += 1;
5797
6223
  bumpClient(turn.client, (m) => {
@@ -5836,6 +6262,7 @@ async function runDoctorCiteCoverage(projectRoot, options) {
5836
6262
  let editsTouched = 0;
5837
6263
  let expectedButMissed = 0;
5838
6264
  let uncorrelatableEdits = 0;
6265
+ let recallBackedEdits = 0;
5839
6266
  for (const edit of editEvents) {
5840
6267
  const sid = edit.session_id;
5841
6268
  const hasSid = typeof sid === "string" && sid.length > 0;
@@ -5846,6 +6273,17 @@ async function runDoctorCiteCoverage(projectRoot, options) {
5846
6273
  }
5847
6274
  editsTouched += 1;
5848
6275
  if (!hasSid) continue;
6276
+ const recalls = plannedBySession.get(sid);
6277
+ if (recalls !== void 0) {
6278
+ for (const recall2 of recalls) {
6279
+ if (recall2.ts > edit.ts) break;
6280
+ if (recallWindowMs > 0 && edit.ts - recall2.ts > recallWindowMs) continue;
6281
+ if (recallPathOverlaps(edit.path, recall2.target_paths)) {
6282
+ recallBackedEdits += 1;
6283
+ break;
6284
+ }
6285
+ }
6286
+ }
5849
6287
  const citedSet = sessionCitedKbs.get(sid) ?? /* @__PURE__ */ new Set();
5850
6288
  for (const [kbId, kb] of kbIndex) {
5851
6289
  if (kb.relevance_scope !== "narrow") continue;
@@ -5860,6 +6298,56 @@ async function runDoctorCiteCoverage(projectRoot, options) {
5860
6298
  const noncompliantCites = expectedButMissed;
5861
6299
  const complianceDenom = compliantCites + noncompliantCites;
5862
6300
  const citeComplianceRate = complianceDenom > 0 ? compliantCites / complianceDenom : null;
6301
+ const recallCoverageRate = editsTouched > 0 ? recallBackedEdits / editsTouched : null;
6302
+ const narrowSurfacedBySession = /* @__PURE__ */ new Map();
6303
+ for (const surface of hookSurfaceEvents) {
6304
+ if (surface.hook_name !== "knowledge-hint-narrow") continue;
6305
+ if (surface.delivery_status !== "delivered") continue;
6306
+ const sid = surface.session_id;
6307
+ if (typeof sid !== "string" || sid.length === 0) continue;
6308
+ const set = narrowSurfacedBySession.get(sid) ?? /* @__PURE__ */ new Set();
6309
+ for (const id of surface.rendered_ids) {
6310
+ if (typeof id === "string" && id.length > 0) set.add(id);
6311
+ }
6312
+ narrowSurfacedBySession.set(sid, set);
6313
+ }
6314
+ const exposedAndMutated = computeExposedAndMutated({
6315
+ narrowSurfacedBySession,
6316
+ dismissedBySession,
6317
+ editPathsBySession: sessionEditPaths,
6318
+ kbIndex,
6319
+ idTypeMap
6320
+ });
6321
+ const surfacedIdsByEvent = /* @__PURE__ */ new Map();
6322
+ for (const surface of hookSurfaceEvents) {
6323
+ surfacedIdsByEvent.set(surface.id, surface.rendered_ids);
6324
+ }
6325
+ const seenMutationKeys = /* @__PURE__ */ new Set();
6326
+ const attributionKeys = /* @__PURE__ */ new Set();
6327
+ let mutationsObserved = 0;
6328
+ let unattributedWorkspaceDirty = 0;
6329
+ for (const mutation of fileMutatedEvents) {
6330
+ if (seenMutationKeys.has(mutation.tool_call_id)) continue;
6331
+ seenMutationKeys.add(mutation.tool_call_id);
6332
+ mutationsObserved += 1;
6333
+ const sourceEventId = mutation.source_event_id;
6334
+ const surfacedRenderedIds = typeof sourceEventId === "string" && sourceEventId.length > 0 ? surfacedIdsByEvent.get(sourceEventId) : void 0;
6335
+ if (surfacedRenderedIds === void 0 || surfacedRenderedIds.length === 0) {
6336
+ unattributedWorkspaceDirty += 1;
6337
+ continue;
6338
+ }
6339
+ const storeId = mutation.store_id ?? "";
6340
+ for (const stableId of surfacedRenderedIds) {
6341
+ if (typeof stableId !== "string" || stableId.length === 0) continue;
6342
+ attributionKeys.add(`${storeId}|${stableId}|${sourceEventId}`);
6343
+ }
6344
+ }
6345
+ const mutationPoolAttributed = attributionKeys.size;
6346
+ const closedSessions = /* @__PURE__ */ new Set();
6347
+ for (const ended of sessionEndedEvents) {
6348
+ const sid = typeof ended.session_id === "string" && ended.session_id.length > 0 ? ended.session_id : ended.id;
6349
+ closedSessions.add(sid);
6350
+ }
5863
6351
  const metrics = {
5864
6352
  edits_touched: editsTouched,
5865
6353
  qualifying_cites: qualifyingCites,
@@ -5869,7 +6357,29 @@ async function runDoctorCiteCoverage(projectRoot, options) {
5869
6357
  cite_compliance_rate: citeComplianceRate,
5870
6358
  compliant_cites: compliantCites,
5871
6359
  noncompliant_cites: noncompliantCites,
5872
- uncorrelatable_edits: uncorrelatableEdits
6360
+ uncorrelatable_edits: uncorrelatableEdits,
6361
+ recall_backed_edits: recallBackedEdits,
6362
+ recall_coverage_rate: recallCoverageRate,
6363
+ exposed_and_mutated: {
6364
+ count: exposedAndMutated.count,
6365
+ ...exposedAndMutated.ids.length > 0 ? { ids: exposedAndMutated.ids } : {}
6366
+ },
6367
+ // lifecycle-refactor W2-T4: PostToolUse mutation funnel (own fields, NEVER
6368
+ // folded into cite_compliance_rate — honesty 铁律).
6369
+ mutations_observed: { count: mutationsObserved },
6370
+ mutation_pool: {
6371
+ attributed: mutationPoolAttributed,
6372
+ unattributed_workspace_dirty: unattributedWorkspaceDirty
6373
+ },
6374
+ sessions_closed: { count: closedSessions.size },
6375
+ // lifecycle-refactor W3-T4 (§2 store 轴): per-store qualifying-cite breakdown.
6376
+ // Diagnostic split of qualifying_cites only — never touches compliance.
6377
+ // Omitted when no cite was observed (empty map → no field).
6378
+ ...Object.keys(byStoreQualifying).length > 0 ? {
6379
+ by_store: Object.fromEntries(
6380
+ Object.entries(byStoreQualifying).map(([store, count]) => [store, { qualifying_cites: count }])
6381
+ )
6382
+ } : {}
5873
6383
  };
5874
6384
  let rollupDaysMerged = 0;
5875
6385
  let rollupTrend;
@@ -5893,9 +6403,11 @@ async function runDoctorCiteCoverage(projectRoot, options) {
5893
6403
  metrics.compliant_cites = (metrics.compliant_cites ?? 0) + (r.metrics.compliant_cites ?? 0);
5894
6404
  metrics.noncompliant_cites = (metrics.noncompliant_cites ?? 0) + (r.metrics.noncompliant_cites ?? 0);
5895
6405
  metrics.uncorrelatable_edits = (metrics.uncorrelatable_edits ?? 0) + (r.metrics.uncorrelatable_edits ?? 0);
6406
+ metrics.recall_backed_edits = (metrics.recall_backed_edits ?? 0) + (r.metrics.recall_backed_edits ?? 0);
5896
6407
  }
5897
6408
  const mergedDenom = (metrics.compliant_cites ?? 0) + (metrics.noncompliant_cites ?? 0);
5898
6409
  metrics.cite_compliance_rate = mergedDenom > 0 ? (metrics.compliant_cites ?? 0) / mergedDenom : null;
6410
+ metrics.recall_coverage_rate = metrics.edits_touched > 0 ? (metrics.recall_backed_edits ?? 0) / metrics.edits_touched : null;
5899
6411
  }
5900
6412
  try {
5901
6413
  const metricsRows = await readMetrics(projectRoot);
@@ -6223,7 +6735,15 @@ var HINT_SILENCE_COUNTER_FILE_REL = posix.join(
6223
6735
  "hint-silence-counter"
6224
6736
  );
6225
6737
  var MS_PER_DAY = 24 * 60 * 60 * 1e3;
6226
- var MATURITY_LINE_PATTERN = /^maturity:\s*("?)(stable|endorsed|draft)\1\s*$/mu;
6738
+ var MATURITY_LINE_PATTERN = /^maturity:\s*("?)(stable|endorsed|draft|verified|proven)\1\s*$/mu;
6739
+ var CANONICAL_TO_LINT_MATURITY = {
6740
+ proven: "stable",
6741
+ verified: "endorsed",
6742
+ draft: "draft",
6743
+ // legacy values pass through unchanged.
6744
+ stable: "stable",
6745
+ endorsed: "endorsed"
6746
+ };
6227
6747
  var CREATED_AT_LINE_PATTERN = /^created_at:\s*("?)([^"\n]+)\1\s*$/mu;
6228
6748
  var TAGS_LINE_PATTERN = /^tags:\s*\[(.*)\]\s*$/mu;
6229
6749
  var RELEVANCE_SCOPE_LINE_PATTERN = /^relevance_scope:\s*("?)(narrow|broad)\1\s*$/mu;
@@ -6479,7 +6999,13 @@ async function runDoctorReport(target) {
6479
6999
  // werewolf-eval failure mode where description.summary == stable_id so
6480
7000
  // hint output is "KT-PIT-0001 · KT-PIT-0001" (AI skips fetch). Built
6481
7001
  // from the same MetaInspection so no extra disk reads.
6482
- createKnowledgeSummaryOpaqueCheck(t, inspectKnowledgeSummaryOpaque(meta)),
7002
+ createKnowledgeSummaryOpaqueCheck(
7003
+ t,
7004
+ // v2.2 全砍 F10: also scan the read-set stores (team + personal) so opaque
7005
+ // store summaries — the personal layer the dogfood flagged — are caught,
7006
+ // not just the project agents.meta entries.
7007
+ inspectKnowledgeSummaryOpaque(meta, collectStoreKnowledgeSummaries(target))
7008
+ ),
6483
7009
  // rc.31 BUG-G2/G5: promote-ledger invariant. Sits adjacent to onboard
6484
7010
  // coverage — both are observability advisories built off events.jsonl.
6485
7011
  ...promoteLedgerInvariant === null ? [] : [createPromoteLedgerInvariantCheck(t, promoteLedgerInvariant)],
@@ -6523,7 +7049,7 @@ async function runDoctorReport(target) {
6523
7049
  warningCount: warnings.length,
6524
7050
  infoCount: infos.length,
6525
7051
  targetFiles: Object.fromEntries(
6526
- TARGET_FILE_PATHS.map((path2) => [path2, existsSync8(join11(projectRoot, path2))])
7052
+ TARGET_FILE_PATHS.map((path2) => [path2, existsSync8(join13(projectRoot, path2))])
6527
7053
  ),
6528
7054
  // v2.0.0-rc.29 TASK-008 (BUG-F2): resolve and surface payload thresholds.
6529
7055
  // Best-effort: a corrupt fabric.config.json should not fail doctor; on
@@ -6568,7 +7094,7 @@ async function runDoctorFix(target) {
6568
7094
  }
6569
7095
  }
6570
7096
  if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
6571
- const snapshotPath = join11(projectRoot, ".fabric", "AGENTS.md");
7097
+ const snapshotPath = join13(projectRoot, ".fabric", "AGENTS.md");
6572
7098
  await ensureParentDirectory(snapshotPath);
6573
7099
  await atomicWriteText4(snapshotPath, BOOTSTRAP_CANONICAL);
6574
7100
  fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
@@ -6663,6 +7189,10 @@ async function runDoctorFix(target) {
6663
7189
  path: rotateResult.archivePath
6664
7190
  });
6665
7191
  }
7192
+ try {
7193
+ await flushMetrics(projectRoot);
7194
+ } catch {
7195
+ }
6666
7196
  if (before.fixable_errors.some((issue) => issue.code === "mcp_config_in_wrong_file")) {
6667
7197
  await fixMcpConfigInWrongFile(projectRoot);
6668
7198
  fixed.push(findIssue(before.fixable_errors, "mcp_config_in_wrong_file"));
@@ -6670,7 +7200,7 @@ async function runDoctorFix(target) {
6670
7200
  if (before.infos.some((issue) => issue.code === "stale_serve_lock")) {
6671
7201
  const lockInspection = inspectStaleServeLock(projectRoot, Date.now());
6672
7202
  if (lockInspection.present && !lockInspection.pidAlive) {
6673
- const lockFilePath = join11(projectRoot, ".fabric", ".serve.lock");
7203
+ const lockFilePath = join13(projectRoot, ".fabric", ".serve.lock");
6674
7204
  try {
6675
7205
  await unlink3(lockFilePath);
6676
7206
  } catch (err) {
@@ -6812,6 +7342,10 @@ function createApplyLintMessage(succeeded, failed, manualErrorCount) {
6812
7342
  );
6813
7343
  return parts.join(" ");
6814
7344
  }
7345
+ var LINT_TO_CANONICAL_MATURITY = {
7346
+ endorsed: "verified",
7347
+ draft: "draft"
7348
+ };
6815
7349
  function rewriteFrontmatterMaturity(source, newMaturity) {
6816
7350
  const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
6817
7351
  const fm = FM_PATTERN.exec(source);
@@ -6819,12 +7353,16 @@ function rewriteFrontmatterMaturity(source, newMaturity) {
6819
7353
  return null;
6820
7354
  }
6821
7355
  const block = fm[1];
6822
- if (!MATURITY_LINE_PATTERN.test(block)) {
7356
+ const matMatch = MATURITY_LINE_PATTERN.exec(block);
7357
+ if (matMatch === null) {
6823
7358
  return null;
6824
7359
  }
7360
+ const currentValue = matMatch[2];
7361
+ const isCanonicalVocab = currentValue === "proven" || currentValue === "verified" || currentValue === "draft";
7362
+ const replacement = isCanonicalVocab ? LINT_TO_CANONICAL_MATURITY[newMaturity] : newMaturity;
6825
7363
  const replacedBlock = block.replace(
6826
7364
  MATURITY_LINE_PATTERN,
6827
- (line) => line.replace(/(stable|endorsed|draft)/u, newMaturity)
7365
+ (line) => line.replace(/(stable|endorsed|draft|verified|proven)/u, replacement)
6828
7366
  );
6829
7367
  const blockStart = source.indexOf(block);
6830
7368
  if (blockStart < 0) {
@@ -6862,7 +7400,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
6862
7400
  };
6863
7401
  }
6864
7402
  const detail = `${candidate.maturity} -> ${next}`;
6865
- const absPath = join11(projectRoot, candidate.path);
7403
+ const absPath = join13(projectRoot, candidate.path);
6866
7404
  try {
6867
7405
  const source = await readFile13(absPath, "utf8");
6868
7406
  const rewritten = rewriteFrontmatterMaturity(source, next);
@@ -6929,11 +7467,11 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
6929
7467
  }
6930
7468
  }
6931
7469
  async function applyStaleArchive(projectRoot, candidate, now) {
6932
- const sourceAbs = join11(projectRoot, candidate.path);
6933
- const destAbs = join11(projectRoot, candidate.archive_path);
7470
+ const sourceAbs = join13(projectRoot, candidate.path);
7471
+ const destAbs = join13(projectRoot, candidate.archive_path);
6934
7472
  const detail = `${candidate.path} -> ${candidate.archive_path}`;
6935
7473
  try {
6936
- await mkdir6(join11(destAbs, ".."), { recursive: true });
7474
+ await mkdir5(join13(destAbs, ".."), { recursive: true });
6937
7475
  try {
6938
7476
  await rename2(sourceAbs, destAbs);
6939
7477
  } catch (renameError) {
@@ -6992,7 +7530,7 @@ async function applyStaleArchive(projectRoot, candidate, now) {
6992
7530
  async function applyPendingAutoArchive(projectRoot, candidate, now) {
6993
7531
  const detail = `${candidate.pending_path} -> ${candidate.archived_to}`;
6994
7532
  try {
6995
- await mkdir6(join11(candidate.archived_to_abs, ".."), { recursive: true });
7533
+ await mkdir5(join13(candidate.archived_to_abs, ".."), { recursive: true });
6996
7534
  let moved = false;
6997
7535
  if (candidate.layer === "team") {
6998
7536
  try {
@@ -7065,11 +7603,11 @@ async function applyPendingAutoArchive(projectRoot, candidate, now) {
7065
7603
  }
7066
7604
  function relativePosix(projectRoot, absolutePath) {
7067
7605
  const rel = nodeRelative(projectRoot, absolutePath);
7068
- return rel.split(sep3).join("/");
7606
+ return rel.split(sep4).join("/");
7069
7607
  }
7070
7608
  async function applySessionHintsStaleCleanup(projectRoot, candidate) {
7071
7609
  const detail = `deleted (${candidate.age_days}d old)`;
7072
- const absPath = join11(projectRoot, candidate.path);
7610
+ const absPath = join13(projectRoot, candidate.path);
7073
7611
  try {
7074
7612
  const { unlink: unlink4 } = await import("fs/promises");
7075
7613
  await unlink4(absPath);
@@ -7090,21 +7628,23 @@ async function applySessionHintsStaleCleanup(projectRoot, candidate) {
7090
7628
  }
7091
7629
  }
7092
7630
  async function applyIndexDriftFix(projectRoot, inspection) {
7093
- const metaPath = join11(projectRoot, ".fabric", "agents.meta.json");
7631
+ const metaPath = join13(projectRoot, ".fabric", "agents.meta.json");
7094
7632
  const detailParts = [];
7095
7633
  try {
7096
- const meta = agentsMetaSchema5.parse(JSON.parse(await readFile13(metaPath, "utf8")));
7097
- const baseCounters = AgentsMetaCountersSchema2.parse(meta.counters ?? void 0);
7098
- const updatedCounters = {
7099
- KP: { ...baseCounters.KP },
7100
- KT: { ...baseCounters.KT }
7101
- };
7102
- for (const drift of inspection.drifts) {
7103
- updatedCounters[drift.layer][drift.type] = drift.proposed_after;
7104
- detailParts.push(`${drift.layer}.${drift.type}: ${drift.counter} -> ${drift.proposed_after}`);
7105
- }
7106
- const updated = { ...meta, counters: updatedCounters };
7107
- await atomicWriteJson3(metaPath, updated, { indent: 2 });
7634
+ await withFileLock4(`${metaPath}.lock`, async () => {
7635
+ const meta = agentsMetaSchema5.parse(JSON.parse(await readFile13(metaPath, "utf8")));
7636
+ const baseCounters = AgentsMetaCountersSchema2.parse(meta.counters ?? void 0);
7637
+ const updatedCounters = {
7638
+ KP: { ...baseCounters.KP },
7639
+ KT: { ...baseCounters.KT }
7640
+ };
7641
+ for (const drift of inspection.drifts) {
7642
+ updatedCounters[drift.layer][drift.type] = drift.proposed_after;
7643
+ detailParts.push(`${drift.layer}.${drift.type}: ${drift.counter} -> ${drift.proposed_after}`);
7644
+ }
7645
+ const updated = { ...meta, counters: updatedCounters };
7646
+ await atomicWriteJson3(metaPath, updated, { indent: 2 });
7647
+ });
7108
7648
  return {
7109
7649
  kind: "knowledge_index_drift",
7110
7650
  path: "agents.meta.json#counters",
@@ -7126,7 +7666,7 @@ function truncateErrorMessage(error) {
7126
7666
  return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
7127
7667
  }
7128
7668
  async function inspectForensic(projectRoot) {
7129
- const path2 = join11(projectRoot, ".fabric", "forensic.json");
7669
+ const path2 = join13(projectRoot, ".fabric", "forensic.json");
7130
7670
  try {
7131
7671
  const parsed = forensicReportSchema.parse(JSON.parse(await readFile13(path2, "utf8")));
7132
7672
  return { present: true, valid: true, report: parsed };
@@ -7138,12 +7678,12 @@ async function inspectForensic(projectRoot) {
7138
7678
  }
7139
7679
  }
7140
7680
  function inspectMcpConfigInWrongFile(projectRoot) {
7141
- const settingsPath = join11(projectRoot, ".claude", "settings.json");
7681
+ const settingsPath = join13(projectRoot, ".claude", "settings.json");
7142
7682
  if (!existsSync8(settingsPath)) {
7143
7683
  return { hasWrongEntry: false, settingsPath };
7144
7684
  }
7145
7685
  try {
7146
- const parsed = JSON.parse(readFileSync3(settingsPath, "utf8"));
7686
+ const parsed = JSON.parse(readFileSync4(settingsPath, "utf8"));
7147
7687
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
7148
7688
  return { hasWrongEntry: false, settingsPath };
7149
7689
  }
@@ -7159,7 +7699,7 @@ function inspectMcpConfigInWrongFile(projectRoot) {
7159
7699
  }
7160
7700
  }
7161
7701
  async function inspectMeta(projectRoot) {
7162
- const metaPath = join11(projectRoot, ".fabric", "agents.meta.json");
7702
+ const metaPath = join13(projectRoot, ".fabric", "agents.meta.json");
7163
7703
  const built = await tryBuildRuleMeta(projectRoot);
7164
7704
  try {
7165
7705
  const raw = await readFile13(metaPath, "utf8");
@@ -7245,7 +7785,7 @@ function inspectContentRefs(projectRoot, meta) {
7245
7785
  if (isPersonalKnowledge) {
7246
7786
  continue;
7247
7787
  }
7248
- if (!existsSync8(join11(projectRoot, contentRef))) {
7788
+ if (!existsSync8(join13(projectRoot, contentRef))) {
7249
7789
  missing.push(contentRef);
7250
7790
  }
7251
7791
  }
@@ -7329,8 +7869,8 @@ function inspectSkillRefMirror(projectRoot) {
7329
7869
  const skillSlugs = ["fabric-archive", "fabric-review", "fabric-import"];
7330
7870
  const driftedPaths = [];
7331
7871
  for (const slug of skillSlugs) {
7332
- const claudeRef = join11(projectRoot, ".claude", "skills", slug, "ref");
7333
- const codexRef = join11(projectRoot, ".codex", "skills", slug, "ref");
7872
+ const claudeRef = join13(projectRoot, ".claude", "skills", slug, "ref");
7873
+ const codexRef = join13(projectRoot, ".codex", "skills", slug, "ref");
7334
7874
  let claudeFiles = null;
7335
7875
  let codexFiles = null;
7336
7876
  try {
@@ -7355,12 +7895,12 @@ function inspectSkillRefMirror(projectRoot) {
7355
7895
  let claudeBody;
7356
7896
  let codexBody;
7357
7897
  try {
7358
- claudeBody = readFileSync3(join11(claudeRef, fname), "utf8");
7898
+ claudeBody = readFileSync4(join13(claudeRef, fname), "utf8");
7359
7899
  } catch {
7360
7900
  continue;
7361
7901
  }
7362
7902
  try {
7363
- codexBody = readFileSync3(join11(codexRef, fname), "utf8");
7903
+ codexBody = readFileSync4(join13(codexRef, fname), "utf8");
7364
7904
  } catch {
7365
7905
  continue;
7366
7906
  }
@@ -7379,10 +7919,10 @@ function inspectSkillTokenBudget(projectRoot) {
7379
7919
  const overSize = [];
7380
7920
  let highestSeverity = "ok";
7381
7921
  for (const slug of skillSlugs) {
7382
- const skillMdPath = join11(projectRoot, ".claude", "skills", slug, "SKILL.md");
7922
+ const skillMdPath = join13(projectRoot, ".claude", "skills", slug, "SKILL.md");
7383
7923
  let body;
7384
7924
  try {
7385
- body = readFileSync3(skillMdPath, "utf8");
7925
+ body = readFileSync4(skillMdPath, "utf8");
7386
7926
  } catch {
7387
7927
  continue;
7388
7928
  }
@@ -7404,10 +7944,10 @@ function inspectSkillDescription(projectRoot) {
7404
7944
  const CJK_PATTERN = /[㐀-䶿一-鿿]/;
7405
7945
  const ASCII_PATTERN = /[a-zA-Z]{2,}/;
7406
7946
  for (const slug of skillSlugs) {
7407
- const skillMdPath = join11(projectRoot, ".claude", "skills", slug, "SKILL.md");
7947
+ const skillMdPath = join13(projectRoot, ".claude", "skills", slug, "SKILL.md");
7408
7948
  let body;
7409
7949
  try {
7410
- body = readFileSync3(skillMdPath, "utf8");
7950
+ body = readFileSync4(skillMdPath, "utf8");
7411
7951
  } catch {
7412
7952
  continue;
7413
7953
  }
@@ -7522,9 +8062,9 @@ function inspectDraftBacklog(projectRoot) {
7522
8062
  const MIN_TOTAL_FOR_RATIO = 10;
7523
8063
  let draftCount = 0;
7524
8064
  let totalCount = 0;
7525
- const knowledgeRoot = join11(projectRoot, ".fabric", "knowledge");
8065
+ const knowledgeRoot = join13(projectRoot, ".fabric", "knowledge");
7526
8066
  for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
7527
- const dir = join11(knowledgeRoot, typeDir);
8067
+ const dir = join13(knowledgeRoot, typeDir);
7528
8068
  if (!existsSync8(dir)) continue;
7529
8069
  let entries;
7530
8070
  try {
@@ -7537,7 +8077,7 @@ function inspectDraftBacklog(projectRoot) {
7537
8077
  if (CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(entry.name) === null) continue;
7538
8078
  let maturity;
7539
8079
  try {
7540
- maturity = extractMaturityFull(readFileSync3(join11(dir, entry.name), "utf8"));
8080
+ maturity = extractMaturityFull(readFileSync4(join13(dir, entry.name), "utf8"));
7541
8081
  } catch {
7542
8082
  continue;
7543
8083
  }
@@ -7581,7 +8121,7 @@ async function inspectDraftAutoPromote(projectRoot, now = Date.now()) {
7581
8121
  if (drifted.has(entry.stable_id)) continue;
7582
8122
  let createdAt;
7583
8123
  try {
7584
- createdAt = extractKnowledgeFrontmatterCreatedAt(readFileSync3(entry.absPath, "utf8"));
8124
+ createdAt = extractKnowledgeFrontmatterCreatedAt(readFileSync4(entry.absPath, "utf8"));
7585
8125
  } catch {
7586
8126
  continue;
7587
8127
  }
@@ -7624,7 +8164,7 @@ async function applyDraftAutoPromote(projectRoot, candidates) {
7624
8164
  for (const candidate of candidates) {
7625
8165
  let source;
7626
8166
  try {
7627
- source = readFileSync3(candidate.absPath, "utf8");
8167
+ source = readFileSync4(candidate.absPath, "utf8");
7628
8168
  } catch {
7629
8169
  continue;
7630
8170
  }
@@ -7654,7 +8194,7 @@ function inspectKnowledgeTagsEmpty(projectRoot) {
7654
8194
  for (const entry of iterateCanonicalEntries(projectRoot, /* @__PURE__ */ new Map())) {
7655
8195
  let source;
7656
8196
  try {
7657
- source = readFileSync3(entry.absPath, "utf8");
8197
+ source = readFileSync4(entry.absPath, "utf8");
7658
8198
  } catch {
7659
8199
  continue;
7660
8200
  }
@@ -7676,7 +8216,7 @@ function inspectKnowledgeTagsEmpty(projectRoot) {
7676
8216
  };
7677
8217
  }
7678
8218
  async function inspectKnowledgeTestIndex(projectRoot) {
7679
- const path2 = join11(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
8219
+ const path2 = join13(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
7680
8220
  const built = await tryBuildRuleMeta(projectRoot);
7681
8221
  try {
7682
8222
  const index = knowledgeTestIndexSchema2.parse(JSON.parse(await readFile13(path2, "utf8")));
@@ -7700,8 +8240,8 @@ async function inspectKnowledgeTestIndex(projectRoot) {
7700
8240
  }
7701
8241
  function inspectBootstrapAnchor(projectRoot) {
7702
8242
  return {
7703
- hasAgentsMd: existsSync8(join11(projectRoot, "AGENTS.md")),
7704
- hasClaudeMd: existsSync8(join11(projectRoot, "CLAUDE.md"))
8243
+ hasAgentsMd: existsSync8(join13(projectRoot, "AGENTS.md")),
8244
+ hasClaudeMd: existsSync8(join13(projectRoot, "CLAUDE.md"))
7705
8245
  };
7706
8246
  }
7707
8247
  var BOOTSTRAP_MARKER_MIGRATION_TARGETS = [
@@ -7713,7 +8253,7 @@ var BOOTSTRAP_MARKER_MIGRATION_TARGETS = [
7713
8253
  async function inspectBootstrapMarkerMigration(target) {
7714
8254
  const filesNeedingMigration = [];
7715
8255
  for (const rel of BOOTSTRAP_MARKER_MIGRATION_TARGETS) {
7716
- const abs = join11(target, rel);
8256
+ const abs = join13(target, rel);
7717
8257
  if (!existsSync8(abs)) {
7718
8258
  continue;
7719
8259
  }
@@ -7751,7 +8291,7 @@ function createBootstrapMarkerMigrationCheck(t, inspection) {
7751
8291
  );
7752
8292
  }
7753
8293
  async function inspectL1BootstrapSnapshotDrift(target) {
7754
- const abs = join11(target, ".fabric", "AGENTS.md");
8294
+ const abs = join13(target, ".fabric", "AGENTS.md");
7755
8295
  if (!existsSync8(abs)) {
7756
8296
  return { status: "missing", canonical: BOOTSTRAP_CANONICAL, onDisk: null };
7757
8297
  }
@@ -7783,7 +8323,7 @@ function createL1BootstrapSnapshotDriftCheck(t, inspection) {
7783
8323
  );
7784
8324
  }
7785
8325
  async function inspectL2ManagedBlockDrift(target) {
7786
- const snapshotPath = join11(target, ".fabric", "AGENTS.md");
8326
+ const snapshotPath = join13(target, ".fabric", "AGENTS.md");
7787
8327
  if (!existsSync8(snapshotPath)) {
7788
8328
  return { status: "ok", drifted: [] };
7789
8329
  }
@@ -7793,7 +8333,7 @@ async function inspectL2ManagedBlockDrift(target) {
7793
8333
  } catch {
7794
8334
  return { status: "ok", drifted: [] };
7795
8335
  }
7796
- const projectRulesPath = join11(target, ".fabric", "project-rules.md");
8336
+ const projectRulesPath = join13(target, ".fabric", "project-rules.md");
7797
8337
  let expectedBody = snapshot;
7798
8338
  if (existsSync8(projectRulesPath)) {
7799
8339
  try {
@@ -7807,8 +8347,8 @@ ${projectRules}`;
7807
8347
  const drifted = [];
7808
8348
  let anyManagedBlockFound = false;
7809
8349
  const blockTargets = [
7810
- join11(target, "AGENTS.md"),
7811
- join11(target, ".cursor", "rules", "fabric-bootstrap.mdc")
8350
+ join13(target, "AGENTS.md"),
8351
+ join13(target, ".cursor", "rules", "fabric-bootstrap.mdc")
7812
8352
  ];
7813
8353
  for (const abs of blockTargets) {
7814
8354
  if (!existsSync8(abs)) {
@@ -7842,7 +8382,7 @@ ${projectRules}`;
7842
8382
  drifted.push({ path: abs, expected: expectedBody, actual: body });
7843
8383
  }
7844
8384
  }
7845
- const claudeMdPath = join11(target, "CLAUDE.md");
8385
+ const claudeMdPath = join13(target, "CLAUDE.md");
7846
8386
  if (existsSync8(claudeMdPath)) {
7847
8387
  let claudeContent;
7848
8388
  try {
@@ -7913,10 +8453,10 @@ function createBootstrapAnchorCheck(t, inspection) {
7913
8453
  );
7914
8454
  }
7915
8455
  function inspectKnowledgeDirMissing(projectRoot) {
7916
- const knowledgeRoot = join11(projectRoot, ".fabric", "knowledge");
8456
+ const knowledgeRoot = join13(projectRoot, ".fabric", "knowledge");
7917
8457
  const missingSubdirs = [];
7918
8458
  for (const sub of KNOWLEDGE_SUBDIRS3) {
7919
- const path2 = join11(knowledgeRoot, sub);
8459
+ const path2 = join13(knowledgeRoot, sub);
7920
8460
  if (!existsSync8(path2)) {
7921
8461
  missingSubdirs.push(`.fabric/knowledge/${sub}`);
7922
8462
  }
@@ -7925,12 +8465,12 @@ function inspectKnowledgeDirMissing(projectRoot) {
7925
8465
  }
7926
8466
  function inspectBaselineFilenameFormat(projectRoot) {
7927
8467
  const offenders = [];
7928
- const knowledgeRoot = join11(projectRoot, ".fabric", "knowledge");
8468
+ const knowledgeRoot = join13(projectRoot, ".fabric", "knowledge");
7929
8469
  if (!existsSync8(knowledgeRoot)) {
7930
8470
  return { offenders };
7931
8471
  }
7932
8472
  for (const sub of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
7933
- const dir = join11(knowledgeRoot, sub);
8473
+ const dir = join13(knowledgeRoot, sub);
7934
8474
  if (!existsSync8(dir)) {
7935
8475
  continue;
7936
8476
  }
@@ -7948,10 +8488,10 @@ function inspectBaselineFilenameFormat(projectRoot) {
7948
8488
  if (BASELINE_ID_PREFIXED_FILENAME_PATTERN.test(entryName)) {
7949
8489
  continue;
7950
8490
  }
7951
- const abs = join11(dir, entryName);
8491
+ const abs = join13(dir, entryName);
7952
8492
  let source;
7953
8493
  try {
7954
- source = readFileSync3(abs, "utf8");
8494
+ source = readFileSync4(abs, "utf8");
7955
8495
  } catch {
7956
8496
  continue;
7957
8497
  }
@@ -8529,17 +9069,17 @@ function okCheck(name, message) {
8529
9069
  return { name, status: "ok", message };
8530
9070
  }
8531
9071
  function inspectHooksWired(projectRoot) {
8532
- const claudeDir = join11(projectRoot, ".claude");
9072
+ const claudeDir = join13(projectRoot, ".claude");
8533
9073
  if (!existsSync8(claudeDir)) {
8534
9074
  return { status: "skipped", missingHooks: [] };
8535
9075
  }
8536
- const settingsPath = join11(projectRoot, ".claude", "settings.json");
9076
+ const settingsPath = join13(projectRoot, ".claude", "settings.json");
8537
9077
  if (!existsSync8(settingsPath)) {
8538
9078
  return { status: "missing-settings", missingHooks: [] };
8539
9079
  }
8540
9080
  let raw;
8541
9081
  try {
8542
- raw = readFileSync3(settingsPath, "utf8");
9082
+ raw = readFileSync4(settingsPath, "utf8");
8543
9083
  } catch {
8544
9084
  return { status: "missing-settings", missingHooks: [] };
8545
9085
  }
@@ -8624,7 +9164,7 @@ function createHooksWiredCheck(t, inspection) {
8624
9164
  function inspectHooksContentDrift(projectRoot) {
8625
9165
  const hookFilesByBasename = /* @__PURE__ */ new Map();
8626
9166
  for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
8627
- const absDir = join11(projectRoot, dir);
9167
+ const absDir = join13(projectRoot, dir);
8628
9168
  if (!existsSync8(absDir)) continue;
8629
9169
  let entries;
8630
9170
  try {
@@ -8634,7 +9174,7 @@ function inspectHooksContentDrift(projectRoot) {
8634
9174
  }
8635
9175
  for (const name of entries) {
8636
9176
  if (!name.endsWith(".cjs")) continue;
8637
- const abs = join11(absDir, name);
9177
+ const abs = join13(absDir, name);
8638
9178
  let stat3;
8639
9179
  try {
8640
9180
  stat3 = statSync4(abs);
@@ -8655,7 +9195,7 @@ function inspectHooksContentDrift(projectRoot) {
8655
9195
  const hashes = [];
8656
9196
  for (const { client, abs } of copies) {
8657
9197
  try {
8658
- const body = readFileSync3(abs);
9198
+ const body = readFileSync4(abs);
8659
9199
  hashes.push({ client, sha: sha256(body.toString("utf8")) });
8660
9200
  } catch {
8661
9201
  }
@@ -8711,7 +9251,7 @@ function inspectHooksRuntime(projectRoot) {
8711
9251
  const issues = [];
8712
9252
  let scanned = 0;
8713
9253
  for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
8714
- const absDir = join11(projectRoot, dir);
9254
+ const absDir = join13(projectRoot, dir);
8715
9255
  if (!existsSync8(absDir)) continue;
8716
9256
  let entries;
8717
9257
  try {
@@ -8721,7 +9261,7 @@ function inspectHooksRuntime(projectRoot) {
8721
9261
  }
8722
9262
  for (const name of entries) {
8723
9263
  if (!name.endsWith(".cjs")) continue;
8724
- const abs = join11(absDir, name);
9264
+ const abs = join13(absDir, name);
8725
9265
  const displayPath = `${dir}/${name}`;
8726
9266
  let stat3;
8727
9267
  try {
@@ -8733,7 +9273,7 @@ function inspectHooksRuntime(projectRoot) {
8733
9273
  scanned += 1;
8734
9274
  let body;
8735
9275
  try {
8736
- body = readFileSync3(abs, "utf8");
9276
+ body = readFileSync4(abs, "utf8");
8737
9277
  } catch (err) {
8738
9278
  issues.push({
8739
9279
  path: displayPath,
@@ -8887,15 +9427,17 @@ function inspectGlobalCliVersion(spawn = defaultGlobalCliSpawn) {
8887
9427
  return { status: "unparseable", detail: `exit ${res.status ?? "?"}` };
8888
9428
  }
8889
9429
  const raw = (res.stdout ?? "").trim();
8890
- const m = /(\d+)\.(\d+)\.(\d+)-rc\.(\d+)/.exec(raw);
9430
+ const m = /(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?/.exec(raw);
8891
9431
  if (!m) {
8892
9432
  return { status: "unparseable", detail: raw.slice(0, 80) };
8893
9433
  }
8894
- const version = `${m[1]}.${m[2]}.${m[3]}-rc.${m[4]}`;
8895
- const observedRc = Number(m[4]);
8896
- const minMatch = /-rc\.(\d+)/.exec(MIN_SUPPORTED_GLOBAL_CLI_VERSION);
8897
- const minRc = minMatch ? Number(minMatch[1]) : 0;
8898
- if (observedRc < minRc) {
9434
+ const hasRc = m[4] !== void 0;
9435
+ const version = hasRc ? `${m[1]}.${m[2]}.${m[3]}-rc.${m[4]}` : `${m[1]}.${m[2]}.${m[3]}`;
9436
+ const minM = /(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?/.exec(MIN_SUPPORTED_GLOBAL_CLI_VERSION);
9437
+ const observed = [Number(m[1]), Number(m[2]), Number(m[3]), hasRc ? Number(m[4]) : Infinity];
9438
+ const min = minM ? [Number(minM[1]), Number(minM[2]), Number(minM[3]), minM[4] !== void 0 ? Number(minM[4]) : Infinity] : [0, 0, 0, 0];
9439
+ const diffAt = observed.findIndex((v, i) => v !== min[i]);
9440
+ if (diffAt !== -1 && observed[diffAt] < min[diffAt]) {
8899
9441
  return { status: "outdated", version, minVersion: MIN_SUPPORTED_GLOBAL_CLI_VERSION };
8900
9442
  }
8901
9443
  return { status: "ok", version };
@@ -8940,7 +9482,7 @@ function createGlobalCliVersionCheck(t, inspection) {
8940
9482
  );
8941
9483
  }
8942
9484
  var KNOWLEDGE_SUMMARY_OPAQUE_THRESHOLD = 0.3;
8943
- function inspectKnowledgeSummaryOpaque(meta) {
9485
+ function inspectKnowledgeSummaryOpaque(meta, storeSummaries = []) {
8944
9486
  const baseline = {
8945
9487
  totalWithDescription: 0,
8946
9488
  opaqueCount: 0,
@@ -8965,6 +9507,14 @@ function inspectKnowledgeSummaryOpaque(meta) {
8965
9507
  opaqueIds.push(stableId);
8966
9508
  }
8967
9509
  }
9510
+ for (const entry of storeSummaries) {
9511
+ total += 1;
9512
+ const summary = entry.summary.trim();
9513
+ const localId = entry.stableId.includes(":") ? entry.stableId.slice(entry.stableId.indexOf(":") + 1) : entry.stableId;
9514
+ if (summary.length === 0 || summary === entry.stableId.trim() || summary === localId.trim()) {
9515
+ opaqueIds.push(entry.stableId);
9516
+ }
9517
+ }
8968
9518
  if (total === 0) {
8969
9519
  return { status: "ok", ...baseline };
8970
9520
  }
@@ -9040,7 +9590,7 @@ function findIssue(issues, code) {
9040
9590
  };
9041
9591
  }
9042
9592
  async function inspectMetaManuallyDiverged(projectRoot) {
9043
- const metaPath = join11(projectRoot, ".fabric", "agents.meta.json");
9593
+ const metaPath = join13(projectRoot, ".fabric", "agents.meta.json");
9044
9594
  if (!existsSync8(metaPath)) {
9045
9595
  return { extraMetaEntries: [], hashMismatchEntries: [], readable: false };
9046
9596
  }
@@ -9066,7 +9616,7 @@ async function inspectMetaManuallyDiverged(projectRoot) {
9066
9616
  continue;
9067
9617
  }
9068
9618
  try {
9069
- const content = readFileSync3(absPath, "utf8");
9619
+ const content = readFileSync4(absPath, "utf8");
9070
9620
  const diskHash = sha256(content);
9071
9621
  if (node.hash !== "" && node.hash !== diskHash) {
9072
9622
  hashMismatchEntries.push(contentRef);
@@ -9079,7 +9629,7 @@ async function inspectMetaManuallyDiverged(projectRoot) {
9079
9629
  }
9080
9630
  function inspectKnowledgeDirUnindexed(projectRoot, meta) {
9081
9631
  const physicalMdFiles = /* @__PURE__ */ new Set();
9082
- collectMdFilesUnder(physicalMdFiles, projectRoot, join11(projectRoot, ".fabric", "knowledge"), ".fabric/knowledge");
9632
+ collectMdFilesUnder(physicalMdFiles, projectRoot, join13(projectRoot, ".fabric", "knowledge"), ".fabric/knowledge");
9083
9633
  if (physicalMdFiles.size === 0) {
9084
9634
  return { unindexedFiles: [] };
9085
9635
  }
@@ -9104,7 +9654,7 @@ function collectMdFilesUnder(out, projectRoot, rootDir, relPrefix) {
9104
9654
  continue;
9105
9655
  }
9106
9656
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
9107
- const abs = join11(dir, entry.name);
9657
+ const abs = join13(dir, entry.name);
9108
9658
  if (entry.isDirectory()) {
9109
9659
  if (entry.name !== "pending" && entry.name !== "archive") {
9110
9660
  stack.push(abs);
@@ -9137,7 +9687,7 @@ function createKnowledgeDirUnindexedCheck(t, inspection) {
9137
9687
  }
9138
9688
  async function inspectStableIdCollisions(projectRoot) {
9139
9689
  const found = [];
9140
- const knowledgeDir = join11(projectRoot, ".fabric", "knowledge");
9690
+ const knowledgeDir = join13(projectRoot, ".fabric", "knowledge");
9141
9691
  if (existsSync8(knowledgeDir)) {
9142
9692
  const stack = [knowledgeDir];
9143
9693
  while (stack.length > 0) {
@@ -9146,7 +9696,7 @@ async function inspectStableIdCollisions(projectRoot) {
9146
9696
  continue;
9147
9697
  }
9148
9698
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
9149
- const abs = join11(dir, entry.name);
9699
+ const abs = join13(dir, entry.name);
9150
9700
  if (entry.isDirectory()) {
9151
9701
  stack.push(abs);
9152
9702
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -9325,17 +9875,17 @@ function createMetaManuallyDivergedCheck(t, inspection) {
9325
9875
  }
9326
9876
  function inspectPreexistingRootFiles(projectRoot) {
9327
9877
  const candidates = ["CLAUDE.md", "AGENTS.md"];
9328
- const detected = candidates.filter((name) => existsSync8(join11(projectRoot, name)));
9878
+ const detected = candidates.filter((name) => existsSync8(join13(projectRoot, name)));
9329
9879
  return { detected };
9330
9880
  }
9331
9881
  async function inspectFilesystemEditFallback(projectRoot) {
9332
- const knowledgeRoot = join11(projectRoot, ".fabric", "knowledge");
9882
+ const knowledgeRoot = join13(projectRoot, ".fabric", "knowledge");
9333
9883
  if (!existsSync8(knowledgeRoot)) {
9334
9884
  return { synthesized: 0, synthesizedStableIds: [] };
9335
9885
  }
9336
9886
  const canonicalIds = /* @__PURE__ */ new Set();
9337
9887
  for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
9338
- const dir = join11(knowledgeRoot, typeDir);
9888
+ const dir = join13(knowledgeRoot, typeDir);
9339
9889
  if (!existsSync8(dir)) {
9340
9890
  continue;
9341
9891
  }
@@ -9571,7 +10121,10 @@ function extractKnowledgeFrontmatterMaturity(source) {
9571
10121
  return null;
9572
10122
  }
9573
10123
  const match = MATURITY_LINE_PATTERN.exec(fm[1]);
9574
- return match === null ? null : match[2];
10124
+ if (match === null) {
10125
+ return null;
10126
+ }
10127
+ return CANONICAL_TO_LINT_MATURITY[match[2]] ?? null;
9575
10128
  }
9576
10129
  function isKnowledgeFrontmatterTagsEmpty(source) {
9577
10130
  const FM_PATTERN = /^(?:)?---\r?\n([\s\S]*?)\r?\n---/u;
@@ -9600,12 +10153,12 @@ function extractKnowledgeFrontmatterCreatedAt(source) {
9600
10153
  return Number.isFinite(parsed) ? parsed : null;
9601
10154
  }
9602
10155
  function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
9603
- const knowledgeRoot = join11(projectRoot, ".fabric", "knowledge");
10156
+ const knowledgeRoot = join13(projectRoot, ".fabric", "knowledge");
9604
10157
  if (!existsSync8(knowledgeRoot)) {
9605
10158
  return;
9606
10159
  }
9607
10160
  for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
9608
- const dir = join11(knowledgeRoot, typeDir);
10161
+ const dir = join13(knowledgeRoot, typeDir);
9609
10162
  if (!existsSync8(dir)) {
9610
10163
  continue;
9611
10164
  }
@@ -9624,10 +10177,10 @@ function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
9624
10177
  continue;
9625
10178
  }
9626
10179
  const stableId = match[1];
9627
- const absPath = join11(dir, entry.name);
10180
+ const absPath = join13(dir, entry.name);
9628
10181
  let source;
9629
10182
  try {
9630
- source = readFileSync3(absPath, "utf8");
10183
+ source = readFileSync4(absPath, "utf8");
9631
10184
  } catch {
9632
10185
  continue;
9633
10186
  }
@@ -9701,8 +10254,8 @@ async function inspectStaleArchive(projectRoot, now) {
9701
10254
  return { candidates };
9702
10255
  }
9703
10256
  function* iteratePendingFiles(projectRoot, now) {
9704
- const teamRoot = join11(projectRoot, ".fabric", "knowledge", "pending");
9705
- const personalRoot = join11(resolvePersonalRootForPending(), ".fabric", "knowledge", "pending");
10257
+ const teamRoot = join13(projectRoot, ".fabric", "knowledge", "pending");
10258
+ const personalRoot = join13(resolvePersonalRootForPending(), ".fabric", "knowledge", "pending");
9706
10259
  for (const [layer, root, displayPrefix] of [
9707
10260
  ["team", teamRoot, ".fabric/knowledge/pending"],
9708
10261
  ["personal", personalRoot, "~/.fabric/knowledge/pending"]
@@ -9717,7 +10270,7 @@ function* iteratePendingFiles(projectRoot, now) {
9717
10270
  continue;
9718
10271
  }
9719
10272
  for (const typeDir of typeDirs) {
9720
- const dir = join11(root, typeDir);
10273
+ const dir = join13(root, typeDir);
9721
10274
  let entries;
9722
10275
  try {
9723
10276
  entries = readdirSync(dir, { withFileTypes: true });
@@ -9728,10 +10281,10 @@ function* iteratePendingFiles(projectRoot, now) {
9728
10281
  if (!entry.isFile() || !entry.name.endsWith(".md")) {
9729
10282
  continue;
9730
10283
  }
9731
- const absPath = join11(dir, entry.name);
10284
+ const absPath = join13(dir, entry.name);
9732
10285
  let source = "";
9733
10286
  try {
9734
- source = readFileSync3(absPath, "utf8");
10287
+ source = readFileSync4(absPath, "utf8");
9735
10288
  } catch {
9736
10289
  continue;
9737
10290
  }
@@ -9803,7 +10356,7 @@ function inspectPendingAutoArchive(projectRoot, now) {
9803
10356
  pending_path: visit.pending_path,
9804
10357
  pending_path_abs: visit.pending_path_abs,
9805
10358
  archived_to: archivedToRel,
9806
- archived_to_abs: join11(projectRoot, archivedToRel),
10359
+ archived_to_abs: join13(projectRoot, archivedToRel),
9807
10360
  age_days: visit.age_days
9808
10361
  });
9809
10362
  } else {
@@ -9812,7 +10365,7 @@ function inspectPendingAutoArchive(projectRoot, now) {
9812
10365
  visit.type,
9813
10366
  visit.filename
9814
10367
  );
9815
- const archivedToAbs = join11(
10368
+ const archivedToAbs = join13(
9816
10369
  resolvePersonalRootForPending(),
9817
10370
  ".fabric",
9818
10371
  ".archive",
@@ -9836,11 +10389,11 @@ function inspectPendingAutoArchive(projectRoot, now) {
9836
10389
  }
9837
10390
  function inspectUnderseeded(projectRoot) {
9838
10391
  const threshold = readUnderseedThresholdFromConfig(projectRoot);
9839
- const knowledgeRoot = join11(projectRoot, ".fabric", "knowledge");
10392
+ const knowledgeRoot = join13(projectRoot, ".fabric", "knowledge");
9840
10393
  let nodeCount = 0;
9841
10394
  if (existsSync8(knowledgeRoot)) {
9842
10395
  for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
9843
- const dir = join11(knowledgeRoot, typeDir);
10396
+ const dir = join13(knowledgeRoot, typeDir);
9844
10397
  if (!existsSync8(dir)) continue;
9845
10398
  let entries;
9846
10399
  try {
@@ -9862,7 +10415,7 @@ function inspectUnderseeded(projectRoot) {
9862
10415
  };
9863
10416
  }
9864
10417
  function inspectSessionHintsStale(projectRoot, now) {
9865
- const cacheDir = join11(projectRoot, ".fabric", ".cache");
10418
+ const cacheDir = join13(projectRoot, ".fabric", ".cache");
9866
10419
  if (!existsSync8(cacheDir)) {
9867
10420
  return { candidates: [] };
9868
10421
  }
@@ -9877,7 +10430,7 @@ function inspectSessionHintsStale(projectRoot, now) {
9877
10430
  if (!entry.isFile()) continue;
9878
10431
  if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
9879
10432
  if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
9880
- const absPath = join11(cacheDir, entry.name);
10433
+ const absPath = join13(cacheDir, entry.name);
9881
10434
  let mtimeMs = 0;
9882
10435
  try {
9883
10436
  mtimeMs = statSync4(absPath).mtimeMs;
@@ -9921,11 +10474,11 @@ function inspectNarrowTooFew(projectRoot, now) {
9921
10474
  const structuralFlagged = total >= NARROW_MIN_TOTAL && narrowRatio < NARROW_RATIO_THRESHOLD;
9922
10475
  const windowStartMs = now - SILENCE_WINDOW_DAYS * MS_PER_DAY;
9923
10476
  const editFires = readCounterTimestamps(
9924
- join11(projectRoot, EDIT_COUNTER_FILE_REL),
10477
+ join13(projectRoot, EDIT_COUNTER_FILE_REL),
9925
10478
  windowStartMs
9926
10479
  );
9927
10480
  const silenceFires = readCounterTimestamps(
9928
- join11(projectRoot, HINT_SILENCE_COUNTER_FILE_REL),
10481
+ join13(projectRoot, HINT_SILENCE_COUNTER_FILE_REL),
9929
10482
  windowStartMs
9930
10483
  );
9931
10484
  const telemetrySkipped = editFires === 0;
@@ -9947,7 +10500,7 @@ function readCounterTimestamps(absPath, windowStartMs) {
9947
10500
  if (!existsSync8(absPath)) return 0;
9948
10501
  let raw;
9949
10502
  try {
9950
- raw = readFileSync3(absPath, "utf8");
10503
+ raw = readFileSync4(absPath, "utf8");
9951
10504
  } catch {
9952
10505
  return 0;
9953
10506
  }
@@ -9963,10 +10516,10 @@ function readCounterTimestamps(absPath, windowStartMs) {
9963
10516
  return count;
9964
10517
  }
9965
10518
  function readUnderseedThresholdFromConfig(projectRoot) {
9966
- const configPath = join11(projectRoot, ".fabric", "fabric-config.json");
10519
+ const configPath = join13(projectRoot, ".fabric", "fabric-config.json");
9967
10520
  if (!existsSync8(configPath)) return DEFAULT_UNDERSEED_NODE_THRESHOLD;
9968
10521
  try {
9969
- const raw = readFileSync3(configPath, "utf8");
10522
+ const raw = readFileSync4(configPath, "utf8");
9970
10523
  const parsed = JSON.parse(raw);
9971
10524
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
9972
10525
  const v = parsed.underseed_node_threshold;
@@ -10160,11 +10713,11 @@ function extractKnowledgeFrontmatterRelevancePaths(source) {
10160
10713
  }
10161
10714
  function* iterateRelevanceFrontmatter(projectRoot) {
10162
10715
  for (const visit of iterateCanonicalFilenames(projectRoot)) {
10163
- const layerRoot = visit.layer === "team" ? join11(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
10164
- const absPath = join11(layerRoot, visit.type, visit.filename);
10716
+ const layerRoot = visit.layer === "team" ? join13(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
10717
+ const absPath = join13(layerRoot, visit.type, visit.filename);
10165
10718
  let source;
10166
10719
  try {
10167
- source = readFileSync3(absPath, "utf8");
10720
+ source = readFileSync4(absPath, "utf8");
10168
10721
  } catch {
10169
10722
  continue;
10170
10723
  }
@@ -10250,7 +10803,7 @@ function collectWorkspacePathsForGlobMatch(projectRoot) {
10250
10803
  continue;
10251
10804
  }
10252
10805
  for (const entry of entries) {
10253
- const abs = join11(current, entry.name);
10806
+ const abs = join13(current, entry.name);
10254
10807
  const rel = normalizePath(abs.slice(projectRoot.length + 1));
10255
10808
  if (rel.length === 0) continue;
10256
10809
  if (entry.isDirectory()) {
@@ -10466,11 +11019,11 @@ function createPersonalLayerPathMisclassifyCheck(t, inspection) {
10466
11019
  function inspectSuspiciousKb(projectRoot) {
10467
11020
  const candidates = [];
10468
11021
  for (const visit of iterateCanonicalFilenames(projectRoot)) {
10469
- const layerRoot = visit.layer === "team" ? join11(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
10470
- const absPath = join11(layerRoot, visit.type, visit.filename);
11022
+ const layerRoot = visit.layer === "team" ? join13(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
11023
+ const absPath = join13(layerRoot, visit.type, visit.filename);
10471
11024
  let body;
10472
11025
  try {
10473
- body = readFileSync3(absPath, "utf8");
11026
+ body = readFileSync4(absPath, "utf8");
10474
11027
  } catch {
10475
11028
  continue;
10476
11029
  }
@@ -10519,8 +11072,8 @@ function inspectRelevanceFieldsMissing(projectRoot) {
10519
11072
  const candidates = [];
10520
11073
  let scannedCount = 0;
10521
11074
  const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
10522
- const teamRoot = join11(projectRoot, ".fabric", "knowledge", "pending");
10523
- const personalRoot = join11(
11075
+ const teamRoot = join13(projectRoot, ".fabric", "knowledge", "pending");
11076
+ const personalRoot = join13(
10524
11077
  resolvePersonalRootForPending(),
10525
11078
  ".fabric",
10526
11079
  "knowledge",
@@ -10540,7 +11093,7 @@ function inspectRelevanceFieldsMissing(projectRoot) {
10540
11093
  continue;
10541
11094
  }
10542
11095
  for (const typeDir of typeDirs) {
10543
- const dir = join11(root, typeDir);
11096
+ const dir = join13(root, typeDir);
10544
11097
  let entries;
10545
11098
  try {
10546
11099
  entries = readdirSync(dir, { withFileTypes: true });
@@ -10551,10 +11104,10 @@ function inspectRelevanceFieldsMissing(projectRoot) {
10551
11104
  if (!entry.isFile() || !entry.name.endsWith(".md")) {
10552
11105
  continue;
10553
11106
  }
10554
- const absPath = join11(dir, entry.name);
11107
+ const absPath = join13(dir, entry.name);
10555
11108
  let source;
10556
11109
  try {
10557
- source = readFileSync3(absPath, "utf8");
11110
+ source = readFileSync4(absPath, "utf8");
10558
11111
  } catch {
10559
11112
  continue;
10560
11113
  }
@@ -10686,7 +11239,7 @@ var SKILL_QUOTED_VALUE_LEADS = /* @__PURE__ */ new Set(['"', "'", "[", "{", ">",
10686
11239
  function inspectSkillMdYamlInvalid(projectRoot) {
10687
11240
  const candidates = [];
10688
11241
  for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
10689
- const rootAbs = join11(projectRoot, rootRel);
11242
+ const rootAbs = join13(projectRoot, rootRel);
10690
11243
  if (!existsSync8(rootAbs)) continue;
10691
11244
  let dirEntries;
10692
11245
  try {
@@ -10696,11 +11249,11 @@ function inspectSkillMdYamlInvalid(projectRoot) {
10696
11249
  }
10697
11250
  for (const dirEntry of dirEntries) {
10698
11251
  if (!dirEntry.isDirectory()) continue;
10699
- const skillFile = join11(rootAbs, dirEntry.name, "SKILL.md");
11252
+ const skillFile = join13(rootAbs, dirEntry.name, "SKILL.md");
10700
11253
  if (!existsSync8(skillFile)) continue;
10701
11254
  let raw;
10702
11255
  try {
10703
- raw = readFileSync3(skillFile, "utf8");
11256
+ raw = readFileSync4(skillFile, "utf8");
10704
11257
  } catch {
10705
11258
  continue;
10706
11259
  }
@@ -10782,10 +11335,10 @@ function inspectOnboardCoverage(projectRoot) {
10782
11335
  for (const slot of ONBOARD_SLOT_NAMES) {
10783
11336
  filled[slot] = [];
10784
11337
  }
10785
- const knowledgeRoot = join11(projectRoot, ".fabric", "knowledge");
11338
+ const knowledgeRoot = join13(projectRoot, ".fabric", "knowledge");
10786
11339
  if (existsSync8(knowledgeRoot)) {
10787
11340
  for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS_FOR_ONBOARD) {
10788
- const dir = join11(knowledgeRoot, typeDir);
11341
+ const dir = join13(knowledgeRoot, typeDir);
10789
11342
  if (!existsSync8(dir)) continue;
10790
11343
  let entries;
10791
11344
  try {
@@ -10796,10 +11349,10 @@ function inspectOnboardCoverage(projectRoot) {
10796
11349
  for (const entry of entries) {
10797
11350
  if (!entry.isFile()) continue;
10798
11351
  if (!entry.name.endsWith(".md")) continue;
10799
- const filePath = join11(dir, entry.name);
11352
+ const filePath = join13(dir, entry.name);
10800
11353
  let content;
10801
11354
  try {
10802
- content = readFileSync3(filePath, "utf8");
11355
+ content = readFileSync4(filePath, "utf8");
10803
11356
  } catch {
10804
11357
  continue;
10805
11358
  }
@@ -10823,11 +11376,11 @@ function inspectOnboardCoverage(projectRoot) {
10823
11376
  return { filled, missing, opted_out: optedOut };
10824
11377
  }
10825
11378
  function readOnboardOptedOut(projectRoot) {
10826
- const path2 = join11(projectRoot, ".fabric", "fabric-config.json");
11379
+ const path2 = join13(projectRoot, ".fabric", "fabric-config.json");
10827
11380
  if (!existsSync8(path2)) return [];
10828
11381
  let raw;
10829
11382
  try {
10830
- raw = readFileSync3(path2, "utf8");
11383
+ raw = readFileSync4(path2, "utf8");
10831
11384
  } catch {
10832
11385
  return [];
10833
11386
  }
@@ -10851,10 +11404,10 @@ function readFrontmatterScalar(content, key) {
10851
11404
  if (block === void 0) return void 0;
10852
11405
  for (const rawLine of block.split(/\r?\n/u)) {
10853
11406
  const line = rawLine.trim();
10854
- const sep4 = line.indexOf(":");
10855
- if (sep4 === -1) continue;
10856
- if (line.slice(0, sep4).trim() !== key) continue;
10857
- let value = line.slice(sep4 + 1).trim();
11407
+ const sep5 = line.indexOf(":");
11408
+ if (sep5 === -1) continue;
11409
+ if (line.slice(0, sep5).trim() !== key) continue;
11410
+ let value = line.slice(sep5 + 1).trim();
10858
11411
  if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
10859
11412
  value = value.slice(1, -1);
10860
11413
  }
@@ -10943,7 +11496,7 @@ function createNarrowTooFewCheck(t, inspection) {
10943
11496
  }
10944
11497
  function resolvePersonalKnowledgeRoot() {
10945
11498
  const home = process.env.FABRIC_HOME ?? homedir6();
10946
- return join11(home, ".fabric", "knowledge");
11499
+ return join13(home, ".fabric", "knowledge");
10947
11500
  }
10948
11501
  function parseStableIdFromCanonicalFilename(filename) {
10949
11502
  const match = CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(filename);
@@ -10963,7 +11516,7 @@ function parseStableIdFromCanonicalFilename(filename) {
10963
11516
  };
10964
11517
  }
10965
11518
  function* iterateCanonicalFilenames(projectRoot) {
10966
- const teamRoot = join11(projectRoot, ".fabric", "knowledge");
11519
+ const teamRoot = join13(projectRoot, ".fabric", "knowledge");
10967
11520
  const personalRoot = resolvePersonalKnowledgeRoot();
10968
11521
  for (const [layer, root, displayPrefix] of [
10969
11522
  ["team", teamRoot, ".fabric/knowledge"],
@@ -10973,7 +11526,7 @@ function* iterateCanonicalFilenames(projectRoot) {
10973
11526
  continue;
10974
11527
  }
10975
11528
  for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
10976
- const dir = join11(root, typeDir);
11529
+ const dir = join13(root, typeDir);
10977
11530
  if (!existsSync8(dir)) {
10978
11531
  continue;
10979
11532
  }
@@ -11145,7 +11698,7 @@ async function migrateBootstrapMarkers(projectRoot) {
11145
11698
  const paths = [];
11146
11699
  const countPerPath = {};
11147
11700
  for (const rel of BOOTSTRAP_MARKER_MIGRATION_TARGETS) {
11148
- const abs = join11(projectRoot, rel);
11701
+ const abs = join13(projectRoot, rel);
11149
11702
  if (!existsSync8(abs)) {
11150
11703
  continue;
11151
11704
  }
@@ -11172,7 +11725,7 @@ async function migrateBootstrapMarkers(projectRoot) {
11172
11725
  return { paths, countPerPath };
11173
11726
  }
11174
11727
  async function rewriteThreeEndManagedBlocks(projectRoot) {
11175
- const snapshotPath = join11(projectRoot, ".fabric", "AGENTS.md");
11728
+ const snapshotPath = join13(projectRoot, ".fabric", "AGENTS.md");
11176
11729
  if (!existsSync8(snapshotPath)) {
11177
11730
  return;
11178
11731
  }
@@ -11182,7 +11735,7 @@ async function rewriteThreeEndManagedBlocks(projectRoot) {
11182
11735
  } catch {
11183
11736
  return;
11184
11737
  }
11185
- const projectRulesPath = join11(projectRoot, ".fabric", "project-rules.md");
11738
+ const projectRulesPath = join13(projectRoot, ".fabric", "project-rules.md");
11186
11739
  const hasProjectRules = existsSync8(projectRulesPath);
11187
11740
  let expectedBody = snapshot;
11188
11741
  if (hasProjectRules) {
@@ -11198,8 +11751,8 @@ ${projectRules}`;
11198
11751
  ${expectedBody}
11199
11752
  ${BOOTSTRAP_MARKER_END}`;
11200
11753
  const blockTargets = [
11201
- join11(projectRoot, "AGENTS.md"),
11202
- join11(projectRoot, ".cursor", "rules", "fabric-bootstrap.mdc")
11754
+ join13(projectRoot, "AGENTS.md"),
11755
+ join13(projectRoot, ".cursor", "rules", "fabric-bootstrap.mdc")
11203
11756
  ];
11204
11757
  for (const abs of blockTargets) {
11205
11758
  if (!existsSync8(abs)) {
@@ -11232,7 +11785,7 @@ ${managedBlock}
11232
11785
  }
11233
11786
  await atomicWriteText4(abs, next);
11234
11787
  }
11235
- const claudeMdPath = join11(projectRoot, "CLAUDE.md");
11788
+ const claudeMdPath = join13(projectRoot, "CLAUDE.md");
11236
11789
  if (existsSync8(claudeMdPath)) {
11237
11790
  let claudeContent;
11238
11791
  try {
@@ -11261,13 +11814,13 @@ ${managedBlock}
11261
11814
  }
11262
11815
  }
11263
11816
  async function fixMcpConfigInWrongFile(projectRoot) {
11264
- const settingsPath = join11(projectRoot, ".claude", "settings.json");
11817
+ const settingsPath = join13(projectRoot, ".claude", "settings.json");
11265
11818
  if (!existsSync8(settingsPath)) {
11266
11819
  return;
11267
11820
  }
11268
11821
  let settings;
11269
11822
  try {
11270
- const parsed = JSON.parse(readFileSync3(settingsPath, "utf8"));
11823
+ const parsed = JSON.parse(readFileSync4(settingsPath, "utf8"));
11271
11824
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
11272
11825
  return;
11273
11826
  }
@@ -11295,38 +11848,40 @@ async function fixMcpConfigInWrongFile(projectRoot) {
11295
11848
  }
11296
11849
  async function ensureKnowledgeSubdirs(projectRoot) {
11297
11850
  for (const sub of KNOWLEDGE_SUBDIRS3) {
11298
- await mkdir6(join11(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
11851
+ await mkdir5(join13(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
11299
11852
  }
11300
11853
  }
11301
11854
  async function fixCounterDesync(projectRoot) {
11302
- const metaPath = join11(projectRoot, ".fabric", "agents.meta.json");
11855
+ const metaPath = join13(projectRoot, ".fabric", "agents.meta.json");
11303
11856
  if (!existsSync8(metaPath)) {
11304
11857
  return;
11305
11858
  }
11306
- let meta;
11307
- try {
11308
- meta = agentsMetaSchema5.parse(JSON.parse(await readFile13(metaPath, "utf8")));
11309
- } catch {
11310
- return;
11311
- }
11312
- const synthetic = {
11313
- present: true,
11314
- valid: true,
11315
- meta,
11316
- revision: meta.revision,
11317
- computedRevision: null,
11318
- ruleCount: 0,
11319
- missingContentRefs: [],
11320
- invalidContentRefs: [],
11321
- stale: false,
11322
- changed: false
11323
- };
11324
- const desync = inspectCounterDesync(synthetic);
11325
- if (desync.desyncs.length === 0 || desync.correctedCounters === null) {
11326
- return;
11327
- }
11328
- const updated = { ...meta, counters: desync.correctedCounters };
11329
- await atomicWriteJson3(metaPath, updated, { indent: 2 });
11859
+ await withFileLock4(`${metaPath}.lock`, async () => {
11860
+ let meta;
11861
+ try {
11862
+ meta = agentsMetaSchema5.parse(JSON.parse(await readFile13(metaPath, "utf8")));
11863
+ } catch {
11864
+ return;
11865
+ }
11866
+ const synthetic = {
11867
+ present: true,
11868
+ valid: true,
11869
+ meta,
11870
+ revision: meta.revision,
11871
+ computedRevision: null,
11872
+ ruleCount: 0,
11873
+ missingContentRefs: [],
11874
+ invalidContentRefs: [],
11875
+ stale: false,
11876
+ changed: false
11877
+ };
11878
+ const desync = inspectCounterDesync(synthetic);
11879
+ if (desync.desyncs.length === 0 || desync.correctedCounters === null) {
11880
+ return;
11881
+ }
11882
+ const updated = { ...meta, counters: desync.correctedCounters };
11883
+ await atomicWriteJson3(metaPath, updated, { indent: 2 });
11884
+ });
11330
11885
  }
11331
11886
  async function ensureEventLedger(projectRoot) {
11332
11887
  const path2 = getEventLedgerPath(projectRoot);
@@ -11364,7 +11919,7 @@ function collectEntryPoints(root) {
11364
11919
  continue;
11365
11920
  }
11366
11921
  for (const entry of readdirSync(current, { withFileTypes: true })) {
11367
- const absolutePath = join11(current, entry.name);
11922
+ const absolutePath = join13(current, entry.name);
11368
11923
  const relativePath = normalizePath(absolutePath.slice(root.length + 1));
11369
11924
  if (relativePath.length === 0) {
11370
11925
  continue;
@@ -11436,8 +11991,8 @@ async function enrichDescriptions(projectRoot, opts = {}) {
11436
11991
  let modified = 0;
11437
11992
  let skipped = 0;
11438
11993
  for (const visit of iterateCanonicalFilenames(projectRoot)) {
11439
- const layerRoot = visit.layer === "team" ? join11(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
11440
- const absPath = join11(layerRoot, visit.type, visit.filename);
11994
+ const layerRoot = visit.layer === "team" ? join13(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
11995
+ const absPath = join13(layerRoot, visit.type, visit.filename);
11441
11996
  scanned += 1;
11442
11997
  let source;
11443
11998
  try {
@@ -11538,6 +12093,144 @@ function yamlQuoteIfNeeded(value) {
11538
12093
  return value;
11539
12094
  }
11540
12095
 
12096
+ // src/services/doctor-conflict.ts
12097
+ import { readFile as readFile14 } from "fs/promises";
12098
+
12099
+ // src/services/conflict-lint.ts
12100
+ var DEFAULT_CONFLICT_SIMILARITY_THRESHOLD = 0.5;
12101
+ function groupKey(entry) {
12102
+ return `${entry.layer}\0${entry.knowledge_type}`;
12103
+ }
12104
+ function pairSimilarity(model, a, b) {
12105
+ const selfA = model.scoreDoc(a.id, a.tokens);
12106
+ const selfB = model.scoreDoc(b.id, b.tokens);
12107
+ if (selfA <= 0 || selfB <= 0) return 0;
12108
+ const aToB = model.scoreDoc(b.id, a.tokens) / selfB;
12109
+ const bToA = model.scoreDoc(a.id, b.tokens) / selfA;
12110
+ const sim = Math.min(aToB, bToA);
12111
+ return sim < 0 ? 0 : sim > 1 ? 1 : sim;
12112
+ }
12113
+ function findConflictCandidates(entries, opts = {}) {
12114
+ const threshold = typeof opts.threshold === "number" && opts.threshold >= 0 && opts.threshold <= 1 ? opts.threshold : DEFAULT_CONFLICT_SIMILARITY_THRESHOLD;
12115
+ const groups = /* @__PURE__ */ new Map();
12116
+ for (const entry of entries) {
12117
+ if (typeof entry.stable_id !== "string" || entry.stable_id.length === 0) continue;
12118
+ const list = groups.get(groupKey(entry)) ?? [];
12119
+ list.push(entry);
12120
+ groups.set(groupKey(entry), list);
12121
+ }
12122
+ const pairs = [];
12123
+ for (const group of groups.values()) {
12124
+ if (group.length < 2) continue;
12125
+ const docs = group.map((e) => ({ id: e.stable_id, tokens: buildQueryTerms(e.text) }));
12126
+ const tokensById = new Map(docs.map((d) => [d.id, d.tokens]));
12127
+ const model = buildBm25Model(docs);
12128
+ for (let i = 0; i < group.length; i += 1) {
12129
+ for (let j = i + 1; j < group.length; j += 1) {
12130
+ const ea = group[i];
12131
+ const eb = group[j];
12132
+ const sim = pairSimilarity(
12133
+ model,
12134
+ { id: ea.stable_id, tokens: tokensById.get(ea.stable_id) ?? [] },
12135
+ { id: eb.stable_id, tokens: tokensById.get(eb.stable_id) ?? [] }
12136
+ );
12137
+ if (sim < threshold) continue;
12138
+ const [a, b] = ea.stable_id <= eb.stable_id ? [ea.stable_id, eb.stable_id] : [eb.stable_id, ea.stable_id];
12139
+ pairs.push({
12140
+ a,
12141
+ b,
12142
+ knowledge_type: ea.knowledge_type,
12143
+ layer: ea.layer,
12144
+ similarity: sim,
12145
+ verdict: "unknown"
12146
+ });
12147
+ }
12148
+ }
12149
+ }
12150
+ pairs.sort((x, y) => y.similarity - x.similarity || (x.a < y.a ? -1 : x.a > y.a ? 1 : x.b < y.b ? -1 : 1));
12151
+ return pairs;
12152
+ }
12153
+ async function lintConflicts(entries, opts = {}) {
12154
+ const candidates = findConflictCandidates(entries, { threshold: opts.threshold });
12155
+ if (opts.judge === void 0 || candidates.length === 0) {
12156
+ return candidates;
12157
+ }
12158
+ const byId = new Map(entries.map((e) => [e.stable_id, e]));
12159
+ const judged = [];
12160
+ for (const pair of candidates) {
12161
+ const ea = byId.get(pair.a);
12162
+ const eb = byId.get(pair.b);
12163
+ if (ea === void 0 || eb === void 0) {
12164
+ judged.push(pair);
12165
+ continue;
12166
+ }
12167
+ try {
12168
+ const verdict = await opts.judge(ea, eb);
12169
+ judged.push({
12170
+ ...pair,
12171
+ verdict: verdict.isConflict ? "conflict" : "similar",
12172
+ rationale: verdict.rationale
12173
+ });
12174
+ } catch {
12175
+ judged.push(pair);
12176
+ }
12177
+ }
12178
+ return judged;
12179
+ }
12180
+
12181
+ // src/services/doctor-conflict.ts
12182
+ function stripFrontmatter(content) {
12183
+ if (!content.startsWith("---")) return content;
12184
+ const end = content.indexOf("\n---", 3);
12185
+ if (end === -1) return content;
12186
+ const after = content.indexOf("\n", end + 1);
12187
+ return after === -1 ? "" : content.slice(after + 1);
12188
+ }
12189
+ async function loadConflictEntries(projectRoot) {
12190
+ let meta;
12191
+ try {
12192
+ meta = await readAgentsMeta(projectRoot);
12193
+ } catch {
12194
+ return [];
12195
+ }
12196
+ const entries = [];
12197
+ for (const node of Object.values(meta.nodes)) {
12198
+ const stableId = node.stable_id;
12199
+ if (typeof stableId !== "string" || stableId.length === 0) continue;
12200
+ const contentRef = node.content_ref;
12201
+ if (typeof contentRef !== "string" || contentRef.length === 0) continue;
12202
+ if (contentRef.includes("/pending/")) continue;
12203
+ const knowledgeType = node.description?.knowledge_type;
12204
+ if (typeof knowledgeType !== "string" || knowledgeType.length === 0) continue;
12205
+ const layer = stableId.startsWith("KP-") ? "personal" : "team";
12206
+ let body;
12207
+ try {
12208
+ body = await readFile14(resolveContentRefPath2(projectRoot, contentRef), "utf8");
12209
+ } catch {
12210
+ continue;
12211
+ }
12212
+ entries.push({ stable_id: stableId, knowledge_type: knowledgeType, layer, text: stripFrontmatter(body) });
12213
+ }
12214
+ return entries;
12215
+ }
12216
+ async function runDoctorConflictLint(projectRoot, opts = {}) {
12217
+ const threshold = typeof opts.threshold === "number" ? opts.threshold : readConflictLintThreshold(projectRoot) ?? DEFAULT_CONFLICT_SIMILARITY_THRESHOLD;
12218
+ const deep = opts.deep === true && opts.judge !== void 0;
12219
+ const entries = await loadConflictEntries(projectRoot);
12220
+ const pairs = await lintConflicts(entries, {
12221
+ threshold,
12222
+ judge: deep ? opts.judge : void 0
12223
+ });
12224
+ return {
12225
+ status: "ok",
12226
+ threshold,
12227
+ deep,
12228
+ candidate_count: pairs.length,
12229
+ conflict_count: pairs.filter((p) => p.verdict === "conflict").length,
12230
+ pairs
12231
+ };
12232
+ }
12233
+
11541
12234
  // src/services/rotation-tick.ts
11542
12235
  var DEFAULT_TICK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
11543
12236
  var tickTimers = /* @__PURE__ */ new Map();
@@ -11573,7 +12266,7 @@ import { IOFabricError as IOFabricError2, RuleError } from "@fenglimg/fabric-sha
11573
12266
 
11574
12267
  // src/services/read-ledger.ts
11575
12268
  import { randomUUID as randomUUID8 } from "crypto";
11576
- import { access as access3, copyFile, readFile as readFile14, rm } from "fs/promises";
12269
+ import { access as access3, copyFile, readFile as readFile15, rm } from "fs/promises";
11577
12270
  import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
11578
12271
  async function resolveLedgerPaths(projectRoot) {
11579
12272
  const primaryPath = getLedgerPath(projectRoot);
@@ -11601,7 +12294,7 @@ async function readLegacyLedger(projectRoot) {
11601
12294
  const { readPath } = await resolveLedgerPaths(projectRoot);
11602
12295
  let raw;
11603
12296
  try {
11604
- raw = await readFile14(readPath, "utf8");
12297
+ raw = await readFile15(readPath, "utf8");
11605
12298
  } catch (error) {
11606
12299
  if (isNodeError(error) && error.code === "ENOENT") {
11607
12300
  return [];
@@ -11858,8 +12551,8 @@ function formatError(error) {
11858
12551
  }
11859
12552
  function formatPreexistingRootMessage(projectRoot) {
11860
12553
  const preexisting = [];
11861
- if (existsSync9(join12(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
11862
- if (existsSync9(join12(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
12554
+ if (existsSync9(join14(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
12555
+ if (existsSync9(join14(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
11863
12556
  if (preexisting.length === 0) return null;
11864
12557
  return `[startup] info: detected ${preexisting.join(", ")} at project root. Note: Fabric serves knowledge from .fabric/knowledge/ via MCP \u2014 root markdown files are not auto-loaded into the AI context.`;
11865
12558
  }
@@ -11887,7 +12580,7 @@ function createFabricServer(tracker) {
11887
12580
  const server = new McpServer(
11888
12581
  {
11889
12582
  name: "fabric-knowledge-server",
11890
- version: "2.2.0-rc.1"
12583
+ version: "2.2.0-rc.3"
11891
12584
  },
11892
12585
  {
11893
12586
  instructions: FABRIC_SERVER_INSTRUCTIONS
@@ -11908,10 +12601,10 @@ function createFabricServer(tracker) {
11908
12601
  },
11909
12602
  async (_uri) => {
11910
12603
  const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
11911
- const path2 = join12(projectRoot, ".fabric", "bootstrap", "README.md");
12604
+ const path2 = join14(projectRoot, ".fabric", "bootstrap", "README.md");
11912
12605
  let text = "";
11913
12606
  if (existsSync9(path2)) {
11914
- text = await readFile15(path2, "utf8");
12607
+ text = await readFile16(path2, "utf8");
11915
12608
  }
11916
12609
  return {
11917
12610
  contents: [
@@ -12016,6 +12709,7 @@ if (isMainModule) {
12016
12709
  }
12017
12710
  export {
12018
12711
  AGENTS_MD_RESOURCE_URI,
12712
+ DEFAULT_CONFLICT_SIMILARITY_THRESHOLD,
12019
12713
  EVENT_LEDGER_PATH,
12020
12714
  FABRIC_SERVER_INSTRUCTIONS,
12021
12715
  KnowledgeIdAllocator,
@@ -12038,6 +12732,7 @@ export {
12038
12732
  enrichDescriptions,
12039
12733
  ensureKnowledgeFresh,
12040
12734
  extractKnowledge,
12735
+ findConflictCandidates,
12041
12736
  flushAndSyncEventLedger,
12042
12737
  flushMetrics,
12043
12738
  formatPreexistingRootMessage,
@@ -12048,7 +12743,10 @@ export {
12048
12743
  getMetricsLedgerPath,
12049
12744
  invalidateKnowledgeSyncCooldown,
12050
12745
  isSameKnowledgeTestIndex,
12746
+ lintConflicts,
12747
+ loadConflictEntries,
12051
12748
  loadKbIdTypeMap,
12749
+ pairSimilarity,
12052
12750
  planContext,
12053
12751
  readAgentsMeta,
12054
12752
  readEventLedger,
@@ -12063,6 +12761,7 @@ export {
12063
12761
  runDoctorApplyLint,
12064
12762
  runDoctorArchiveHistory,
12065
12763
  runDoctorCiteCoverage,
12764
+ runDoctorConflictLint,
12066
12765
  runDoctorFix,
12067
12766
  runDoctorHistoryAll,
12068
12767
  runDoctorReport,