@fenglimg/fabric-server 2.3.0-rc.2 → 2.3.0-rc.4

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 +200 -2
  2. package/dist/index.js +586 -161
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/index.ts
2
- import { existsSync as existsSync9 } from "fs";
3
- import { readFile as readFile20 } from "fs/promises";
4
- import { join as join24, resolve as resolve4 } from "path";
2
+ import { existsSync as existsSync10 } from "fs";
3
+ import { readFile as readFile21 } from "fs/promises";
4
+ import { join as join26, resolve as resolve4 } 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";
@@ -820,9 +820,10 @@ function readOrphanDemoteThresholdDays(projectRoot) {
820
820
  function readFusion(projectRoot) {
821
821
  try {
822
822
  const raw = readFabricConfig(projectRoot).fusion;
823
- return raw === "rrf" ? "rrf" : "additive";
823
+ if (raw === "rrf" || raw === "additive") return raw;
824
+ return "auto";
824
825
  } catch {
825
- return "additive";
826
+ return "auto";
826
827
  }
827
828
  }
828
829
  function readConflictLintThreshold(projectRoot) {
@@ -892,7 +893,7 @@ async function awaitFirstReconcileGate(timeoutMs = 5e3) {
892
893
 
893
894
  // src/services/extract-knowledge.ts
894
895
  import { existsSync as existsSync3 } from "fs";
895
- import { readFile as readFile4 } from "fs/promises";
896
+ import { readFile as readFile5 } from "fs/promises";
896
897
  import { join as join7 } from "path";
897
898
  import {
898
899
  PROPOSED_REASON_DESCRIPTIONS_BY_LOCALE
@@ -900,6 +901,8 @@ import {
900
901
  import { hasSecrets, isPersonalScope, redactPii, resolveGlobalLocale } from "@fenglimg/fabric-shared";
901
902
 
902
903
  // src/services/cross-store-write.ts
904
+ import { createHash as createHash2 } from "crypto";
905
+ import { readFile as readFile3 } from "fs/promises";
903
906
  import { join as join5 } from "path";
904
907
  import {
905
908
  STORE_LAYOUT,
@@ -915,6 +918,7 @@ import {
915
918
  PersonalScopeLeakError,
916
919
  StoreWriteTargetUnresolvedError
917
920
  } from "@fenglimg/fabric-shared/errors";
921
+ import { withFileLock as withFileLock2 } from "@fenglimg/fabric-shared/node/atomic-write";
918
922
  function writeTargetUnresolved(scope, layer) {
919
923
  const actionHint = layer === "personal" ? "run `fabric install --global` to mint your personal store, then retry" : `mount + bind a shared store, then set an explicit route: \`fabric store switch-write <alias> --scope ${scope}\``;
920
924
  return new StoreWriteTargetUnresolvedError(
@@ -977,7 +981,7 @@ function resolveWriteScopeMeta(layer, projectRoot, semanticScope) {
977
981
  }
978
982
 
979
983
  // src/services/cross-store-recall.ts
980
- import { readFile as readFile3, stat } from "fs/promises";
984
+ import { readFile as readFile4, stat } from "fs/promises";
981
985
  import { join as join6 } from "path";
982
986
  import {
983
987
  buildStoreResolveInput as buildStoreResolveInput2,
@@ -1349,7 +1353,7 @@ async function walkReadSetStoresUncached(snapshot) {
1349
1353
  const entries = await Promise.all(snapshot.refs.map(async (ref) => {
1350
1354
  let source;
1351
1355
  try {
1352
- source = await readFile3(ref.file, "utf8");
1356
+ source = await readFile4(ref.file, "utf8");
1353
1357
  } catch {
1354
1358
  return null;
1355
1359
  }
@@ -1621,6 +1625,144 @@ function serializeBm25Model(model) {
1621
1625
  function rehydrateBm25Model(serialized) {
1622
1626
  return modelFromStats(serialized);
1623
1627
  }
1628
+ var SYNONYM_PAIRS = {
1629
+ // Code / engineering actions
1630
+ refactor: ["restructure", "rewrite", "redesign", "reorganize", "clean", "rework"],
1631
+ optimize: ["improve", "speed-up", "tune", "accelerate", "perf", "performance"],
1632
+ debug: ["fix", "troubleshoot", "diagnose", "investigate", "resolve", "correct"],
1633
+ migrate: ["port", "move", "transfer", "upgrade", "transition", "convert"],
1634
+ "set up": ["init", "initialize", "configure", "bootstrap", "install", "onboard"],
1635
+ implement: ["add", "build", "create", "develop", "write", "introduce", "introduce"],
1636
+ // Architecture / design
1637
+ architecture: ["design", "structure", "layout", "organization", "pattern", "system"],
1638
+ "data model": ["schema", "entity", "type", "structure", "contract", "shape"],
1639
+ // Quality / correctness
1640
+ test: ["verify", "validate", "assert", "check", "spec", "coverage"],
1641
+ lint: ["check", "validate", "audit", "inspect", "analyze"],
1642
+ // Communication / impact
1643
+ documentation: ["docs", "readme", "guide", "spec", "explanation", "reference"],
1644
+ decision: ["adr", "rationale", "why", "motivation", "reason", "trade-off"],
1645
+ // Change management
1646
+ release: ["deploy", "ship", "publish", "cut", "version", "tag"],
1647
+ rollback: ["revert", "undo", "back-out", "backout", "restore"],
1648
+ // Containers / infra
1649
+ container: ["docker", "image", "oci", "cri-o"],
1650
+ deploy: ["release", "rollout", "ship", "publish", "promote"],
1651
+ // Project / process
1652
+ on_boarding: ["getting-started", "quickstart", "newcomer", "new-hire", "first-time"],
1653
+ best_practice: ["convention", "guideline", "standard", "rule", "recommendation"],
1654
+ // Tech stacks
1655
+ api: ["endpoint", "route", "service", "interface", "rpc", "rest"],
1656
+ typescript: ["ts", "types", "type-safe"],
1657
+ react: ["jsx", "tsx", "component", "ui"],
1658
+ node: ["nodejs", "runtime", "backend", "server"],
1659
+ database: ["db", "sql", "nosql", "storage", "persistence"]
1660
+ };
1661
+ var STEMMING_PATTERNS = [
1662
+ { suffix: "e", alternatives: ["es", "ed", "ing", "ation"] },
1663
+ { suffix: "y", alternatives: ["ies", "ied", "ying"] }
1664
+ // Catch-all for non-verb / already-stemmed query terms: no-op by default.
1665
+ ];
1666
+ var DEFAULT_IDF_WEIGHT = 1;
1667
+ var COMMON_TERMS = /* @__PURE__ */ new Set([
1668
+ "a",
1669
+ "an",
1670
+ "the",
1671
+ "and",
1672
+ "or",
1673
+ "but",
1674
+ "in",
1675
+ "on",
1676
+ "at",
1677
+ "to",
1678
+ "for",
1679
+ "of",
1680
+ "with",
1681
+ "by",
1682
+ "from",
1683
+ "as",
1684
+ "is",
1685
+ "it",
1686
+ "at",
1687
+ "be",
1688
+ "do",
1689
+ "has",
1690
+ "have",
1691
+ "was",
1692
+ "are",
1693
+ "been",
1694
+ "this",
1695
+ "that",
1696
+ "these",
1697
+ "those",
1698
+ "will",
1699
+ "can",
1700
+ "may",
1701
+ "would",
1702
+ "could",
1703
+ "should",
1704
+ "does",
1705
+ "not",
1706
+ "no",
1707
+ "if",
1708
+ "so",
1709
+ "up",
1710
+ "out",
1711
+ "all",
1712
+ "each",
1713
+ "every",
1714
+ "both",
1715
+ "some",
1716
+ "any",
1717
+ "such",
1718
+ "only",
1719
+ "own",
1720
+ "same",
1721
+ "too",
1722
+ "very",
1723
+ "just",
1724
+ "about",
1725
+ "over",
1726
+ "than",
1727
+ "then",
1728
+ "also"
1729
+ ]);
1730
+ function idfWeight(term) {
1731
+ return COMMON_TERMS.has(term) ? 0.3 : DEFAULT_IDF_WEIGHT;
1732
+ }
1733
+ function expandQueryTerms(text) {
1734
+ const baseTerms = tokenize(text);
1735
+ const weighted = /* @__PURE__ */ new Map();
1736
+ for (const term of baseTerms) {
1737
+ const lower = term.toLowerCase();
1738
+ const aliases = /* @__PURE__ */ new Set();
1739
+ const syns = SYNONYM_PAIRS[lower];
1740
+ if (syns !== void 0) {
1741
+ for (const s of syns) aliases.add(s);
1742
+ }
1743
+ for (const [key, values] of Object.entries(SYNONYM_PAIRS)) {
1744
+ if (values.includes(lower)) {
1745
+ aliases.add(key);
1746
+ }
1747
+ }
1748
+ for (const { suffix, alternatives } of STEMMING_PATTERNS) {
1749
+ if (lower.endsWith(suffix) && lower.length > suffix.length) {
1750
+ const stem = lower.slice(0, -suffix.length);
1751
+ for (const alt of alternatives) {
1752
+ aliases.add(stem + alt);
1753
+ }
1754
+ }
1755
+ }
1756
+ const originalWeight = idfWeight(lower);
1757
+ weighted.set(term, originalWeight);
1758
+ for (const alias of aliases) {
1759
+ if (!weighted.has(alias)) {
1760
+ weighted.set(alias, originalWeight * 0.5);
1761
+ }
1762
+ }
1763
+ }
1764
+ return weighted;
1765
+ }
1624
1766
  function buildQueryTerms(text) {
1625
1767
  return tokenize(text);
1626
1768
  }
@@ -2020,7 +2162,7 @@ async function extractKnowledge(projectRoot, input) {
2020
2162
  const writeScopeMeta = resolveWriteScopeMeta(layer, projectRoot, semanticScope);
2021
2163
  await ensureParentDirectory(absolutePath);
2022
2164
  if (existsSync3(absolutePath)) {
2023
- const existing = await readFile4(absolutePath, "utf8");
2165
+ const existing = await readFile5(absolutePath, "utf8");
2024
2166
  const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
2025
2167
  if (existingKey === effectiveIdempotencyKey) {
2026
2168
  const fresh2 = renderFreshEntry({
@@ -2153,7 +2295,7 @@ async function resolveDisambiguatedSlugPath(args) {
2153
2295
  idempotencyKey: candidateKey
2154
2296
  };
2155
2297
  }
2156
- const existing = await readFile4(candidatePath, "utf8");
2298
+ const existing = await readFile5(candidatePath, "utf8");
2157
2299
  const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
2158
2300
  if (existingKey === candidateKey) {
2159
2301
  return {
@@ -2751,12 +2893,24 @@ var LEDGER_DUAL_WRITE_METRIC_NAMES = {
2751
2893
  };
2752
2894
 
2753
2895
  // src/services/plan-context.ts
2754
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
2755
- import { join as join9 } from "path";
2896
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
2897
+ import { join as join10 } from "path";
2756
2898
 
2757
2899
  // src/services/vector-retrieval.ts
2900
+ import { mkdirSync } from "fs";
2901
+ import { createRequire } from "module";
2902
+ import { join as join9 } from "path";
2903
+ import { resolveGlobalRoot as resolveGlobalRoot3 } from "@fenglimg/fabric-shared";
2758
2904
  var embedderLoad;
2759
2905
  var OPTIONAL_EMBED_PACKAGE = "fastembed";
2906
+ function isEmbedderResolvable() {
2907
+ try {
2908
+ createRequire(import.meta.url).resolve(OPTIONAL_EMBED_PACKAGE);
2909
+ return true;
2910
+ } catch {
2911
+ return false;
2912
+ }
2913
+ }
2760
2914
  var embedderModuleLoader = (name) => import(name);
2761
2915
  var MISSING_EMBEDDER_HINT = "[fabric] vector semantic recall is enabled but the optional 'fastembed' package is unavailable \u2014 falling back to text-only ranking. Install it where the server resolves modules (e.g. `npm i -g fastembed`) to enable embeddings, or set embed_enabled:false to silence this.\n";
2762
2916
  var defaultMissingEmbedderHint = () => {
@@ -2771,10 +2925,13 @@ function hintMissingEmbedderOnce() {
2771
2925
  missingEmbedderHinted = true;
2772
2926
  emitMissingEmbedderHint();
2773
2927
  }
2928
+ function defaultEmbedCacheDir() {
2929
+ return join9(resolveGlobalRoot3(), "cache", "embed");
2930
+ }
2774
2931
  function buildEmbedInitOptions(modelName) {
2775
2932
  return {
2776
2933
  maxLength: 512,
2777
- cacheDir: process.env.FABRIC_EMBED_CACHE_DIR,
2934
+ cacheDir: process.env.FABRIC_EMBED_CACHE_DIR ?? defaultEmbedCacheDir(),
2778
2935
  ...typeof modelName === "string" && modelName.length > 0 ? { model: modelName } : {}
2779
2936
  };
2780
2937
  }
@@ -2788,7 +2945,9 @@ async function loadEmbedder(modelName) {
2788
2945
  hintMissingEmbedderOnce();
2789
2946
  return null;
2790
2947
  }
2791
- const model = await mod.FlagEmbedding.init(buildEmbedInitOptions(modelName));
2948
+ const initOpts = buildEmbedInitOptions(modelName);
2949
+ mkdirSync(initOpts.cacheDir, { recursive: true });
2950
+ const model = await mod.FlagEmbedding.init(initOpts);
2792
2951
  return {
2793
2952
  async embed(texts) {
2794
2953
  const out = [];
@@ -3157,11 +3316,11 @@ var bm25BuildCount = 0;
3157
3316
  var BM25_CACHE_DIR = ".fabric/cache/bm25";
3158
3317
  function bm25CachePath(projectRoot, revision) {
3159
3318
  const safe = revision.replace(/[^A-Za-z0-9_-]/g, "_");
3160
- return join9(projectRoot, BM25_CACHE_DIR, `${safe}.json`);
3319
+ return join10(projectRoot, BM25_CACHE_DIR, `${safe}.json`);
3161
3320
  }
3162
3321
  async function loadBm25ModelFromDisk(projectRoot, revision) {
3163
3322
  try {
3164
- const raw = await readFile5(bm25CachePath(projectRoot, revision), "utf8");
3323
+ const raw = await readFile6(bm25CachePath(projectRoot, revision), "utf8");
3165
3324
  const parsed = JSON.parse(raw);
3166
3325
  if (parsed.version !== 1) return null;
3167
3326
  return rehydrateBm25Model(parsed);
@@ -3172,7 +3331,7 @@ async function loadBm25ModelFromDisk(projectRoot, revision) {
3172
3331
  async function saveBm25ModelToDisk(projectRoot, revision, model) {
3173
3332
  try {
3174
3333
  const path = bm25CachePath(projectRoot, revision);
3175
- await mkdir4(join9(projectRoot, BM25_CACHE_DIR), { recursive: true });
3334
+ await mkdir4(join10(projectRoot, BM25_CACHE_DIR), { recursive: true });
3176
3335
  await writeFile2(path, JSON.stringify(serializeBm25Model(model)), "utf8");
3177
3336
  } catch {
3178
3337
  }
@@ -3271,7 +3430,13 @@ async function buildScoringContext(projectRoot, revision, rawItems, opts) {
3271
3430
  scoringContext.vectorWeight = embedConfig.weight;
3272
3431
  }
3273
3432
  }
3274
- scoringContext.fusion = readFusion(projectRoot);
3433
+ const configuredFusion = readFusion(projectRoot);
3434
+ const vectorActive = scoringContext.vectorScores !== void 0 && scoringContext.vectorScores.size > 0;
3435
+ scoringContext.fusion = configuredFusion === "auto" ? vectorActive ? "rrf" : "additive" : configuredFusion;
3436
+ let queryTermWeights;
3437
+ if (scoringContext.queryTerms.length > 0) {
3438
+ queryTermWeights = expandQueryTerms(opts.queryText);
3439
+ }
3275
3440
  if (scoringContext.fusion === "rrf" && scoringContext.queryTerms.length > 0 && rawItems.length > 0) {
3276
3441
  const rankIds = rawItems.map((item) => item.stable_id).sort((a, b) => compareStableIds(a, b));
3277
3442
  if (scoringContext.bm25 !== void 0) {
@@ -3283,6 +3448,49 @@ async function buildScoringContext(projectRoot, revision, rawItems, opts) {
3283
3448
  }
3284
3449
  return scoringContext;
3285
3450
  }
3451
+ var PROXIMITY_WINDOW = 6;
3452
+ var PROXIMITY_BOOST_CAP = 0.15;
3453
+ function proximityBoost(item, context, contentScore2) {
3454
+ if (contentScore2 <= 0) return 0;
3455
+ const text = context.docTexts?.get(item.stable_id);
3456
+ if (text === void 0 || text.length === 0) return 0;
3457
+ const tokens = text.toLowerCase().split(/[^a-z0-9_$#]+/u).filter(Boolean);
3458
+ const queryTerms = context.queryTerms;
3459
+ if (queryTerms.length < 2) return 0;
3460
+ const positions = /* @__PURE__ */ new Map();
3461
+ for (const qt of queryTerms) {
3462
+ const qtLower = qt.toLowerCase();
3463
+ const pos = [];
3464
+ for (let i = 0; i < tokens.length; i++) {
3465
+ if (tokens[i] === qtLower) {
3466
+ pos.push(i);
3467
+ }
3468
+ }
3469
+ if (pos.length > 0) {
3470
+ positions.set(qtLower, pos);
3471
+ }
3472
+ }
3473
+ if (positions.size < 2) return 0;
3474
+ const termList = [...positions.entries()];
3475
+ let minDist = Infinity;
3476
+ for (let i = 0; i < termList.length; i++) {
3477
+ const [, posI] = termList[i];
3478
+ for (let j = i + 1; j < termList.length; j++) {
3479
+ const [, posJ] = termList[j];
3480
+ for (const pi of posI) {
3481
+ for (const pj of posJ) {
3482
+ const dist = Math.abs(pi - pj);
3483
+ if (dist < minDist) minDist = dist;
3484
+ }
3485
+ }
3486
+ }
3487
+ }
3488
+ if (!Number.isFinite(minDist)) return 0;
3489
+ if (minDist >= PROXIMITY_WINDOW) return 0;
3490
+ const ratio = 1 - minDist / PROXIMITY_WINDOW;
3491
+ const boost = contentScore2 * PROXIMITY_BOOST_CAP * ratio;
3492
+ return boost;
3493
+ }
3286
3494
  function sortDescriptionItems(rawItems, scoringContext) {
3287
3495
  if (scoringContext === void 0) {
3288
3496
  return [...rawItems].sort((left, right) => compareStableIds(left.stable_id, right.stable_id)).map((item) => ({ item, score: 0 }));
@@ -3425,7 +3633,8 @@ function structuralScaleFor(context) {
3425
3633
  function scoreDescriptionItem(item, context) {
3426
3634
  const content = contentScore(item, context);
3427
3635
  const structural = salienceScore(item) + recencyBoost(item, context) + localityBoost(item, context);
3428
- return content + structuralScaleFor(context) * structural;
3636
+ const proximity = proximityBoost(item, context, content);
3637
+ return content + structuralScaleFor(context) * structural + proximity;
3429
3638
  }
3430
3639
  function scoreBreakdownForItem(item, context) {
3431
3640
  const hasQuery = context.queryTerms.length > 0;
@@ -3907,9 +4116,9 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
3907
4116
  // src/services/review.ts
3908
4117
  import { execFileSync } from "child_process";
3909
4118
  import { existsSync as existsSync5 } from "fs";
3910
- import { readFile as readFile7, readdir as readdir2, stat as stat2, unlink as unlink2 } from "fs/promises";
4119
+ import { readFile as readFile8, readdir as readdir2, stat as stat2, unlink as unlink2 } from "fs/promises";
3911
4120
  import { homedir } from "os";
3912
- import { basename, isAbsolute, join as join11, relative, resolve as resolve2, sep as sep2 } from "path";
4121
+ import { basename, isAbsolute, join as join12, relative, resolve as resolve2, sep as sep2 } from "path";
3913
4122
 
3914
4123
  // src/services/promotion-gate.ts
3915
4124
  function toLocalId(id) {
@@ -3949,8 +4158,8 @@ import { allocateStoreKnowledgeId, isPersonalScope as isPersonalScope2 } from "@
3949
4158
 
3950
4159
  // src/services/pending-dedupe.ts
3951
4160
  import { existsSync as existsSync4 } from "fs";
3952
- import { readdir, readFile as readFile6, unlink } from "fs/promises";
3953
- import { join as join10 } from "path";
4161
+ import { readdir, readFile as readFile7, unlink } from "fs/promises";
4162
+ import { join as join11 } from "path";
3954
4163
  var PENDING_TYPES = ["decisions", "pitfalls", "guidelines", "models", "processes"];
3955
4164
  var DISAMBIGUATION_SUFFIX = /^(.+)-([2-9])\.md$/u;
3956
4165
  var SOURCE_SESSIONS_LINE = /^source_sessions:\s*\[(.*)\]\s*$/mu;
@@ -4023,7 +4232,7 @@ async function mergePendingTwins(projectRoot) {
4023
4232
  continue;
4024
4233
  }
4025
4234
  for (const type of PENDING_TYPES) {
4026
- const dir = join10(pendingBase2, type);
4235
+ const dir = join11(pendingBase2, type);
4027
4236
  if (!existsSync4(dir)) continue;
4028
4237
  let names;
4029
4238
  try {
@@ -4044,10 +4253,10 @@ async function mergePendingTwins(projectRoot) {
4044
4253
  if (groupNames.length < 2) continue;
4045
4254
  const parsed = [];
4046
4255
  for (const name of groupNames) {
4047
- const abs = join10(dir, name);
4256
+ const abs = join11(dir, name);
4048
4257
  let content;
4049
4258
  try {
4050
- content = await readFile6(abs, "utf8");
4259
+ content = await readFile7(abs, "utf8");
4051
4260
  } catch {
4052
4261
  continue;
4053
4262
  }
@@ -4227,7 +4436,7 @@ async function listPending(projectRoot, filters) {
4227
4436
  }
4228
4437
  for (const source of sources) {
4229
4438
  for (const type of typesToScan) {
4230
- const dir = join11(source.root, type);
4439
+ const dir = join12(source.root, type);
4231
4440
  if (!existsSync5(dir)) {
4232
4441
  continue;
4233
4442
  }
@@ -4239,10 +4448,10 @@ async function listPending(projectRoot, filters) {
4239
4448
  }
4240
4449
  for (const name of entries) {
4241
4450
  if (!name.endsWith(".md")) continue;
4242
- const absolutePath = join11(dir, name);
4451
+ const absolutePath = join12(dir, name);
4243
4452
  let content;
4244
4453
  try {
4245
- content = await readFile7(absolutePath, "utf8");
4454
+ content = await readFile8(absolutePath, "utf8");
4246
4455
  } catch {
4247
4456
  continue;
4248
4457
  }
@@ -4352,7 +4561,7 @@ async function approveOne(projectRoot, pendingPath) {
4352
4561
  let targetAbs;
4353
4562
  let writtenTarget = false;
4354
4563
  try {
4355
- const content = await readFile7(sourceAbs, "utf8");
4564
+ const content = await readFile8(sourceAbs, "utf8");
4356
4565
  const fm = parseFrontmatter(content);
4357
4566
  const pluralType = fm.type;
4358
4567
  if (pluralType === void 0 || !PLURAL_TYPES.includes(pluralType)) {
@@ -4366,7 +4575,7 @@ async function approveOne(projectRoot, pendingPath) {
4366
4575
  );
4367
4576
  allocatedId = stableId;
4368
4577
  const newFilename = `${stableId}--${slug}.md`;
4369
- targetAbs = join11(resolveStoreCanonicalBase(layer, projectRoot), pluralType, newFilename);
4578
+ targetAbs = join12(resolveStoreCanonicalBase(layer, projectRoot), pluralType, newFilename);
4370
4579
  await ensureParentDirectory(targetAbs);
4371
4580
  const rewritten = rewriteFrontmatterMerge(
4372
4581
  rewriteFrontmatterForPromote(content, stableId),
@@ -4424,7 +4633,7 @@ async function rejectAll(projectRoot, pendingPaths, reason) {
4424
4633
  try {
4425
4634
  const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
4426
4635
  if (existsSync5(sandboxed.abs)) {
4427
- const content = await readFile7(sandboxed.abs, "utf8");
4636
+ const content = await readFile8(sandboxed.abs, "utf8");
4428
4637
  const merged = rewriteFrontmatterMerge(content, { status: "rejected" });
4429
4638
  const rejectedAbs = sandboxed.abs.includes(`${sep2}pending${sep2}`) ? sandboxed.abs.replace(`${sep2}pending${sep2}`, `${sep2}rejected${sep2}`) : null;
4430
4639
  if (rejectedAbs !== null) {
@@ -4451,7 +4660,7 @@ async function modifyEntry(projectRoot, pendingPath, changes) {
4451
4660
  if (target === null) {
4452
4661
  throw new Error(`modify target not found: ${pendingPath}`);
4453
4662
  }
4454
- const content = await readFile7(target.absPath, "utf8");
4663
+ const content = await readFile8(target.absPath, "utf8");
4455
4664
  const fm = parseFrontmatter(content);
4456
4665
  const currentLayer = fm.layer ?? "team";
4457
4666
  if (fm.maturity === "verified" && changes.maturity === "proven" && fm.id !== void 0) {
@@ -4553,7 +4762,7 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
4553
4762
  pluralType,
4554
4763
  resolveWriteTargetStoreDir(toLayer, projectRoot)
4555
4764
  );
4556
- const toAbs = join11(
4765
+ const toAbs = join12(
4557
4766
  resolveStoreCanonicalBase(toLayer, projectRoot),
4558
4767
  pluralType,
4559
4768
  `${newStableId}--${slug}.md`
@@ -4653,7 +4862,7 @@ function getSearchDirectoryCache(cacheKey) {
4653
4862
  return created;
4654
4863
  }
4655
4864
  async function listIndexedSearchEntries(source, type) {
4656
- const dir = join11(source.root, type);
4865
+ const dir = join12(source.root, type);
4657
4866
  let entries;
4658
4867
  try {
4659
4868
  entries = await readdir2(dir);
@@ -4666,7 +4875,7 @@ async function listIndexedSearchEntries(source, type) {
4666
4875
  const indexed = [];
4667
4876
  for (const name of entries) {
4668
4877
  if (!name.endsWith(".md")) continue;
4669
- const absolutePath = join11(dir, name);
4878
+ const absolutePath = join12(dir, name);
4670
4879
  let fingerprint;
4671
4880
  try {
4672
4881
  const st = await stat2(absolutePath);
@@ -4683,7 +4892,7 @@ async function listIndexedSearchEntries(source, type) {
4683
4892
  }
4684
4893
  let content;
4685
4894
  try {
4686
- content = await readFile7(absolutePath, "utf8");
4895
+ content = await readFile8(absolutePath, "utf8");
4687
4896
  searchEntryIndexContentReads += 1;
4688
4897
  } catch {
4689
4898
  directoryCache.files.delete(absolutePath);
@@ -4852,7 +5061,7 @@ async function deferAll(projectRoot, pendingPaths, until, reason) {
4852
5061
  try {
4853
5062
  const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
4854
5063
  if (existsSync5(sandboxed.abs)) {
4855
- const content = await readFile7(sandboxed.abs, "utf8");
5064
+ const content = await readFile8(sandboxed.abs, "utf8");
4856
5065
  stableId = parseFrontmatter(content).id;
4857
5066
  const patch = {
4858
5067
  status: "deferred",
@@ -5193,9 +5402,9 @@ function registerPending(server, tracker) {
5193
5402
  }
5194
5403
 
5195
5404
  // src/services/doctor.ts
5196
- import { access as access4, readFile as readFile17, readdir as readdirAsync, stat as statAsync, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
5405
+ import { access as access4, readFile as readFile18, readdir as readdirAsync, stat as statAsync, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
5197
5406
  import { constants as constants2 } from "fs";
5198
- import { isAbsolute as isAbsolute2, join as join22, posix as posix5, resolve as resolve3 } from "path";
5407
+ import { isAbsolute as isAbsolute2, join as join23, posix as posix5, resolve as resolve3 } from "path";
5199
5408
  import {
5200
5409
  createTranslator,
5201
5410
  forensicReportSchema,
@@ -5409,13 +5618,13 @@ function createKnowledgeSummaryOpaqueCheck(t, inspection) {
5409
5618
  }
5410
5619
 
5411
5620
  // src/services/doctor-stable-id-collision.ts
5412
- import { readFile as readFile8 } from "fs/promises";
5413
- import { basename as basename2, join as join12 } from "path";
5621
+ import { readFile as readFile9 } from "fs/promises";
5622
+ import { basename as basename2, join as join13 } from "path";
5414
5623
  import {
5415
5624
  buildStoreResolveInput as buildStoreResolveInput4,
5416
5625
  createStoreResolver as createStoreResolver4,
5417
5626
  readKnowledgeAcrossStores as readKnowledgeAcrossStores2,
5418
- resolveGlobalRoot as resolveGlobalRoot3,
5627
+ resolveGlobalRoot as resolveGlobalRoot4,
5419
5628
  storeRelativePathForMount as storeRelativePathForMount3
5420
5629
  } from "@fenglimg/fabric-shared";
5421
5630
  var ID_LINE = /^id:\s*"?([^"\n]+?)"?\s*$/mu;
@@ -5431,13 +5640,13 @@ function resolveIntegrityStores(projectRoot) {
5431
5640
  const personalUuids = new Set(
5432
5641
  input.mountedStores.filter((s) => s.personal).map((s) => s.store_uuid)
5433
5642
  );
5434
- const globalRoot = resolveGlobalRoot3();
5643
+ const globalRoot = resolveGlobalRoot4();
5435
5644
  const dirs = readSet.stores.map((entry) => {
5436
5645
  const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
5437
5646
  return {
5438
5647
  store_uuid: entry.store_uuid,
5439
5648
  alias: entry.alias,
5440
- dir: join12(globalRoot, storeRelativePathForMount3(mounted ?? { store_uuid: entry.store_uuid }))
5649
+ dir: join13(globalRoot, storeRelativePathForMount3(mounted ?? { store_uuid: entry.store_uuid }))
5441
5650
  };
5442
5651
  });
5443
5652
  return { dirs, personalUuids };
@@ -5456,7 +5665,7 @@ async function inspectStoreStableIdIntegrity(projectRoot) {
5456
5665
  for (const ref of await readKnowledgeAcrossStores2(resolved.dirs)) {
5457
5666
  let source;
5458
5667
  try {
5459
- source = await readFile8(ref.file, "utf8");
5668
+ source = await readFile9(ref.file, "utf8");
5460
5669
  } catch {
5461
5670
  continue;
5462
5671
  }
@@ -5541,7 +5750,7 @@ function createLayerMismatchCheck(t, inspection) {
5541
5750
  // src/services/doctor-relevance-paths.ts
5542
5751
  import { execFileSync as execFileSync2 } from "child_process";
5543
5752
  import { existsSync as existsSync6, readdirSync, statSync as statSync3 } from "fs";
5544
- import { join as join13, sep as sep3 } from "path";
5753
+ import { join as join14, sep as sep3 } from "path";
5545
5754
  import { minimatch } from "minimatch";
5546
5755
  var MS_PER_DAY = 24 * 60 * 60 * 1e3;
5547
5756
  var RELEVANCE_PATHS_DRIFT_WINDOW_DAYS = 90;
@@ -5584,7 +5793,7 @@ function collectWorkspacePaths(projectRoot) {
5584
5793
  continue;
5585
5794
  }
5586
5795
  for (const entry of entries) {
5587
- const abs = join13(current, entry.name);
5796
+ const abs = join14(current, entry.name);
5588
5797
  const rel = toPosix(abs.slice(projectRoot.length + 1));
5589
5798
  if (rel.length === 0) continue;
5590
5799
  if (entry.isDirectory()) {
@@ -5777,16 +5986,16 @@ function createNarrowNoPathsCheck(t, inspection) {
5777
5986
  }
5778
5987
 
5779
5988
  // src/services/doctor-broad-index.ts
5780
- import { readFile as readFile9 } from "fs/promises";
5781
- import { join as join14 } from "path";
5989
+ import { readFile as readFile10 } from "fs/promises";
5990
+ import { join as join15 } from "path";
5782
5991
  var DEFAULT_BROAD_INDEX_BACKSTOP = 50;
5783
5992
  var BROAD_INDEX_BACKSTOP_MIN = 20;
5784
5993
  var BROAD_INDEX_BACKSTOP_MAX = 500;
5785
5994
  var BROAD_INDEX_DRIFT_RATIO = 0.8;
5786
5995
  async function readBroadIndexBackstop(projectRoot) {
5787
- const configPath = join14(projectRoot, ".fabric", "fabric-config.json");
5996
+ const configPath = join15(projectRoot, ".fabric", "fabric-config.json");
5788
5997
  try {
5789
- const raw = await readFile9(configPath, "utf8");
5998
+ const raw = await readFile10(configPath, "utf8");
5790
5999
  const parsed = JSON.parse(raw);
5791
6000
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
5792
6001
  const v = parsed.broad_index_backstop;
@@ -6103,15 +6312,15 @@ function createBroadReviewRecheckCheck(t, inspection) {
6103
6312
  }
6104
6313
 
6105
6314
  // src/services/doctor-scope-lint.ts
6106
- import { readFile as readFile10 } from "fs/promises";
6107
- import { join as join15 } from "path";
6315
+ import { readFile as readFile11 } from "fs/promises";
6316
+ import { join as join16 } from "path";
6108
6317
  import {
6109
6318
  buildStoreResolveInput as buildStoreResolveInput5,
6110
6319
  createStoreResolver as createStoreResolver5,
6111
6320
  isPersonalScope as isPersonalScope3,
6112
6321
  readKnowledgeAcrossStores as readKnowledgeAcrossStores3,
6113
6322
  readStoreProjects,
6114
- resolveGlobalRoot as resolveGlobalRoot4,
6323
+ resolveGlobalRoot as resolveGlobalRoot5,
6115
6324
  scopeRoot as scopeRoot2,
6116
6325
  storeRelativePathForMount as storeRelativePathForMount4
6117
6326
  } from "@fenglimg/fabric-shared";
@@ -6136,10 +6345,10 @@ async function resolveLintStores(projectRoot) {
6136
6345
  const personalUuids = new Set(
6137
6346
  input.mountedStores.filter((s) => s.personal).map((s) => s.store_uuid)
6138
6347
  );
6139
- const globalRoot = resolveGlobalRoot4();
6348
+ const globalRoot = resolveGlobalRoot5();
6140
6349
  return Promise.all(readSet.stores.map(async (entry) => {
6141
6350
  const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
6142
- const dir = join15(
6351
+ const dir = join16(
6143
6352
  globalRoot,
6144
6353
  storeRelativePathForMount4(mounted ?? { store_uuid: entry.store_uuid })
6145
6354
  );
@@ -6171,7 +6380,7 @@ async function lintStoreScopes(projectRoot) {
6171
6380
  }
6172
6381
  let source;
6173
6382
  try {
6174
- source = await readFile10(ref.file, "utf8");
6383
+ source = await readFile11(ref.file, "utf8");
6175
6384
  } catch {
6176
6385
  continue;
6177
6386
  }
@@ -6294,7 +6503,7 @@ function createUnboundProjectCheck(t, violation) {
6294
6503
 
6295
6504
  // src/services/doctor-store-counters.ts
6296
6505
  import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
6297
- import { join as join16 } from "path";
6506
+ import { join as join17 } from "path";
6298
6507
  import {
6299
6508
  buildStoreResolveInput as buildStoreResolveInput6,
6300
6509
  createStoreResolver as createStoreResolver6,
@@ -6303,7 +6512,7 @@ import {
6303
6512
  parseKnowledgeId as parseKnowledgeId2,
6304
6513
  readStoreCounters,
6305
6514
  reconcileStoreCounters,
6306
- resolveGlobalRoot as resolveGlobalRoot5,
6515
+ resolveGlobalRoot as resolveGlobalRoot6,
6307
6516
  STORE_KNOWLEDGE_TYPE_DIRS,
6308
6517
  STORE_LAYOUT as STORE_LAYOUT2,
6309
6518
  storeRelativePathForMount as storeRelativePathForMount5
@@ -6317,11 +6526,11 @@ function resolveCounterStores(projectRoot) {
6317
6526
  if (readSet.stores.length === 0) {
6318
6527
  return [];
6319
6528
  }
6320
- const globalRoot = resolveGlobalRoot5();
6529
+ const globalRoot = resolveGlobalRoot6();
6321
6530
  return readSet.stores.map((entry) => ({
6322
6531
  uuid: entry.store_uuid,
6323
6532
  alias: entry.alias,
6324
- dir: join16(
6533
+ dir: join17(
6325
6534
  globalRoot,
6326
6535
  storeRelativePathForMount5(
6327
6536
  input.mountedStores.find((s) => s.store_uuid === entry.store_uuid) ?? {
@@ -6349,7 +6558,7 @@ function readEntryId(file) {
6349
6558
  function computeStoreDiskMax(storeDir) {
6350
6559
  const max = defaultAgentsMetaCounters();
6351
6560
  for (const type of STORE_KNOWLEDGE_TYPE_DIRS) {
6352
- const dir = join16(storeDir, STORE_LAYOUT2.knowledgeDir, type);
6561
+ const dir = join17(storeDir, STORE_LAYOUT2.knowledgeDir, type);
6353
6562
  if (!existsSync7(dir)) {
6354
6563
  continue;
6355
6564
  }
@@ -6363,7 +6572,7 @@ function computeStoreDiskMax(storeDir) {
6363
6572
  if (!name.endsWith(".md")) {
6364
6573
  continue;
6365
6574
  }
6366
- const parsed = parseKnowledgeId2(readEntryId(join16(dir, name)) ?? "");
6575
+ const parsed = parseKnowledgeId2(readEntryId(join17(dir, name)) ?? "");
6367
6576
  if (parsed === null) {
6368
6577
  continue;
6369
6578
  }
@@ -6446,14 +6655,14 @@ function createStoreCounterCheck(t, drifts) {
6446
6655
 
6447
6656
  // src/services/doctor-store-orphan.ts
6448
6657
  import { readdirSync as readdirSync3 } from "fs";
6449
- import { join as join17 } from "path";
6658
+ import { join as join18 } from "path";
6450
6659
  import {
6451
6660
  STORES_ROOT_DIR,
6452
6661
  addMountedStore,
6453
6662
  disambiguateAlias,
6454
6663
  loadGlobalConfig,
6455
6664
  readStoreIdentity,
6456
- resolveGlobalRoot as resolveGlobalRoot6,
6665
+ resolveGlobalRoot as resolveGlobalRoot7,
6457
6666
  saveGlobalConfig
6458
6667
  } from "@fenglimg/fabric-shared";
6459
6668
  var STORE_BY_ALIAS_DIR = "by-alias";
@@ -6464,21 +6673,21 @@ function listDir(dir) {
6464
6673
  return [];
6465
6674
  }
6466
6675
  }
6467
- function inspectStoreOrphans(globalRoot = resolveGlobalRoot6()) {
6676
+ function inspectStoreOrphans(globalRoot = resolveGlobalRoot7()) {
6468
6677
  const config = loadGlobalConfig(globalRoot);
6469
6678
  if (config === null) {
6470
6679
  return [];
6471
6680
  }
6472
6681
  const registered = new Set(config.stores.map((s) => s.store_uuid));
6473
- const storesRoot = join17(globalRoot, STORES_ROOT_DIR);
6682
+ const storesRoot = join18(globalRoot, STORES_ROOT_DIR);
6474
6683
  const orphans = [];
6475
6684
  for (const group of listDir(storesRoot)) {
6476
6685
  if (group === STORE_BY_ALIAS_DIR) {
6477
6686
  continue;
6478
6687
  }
6479
- const groupDir = join17(storesRoot, group);
6688
+ const groupDir = join18(storesRoot, group);
6480
6689
  for (const mount of listDir(groupDir)) {
6481
- const dir = join17(groupDir, mount);
6690
+ const dir = join18(groupDir, mount);
6482
6691
  const identity = readStoreIdentity(dir);
6483
6692
  if (identity === null || registered.has(identity.store_uuid)) {
6484
6693
  continue;
@@ -6488,7 +6697,7 @@ function inspectStoreOrphans(globalRoot = resolveGlobalRoot6()) {
6488
6697
  }
6489
6698
  return orphans;
6490
6699
  }
6491
- function fixStoreOrphans(globalRoot = resolveGlobalRoot6()) {
6700
+ function fixStoreOrphans(globalRoot = resolveGlobalRoot7()) {
6492
6701
  let config = loadGlobalConfig(globalRoot);
6493
6702
  if (config === null) {
6494
6703
  return [];
@@ -6615,8 +6824,8 @@ async function inspectEventsJsonlGates(projectRoot, options = {}) {
6615
6824
  }
6616
6825
 
6617
6826
  // src/services/doctor-skill-lints.ts
6618
- import { readdir as readdir3, readFile as readFile11 } from "fs/promises";
6619
- import { join as join18, posix as posix2 } from "path";
6827
+ import { readdir as readdir3, readFile as readFile12 } from "fs/promises";
6828
+ import { join as join19, posix as posix2 } from "path";
6620
6829
  var FABRIC_SKILL_SLUGS = ["fabric-archive", "fabric-review"];
6621
6830
  var SKILL_MD_FRONTMATTER_ROOTS = [".claude/skills", ".codex/skills"];
6622
6831
  var SKILL_FRONTMATTER_KEY_PATTERN = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]+(.+?)[ \t]*$/u;
@@ -6646,8 +6855,8 @@ async function listMarkdownFiles(dir) {
6646
6855
  async function inspectSkillRefMirror(projectRoot) {
6647
6856
  const driftedPaths = [];
6648
6857
  for (const slug of FABRIC_SKILL_SLUGS) {
6649
- const claudeRef = join18(projectRoot, ".claude", "skills", slug, "ref");
6650
- const codexRef = join18(projectRoot, ".codex", "skills", slug, "ref");
6858
+ const claudeRef = join19(projectRoot, ".claude", "skills", slug, "ref");
6859
+ const codexRef = join19(projectRoot, ".codex", "skills", slug, "ref");
6651
6860
  const [claudeFiles, codexFiles] = await Promise.all([listMarkdownFiles(claudeRef), listMarkdownFiles(codexRef)]);
6652
6861
  if (claudeFiles === null || codexFiles === null) continue;
6653
6862
  const claudeSet = new Set(claudeFiles);
@@ -6664,8 +6873,8 @@ async function inspectSkillRefMirror(projectRoot) {
6664
6873
  let codexBody;
6665
6874
  try {
6666
6875
  [claudeBody, codexBody] = await Promise.all([
6667
- readFile11(join18(claudeRef, fname), "utf8"),
6668
- readFile11(join18(codexRef, fname), "utf8")
6876
+ readFile12(join19(claudeRef, fname), "utf8"),
6877
+ readFile12(join19(codexRef, fname), "utf8")
6669
6878
  ]);
6670
6879
  } catch {
6671
6880
  continue;
@@ -6684,10 +6893,10 @@ async function inspectSkillTokenBudget(projectRoot) {
6684
6893
  const overSize = [];
6685
6894
  let highestSeverity = "ok";
6686
6895
  for (const slug of FABRIC_SKILL_SLUGS) {
6687
- const skillMdPath = join18(projectRoot, ".claude", "skills", slug, "SKILL.md");
6896
+ const skillMdPath = join19(projectRoot, ".claude", "skills", slug, "SKILL.md");
6688
6897
  let body;
6689
6898
  try {
6690
- body = await readFile11(skillMdPath, "utf8");
6899
+ body = await readFile12(skillMdPath, "utf8");
6691
6900
  } catch {
6692
6901
  continue;
6693
6902
  }
@@ -6708,10 +6917,10 @@ async function inspectSkillDescription(projectRoot) {
6708
6917
  const CJK_PATTERN = /[\u3400-\u4dbf\u4e00-\u9fff]/u;
6709
6918
  const ASCII_PATTERN = /[a-zA-Z]{2,}/u;
6710
6919
  for (const slug of FABRIC_SKILL_SLUGS) {
6711
- const skillMdPath = join18(projectRoot, ".claude", "skills", slug, "SKILL.md");
6920
+ const skillMdPath = join19(projectRoot, ".claude", "skills", slug, "SKILL.md");
6712
6921
  let body;
6713
6922
  try {
6714
- body = await readFile11(skillMdPath, "utf8");
6923
+ body = await readFile12(skillMdPath, "utf8");
6715
6924
  } catch {
6716
6925
  continue;
6717
6926
  }
@@ -6742,7 +6951,7 @@ async function inspectSkillDescription(projectRoot) {
6742
6951
  async function inspectSkillMdYamlInvalid(projectRoot) {
6743
6952
  const candidates = [];
6744
6953
  for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
6745
- const rootAbs = join18(projectRoot, rootRel);
6954
+ const rootAbs = join19(projectRoot, rootRel);
6746
6955
  let dirEntries;
6747
6956
  try {
6748
6957
  dirEntries = await readdir3(rootAbs, { withFileTypes: true });
@@ -6751,10 +6960,10 @@ async function inspectSkillMdYamlInvalid(projectRoot) {
6751
6960
  }
6752
6961
  for (const dirEntry of dirEntries) {
6753
6962
  if (!dirEntry.isDirectory()) continue;
6754
- const skillFile = join18(rootAbs, dirEntry.name, "SKILL.md");
6963
+ const skillFile = join19(rootAbs, dirEntry.name, "SKILL.md");
6755
6964
  let raw;
6756
6965
  try {
6757
- raw = await readFile11(skillFile, "utf8");
6966
+ raw = await readFile12(skillFile, "utf8");
6758
6967
  } catch {
6759
6968
  continue;
6760
6969
  }
@@ -6877,8 +7086,8 @@ function createSkillMdYamlInvalidCheck(t, inspection) {
6877
7086
  }
6878
7087
 
6879
7088
  // src/services/doctor-retired-references-lint.ts
6880
- import { readdir as readdir4, readFile as readFile12, stat as stat3 } from "fs/promises";
6881
- import { join as join19, posix as posix3, relative as relative2 } from "path";
7089
+ import { readdir as readdir4, readFile as readFile13, stat as stat3 } from "fs/promises";
7090
+ import { join as join20, posix as posix3, relative as relative2 } from "path";
6882
7091
  var RETIRED_TOKENS = [
6883
7092
  { token: "fab_plan_context", replacement: "fab_recall", reason: "retrieval collapsed to one lean fab_recall (KT-DEC-0026)" },
6884
7093
  { token: "fab_get_knowledge_sections", replacement: "fab_recall", reason: "two-step fetch retired (KT-DEC-0026)" },
@@ -6901,7 +7110,7 @@ var RETIRED_TOKENS = [
6901
7110
  ];
6902
7111
  var HOOK_DIRS = [".claude/hooks", ".codex/hooks"];
6903
7112
  var SKILL_DIRS = [".claude/skills", ".codex/skills"];
6904
- var BOOTSTRAP_FILES = ["AGENTS.md", "CLAUDE.md", join19(".fabric", "AGENTS.md")];
7113
+ var BOOTSTRAP_FILES = ["AGENTS.md", "CLAUDE.md", join20(".fabric", "AGENTS.md")];
6905
7114
  function isCommentLine(line) {
6906
7115
  const t = line.trim();
6907
7116
  return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*");
@@ -6915,7 +7124,7 @@ async function walkFiles(dir, exts) {
6915
7124
  }
6916
7125
  const out = [];
6917
7126
  for (const e of entries) {
6918
- const full = join19(dir, e.name);
7127
+ const full = join20(dir, e.name);
6919
7128
  if (e.isDirectory()) {
6920
7129
  out.push(...await walkFiles(full, exts));
6921
7130
  } else if (exts.some((ext) => e.name.endsWith(ext))) {
@@ -6941,29 +7150,29 @@ async function inspectRetiredReferences(projectRoot) {
6941
7150
  let scannedFiles = 0;
6942
7151
  const toRel = (p) => posix3.normalize(relative2(projectRoot, p).split("\\").join("/"));
6943
7152
  for (const rel of BOOTSTRAP_FILES) {
6944
- const abs = join19(projectRoot, rel);
7153
+ const abs = join20(projectRoot, rel);
6945
7154
  try {
6946
7155
  if ((await stat3(abs)).isFile()) {
6947
7156
  scannedFiles += 1;
6948
- scanText(toRel(abs), await readFile12(abs, "utf8"), false, hits);
7157
+ scanText(toRel(abs), await readFile13(abs, "utf8"), false, hits);
6949
7158
  }
6950
7159
  } catch {
6951
7160
  }
6952
7161
  }
6953
7162
  for (const dir of SKILL_DIRS) {
6954
- for (const file of await walkFiles(join19(projectRoot, dir), [".md"])) {
7163
+ for (const file of await walkFiles(join20(projectRoot, dir), [".md"])) {
6955
7164
  scannedFiles += 1;
6956
7165
  try {
6957
- scanText(toRel(file), await readFile12(file, "utf8"), false, hits);
7166
+ scanText(toRel(file), await readFile13(file, "utf8"), false, hits);
6958
7167
  } catch {
6959
7168
  }
6960
7169
  }
6961
7170
  }
6962
7171
  for (const dir of HOOK_DIRS) {
6963
- for (const file of await walkFiles(join19(projectRoot, dir), [".cjs"])) {
7172
+ for (const file of await walkFiles(join20(projectRoot, dir), [".cjs"])) {
6964
7173
  scannedFiles += 1;
6965
7174
  try {
6966
- scanText(toRel(file), await readFile12(file, "utf8"), true, hits);
7175
+ scanText(toRel(file), await readFile13(file, "utf8"), true, hits);
6967
7176
  } catch {
6968
7177
  }
6969
7178
  }
@@ -6998,8 +7207,8 @@ function createRetiredReferenceCheck(t, inspection) {
6998
7207
 
6999
7208
  // src/services/doctor-hooks-lints.ts
7000
7209
  import { constants } from "fs";
7001
- import { access, readdir as readdir5, readFile as readFile13, stat as stat4 } from "fs/promises";
7002
- import { join as join20, posix as posix4 } from "path";
7210
+ import { access, readdir as readdir5, readFile as readFile14, stat as stat4 } from "fs/promises";
7211
+ import { join as join21, posix as posix4 } from "path";
7003
7212
  import { Script } from "vm";
7004
7213
  var HOOKS_RUNTIME_CLIENT_DIRS = [
7005
7214
  { client: "claude", dir: ".claude/hooks" },
@@ -7061,14 +7270,14 @@ async function isFile(absPath) {
7061
7270
  }
7062
7271
  }
7063
7272
  async function inspectHooksWired(projectRoot) {
7064
- const claudeEntries = await readDirectoryFileNames(join20(projectRoot, ".claude"));
7273
+ const claudeEntries = await readDirectoryFileNames(join21(projectRoot, ".claude"));
7065
7274
  if (claudeEntries === null) {
7066
7275
  return { status: "skipped", missingHooks: [] };
7067
7276
  }
7068
- const settingsPath = join20(projectRoot, ".claude", "settings.json");
7277
+ const settingsPath = join21(projectRoot, ".claude", "settings.json");
7069
7278
  let parsed;
7070
7279
  try {
7071
- parsed = JSON.parse(await readFile13(settingsPath, "utf8"));
7280
+ parsed = JSON.parse(await readFile14(settingsPath, "utf8"));
7072
7281
  } catch {
7073
7282
  return { status: "missing-settings", missingHooks: [] };
7074
7283
  }
@@ -7091,8 +7300,8 @@ async function inspectHooksWired(projectRoot) {
7091
7300
  }
7092
7301
  async function inspectHookCacheWritability(projectRoot) {
7093
7302
  const relPath = posix4.join(".fabric", ".cache");
7094
- const fabricDir = join20(projectRoot, ".fabric");
7095
- const cacheDir = join20(projectRoot, ".fabric", ".cache");
7303
+ const fabricDir = join21(projectRoot, ".fabric");
7304
+ const cacheDir = join21(projectRoot, ".fabric", ".cache");
7096
7305
  try {
7097
7306
  try {
7098
7307
  const cacheStats = await stat4(cacheDir);
@@ -7141,12 +7350,12 @@ async function inspectHookCacheWritability(projectRoot) {
7141
7350
  async function inspectHooksContentDrift(projectRoot) {
7142
7351
  const hookFilesByBasename = /* @__PURE__ */ new Map();
7143
7352
  for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
7144
- const absDir = join20(projectRoot, dir);
7353
+ const absDir = join21(projectRoot, dir);
7145
7354
  const entries = await readDirectoryFileNames(absDir);
7146
7355
  if (entries === null) continue;
7147
7356
  for (const name of entries) {
7148
7357
  if (!name.endsWith(".cjs")) continue;
7149
- const abs = join20(absDir, name);
7358
+ const abs = join21(absDir, name);
7150
7359
  if (!await isFile(abs)) continue;
7151
7360
  const arr = hookFilesByBasename.get(name) ?? [];
7152
7361
  arr.push({ client, abs });
@@ -7161,7 +7370,7 @@ async function inspectHooksContentDrift(projectRoot) {
7161
7370
  const hashes = [];
7162
7371
  for (const { client, abs } of copies) {
7163
7372
  try {
7164
- const body = await readFile13(abs, "utf8");
7373
+ const body = await readFile14(abs, "utf8");
7165
7374
  hashes.push({ client, sha: sha256(body) });
7166
7375
  } catch {
7167
7376
  }
@@ -7183,18 +7392,18 @@ async function inspectHooksRuntime(projectRoot) {
7183
7392
  const issues = [];
7184
7393
  let scanned = 0;
7185
7394
  for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
7186
- const absDir = join20(projectRoot, dir);
7395
+ const absDir = join21(projectRoot, dir);
7187
7396
  const entries = await readDirectoryFileNames(absDir);
7188
7397
  if (entries === null) continue;
7189
7398
  for (const name of entries) {
7190
7399
  if (!name.endsWith(".cjs")) continue;
7191
- const abs = join20(absDir, name);
7400
+ const abs = join21(absDir, name);
7192
7401
  const displayPath = `${dir}/${name}`;
7193
7402
  if (!await isFile(abs)) continue;
7194
7403
  scanned += 1;
7195
7404
  let body;
7196
7405
  try {
7197
- body = await readFile13(abs, "utf8");
7406
+ body = await readFile14(abs, "utf8");
7198
7407
  } catch (err) {
7199
7408
  issues.push({
7200
7409
  path: displayPath,
@@ -7331,8 +7540,8 @@ function createHookCacheWritabilityCheck(t, inspection) {
7331
7540
  }
7332
7541
 
7333
7542
  // src/services/doctor-bootstrap-lints.ts
7334
- import { access as access2, readFile as readFile14 } from "fs/promises";
7335
- import { join as join21 } from "path";
7543
+ import { access as access2, readFile as readFile15 } from "fs/promises";
7544
+ import { join as join22 } from "path";
7336
7545
  import {
7337
7546
  BOOTSTRAP_MARKER_BEGIN,
7338
7547
  BOOTSTRAP_MARKER_END,
@@ -7364,17 +7573,17 @@ async function fileExists(path) {
7364
7573
  }
7365
7574
  async function inspectBootstrapAnchor(projectRoot) {
7366
7575
  const [hasAgentsMd, hasClaudeMd] = await Promise.all([
7367
- fileExists(join21(projectRoot, "AGENTS.md")),
7368
- fileExists(join21(projectRoot, "CLAUDE.md"))
7576
+ fileExists(join22(projectRoot, "AGENTS.md")),
7577
+ fileExists(join22(projectRoot, "CLAUDE.md"))
7369
7578
  ]);
7370
7579
  return { hasAgentsMd, hasClaudeMd };
7371
7580
  }
7372
7581
  async function inspectL1BootstrapSnapshotDrift(target) {
7373
- const abs = join21(target, ".fabric", "AGENTS.md");
7582
+ const abs = join22(target, ".fabric", "AGENTS.md");
7374
7583
  const canonical = resolveBootstrapCanonical();
7375
7584
  let onDisk;
7376
7585
  try {
7377
- onDisk = await readFile14(abs, "utf8");
7586
+ onDisk = await readFile15(abs, "utf8");
7378
7587
  } catch {
7379
7588
  return { status: "missing", canonical, onDisk: null };
7380
7589
  }
@@ -7400,17 +7609,17 @@ function createL1BootstrapSnapshotDriftCheck(t, inspection) {
7400
7609
  );
7401
7610
  }
7402
7611
  async function inspectL2ManagedBlockDrift(target) {
7403
- const snapshotPath = join21(target, ".fabric", "AGENTS.md");
7612
+ const snapshotPath = join22(target, ".fabric", "AGENTS.md");
7404
7613
  let snapshot;
7405
7614
  try {
7406
- snapshot = await readFile14(snapshotPath, "utf8");
7615
+ snapshot = await readFile15(snapshotPath, "utf8");
7407
7616
  } catch {
7408
7617
  return { status: "ok", drifted: [] };
7409
7618
  }
7410
- const projectRulesPath = join21(target, ".fabric", "project-rules.md");
7619
+ const projectRulesPath = join22(target, ".fabric", "project-rules.md");
7411
7620
  let expectedBody = snapshot;
7412
7621
  try {
7413
- const projectRules = await readFile14(projectRulesPath, "utf8");
7622
+ const projectRules = await readFile15(projectRulesPath, "utf8");
7414
7623
  expectedBody = `${snapshot}
7415
7624
  ---
7416
7625
  ${projectRules}`;
@@ -7419,12 +7628,12 @@ ${projectRules}`;
7419
7628
  const drifted = [];
7420
7629
  let anyManagedBlockFound = false;
7421
7630
  const blockTargets = [
7422
- join21(target, "AGENTS.md")
7631
+ join22(target, "AGENTS.md")
7423
7632
  ];
7424
7633
  for (const abs of blockTargets) {
7425
7634
  let content;
7426
7635
  try {
7427
- content = await readFile14(abs, "utf8");
7636
+ content = await readFile15(abs, "utf8");
7428
7637
  } catch {
7429
7638
  continue;
7430
7639
  }
@@ -7447,9 +7656,9 @@ ${projectRules}`;
7447
7656
  drifted.push({ path: abs, expected: expectedBody, actual: body });
7448
7657
  }
7449
7658
  }
7450
- const claudeMdPath = join21(target, "CLAUDE.md");
7659
+ const claudeMdPath = join22(target, "CLAUDE.md");
7451
7660
  try {
7452
- const claudeContent = await readFile14(claudeMdPath, "utf8");
7661
+ const claudeContent = await readFile15(claudeMdPath, "utf8");
7453
7662
  anyManagedBlockFound = true;
7454
7663
  const lines = claudeContent.split(/\r?\n/u);
7455
7664
  const hasAtImport = lines.some((line) => line.trim() === "@.fabric/AGENTS.md");
@@ -7517,7 +7726,7 @@ import { appendFile as appendFile3 } from "fs/promises";
7517
7726
  import { minimatch as minimatch2 } from "minimatch";
7518
7727
 
7519
7728
  // src/services/cite-rollup.ts
7520
- import { readFile as readFile15 } from "fs/promises";
7729
+ import { readFile as readFile16 } from "fs/promises";
7521
7730
  import { createLedgerWriteQueue as createLedgerWriteQueue3 } from "@fenglimg/fabric-shared/node/atomic-write";
7522
7731
  var citeRollupQueue = createLedgerWriteQueue3();
7523
7732
  async function appendCiteRollupRow(projectRoot, row) {
@@ -7529,7 +7738,7 @@ async function readCiteRollup(projectRoot) {
7529
7738
  const path = getCiteRollupPath(projectRoot);
7530
7739
  let raw;
7531
7740
  try {
7532
- raw = await readFile15(path, "utf8");
7741
+ raw = await readFile16(path, "utf8");
7533
7742
  } catch (error) {
7534
7743
  if (isNodeError(error) && error.code === "ENOENT") return [];
7535
7744
  throw error;
@@ -8120,6 +8329,18 @@ async function runDoctorCiteCoverage(projectRoot, options) {
8120
8329
  }
8121
8330
  }
8122
8331
  }
8332
+ const editSessionIds = /* @__PURE__ */ new Set();
8333
+ for (const edit of editEvents) {
8334
+ if (typeof edit.session_id === "string" && edit.session_id.length > 0) {
8335
+ editSessionIds.add(edit.session_id);
8336
+ }
8337
+ }
8338
+ let recallsInWindow = 0;
8339
+ for (const list of plannedBySession.values()) recallsInWindow += list.length;
8340
+ let recallSessionsCorrelated = 0;
8341
+ for (const sid of plannedBySession.keys()) {
8342
+ if (editSessionIds.has(sid)) recallSessionsCorrelated += 1;
8343
+ }
8123
8344
  const noneTotal = Object.values(noneHistogram).reduce((a, b) => a + b, 0);
8124
8345
  const compliantCites = qualifyingCites + noneTotal;
8125
8346
  const noncompliantCites = expectedButMissed;
@@ -8187,6 +8408,11 @@ async function runDoctorCiteCoverage(projectRoot, options) {
8187
8408
  uncorrelatable_edits: uncorrelatableEdits,
8188
8409
  recall_backed_edits: recallBackedEdits,
8189
8410
  recall_coverage_rate: recallCoverageRate,
8411
+ recall_diagnostics: {
8412
+ recalls_in_window: recallsInWindow,
8413
+ recall_sessions: plannedBySession.size,
8414
+ recall_sessions_correlated: recallSessionsCorrelated
8415
+ },
8190
8416
  exposed_and_mutated: {
8191
8417
  count: exposedAndMutated.count,
8192
8418
  ...exposedAndMutated.ids.length > 0 ? { ids: exposedAndMutated.ids } : {}
@@ -8659,7 +8885,7 @@ async function runDoctorReport(target) {
8659
8885
  const globalCliVersion = process.env.VITEST === "true" ? { status: "ok", version: "test-skipped" } : inspectGlobalCliVersion();
8660
8886
  const targetFiles = Object.fromEntries(
8661
8887
  await Promise.all(
8662
- TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(join22(projectRoot, path))])
8888
+ TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(join23(projectRoot, path))])
8663
8889
  )
8664
8890
  );
8665
8891
  const checks = [
@@ -8863,7 +9089,7 @@ async function runDoctorFix(target) {
8863
9089
  const fixed = [];
8864
9090
  const ledgerWarnings = [];
8865
9091
  if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
8866
- const snapshotPath = join22(projectRoot, ".fabric", "AGENTS.md");
9092
+ const snapshotPath = join23(projectRoot, ".fabric", "AGENTS.md");
8867
9093
  await ensureParentDirectory(snapshotPath);
8868
9094
  await atomicWriteText4(snapshotPath, resolveBootstrapCanonical2());
8869
9095
  fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
@@ -8936,7 +9162,7 @@ async function runDoctorFix(target) {
8936
9162
  if (before.infos.some((issue) => issue.code === "stale_serve_lock")) {
8937
9163
  const lockInspection = inspectStaleServeLock(projectRoot, Date.now());
8938
9164
  if (lockInspection.present && !lockInspection.pidAlive) {
8939
- const lockFilePath = join22(projectRoot, ".fabric", ".serve.lock");
9165
+ const lockFilePath = join23(projectRoot, ".fabric", ".serve.lock");
8940
9166
  try {
8941
9167
  await unlink4(lockFilePath);
8942
9168
  } catch (err) {
@@ -9053,7 +9279,7 @@ function createApplyLintMessage(succeeded, failed, manualErrorCount) {
9053
9279
  }
9054
9280
  async function applySessionHintsStaleCleanup(projectRoot, candidate) {
9055
9281
  const detail = `deleted (${candidate.age_days}d old)`;
9056
- const absPath = join22(projectRoot, candidate.path);
9282
+ const absPath = join23(projectRoot, candidate.path);
9057
9283
  try {
9058
9284
  const { unlink: unlink5 } = await import("fs/promises");
9059
9285
  await unlink5(absPath);
@@ -9078,9 +9304,9 @@ function truncateErrorMessage(error) {
9078
9304
  return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
9079
9305
  }
9080
9306
  async function inspectForensic(projectRoot) {
9081
- const path = join22(projectRoot, ".fabric", "forensic.json");
9307
+ const path = join23(projectRoot, ".fabric", "forensic.json");
9082
9308
  try {
9083
- const parsed = forensicReportSchema.parse(JSON.parse(await readFile17(path, "utf8")));
9309
+ const parsed = forensicReportSchema.parse(JSON.parse(await readFile18(path, "utf8")));
9084
9310
  return { present: true, valid: true, report: parsed };
9085
9311
  } catch (error) {
9086
9312
  if (isMissingFileError(error)) {
@@ -9110,7 +9336,7 @@ async function inspectEventLedger(projectRoot) {
9110
9336
  try {
9111
9337
  await access4(path, constants2.W_OK);
9112
9338
  const { warnings } = await readEventLedger(projectRoot);
9113
- const raw = await readFile17(path, "utf8");
9339
+ const raw = await readFile18(path, "utf8");
9114
9340
  const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
9115
9341
  const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
9116
9342
  const schemaVersionSamples = [];
@@ -9655,7 +9881,7 @@ async function inspectPreexistingRootFiles(projectRoot) {
9655
9881
  const candidates = ["CLAUDE.md", "AGENTS.md"];
9656
9882
  const detected = [];
9657
9883
  for (const name of candidates) {
9658
- if (await pathExists(join22(projectRoot, name))) {
9884
+ if (await pathExists(join23(projectRoot, name))) {
9659
9885
  detected.push(name);
9660
9886
  }
9661
9887
  }
@@ -9740,7 +9966,7 @@ async function buildLastActiveIndex(projectRoot) {
9740
9966
  return map;
9741
9967
  }
9742
9968
  async function inspectSessionHintsStale(projectRoot, now) {
9743
- const cacheDir = join22(projectRoot, ".fabric", ".cache");
9969
+ const cacheDir = join23(projectRoot, ".fabric", ".cache");
9744
9970
  let entries;
9745
9971
  try {
9746
9972
  entries = await readdirAsync(cacheDir, { withFileTypes: true });
@@ -9752,7 +9978,7 @@ async function inspectSessionHintsStale(projectRoot, now) {
9752
9978
  if (!entry.isFile()) continue;
9753
9979
  if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
9754
9980
  if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
9755
- const absPath = join22(cacheDir, entry.name);
9981
+ const absPath = join23(cacheDir, entry.name);
9756
9982
  let mtimeMs = 0;
9757
9983
  try {
9758
9984
  mtimeMs = (await statAsync(absPath)).mtimeMs;
@@ -9784,9 +10010,9 @@ function inspectStaleServeLock(projectRoot, now) {
9784
10010
  };
9785
10011
  }
9786
10012
  async function readUnderseedThresholdFromConfig(projectRoot) {
9787
- const configPath = join22(projectRoot, ".fabric", "fabric-config.json");
10013
+ const configPath = join23(projectRoot, ".fabric", "fabric-config.json");
9788
10014
  try {
9789
- const raw = await readFile17(configPath, "utf8");
10015
+ const raw = await readFile18(configPath, "utf8");
9790
10016
  const parsed = JSON.parse(raw);
9791
10017
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
9792
10018
  const v = parsed.underseed_node_threshold;
@@ -9902,10 +10128,10 @@ async function inspectOnboardCoverage(projectRoot) {
9902
10128
  return { filled, missing, opted_out: optedOut };
9903
10129
  }
9904
10130
  async function readOnboardOptedOut(projectRoot) {
9905
- const path = join22(projectRoot, ".fabric", "fabric-config.json");
10131
+ const path = join23(projectRoot, ".fabric", "fabric-config.json");
9906
10132
  let raw;
9907
10133
  try {
9908
- raw = await readFile17(path, "utf8");
10134
+ raw = await readFile18(path, "utf8");
9909
10135
  } catch {
9910
10136
  return [];
9911
10137
  }
@@ -10007,22 +10233,22 @@ async function* iterateCanonicalFilenames(projectRoot) {
10007
10233
  }
10008
10234
  }
10009
10235
  async function rewriteThreeEndManagedBlocks(projectRoot) {
10010
- const snapshotPath = join22(projectRoot, ".fabric", "AGENTS.md");
10236
+ const snapshotPath = join23(projectRoot, ".fabric", "AGENTS.md");
10011
10237
  if (!await pathExists(snapshotPath)) {
10012
10238
  return;
10013
10239
  }
10014
10240
  let snapshot;
10015
10241
  try {
10016
- snapshot = await readFile17(snapshotPath, "utf8");
10242
+ snapshot = await readFile18(snapshotPath, "utf8");
10017
10243
  } catch {
10018
10244
  return;
10019
10245
  }
10020
- const projectRulesPath = join22(projectRoot, ".fabric", "project-rules.md");
10246
+ const projectRulesPath = join23(projectRoot, ".fabric", "project-rules.md");
10021
10247
  const hasProjectRules = await pathExists(projectRulesPath);
10022
10248
  let expectedBody = snapshot;
10023
10249
  if (hasProjectRules) {
10024
10250
  try {
10025
- const projectRules = await readFile17(projectRulesPath, "utf8");
10251
+ const projectRules = await readFile18(projectRulesPath, "utf8");
10026
10252
  expectedBody = `${snapshot}
10027
10253
  ---
10028
10254
  ${projectRules}`;
@@ -10033,7 +10259,7 @@ ${projectRules}`;
10033
10259
  ${expectedBody}
10034
10260
  ${BOOTSTRAP_MARKER_END2}`;
10035
10261
  const blockTargets = [
10036
- join22(projectRoot, "AGENTS.md")
10262
+ join23(projectRoot, "AGENTS.md")
10037
10263
  ];
10038
10264
  for (const abs of blockTargets) {
10039
10265
  if (!await pathExists(abs)) {
@@ -10041,7 +10267,7 @@ ${BOOTSTRAP_MARKER_END2}`;
10041
10267
  }
10042
10268
  let existing;
10043
10269
  try {
10044
- existing = await readFile17(abs, "utf8");
10270
+ existing = await readFile18(abs, "utf8");
10045
10271
  } catch {
10046
10272
  continue;
10047
10273
  }
@@ -10066,11 +10292,11 @@ ${managedBlock}
10066
10292
  }
10067
10293
  await atomicWriteText4(abs, next);
10068
10294
  }
10069
- const claudeMdPath = join22(projectRoot, "CLAUDE.md");
10295
+ const claudeMdPath = join23(projectRoot, "CLAUDE.md");
10070
10296
  if (await pathExists(claudeMdPath)) {
10071
10297
  let claudeContent;
10072
10298
  try {
10073
- claudeContent = await readFile17(claudeMdPath, "utf8");
10299
+ claudeContent = await readFile18(claudeMdPath, "utf8");
10074
10300
  } catch {
10075
10301
  return;
10076
10302
  }
@@ -10136,7 +10362,7 @@ async function collectEntryPoints(root) {
10136
10362
  continue;
10137
10363
  }
10138
10364
  for (const entry of await readdirAsync(current, { withFileTypes: true })) {
10139
- const absolutePath = join22(current, entry.name);
10365
+ const absolutePath = join23(current, entry.name);
10140
10366
  const relativePath = normalizePath2(absolutePath.slice(root.length + 1));
10141
10367
  if (relativePath.length === 0) {
10142
10368
  continue;
@@ -10212,7 +10438,7 @@ async function enrichDescriptions(projectRoot, opts = {}) {
10212
10438
  scanned += 1;
10213
10439
  let source;
10214
10440
  try {
10215
- source = await readFile17(absPath, "utf8");
10441
+ source = await readFile18(absPath, "utf8");
10216
10442
  } catch {
10217
10443
  continue;
10218
10444
  }
@@ -10350,14 +10576,14 @@ async function runDoctorConflictLint(projectRoot, opts = {}) {
10350
10576
  }
10351
10577
 
10352
10578
  // src/services/why-not-surfaced.ts
10353
- import { readFile as readFile18 } from "fs/promises";
10354
- import { basename as basename3, join as join23 } from "path";
10579
+ import { readFile as readFile19 } from "fs/promises";
10580
+ import { basename as basename3, join as join24 } from "path";
10355
10581
  import {
10356
10582
  buildStoreResolveInput as buildStoreResolveInput7,
10357
10583
  createStoreResolver as createStoreResolver7,
10358
10584
  loadProjectConfig as loadProjectConfig4,
10359
10585
  readKnowledgeAcrossStores as readKnowledgeAcrossStores4,
10360
- resolveGlobalRoot as resolveGlobalRoot7,
10586
+ resolveGlobalRoot as resolveGlobalRoot8,
10361
10587
  scopeRoot as scopeRoot3,
10362
10588
  storeRelativePathForMount as storeRelativePathForMount6
10363
10589
  } from "@fenglimg/fabric-shared";
@@ -10387,11 +10613,11 @@ async function explainWhyNotSurfaced(projectRoot, query) {
10387
10613
  if (input === null) {
10388
10614
  return base;
10389
10615
  }
10390
- const globalRoot = resolveGlobalRoot7();
10616
+ const globalRoot = resolveGlobalRoot8();
10391
10617
  const allStores = input.mountedStores.map((s) => ({
10392
10618
  store_uuid: s.store_uuid,
10393
10619
  alias: s.alias,
10394
- dir: join23(globalRoot, storeRelativePathForMount6(s))
10620
+ dir: join24(globalRoot, storeRelativePathForMount6(s))
10395
10621
  }));
10396
10622
  const refs = await readKnowledgeAcrossStores4(allStores);
10397
10623
  const candidate = refs.find((ref) => fileMatchesId(ref.file, localId));
@@ -10400,7 +10626,7 @@ async function explainWhyNotSurfaced(projectRoot, query) {
10400
10626
  }
10401
10627
  let source;
10402
10628
  try {
10403
- source = await readFile18(candidate.file, "utf8");
10629
+ source = await readFile19(candidate.file, "utf8");
10404
10630
  } catch {
10405
10631
  return base;
10406
10632
  }
@@ -10453,6 +10679,191 @@ function buildColdEvalBatch(candidates) {
10453
10679
  return { rubric: COLD_EVAL_RUBRIC, candidates: judgeable };
10454
10680
  }
10455
10681
 
10682
+ // src/services/doctor-related-graph.ts
10683
+ function buildRelatedGraph(nodes) {
10684
+ const brokenLinks = [];
10685
+ const inDegree = /* @__PURE__ */ new Map();
10686
+ const allIds = /* @__PURE__ */ new Set();
10687
+ const edgeSources = [];
10688
+ for (const node of nodes) {
10689
+ const bareId = extractBareStableId(node.qualifiedId);
10690
+ allIds.add(node.qualifiedId);
10691
+ if (bareId !== null) {
10692
+ allIds.add(bareId);
10693
+ }
10694
+ const related = node.related;
10695
+ if (related !== void 0 && related.length > 0) {
10696
+ edgeSources.push({ source: node.qualifiedId, related });
10697
+ for (const rel of related) {
10698
+ inDegree.set(rel, (inDegree.get(rel) ?? 0) + 1);
10699
+ }
10700
+ }
10701
+ }
10702
+ for (const { source, related } of edgeSources) {
10703
+ for (const rel of related) {
10704
+ if (!allIds.has(rel)) {
10705
+ brokenLinks.push({ source, target: rel });
10706
+ }
10707
+ }
10708
+ }
10709
+ const hubEntries = [...inDegree.entries()].map(([stableId, degree]) => ({ stableId, inDegree: degree })).sort((a, b) => b.inDegree - a.inDegree || a.stableId.localeCompare(b.stableId));
10710
+ return {
10711
+ brokenLinks,
10712
+ hubEntries,
10713
+ totalEntries: nodes.length
10714
+ };
10715
+ }
10716
+ function extractBareStableId(qualifiedId) {
10717
+ const colonIdx = qualifiedId.indexOf(":");
10718
+ if (colonIdx > 0 && colonIdx < qualifiedId.length - 1) {
10719
+ return qualifiedId.slice(colonIdx + 1);
10720
+ }
10721
+ return null;
10722
+ }
10723
+ async function inspectRelatedGraph(projectRoot) {
10724
+ const entries = await collectStoreCanonicalEntries(projectRoot);
10725
+ return buildRelatedGraph(
10726
+ entries.map((entry) => ({
10727
+ qualifiedId: entry.qualifiedId,
10728
+ related: entry.description.related
10729
+ }))
10730
+ );
10731
+ }
10732
+
10733
+ // src/services/store-precheck.ts
10734
+ import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
10735
+ import { join as join25 } from "path";
10736
+ import {
10737
+ buildStoreResolveInput as buildStoreResolveInput8,
10738
+ createStoreResolver as createStoreResolver8,
10739
+ resolveGlobalRoot as resolveGlobalRoot9,
10740
+ storeRelativePathForMount as storeRelativePathForMount7
10741
+ } from "@fenglimg/fabric-shared";
10742
+ var CACHE_TTL_MS = 6e4;
10743
+ var cache = /* @__PURE__ */ new Map();
10744
+ function clearPrecheckCache() {
10745
+ cache.clear();
10746
+ }
10747
+ function evaluateStoreDir(storeDir, identity) {
10748
+ if (!existsSync9(storeDir)) {
10749
+ return {
10750
+ uuid: identity.uuid,
10751
+ alias: identity.alias,
10752
+ reachable: false,
10753
+ reason: `directory not found at ${storeDir}`
10754
+ };
10755
+ }
10756
+ const storeJsonPath = join25(storeDir, "store.json");
10757
+ const gitDirPath = join25(storeDir, ".git");
10758
+ const hasStoreJson = existsSync9(storeJsonPath) && isValidStoreJson(storeJsonPath);
10759
+ const hasGitDir = existsSync9(gitDirPath);
10760
+ if (!hasStoreJson && !hasGitDir) {
10761
+ return {
10762
+ uuid: identity.uuid,
10763
+ alias: identity.alias,
10764
+ reachable: false,
10765
+ reason: `no store.json or .git found in ${storeDir}`
10766
+ };
10767
+ }
10768
+ return { uuid: identity.uuid, alias: identity.alias, reachable: true };
10769
+ }
10770
+ function isValidStoreJson(path) {
10771
+ try {
10772
+ JSON.parse(readFileSync4(path, "utf8"));
10773
+ return true;
10774
+ } catch {
10775
+ return false;
10776
+ }
10777
+ }
10778
+ async function precheckStoreReachability(projectRoot, globalRoot = resolveGlobalRoot9(), now = Date.now()) {
10779
+ const cached = cache.get(projectRoot);
10780
+ if (cached !== void 0 && cached.expiresAt > now) {
10781
+ return cached.result;
10782
+ }
10783
+ const input = buildStoreResolveInput8(projectRoot, globalRoot);
10784
+ if (input === null) {
10785
+ const result2 = { stores: [], allReachable: true };
10786
+ cache.set(projectRoot, { result: result2, expiresAt: now + CACHE_TTL_MS });
10787
+ return result2;
10788
+ }
10789
+ const readSet = createStoreResolver8().resolveReadSet(input);
10790
+ const stores = readSet.stores.map((entry) => {
10791
+ const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
10792
+ const storeDir = join25(
10793
+ globalRoot,
10794
+ storeRelativePathForMount7(mounted ?? { store_uuid: entry.store_uuid })
10795
+ );
10796
+ return evaluateStoreDir(storeDir, { uuid: entry.store_uuid, alias: entry.alias });
10797
+ });
10798
+ const result = {
10799
+ stores,
10800
+ allReachable: stores.every((s) => s.reachable)
10801
+ };
10802
+ cache.set(projectRoot, { result, expiresAt: now + CACHE_TTL_MS });
10803
+ return result;
10804
+ }
10805
+
10806
+ // src/services/doctor-consumption-lint.ts
10807
+ var DEFAULT_WINDOW_DAYS = 30;
10808
+ var DEFAULT_TOP_N = 10;
10809
+ var DEFAULT_MIN_TOTAL_ENTRIES = 20;
10810
+ var DEFAULT_MIN_CONSUMED_WINDOWS = 30;
10811
+ var DEFAULT_MIN_CONSUMED_EVENTS = 50;
10812
+ var CONSUMED_PREFIX = "knowledge_consumed:";
10813
+ var MS_PER_DAY5 = 24 * 60 * 60 * 1e3;
10814
+ function resolveConfig(config) {
10815
+ return {
10816
+ windowDays: config?.windowDays ?? DEFAULT_WINDOW_DAYS,
10817
+ topN: config?.topN ?? DEFAULT_TOP_N,
10818
+ minTotalEntries: config?.minTotalEntries ?? DEFAULT_MIN_TOTAL_ENTRIES,
10819
+ minConsumedWindows: config?.minConsumedWindows ?? DEFAULT_MIN_CONSUMED_WINDOWS,
10820
+ minConsumedEvents: config?.minConsumedEvents ?? DEFAULT_MIN_CONSUMED_EVENTS
10821
+ };
10822
+ }
10823
+ function aggregateConsumption(rows, allQualifiedIds, now, config) {
10824
+ const cfg = resolveConfig(config);
10825
+ const cutoffMs = now - cfg.windowDays * MS_PER_DAY5;
10826
+ const consumed = /* @__PURE__ */ new Map();
10827
+ let consumedWindows = 0;
10828
+ let totalConsumedEvents = 0;
10829
+ for (const row of rows) {
10830
+ const rowTs = Date.parse(row.timestamp);
10831
+ if (!Number.isFinite(rowTs) || rowTs < cutoffMs) continue;
10832
+ if (row.counters === null || typeof row.counters !== "object") continue;
10833
+ let windowHadConsumption = false;
10834
+ for (const [counterName, rawCount] of Object.entries(row.counters)) {
10835
+ if (!counterName.startsWith(CONSUMED_PREFIX)) continue;
10836
+ const count = typeof rawCount === "number" ? rawCount : 0;
10837
+ if (count <= 0) continue;
10838
+ const id = counterName.slice(CONSUMED_PREFIX.length);
10839
+ if (id.length === 0) continue;
10840
+ consumed.set(id, (consumed.get(id) ?? 0) + count);
10841
+ totalConsumedEvents += count;
10842
+ windowHadConsumption = true;
10843
+ }
10844
+ if (windowHadConsumption) consumedWindows += 1;
10845
+ }
10846
+ const topConsumed = [...consumed.entries()].map(([stableId, count]) => ({ stableId, count })).sort((a, b) => b.count - a.count || a.stableId.localeCompare(b.stableId)).slice(0, cfg.topN);
10847
+ const dataMature = consumedWindows >= cfg.minConsumedWindows && totalConsumedEvents >= cfg.minConsumedEvents && allQualifiedIds.length >= cfg.minTotalEntries;
10848
+ const zeroConsumed = dataMature ? allQualifiedIds.filter((id) => !consumed.has(id)).sort((a, b) => a.localeCompare(b)) : [];
10849
+ return {
10850
+ topConsumed,
10851
+ zeroConsumed,
10852
+ totalEntries: allQualifiedIds.length,
10853
+ consumedEntries: consumed.size,
10854
+ consumedWindows,
10855
+ totalConsumedEvents,
10856
+ dataMature,
10857
+ windowDays: cfg.windowDays
10858
+ };
10859
+ }
10860
+ async function inspectConsumption(projectRoot, config, now = Date.now()) {
10861
+ const rows = await readMetrics(projectRoot);
10862
+ const entries = await collectStoreCanonicalEntries(projectRoot);
10863
+ const allQualifiedIds = entries.map((entry) => entry.qualifiedId);
10864
+ return aggregateConsumption(rows, allQualifiedIds, now, config);
10865
+ }
10866
+
10456
10867
  // src/services/rotation-tick.ts
10457
10868
  var DEFAULT_TICK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
10458
10869
  var tickTimers = /* @__PURE__ */ new Map();
@@ -10488,7 +10899,7 @@ import { IOFabricError as IOFabricError2, RuleError } from "@fenglimg/fabric-sha
10488
10899
 
10489
10900
  // src/services/read-ledger.ts
10490
10901
  import { randomUUID as randomUUID7 } from "crypto";
10491
- import { access as access5, copyFile, readFile as readFile19, rm } from "fs/promises";
10902
+ import { access as access5, copyFile, readFile as readFile20, rm } from "fs/promises";
10492
10903
  import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
10493
10904
  async function resolveLedgerPaths(projectRoot) {
10494
10905
  const primaryPath = getLedgerPath(projectRoot);
@@ -10516,7 +10927,7 @@ async function readLegacyLedger(projectRoot) {
10516
10927
  const { readPath } = await resolveLedgerPaths(projectRoot);
10517
10928
  let raw;
10518
10929
  try {
10519
- raw = await readFile19(readPath, "utf8");
10930
+ raw = await readFile20(readPath, "utf8");
10520
10931
  } catch (error) {
10521
10932
  if (isNodeError(error) && error.code === "ENOENT") {
10522
10933
  return [];
@@ -10771,8 +11182,8 @@ function formatError(error) {
10771
11182
  }
10772
11183
  function formatPreexistingRootMessage(projectRoot) {
10773
11184
  const preexisting = [];
10774
- if (existsSync9(join24(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
10775
- if (existsSync9(join24(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
11185
+ if (existsSync10(join26(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
11186
+ if (existsSync10(join26(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
10776
11187
  if (preexisting.length === 0) return null;
10777
11188
  return `[startup] info: detected ${preexisting.join(", ")} at project root. Note: Fabric serves knowledge from mounted stores via MCP \u2014 root markdown files are not auto-loaded into the AI context.`;
10778
11189
  }
@@ -10799,7 +11210,7 @@ function createFabricServer(tracker) {
10799
11210
  const server = new McpServer(
10800
11211
  {
10801
11212
  name: "fabric-knowledge-server",
10802
- version: "2.3.0-rc.2"
11213
+ version: "2.3.0-rc.4"
10803
11214
  },
10804
11215
  {
10805
11216
  instructions: FABRIC_SERVER_INSTRUCTIONS
@@ -10819,10 +11230,10 @@ function createFabricServer(tracker) {
10819
11230
  },
10820
11231
  async (_uri) => {
10821
11232
  const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
10822
- const path = join24(projectRoot, ".fabric", "bootstrap", "README.md");
11233
+ const path = join26(projectRoot, ".fabric", "bootstrap", "README.md");
10823
11234
  let text = "";
10824
- if (existsSync9(path)) {
10825
- text = await readFile20(path, "utf8");
11235
+ if (existsSync10(path)) {
11236
+ text = await readFile21(path, "utf8");
10826
11237
  }
10827
11238
  return {
10828
11239
  contents: [
@@ -10920,19 +11331,26 @@ export {
10920
11331
  LEGACY_LEDGER_PATH,
10921
11332
  METRICS_LEDGER_PATH,
10922
11333
  METRIC_COUNTER_NAMES,
11334
+ OPTIONAL_EMBED_PACKAGE,
10923
11335
  RETIRED_TOKENS,
11336
+ aggregateConsumption,
10924
11337
  appendEventLedgerEvent,
10925
11338
  buildAlwaysActiveBodies,
10926
11339
  buildColdEvalBatch,
10927
11340
  buildKnowledgeCensus,
11341
+ buildRelatedGraph,
10928
11342
  bumpCounter,
11343
+ clearPrecheckCache,
11344
+ collectStoreCanonicalEntries,
10929
11345
  contextCache,
10930
11346
  createFabricServer,
10931
11347
  createInFlightTracker,
10932
11348
  createShutdownHandler,
11349
+ defaultEmbedCacheDir,
10933
11350
  detectUnboundProject,
10934
11351
  drainCounters,
10935
11352
  enrichDescriptions,
11353
+ evaluateStoreDir,
10936
11354
  explainWhyNotSurfaced,
10937
11355
  extractKnowledge,
10938
11356
  findConflictCandidates,
@@ -10943,12 +11361,19 @@ export {
10943
11361
  getLedgerPath,
10944
11362
  getLegacyLedgerPath,
10945
11363
  getMetricsLedgerPath,
11364
+ inspectConsumption,
11365
+ inspectRelatedGraph,
10946
11366
  inspectRetiredReferences,
11367
+ isEmbedderResolvable,
10947
11368
  lintConflicts,
10948
11369
  loadConflictEntries,
11370
+ loadEmbedder,
10949
11371
  pairSimilarity,
10950
11372
  planContext,
11373
+ precheckStoreReachability,
11374
+ readEmbedConfig,
10951
11375
  readEventLedger,
11376
+ readFusion,
10952
11377
  readLedger,
10953
11378
  readMetrics,
10954
11379
  readSelectionToken,