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

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.
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 readFile19 } from "fs/promises";
4
- import { join as join23, resolve as resolve4 } from "path";
3
+ import { readFile as readFile20 } from "fs/promises";
4
+ import { join as join24, 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";
@@ -742,7 +742,7 @@ var SUPPORTED_EMBED_MODELS = /* @__PURE__ */ new Set([
742
742
  function readEmbedConfig(projectRoot) {
743
743
  try {
744
744
  const config = readFabricConfig(projectRoot);
745
- const enabled = config.embed_enabled === true;
745
+ const enabled = config.embed_enabled !== false;
746
746
  const rawWeight = config.embed_weight;
747
747
  const weight = typeof rawWeight === "number" && Number.isInteger(rawWeight) && rawWeight >= 0 && rawWeight <= 49 ? rawWeight : 30;
748
748
  const rawModel = config.embed_model;
@@ -817,6 +817,14 @@ function readOrphanDemoteThresholdDays(projectRoot) {
817
817
  return {};
818
818
  }
819
819
  }
820
+ function readFusion(projectRoot) {
821
+ try {
822
+ const raw = readFabricConfig(projectRoot).fusion;
823
+ return raw === "rrf" ? "rrf" : "additive";
824
+ } catch {
825
+ return "additive";
826
+ }
827
+ }
820
828
  function readConflictLintThreshold(projectRoot) {
821
829
  try {
822
830
  const cfgPath = join4(projectRoot, ".fabric", "fabric-config.json");
@@ -1534,11 +1542,44 @@ function buildBm25Model(docs) {
1534
1542
  for (const field of BM25_FIELDS) {
1535
1543
  avgFieldLength[field] = totalDocs > 0 ? totalFieldLength[field] / totalDocs : 0;
1536
1544
  }
1545
+ const serialized = {
1546
+ version: 1,
1547
+ totalDocs,
1548
+ documentFrequency: [...documentFrequency],
1549
+ avgFieldLength,
1550
+ perDoc: [...perDoc].map(([id, stats]) => ({
1551
+ id,
1552
+ fieldTermFreq: emptyFieldRecord(() => []),
1553
+ fieldLength: { ...stats.fieldLength }
1554
+ }))
1555
+ };
1556
+ for (const entry of serialized.perDoc) {
1557
+ const stats = perDoc.get(entry.id);
1558
+ if (stats === void 0) continue;
1559
+ for (const field of BM25_FIELDS) {
1560
+ entry.fieldTermFreq[field] = [...stats.fieldTermFreq[field]];
1561
+ }
1562
+ }
1563
+ return modelFromStats(serialized);
1564
+ }
1565
+ function modelFromStats(serialized) {
1566
+ const totalDocs = serialized.totalDocs;
1567
+ const documentFrequency = new Map(serialized.documentFrequency);
1568
+ const avgFieldLength = serialized.avgFieldLength;
1569
+ const perDoc = /* @__PURE__ */ new Map();
1570
+ for (const entry of serialized.perDoc) {
1571
+ const fieldTermFreq = emptyFieldRecord(() => /* @__PURE__ */ new Map());
1572
+ for (const field of BM25_FIELDS) {
1573
+ fieldTermFreq[field] = new Map(entry.fieldTermFreq[field]);
1574
+ }
1575
+ perDoc.set(entry.id, { fieldTermFreq, fieldLength: entry.fieldLength });
1576
+ }
1537
1577
  const idf = (term) => {
1538
1578
  const n = documentFrequency.get(term) ?? 0;
1539
1579
  return Math.log(1 + (totalDocs - n + 0.5) / (n + 0.5));
1540
1580
  };
1541
1581
  return {
1582
+ __serialized: serialized,
1542
1583
  scoreDoc(id, queryTerms) {
1543
1584
  const data = perDoc.get(id);
1544
1585
  if (data === void 0 || queryTerms.length === 0) {
@@ -1574,9 +1615,48 @@ function buildBm25Model(docs) {
1574
1615
  }
1575
1616
  };
1576
1617
  }
1618
+ function serializeBm25Model(model) {
1619
+ return model.__serialized;
1620
+ }
1621
+ function rehydrateBm25Model(serialized) {
1622
+ return modelFromStats(serialized);
1623
+ }
1577
1624
  function buildQueryTerms(text) {
1578
1625
  return tokenize(text);
1579
1626
  }
1627
+ function rankDocuments(model, ids, queryTerms) {
1628
+ if (queryTerms.length === 0) {
1629
+ return /* @__PURE__ */ new Map();
1630
+ }
1631
+ const scored = [];
1632
+ ids.forEach((id, order) => {
1633
+ const score = model.scoreDoc(id, queryTerms);
1634
+ if (score > 0) {
1635
+ scored.push({ id, score, order });
1636
+ }
1637
+ });
1638
+ scored.sort((a, b) => a.score !== b.score ? b.score - a.score : a.order - b.order);
1639
+ const ranks = /* @__PURE__ */ new Map();
1640
+ scored.forEach((entry, index) => {
1641
+ ranks.set(entry.id, index + 1);
1642
+ });
1643
+ return ranks;
1644
+ }
1645
+ function rankByScore(ids, scores) {
1646
+ const scored = [];
1647
+ ids.forEach((id, order) => {
1648
+ const score = scores.get(id) ?? 0;
1649
+ if (score > 0) {
1650
+ scored.push({ id, score, order });
1651
+ }
1652
+ });
1653
+ scored.sort((a, b) => a.score !== b.score ? b.score - a.score : a.order - b.order);
1654
+ const ranks = /* @__PURE__ */ new Map();
1655
+ scored.forEach((entry, index) => {
1656
+ ranks.set(entry.id, index + 1);
1657
+ });
1658
+ return ranks;
1659
+ }
1580
1660
 
1581
1661
  // src/services/conflict-lint.ts
1582
1662
  function buildSimilarityModel(docs) {
@@ -2670,9 +2750,27 @@ var LEDGER_DUAL_WRITE_METRIC_NAMES = {
2670
2750
  edit_intent_checked: METRIC_COUNTER_NAMES.edit_intent_checked
2671
2751
  };
2672
2752
 
2753
+ // 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";
2756
+
2673
2757
  // src/services/vector-retrieval.ts
2674
2758
  var embedderLoad;
2675
2759
  var OPTIONAL_EMBED_PACKAGE = "fastembed";
2760
+ var embedderModuleLoader = (name) => import(name);
2761
+ 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
+ var defaultMissingEmbedderHint = () => {
2763
+ process.stderr.write(MISSING_EMBEDDER_HINT);
2764
+ };
2765
+ var missingEmbedderHinted = false;
2766
+ var emitMissingEmbedderHint = defaultMissingEmbedderHint;
2767
+ function hintMissingEmbedderOnce() {
2768
+ if (missingEmbedderHinted) {
2769
+ return;
2770
+ }
2771
+ missingEmbedderHinted = true;
2772
+ emitMissingEmbedderHint();
2773
+ }
2676
2774
  function buildEmbedInitOptions(modelName) {
2677
2775
  return {
2678
2776
  maxLength: 512,
@@ -2685,8 +2783,9 @@ async function loadEmbedder(modelName) {
2685
2783
  embedderLoad = (async () => {
2686
2784
  try {
2687
2785
  const moduleName = OPTIONAL_EMBED_PACKAGE;
2688
- const mod = await import(moduleName);
2786
+ const mod = await embedderModuleLoader(moduleName);
2689
2787
  if (mod?.FlagEmbedding?.init === void 0) {
2788
+ hintMissingEmbedderOnce();
2690
2789
  return null;
2691
2790
  }
2692
2791
  const model = await mod.FlagEmbedding.init(buildEmbedInitOptions(modelName));
@@ -2702,6 +2801,7 @@ async function loadEmbedder(modelName) {
2702
2801
  }
2703
2802
  };
2704
2803
  } catch {
2804
+ hintMissingEmbedderOnce();
2705
2805
  return null;
2706
2806
  }
2707
2807
  })();
@@ -2732,7 +2832,7 @@ function cosineSimilarity(a, b) {
2732
2832
  if (!Number.isFinite(sim)) {
2733
2833
  return 0;
2734
2834
  }
2735
- return Math.max(-1, Math.min(1, sim));
2835
+ return Math.max(0, Math.min(1, sim));
2736
2836
  }
2737
2837
  var docVectorCache = /* @__PURE__ */ new Map();
2738
2838
  var DOC_VECTOR_CACHE_MAX = 1e4;
@@ -2837,56 +2937,28 @@ async function planContext(projectRoot, input) {
2837
2937
  ...input.known_tech ?? [],
2838
2938
  ...Object.values(input.detected_entities ?? {}).flat()
2839
2939
  ].join(" ");
2840
- const scoringContext = {
2841
- nowMs: Date.now(),
2842
- targetPaths: input.target_paths ?? dedupePaths(input.paths),
2843
- queryTerms: buildQueryTerms(queryText)
2844
- };
2845
2940
  const storeRawItems = await buildCrossStoreRawItems(projectRoot).catch(() => []);
2846
2941
  const { rawItems: allRawItems, suppressedStableIds } = partitionEmptyShells(storeRawItems);
2847
2942
  const effectiveLayerFilter = input.layer_filter ?? readDefaultLayerFilter(projectRoot);
2848
2943
  const rawItems = effectiveLayerFilter === "both" ? allRawItems : allRawItems.filter((item) => item.description.knowledge_layer === effectiveLayerFilter);
2849
- const docTexts = /* @__PURE__ */ new Map();
2850
- for (const item of rawItems) {
2851
- docTexts.set(item.stable_id, documentTextForItem(item.description));
2852
- }
2853
- scoringContext.docTexts = docTexts;
2854
- if (scoringContext.queryTerms.length > 0 && rawItems.length > 0) {
2855
- scoringContext.bm25 = getOrBuildBm25Model(revision, rawItems, docTexts);
2856
- }
2857
- scoringContext.scopeRank = buildScopeRankMap(rawItems, projectRoot);
2858
- const embedConfig = readEmbedConfig(projectRoot);
2859
- if (embedConfig.enabled && queryText.trim().length > 0 && rawItems.length > 0) {
2860
- const embedder = await loadEmbedder(embedConfig.model);
2861
- const vectorScores = await buildVectorScores(
2862
- embedder,
2863
- queryText,
2864
- rawItems.map((item) => ({
2865
- stable_id: item.stable_id,
2866
- text: docTexts.get(item.stable_id) ?? documentTextForItem(item.description)
2867
- }))
2868
- );
2869
- if (vectorScores !== null) {
2870
- scoringContext.vectorScores = vectorScores;
2871
- scoringContext.vectorWeight = embedConfig.weight;
2872
- }
2873
- }
2874
- const scoredSorted = sortDescriptionItems(rawItems, scoringContext);
2875
- const seenStableIds = /* @__PURE__ */ new Set();
2876
- const rankedScored = scoredSorted.filter(({ item }) => {
2877
- if (seenStableIds.has(item.stable_id)) return false;
2878
- seenStableIds.add(item.stable_id);
2879
- return true;
2944
+ const scoringContext = await buildScoringContext(projectRoot, revision, rawItems, {
2945
+ queryText,
2946
+ targetPaths: input.target_paths ?? dedupePaths(input.paths)
2880
2947
  });
2948
+ const rankedScored = rankDescriptionItems(rawItems, scoringContext, "triage");
2881
2949
  const rankedCandidates = rankedScored.map((entry) => entry.item);
2882
- const topK = readPlanContextTopK(projectRoot);
2883
- const cappedScored = rankedScored.slice(0, topK);
2884
- const relevanceRatio = readRecallRelevanceRatio(projectRoot);
2885
- const hasQuery = scoringContext.queryTerms.length > 0;
2886
- const maxScore = rankedScored.length > 0 ? rankedScored[0].score : 0;
2887
- const relevanceFloor = maxScore * relevanceRatio;
2888
- const survivingScored = hasQuery && maxScore > 0 && relevanceRatio > 0 ? cappedScored.filter((entry) => entry.score >= relevanceFloor) : cappedScored;
2950
+ const survivingScored = rankDescriptionItems(rawItems, scoringContext, "recall", {
2951
+ topK: readPlanContextTopK(projectRoot),
2952
+ relevanceRatio: readRecallRelevanceRatio(projectRoot)
2953
+ });
2889
2954
  const topKCandidates = survivingScored.map((entry) => entry.item);
2955
+ const candidateScores = /* @__PURE__ */ new Map();
2956
+ for (const entry of survivingScored) {
2957
+ candidateScores.set(entry.item.stable_id, {
2958
+ score: entry.score,
2959
+ score_breakdown: scoreBreakdownForItem(entry.item, scoringContext)
2960
+ });
2961
+ }
2890
2962
  const topKIds = new Set(topKCandidates.map((item) => item.stable_id));
2891
2963
  const retrievalDropped = rankedCandidates.filter((item) => !topKIds.has(item.stable_id)).map((item) => ({ id: item.stable_id, reason: "retrieval_budget" }));
2892
2964
  const omittedCandidateCount = retrievalDropped.length;
@@ -2992,6 +3064,8 @@ async function planContext(projectRoot, input) {
2992
3064
  // ONLY when at least one neighbour was actually appended. Empty (graph-empty
2993
3065
  // no-op) → field omitted, steady-state wire shape unchanged.
2994
3066
  ...Object.keys(relatedAppended).length > 0 ? { related_appended: relatedAppended } : {},
3067
+ // P1 recall-observability: runtime-only score channel (Map → {} on the wire).
3068
+ ...candidateScores.size > 0 ? { candidate_scores: candidateScores } : {},
2995
3069
  ...payloadTrimDropped > 0 ? { payload_trimmed: true } : {},
2996
3070
  ...payloadOverBudget ? { payload_over_budget: true } : {}
2997
3071
  };
@@ -3080,10 +3154,38 @@ function partitionEmptyShells(items) {
3080
3154
  }
3081
3155
  var bm25ModelCache = null;
3082
3156
  var bm25BuildCount = 0;
3083
- function getOrBuildBm25Model(revision, rawItems, docTexts) {
3157
+ var BM25_CACHE_DIR = ".fabric/cache/bm25";
3158
+ function bm25CachePath(projectRoot, revision) {
3159
+ const safe = revision.replace(/[^A-Za-z0-9_-]/g, "_");
3160
+ return join9(projectRoot, BM25_CACHE_DIR, `${safe}.json`);
3161
+ }
3162
+ async function loadBm25ModelFromDisk(projectRoot, revision) {
3163
+ try {
3164
+ const raw = await readFile5(bm25CachePath(projectRoot, revision), "utf8");
3165
+ const parsed = JSON.parse(raw);
3166
+ if (parsed.version !== 1) return null;
3167
+ return rehydrateBm25Model(parsed);
3168
+ } catch {
3169
+ return null;
3170
+ }
3171
+ }
3172
+ async function saveBm25ModelToDisk(projectRoot, revision, model) {
3173
+ try {
3174
+ const path = bm25CachePath(projectRoot, revision);
3175
+ await mkdir4(join9(projectRoot, BM25_CACHE_DIR), { recursive: true });
3176
+ await writeFile2(path, JSON.stringify(serializeBm25Model(model)), "utf8");
3177
+ } catch {
3178
+ }
3179
+ }
3180
+ async function getOrBuildBm25Model(projectRoot, revision, rawItems, docTexts) {
3084
3181
  if (bm25ModelCache !== null && bm25ModelCache.revision === revision) {
3085
3182
  return bm25ModelCache.model;
3086
3183
  }
3184
+ const fromDisk = await loadBm25ModelFromDisk(projectRoot, revision);
3185
+ if (fromDisk !== null) {
3186
+ bm25ModelCache = { revision, model: fromDisk };
3187
+ return fromDisk;
3188
+ }
3087
3189
  bm25BuildCount += 1;
3088
3190
  const model = buildBm25Model(
3089
3191
  rawItems.map((item) => ({
@@ -3092,6 +3194,7 @@ function getOrBuildBm25Model(revision, rawItems, docTexts) {
3092
3194
  }))
3093
3195
  );
3094
3196
  bm25ModelCache = { revision, model };
3197
+ await saveBm25ModelToDisk(projectRoot, revision, model);
3095
3198
  return model;
3096
3199
  }
3097
3200
  function compareStableIds(a, b) {
@@ -3137,6 +3240,49 @@ function compareScopeThenId(left, right, scopeRank) {
3137
3240
  }
3138
3241
  return compareStableIds(left.stable_id, right.stable_id);
3139
3242
  }
3243
+ async function buildScoringContext(projectRoot, revision, rawItems, opts) {
3244
+ const scoringContext = {
3245
+ nowMs: Date.now(),
3246
+ targetPaths: opts.targetPaths,
3247
+ queryTerms: buildQueryTerms(opts.queryText)
3248
+ };
3249
+ const docTexts = /* @__PURE__ */ new Map();
3250
+ for (const item of rawItems) {
3251
+ docTexts.set(item.stable_id, documentTextForItem(item.description));
3252
+ }
3253
+ scoringContext.docTexts = docTexts;
3254
+ if (scoringContext.queryTerms.length > 0 && rawItems.length > 0) {
3255
+ scoringContext.bm25 = await getOrBuildBm25Model(projectRoot, revision, rawItems, docTexts);
3256
+ }
3257
+ scoringContext.scopeRank = buildScopeRankMap(rawItems, projectRoot);
3258
+ const embedConfig = readEmbedConfig(projectRoot);
3259
+ if (embedConfig.enabled && opts.queryText.trim().length > 0 && rawItems.length > 0) {
3260
+ const embedder = await loadEmbedder(embedConfig.model);
3261
+ const vectorScores = await buildVectorScores(
3262
+ embedder,
3263
+ opts.queryText,
3264
+ rawItems.map((item) => ({
3265
+ stable_id: item.stable_id,
3266
+ text: docTexts.get(item.stable_id) ?? documentTextForItem(item.description)
3267
+ }))
3268
+ );
3269
+ if (vectorScores !== null) {
3270
+ scoringContext.vectorScores = vectorScores;
3271
+ scoringContext.vectorWeight = embedConfig.weight;
3272
+ }
3273
+ }
3274
+ scoringContext.fusion = readFusion(projectRoot);
3275
+ if (scoringContext.fusion === "rrf" && scoringContext.queryTerms.length > 0 && rawItems.length > 0) {
3276
+ const rankIds = rawItems.map((item) => item.stable_id).sort((a, b) => compareStableIds(a, b));
3277
+ if (scoringContext.bm25 !== void 0) {
3278
+ scoringContext.bm25Ranks = rankDocuments(scoringContext.bm25, rankIds, scoringContext.queryTerms);
3279
+ }
3280
+ if (scoringContext.vectorScores !== void 0) {
3281
+ scoringContext.vectorRanks = rankByScore(rankIds, scoringContext.vectorScores);
3282
+ }
3283
+ }
3284
+ return scoringContext;
3285
+ }
3140
3286
  function sortDescriptionItems(rawItems, scoringContext) {
3141
3287
  if (scoringContext === void 0) {
3142
3288
  return [...rawItems].sort((left, right) => compareStableIds(left.stable_id, right.stable_id)).map((item) => ({ item, score: 0 }));
@@ -3151,6 +3297,25 @@ function sortDescriptionItems(rawItems, scoringContext) {
3151
3297
  );
3152
3298
  return scored;
3153
3299
  }
3300
+ function rankDescriptionItems(items, scoringContext, mode, options = {}) {
3301
+ const sorted = sortDescriptionItems(items, scoringContext);
3302
+ const seen = /* @__PURE__ */ new Set();
3303
+ const rankedScored = sorted.filter(({ item }) => {
3304
+ if (seen.has(item.stable_id)) return false;
3305
+ seen.add(item.stable_id);
3306
+ return true;
3307
+ });
3308
+ if (mode === "triage") {
3309
+ return rankedScored;
3310
+ }
3311
+ const topK = options.topK ?? rankedScored.length;
3312
+ const cappedScored = rankedScored.slice(0, topK);
3313
+ const relevanceRatio = options.relevanceRatio ?? 0;
3314
+ const hasQuery = scoringContext.queryTerms.length > 0;
3315
+ const maxScore = rankedScored.length > 0 ? rankedScored[0].score : 0;
3316
+ const relevanceFloor = maxScore * relevanceRatio;
3317
+ return hasQuery && maxScore > 0 && relevanceRatio > 0 ? cappedScored.filter((entry) => entry.score >= relevanceFloor) : cappedScored;
3318
+ }
3154
3319
  function documentTextForItem(description) {
3155
3320
  return [
3156
3321
  description.summary,
@@ -3199,6 +3364,9 @@ var BM25_WEIGHT = 50;
3199
3364
  var SALIENCE_PROVEN = 15;
3200
3365
  var SALIENCE_VERIFIED = 8;
3201
3366
  var SALIENCE_DRAFT = 0;
3367
+ var RRF_K = 10;
3368
+ var RRF_NORMALIZATION = 2e3;
3369
+ var RRF_STRUCTURAL_SCALE = 0.2;
3202
3370
  function salienceScore(item) {
3203
3371
  switch (item.description?.maturity) {
3204
3372
  case "proven":
@@ -3209,35 +3377,87 @@ function salienceScore(item) {
3209
3377
  return SALIENCE_DRAFT;
3210
3378
  }
3211
3379
  }
3212
- function scoreDescriptionItem(item, context) {
3213
- let score = 0;
3214
- if (context.bm25 !== void 0 && context.queryTerms.length > 0) {
3215
- score += BM25_WEIGHT * context.bm25.scoreDoc(item.stable_id, context.queryTerms);
3380
+ function contentScore(item, context) {
3381
+ const hasQuery = context.queryTerms.length > 0;
3382
+ if (context.fusion === "rrf" && hasQuery) {
3383
+ let rrf = 0;
3384
+ const bm25Rank = context.bm25Ranks?.get(item.stable_id);
3385
+ if (bm25Rank !== void 0) rrf += 1 / (RRF_K + bm25Rank);
3386
+ const vectorRank = context.vectorRanks?.get(item.stable_id);
3387
+ if (vectorRank !== void 0) rrf += 1 / (RRF_K + vectorRank);
3388
+ return RRF_NORMALIZATION * rrf;
3389
+ }
3390
+ let content = 0;
3391
+ if (context.bm25 !== void 0 && hasQuery) {
3392
+ content += BM25_WEIGHT * context.bm25.scoreDoc(item.stable_id, context.queryTerms);
3216
3393
  }
3217
3394
  if (context.vectorScores !== void 0) {
3218
- score += (context.vectorWeight ?? 0) * (context.vectorScores.get(item.stable_id) ?? 0);
3395
+ content += (context.vectorWeight ?? 0) * (context.vectorScores.get(item.stable_id) ?? 0);
3219
3396
  }
3220
- score += salienceScore(item);
3397
+ return content;
3398
+ }
3399
+ function recencyBoost(item, context) {
3221
3400
  const createdAtRaw = item.description?.created_at;
3222
3401
  if (typeof createdAtRaw === "string" && createdAtRaw.length > 0) {
3223
3402
  const createdMs = Date.parse(createdAtRaw);
3224
3403
  if (Number.isFinite(createdMs) && context.nowMs - createdMs < RECENCY_WINDOW_MS) {
3225
- score += RECENCY_BOOST;
3404
+ return RECENCY_BOOST;
3226
3405
  }
3227
3406
  }
3228
- if (context.targetPaths.length > 0) {
3229
- const relevancePaths = item.description?.relevance_paths ?? [];
3230
- let best = 0;
3231
- outer: for (const rp of relevancePaths) {
3232
- for (const tp of context.targetPaths) {
3233
- const tier = localityTier(rp, tp);
3234
- if (tier > best) best = tier;
3235
- if (best === LOCALITY_SAME_FILE) break outer;
3236
- }
3407
+ return 0;
3408
+ }
3409
+ function localityBoost(item, context) {
3410
+ if (context.targetPaths.length === 0) return 0;
3411
+ const relevancePaths = item.description?.relevance_paths ?? [];
3412
+ let best = 0;
3413
+ outer: for (const rp of relevancePaths) {
3414
+ for (const tp of context.targetPaths) {
3415
+ const tier = localityTier(rp, tp);
3416
+ if (tier > best) best = tier;
3417
+ if (best === LOCALITY_SAME_FILE) break outer;
3237
3418
  }
3238
- score += best;
3239
3419
  }
3240
- return score;
3420
+ return best;
3421
+ }
3422
+ function structuralScaleFor(context) {
3423
+ return context.fusion === "rrf" && context.queryTerms.length > 0 ? RRF_STRUCTURAL_SCALE : 1;
3424
+ }
3425
+ function scoreDescriptionItem(item, context) {
3426
+ const content = contentScore(item, context);
3427
+ const structural = salienceScore(item) + recencyBoost(item, context) + localityBoost(item, context);
3428
+ return content + structuralScaleFor(context) * structural;
3429
+ }
3430
+ function scoreBreakdownForItem(item, context) {
3431
+ const hasQuery = context.queryTerms.length > 0;
3432
+ const rrfMode = context.fusion === "rrf" && hasQuery;
3433
+ let bm25 = 0;
3434
+ let vector = 0;
3435
+ let bm25Rank;
3436
+ let vectorRank;
3437
+ if (rrfMode) {
3438
+ bm25Rank = context.bm25Ranks?.get(item.stable_id);
3439
+ if (bm25Rank !== void 0) bm25 = RRF_NORMALIZATION * (1 / (RRF_K + bm25Rank));
3440
+ vectorRank = context.vectorRanks?.get(item.stable_id);
3441
+ if (vectorRank !== void 0) vector = RRF_NORMALIZATION * (1 / (RRF_K + vectorRank));
3442
+ } else {
3443
+ bm25 = context.bm25 !== void 0 && hasQuery ? BM25_WEIGHT * context.bm25.scoreDoc(item.stable_id, context.queryTerms) : 0;
3444
+ vector = context.vectorScores !== void 0 ? (context.vectorWeight ?? 0) * (context.vectorScores.get(item.stable_id) ?? 0) : 0;
3445
+ }
3446
+ const scale = structuralScaleFor(context);
3447
+ const salience = salienceScore(item) * scale;
3448
+ const recency = recencyBoost(item, context) * scale;
3449
+ const locality = localityBoost(item, context) * scale;
3450
+ const final = bm25 + vector + salience + recency + locality;
3451
+ return {
3452
+ final,
3453
+ ...bm25 !== 0 ? { bm25 } : {},
3454
+ ...bm25Rank !== void 0 ? { bm25_rank: bm25Rank } : {},
3455
+ ...vector !== 0 ? { vector } : {},
3456
+ ...vectorRank !== void 0 ? { vector_rank: vectorRank } : {},
3457
+ salience,
3458
+ recency,
3459
+ locality
3460
+ };
3241
3461
  }
3242
3462
  function localityTier(relevancePath, targetPath) {
3243
3463
  if (relevancePath === targetPath) return LOCALITY_SAME_FILE;
@@ -3274,7 +3494,13 @@ function relatedLookupKeys(stableId) {
3274
3494
  var RECALL_DIRECTIVE = "These entries are auto-accounted as citations for edits whose paths overlap this recall \u2014 no first-line cite needed. Speak up only to dismiss one you judge inapplicable: `dismissed: <id> (<reason>)`.";
3275
3495
  async function recall(projectRoot, input) {
3276
3496
  const planResult = await planContext(projectRoot, input);
3277
- const { selection_token: _token, payload_trimmed: _pt, payload_over_budget: _pob, ...planRest } = planResult;
3497
+ const {
3498
+ selection_token: _token,
3499
+ payload_trimmed: _pt,
3500
+ payload_over_budget: _pob,
3501
+ candidate_scores: candidateScores,
3502
+ ...planRest
3503
+ } = planResult;
3278
3504
  let bodyIndex;
3279
3505
  try {
3280
3506
  bodyIndex = await buildCrossStoreBodyIndex(projectRoot);
@@ -3323,13 +3549,15 @@ async function recall(projectRoot, input) {
3323
3549
  const pathByStableId = new Map(paths.map((p) => [p.stable_id, p]));
3324
3550
  const entries = planRest.candidates.map((c, index) => {
3325
3551
  const readPath = pathByStableId.get(c.stable_id);
3552
+ const scored = candidateScores?.get(c.stable_id);
3326
3553
  return {
3327
3554
  stable_id: c.stable_id,
3328
3555
  rank: index + 1,
3329
3556
  description: c.description,
3330
3557
  ...readPath ? { read_path: readPath.path } : {},
3331
3558
  ...readPath?.store ? { store: readPath.store } : {},
3332
- ...isAlwaysActive(c) ? { body_in_context: true } : {}
3559
+ ...isAlwaysActive(c) ? { body_in_context: true } : {},
3560
+ ...scored ? { score: scored.score, score_breakdown: scored.score_breakdown } : {}
3333
3561
  };
3334
3562
  });
3335
3563
  const { entries: _reqProfiles, candidates: _candidates, ...planRestNoLists } = planRest;
@@ -3679,9 +3907,9 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
3679
3907
  // src/services/review.ts
3680
3908
  import { execFileSync } from "child_process";
3681
3909
  import { existsSync as existsSync5 } from "fs";
3682
- import { readFile as readFile6, readdir as readdir2, stat as stat2, unlink as unlink2 } from "fs/promises";
3910
+ import { readFile as readFile7, readdir as readdir2, stat as stat2, unlink as unlink2 } from "fs/promises";
3683
3911
  import { homedir } from "os";
3684
- import { basename, isAbsolute, join as join10, relative, resolve as resolve2, sep as sep2 } from "path";
3912
+ import { basename, isAbsolute, join as join11, relative, resolve as resolve2, sep as sep2 } from "path";
3685
3913
 
3686
3914
  // src/services/promotion-gate.ts
3687
3915
  function toLocalId(id) {
@@ -3721,8 +3949,8 @@ import { allocateStoreKnowledgeId, isPersonalScope as isPersonalScope2 } from "@
3721
3949
 
3722
3950
  // src/services/pending-dedupe.ts
3723
3951
  import { existsSync as existsSync4 } from "fs";
3724
- import { readdir, readFile as readFile5, unlink } from "fs/promises";
3725
- import { join as join9 } from "path";
3952
+ import { readdir, readFile as readFile6, unlink } from "fs/promises";
3953
+ import { join as join10 } from "path";
3726
3954
  var PENDING_TYPES = ["decisions", "pitfalls", "guidelines", "models", "processes"];
3727
3955
  var DISAMBIGUATION_SUFFIX = /^(.+)-([2-9])\.md$/u;
3728
3956
  var SOURCE_SESSIONS_LINE = /^source_sessions:\s*\[(.*)\]\s*$/mu;
@@ -3795,7 +4023,7 @@ async function mergePendingTwins(projectRoot) {
3795
4023
  continue;
3796
4024
  }
3797
4025
  for (const type of PENDING_TYPES) {
3798
- const dir = join9(pendingBase2, type);
4026
+ const dir = join10(pendingBase2, type);
3799
4027
  if (!existsSync4(dir)) continue;
3800
4028
  let names;
3801
4029
  try {
@@ -3816,10 +4044,10 @@ async function mergePendingTwins(projectRoot) {
3816
4044
  if (groupNames.length < 2) continue;
3817
4045
  const parsed = [];
3818
4046
  for (const name of groupNames) {
3819
- const abs = join9(dir, name);
4047
+ const abs = join10(dir, name);
3820
4048
  let content;
3821
4049
  try {
3822
- content = await readFile5(abs, "utf8");
4050
+ content = await readFile6(abs, "utf8");
3823
4051
  } catch {
3824
4052
  continue;
3825
4053
  }
@@ -3919,7 +4147,7 @@ async function reviewPending(projectRoot, input) {
3919
4147
  case "search":
3920
4148
  return {
3921
4149
  action: "search",
3922
- items: await searchEntries(projectRoot, input.query, input.filters)
4150
+ items: await triageSearch(projectRoot, input.query, input.filters)
3923
4151
  };
3924
4152
  default: {
3925
4153
  const exhaustive = input;
@@ -3999,7 +4227,7 @@ async function listPending(projectRoot, filters) {
3999
4227
  }
4000
4228
  for (const source of sources) {
4001
4229
  for (const type of typesToScan) {
4002
- const dir = join10(source.root, type);
4230
+ const dir = join11(source.root, type);
4003
4231
  if (!existsSync5(dir)) {
4004
4232
  continue;
4005
4233
  }
@@ -4011,10 +4239,10 @@ async function listPending(projectRoot, filters) {
4011
4239
  }
4012
4240
  for (const name of entries) {
4013
4241
  if (!name.endsWith(".md")) continue;
4014
- const absolutePath = join10(dir, name);
4242
+ const absolutePath = join11(dir, name);
4015
4243
  let content;
4016
4244
  try {
4017
- content = await readFile6(absolutePath, "utf8");
4245
+ content = await readFile7(absolutePath, "utf8");
4018
4246
  } catch {
4019
4247
  continue;
4020
4248
  }
@@ -4124,7 +4352,7 @@ async function approveOne(projectRoot, pendingPath) {
4124
4352
  let targetAbs;
4125
4353
  let writtenTarget = false;
4126
4354
  try {
4127
- const content = await readFile6(sourceAbs, "utf8");
4355
+ const content = await readFile7(sourceAbs, "utf8");
4128
4356
  const fm = parseFrontmatter(content);
4129
4357
  const pluralType = fm.type;
4130
4358
  if (pluralType === void 0 || !PLURAL_TYPES.includes(pluralType)) {
@@ -4138,7 +4366,7 @@ async function approveOne(projectRoot, pendingPath) {
4138
4366
  );
4139
4367
  allocatedId = stableId;
4140
4368
  const newFilename = `${stableId}--${slug}.md`;
4141
- targetAbs = join10(resolveStoreCanonicalBase(layer, projectRoot), pluralType, newFilename);
4369
+ targetAbs = join11(resolveStoreCanonicalBase(layer, projectRoot), pluralType, newFilename);
4142
4370
  await ensureParentDirectory(targetAbs);
4143
4371
  const rewritten = rewriteFrontmatterMerge(
4144
4372
  rewriteFrontmatterForPromote(content, stableId),
@@ -4196,7 +4424,7 @@ async function rejectAll(projectRoot, pendingPaths, reason) {
4196
4424
  try {
4197
4425
  const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
4198
4426
  if (existsSync5(sandboxed.abs)) {
4199
- const content = await readFile6(sandboxed.abs, "utf8");
4427
+ const content = await readFile7(sandboxed.abs, "utf8");
4200
4428
  const merged = rewriteFrontmatterMerge(content, { status: "rejected" });
4201
4429
  const rejectedAbs = sandboxed.abs.includes(`${sep2}pending${sep2}`) ? sandboxed.abs.replace(`${sep2}pending${sep2}`, `${sep2}rejected${sep2}`) : null;
4202
4430
  if (rejectedAbs !== null) {
@@ -4223,7 +4451,7 @@ async function modifyEntry(projectRoot, pendingPath, changes) {
4223
4451
  if (target === null) {
4224
4452
  throw new Error(`modify target not found: ${pendingPath}`);
4225
4453
  }
4226
- const content = await readFile6(target.absPath, "utf8");
4454
+ const content = await readFile7(target.absPath, "utf8");
4227
4455
  const fm = parseFrontmatter(content);
4228
4456
  const currentLayer = fm.layer ?? "team";
4229
4457
  if (fm.maturity === "verified" && changes.maturity === "proven" && fm.id !== void 0) {
@@ -4325,7 +4553,7 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
4325
4553
  pluralType,
4326
4554
  resolveWriteTargetStoreDir(toLayer, projectRoot)
4327
4555
  );
4328
- const toAbs = join10(
4556
+ const toAbs = join11(
4329
4557
  resolveStoreCanonicalBase(toLayer, projectRoot),
4330
4558
  pluralType,
4331
4559
  `${newStableId}--${slug}.md`
@@ -4425,7 +4653,7 @@ function getSearchDirectoryCache(cacheKey) {
4425
4653
  return created;
4426
4654
  }
4427
4655
  async function listIndexedSearchEntries(source, type) {
4428
- const dir = join10(source.root, type);
4656
+ const dir = join11(source.root, type);
4429
4657
  let entries;
4430
4658
  try {
4431
4659
  entries = await readdir2(dir);
@@ -4438,7 +4666,7 @@ async function listIndexedSearchEntries(source, type) {
4438
4666
  const indexed = [];
4439
4667
  for (const name of entries) {
4440
4668
  if (!name.endsWith(".md")) continue;
4441
- const absolutePath = join10(dir, name);
4669
+ const absolutePath = join11(dir, name);
4442
4670
  let fingerprint;
4443
4671
  try {
4444
4672
  const st = await stat2(absolutePath);
@@ -4455,7 +4683,7 @@ async function listIndexedSearchEntries(source, type) {
4455
4683
  }
4456
4684
  let content;
4457
4685
  try {
4458
- content = await readFile6(absolutePath, "utf8");
4686
+ content = await readFile7(absolutePath, "utf8");
4459
4687
  searchEntryIndexContentReads += 1;
4460
4688
  } catch {
4461
4689
  directoryCache.files.delete(absolutePath);
@@ -4482,9 +4710,41 @@ async function listIndexedSearchEntries(source, type) {
4482
4710
  }
4483
4711
  return indexed;
4484
4712
  }
4485
- async function searchEntries(projectRoot, query, filters) {
4713
+ function matchesTriageQuery(indexed, lowerQuery, includeBody) {
4714
+ const haystacks = [
4715
+ indexed.fm.title ?? "",
4716
+ indexed.fm.summary ?? "",
4717
+ ...indexed.fm.tags ?? [],
4718
+ indexed.name,
4719
+ includeBody ? indexed.body : ""
4720
+ ].map((s) => s.toLowerCase());
4721
+ return haystacks.some((h) => h.includes(lowerQuery));
4722
+ }
4723
+ function pendingEntryToRankerItem(indexed) {
4724
+ const { fm, name } = indexed;
4725
+ const slug = name.replace(/\.md$/u, "");
4726
+ const summary = fm.summary ?? fm.title ?? slug;
4727
+ const description = {
4728
+ summary,
4729
+ intent_clues: [],
4730
+ tech_stack: [],
4731
+ impact: [],
4732
+ must_read_if: fm.title ?? summary,
4733
+ ...fm.id !== void 0 ? { id: fm.id } : {},
4734
+ ...fm.type !== void 0 ? { knowledge_type: fm.type } : {},
4735
+ maturity: fm.maturity ?? "draft",
4736
+ knowledge_layer: indexed.layer,
4737
+ ...fm.semantic_scope !== void 0 ? { semantic_scope: fm.semantic_scope } : {},
4738
+ ...fm.created_at !== void 0 ? { created_at: fm.created_at } : {},
4739
+ tags: fm.tags ?? [],
4740
+ relevance_scope: fm.relevance_scope ?? "broad",
4741
+ relevance_paths: fm.relevance_paths ?? []
4742
+ };
4743
+ return { stable_id: indexed.absolutePath, description };
4744
+ }
4745
+ async function triageSearch(projectRoot, query, filters) {
4486
4746
  const lowerQuery = query.toLowerCase();
4487
- const items = [];
4747
+ const includeBody = filters?.include_body === true;
4488
4748
  const sources = [];
4489
4749
  for (const layer of ["team", "personal"]) {
4490
4750
  const isPersonal = layer === "personal";
@@ -4507,10 +4767,12 @@ async function searchEntries(projectRoot, query, filters) {
4507
4767
  }
4508
4768
  }
4509
4769
  const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
4770
+ const matchedByKey = /* @__PURE__ */ new Map();
4771
+ const rankerItems = [];
4510
4772
  for (const source of sources) {
4511
4773
  for (const type of typesToScan) {
4512
4774
  for (const indexed of await listIndexedSearchEntries(source, type)) {
4513
- const { absolutePath, fm, layer, maturity, name } = indexed;
4775
+ const { fm, layer, maturity } = indexed;
4514
4776
  if (filters?.layer !== void 0 && filters.layer !== "both" && filters.layer !== layer) {
4515
4777
  continue;
4516
4778
  }
@@ -4531,42 +4793,56 @@ async function searchEntries(projectRoot, query, filters) {
4531
4793
  if (!isVisibleByLifecycle(fm, filters)) {
4532
4794
  continue;
4533
4795
  }
4534
- const bodyForSearch = filters?.include_body === true ? indexed.body : "";
4535
- const haystacks = [
4536
- fm.title ?? "",
4537
- fm.summary ?? "",
4538
- ...fm.tags ?? [],
4539
- name,
4540
- bodyForSearch
4541
- ].map((s) => s.toLowerCase());
4542
- const matches = haystacks.some((h) => h.includes(lowerQuery));
4543
- if (!matches) continue;
4544
- const reportedPath = source.isStore ? absolutePath : source.isPersonal ? `~/${relative(resolvePersonalRoot(), absolutePath)}` : relative(projectRoot, absolutePath);
4545
- items.push({
4546
- area: source.isPending ? "pending" : "canonical",
4547
- path: reportedPath,
4548
- ...source.isPersonal ? { path_absolute: absolutePath } : {},
4549
- type,
4550
- layer,
4551
- maturity,
4552
- // Only pending entries carry an origin tag (canonical hits live
4553
- // outside the dual-pending-root convention).
4554
- ...source.isPending ? { origin: source.isPersonal ? "personal" : "team" } : {},
4555
- ...fm.tags !== void 0 && fm.tags.length > 0 ? { tags: fm.tags } : {},
4556
- ...fm.title !== void 0 ? { title: fm.title } : {},
4557
- ...fm.summary !== void 0 ? { summary: fm.summary } : {},
4558
- ...fm.status !== void 0 ? { status: fm.status } : {},
4559
- ...fm.deferred_until !== void 0 ? { deferred_until: fm.deferred_until } : {},
4560
- // v2.0.0-rc.27 TASK-006 (audit §2.23): body emission when opted in.
4561
- ...filters?.include_body === true ? { body: bodyForSearch } : {},
4562
- // Canonical hits always have an id; pending hits typically don't
4563
- // yet — surface the frontmatter id when present so consumers can
4564
- // dedupe across runs.
4565
- ...fm.id !== void 0 ? { stable_id: fm.id } : {}
4566
- });
4796
+ if (!matchesTriageQuery(indexed, lowerQuery, includeBody)) continue;
4797
+ const item = pendingEntryToRankerItem(indexed);
4798
+ matchedByKey.set(item.stable_id, { indexed, source, type });
4799
+ rankerItems.push(item);
4567
4800
  }
4568
4801
  }
4569
4802
  }
4803
+ if (rankerItems.length === 0) {
4804
+ return [];
4805
+ }
4806
+ let revision;
4807
+ try {
4808
+ revision = await computeReadSetRevision(projectRoot);
4809
+ } catch {
4810
+ revision = "triage-search";
4811
+ }
4812
+ const scoringContext = await buildScoringContext(projectRoot, revision, rankerItems, {
4813
+ queryText: query,
4814
+ targetPaths: []
4815
+ });
4816
+ const ranked = rankDescriptionItems(rankerItems, scoringContext, "triage");
4817
+ const items = [];
4818
+ for (const { item } of ranked) {
4819
+ const match = matchedByKey.get(item.stable_id);
4820
+ if (match === void 0) continue;
4821
+ const { indexed, source, type } = match;
4822
+ const { absolutePath, fm, layer, maturity } = indexed;
4823
+ const reportedPath = source.isStore ? absolutePath : source.isPersonal ? `~/${relative(resolvePersonalRoot(), absolutePath)}` : relative(projectRoot, absolutePath);
4824
+ items.push({
4825
+ area: source.isPending ? "pending" : "canonical",
4826
+ path: reportedPath,
4827
+ ...source.isPersonal ? { path_absolute: absolutePath } : {},
4828
+ type,
4829
+ layer,
4830
+ maturity,
4831
+ // Only pending entries carry an origin tag (canonical hits live
4832
+ // outside the dual-pending-root convention).
4833
+ ...source.isPending ? { origin: source.isPersonal ? "personal" : "team" } : {},
4834
+ ...fm.tags !== void 0 && fm.tags.length > 0 ? { tags: fm.tags } : {},
4835
+ ...fm.title !== void 0 ? { title: fm.title } : {},
4836
+ ...fm.summary !== void 0 ? { summary: fm.summary } : {},
4837
+ ...fm.status !== void 0 ? { status: fm.status } : {},
4838
+ ...fm.deferred_until !== void 0 ? { deferred_until: fm.deferred_until } : {},
4839
+ // v2.0.0-rc.27 TASK-006 (audit §2.23): body emission when opted in.
4840
+ ...includeBody ? { body: indexed.body } : {},
4841
+ // Canonical hits always have an id; pending hits typically don't yet —
4842
+ // surface the frontmatter id when present so consumers can dedupe.
4843
+ ...fm.id !== void 0 ? { stable_id: fm.id } : {}
4844
+ });
4845
+ }
4570
4846
  return items;
4571
4847
  }
4572
4848
  async function deferAll(projectRoot, pendingPaths, until, reason) {
@@ -4576,7 +4852,7 @@ async function deferAll(projectRoot, pendingPaths, until, reason) {
4576
4852
  try {
4577
4853
  const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
4578
4854
  if (existsSync5(sandboxed.abs)) {
4579
- const content = await readFile6(sandboxed.abs, "utf8");
4855
+ const content = await readFile7(sandboxed.abs, "utf8");
4580
4856
  stableId = parseFrontmatter(content).id;
4581
4857
  const patch = {
4582
4858
  status: "deferred",
@@ -4917,9 +5193,9 @@ function registerPending(server, tracker) {
4917
5193
  }
4918
5194
 
4919
5195
  // src/services/doctor.ts
4920
- import { access as access4, readFile as readFile16, readdir as readdirAsync, stat as statAsync, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
5196
+ import { access as access4, readFile as readFile17, readdir as readdirAsync, stat as statAsync, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
4921
5197
  import { constants as constants2 } from "fs";
4922
- import { isAbsolute as isAbsolute2, join as join21, posix as posix5, resolve as resolve3 } from "path";
5198
+ import { isAbsolute as isAbsolute2, join as join22, posix as posix5, resolve as resolve3 } from "path";
4923
5199
  import {
4924
5200
  createTranslator,
4925
5201
  forensicReportSchema,
@@ -5133,8 +5409,8 @@ function createKnowledgeSummaryOpaqueCheck(t, inspection) {
5133
5409
  }
5134
5410
 
5135
5411
  // src/services/doctor-stable-id-collision.ts
5136
- import { readFile as readFile7 } from "fs/promises";
5137
- import { basename as basename2, join as join11 } from "path";
5412
+ import { readFile as readFile8 } from "fs/promises";
5413
+ import { basename as basename2, join as join12 } from "path";
5138
5414
  import {
5139
5415
  buildStoreResolveInput as buildStoreResolveInput4,
5140
5416
  createStoreResolver as createStoreResolver4,
@@ -5161,7 +5437,7 @@ function resolveIntegrityStores(projectRoot) {
5161
5437
  return {
5162
5438
  store_uuid: entry.store_uuid,
5163
5439
  alias: entry.alias,
5164
- dir: join11(globalRoot, storeRelativePathForMount3(mounted ?? { store_uuid: entry.store_uuid }))
5440
+ dir: join12(globalRoot, storeRelativePathForMount3(mounted ?? { store_uuid: entry.store_uuid }))
5165
5441
  };
5166
5442
  });
5167
5443
  return { dirs, personalUuids };
@@ -5180,7 +5456,7 @@ async function inspectStoreStableIdIntegrity(projectRoot) {
5180
5456
  for (const ref of await readKnowledgeAcrossStores2(resolved.dirs)) {
5181
5457
  let source;
5182
5458
  try {
5183
- source = await readFile7(ref.file, "utf8");
5459
+ source = await readFile8(ref.file, "utf8");
5184
5460
  } catch {
5185
5461
  continue;
5186
5462
  }
@@ -5265,7 +5541,7 @@ function createLayerMismatchCheck(t, inspection) {
5265
5541
  // src/services/doctor-relevance-paths.ts
5266
5542
  import { execFileSync as execFileSync2 } from "child_process";
5267
5543
  import { existsSync as existsSync6, readdirSync, statSync as statSync3 } from "fs";
5268
- import { join as join12, sep as sep3 } from "path";
5544
+ import { join as join13, sep as sep3 } from "path";
5269
5545
  import { minimatch } from "minimatch";
5270
5546
  var MS_PER_DAY = 24 * 60 * 60 * 1e3;
5271
5547
  var RELEVANCE_PATHS_DRIFT_WINDOW_DAYS = 90;
@@ -5308,7 +5584,7 @@ function collectWorkspacePaths(projectRoot) {
5308
5584
  continue;
5309
5585
  }
5310
5586
  for (const entry of entries) {
5311
- const abs = join12(current, entry.name);
5587
+ const abs = join13(current, entry.name);
5312
5588
  const rel = toPosix(abs.slice(projectRoot.length + 1));
5313
5589
  if (rel.length === 0) continue;
5314
5590
  if (entry.isDirectory()) {
@@ -5501,16 +5777,16 @@ function createNarrowNoPathsCheck(t, inspection) {
5501
5777
  }
5502
5778
 
5503
5779
  // src/services/doctor-broad-index.ts
5504
- import { readFile as readFile8 } from "fs/promises";
5505
- import { join as join13 } from "path";
5780
+ import { readFile as readFile9 } from "fs/promises";
5781
+ import { join as join14 } from "path";
5506
5782
  var DEFAULT_BROAD_INDEX_BACKSTOP = 50;
5507
5783
  var BROAD_INDEX_BACKSTOP_MIN = 20;
5508
5784
  var BROAD_INDEX_BACKSTOP_MAX = 500;
5509
5785
  var BROAD_INDEX_DRIFT_RATIO = 0.8;
5510
5786
  async function readBroadIndexBackstop(projectRoot) {
5511
- const configPath = join13(projectRoot, ".fabric", "fabric-config.json");
5787
+ const configPath = join14(projectRoot, ".fabric", "fabric-config.json");
5512
5788
  try {
5513
- const raw = await readFile8(configPath, "utf8");
5789
+ const raw = await readFile9(configPath, "utf8");
5514
5790
  const parsed = JSON.parse(raw);
5515
5791
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
5516
5792
  const v = parsed.broad_index_backstop;
@@ -5827,8 +6103,8 @@ function createBroadReviewRecheckCheck(t, inspection) {
5827
6103
  }
5828
6104
 
5829
6105
  // src/services/doctor-scope-lint.ts
5830
- import { readFile as readFile9 } from "fs/promises";
5831
- import { join as join14 } from "path";
6106
+ import { readFile as readFile10 } from "fs/promises";
6107
+ import { join as join15 } from "path";
5832
6108
  import {
5833
6109
  buildStoreResolveInput as buildStoreResolveInput5,
5834
6110
  createStoreResolver as createStoreResolver5,
@@ -5863,7 +6139,7 @@ async function resolveLintStores(projectRoot) {
5863
6139
  const globalRoot = resolveGlobalRoot4();
5864
6140
  return Promise.all(readSet.stores.map(async (entry) => {
5865
6141
  const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
5866
- const dir = join14(
6142
+ const dir = join15(
5867
6143
  globalRoot,
5868
6144
  storeRelativePathForMount4(mounted ?? { store_uuid: entry.store_uuid })
5869
6145
  );
@@ -5895,7 +6171,7 @@ async function lintStoreScopes(projectRoot) {
5895
6171
  }
5896
6172
  let source;
5897
6173
  try {
5898
- source = await readFile9(ref.file, "utf8");
6174
+ source = await readFile10(ref.file, "utf8");
5899
6175
  } catch {
5900
6176
  continue;
5901
6177
  }
@@ -6018,7 +6294,7 @@ function createUnboundProjectCheck(t, violation) {
6018
6294
 
6019
6295
  // src/services/doctor-store-counters.ts
6020
6296
  import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
6021
- import { join as join15 } from "path";
6297
+ import { join as join16 } from "path";
6022
6298
  import {
6023
6299
  buildStoreResolveInput as buildStoreResolveInput6,
6024
6300
  createStoreResolver as createStoreResolver6,
@@ -6045,7 +6321,7 @@ function resolveCounterStores(projectRoot) {
6045
6321
  return readSet.stores.map((entry) => ({
6046
6322
  uuid: entry.store_uuid,
6047
6323
  alias: entry.alias,
6048
- dir: join15(
6324
+ dir: join16(
6049
6325
  globalRoot,
6050
6326
  storeRelativePathForMount5(
6051
6327
  input.mountedStores.find((s) => s.store_uuid === entry.store_uuid) ?? {
@@ -6073,7 +6349,7 @@ function readEntryId(file) {
6073
6349
  function computeStoreDiskMax(storeDir) {
6074
6350
  const max = defaultAgentsMetaCounters();
6075
6351
  for (const type of STORE_KNOWLEDGE_TYPE_DIRS) {
6076
- const dir = join15(storeDir, STORE_LAYOUT2.knowledgeDir, type);
6352
+ const dir = join16(storeDir, STORE_LAYOUT2.knowledgeDir, type);
6077
6353
  if (!existsSync7(dir)) {
6078
6354
  continue;
6079
6355
  }
@@ -6087,7 +6363,7 @@ function computeStoreDiskMax(storeDir) {
6087
6363
  if (!name.endsWith(".md")) {
6088
6364
  continue;
6089
6365
  }
6090
- const parsed = parseKnowledgeId2(readEntryId(join15(dir, name)) ?? "");
6366
+ const parsed = parseKnowledgeId2(readEntryId(join16(dir, name)) ?? "");
6091
6367
  if (parsed === null) {
6092
6368
  continue;
6093
6369
  }
@@ -6170,7 +6446,7 @@ function createStoreCounterCheck(t, drifts) {
6170
6446
 
6171
6447
  // src/services/doctor-store-orphan.ts
6172
6448
  import { readdirSync as readdirSync3 } from "fs";
6173
- import { join as join16 } from "path";
6449
+ import { join as join17 } from "path";
6174
6450
  import {
6175
6451
  STORES_ROOT_DIR,
6176
6452
  addMountedStore,
@@ -6194,15 +6470,15 @@ function inspectStoreOrphans(globalRoot = resolveGlobalRoot6()) {
6194
6470
  return [];
6195
6471
  }
6196
6472
  const registered = new Set(config.stores.map((s) => s.store_uuid));
6197
- const storesRoot = join16(globalRoot, STORES_ROOT_DIR);
6473
+ const storesRoot = join17(globalRoot, STORES_ROOT_DIR);
6198
6474
  const orphans = [];
6199
6475
  for (const group of listDir(storesRoot)) {
6200
6476
  if (group === STORE_BY_ALIAS_DIR) {
6201
6477
  continue;
6202
6478
  }
6203
- const groupDir = join16(storesRoot, group);
6479
+ const groupDir = join17(storesRoot, group);
6204
6480
  for (const mount of listDir(groupDir)) {
6205
- const dir = join16(groupDir, mount);
6481
+ const dir = join17(groupDir, mount);
6206
6482
  const identity = readStoreIdentity(dir);
6207
6483
  if (identity === null || registered.has(identity.store_uuid)) {
6208
6484
  continue;
@@ -6339,8 +6615,8 @@ async function inspectEventsJsonlGates(projectRoot, options = {}) {
6339
6615
  }
6340
6616
 
6341
6617
  // src/services/doctor-skill-lints.ts
6342
- import { readdir as readdir3, readFile as readFile10 } from "fs/promises";
6343
- import { join as join17, posix as posix2 } from "path";
6618
+ import { readdir as readdir3, readFile as readFile11 } from "fs/promises";
6619
+ import { join as join18, posix as posix2 } from "path";
6344
6620
  var FABRIC_SKILL_SLUGS = ["fabric-archive", "fabric-review"];
6345
6621
  var SKILL_MD_FRONTMATTER_ROOTS = [".claude/skills", ".codex/skills"];
6346
6622
  var SKILL_FRONTMATTER_KEY_PATTERN = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]+(.+?)[ \t]*$/u;
@@ -6370,8 +6646,8 @@ async function listMarkdownFiles(dir) {
6370
6646
  async function inspectSkillRefMirror(projectRoot) {
6371
6647
  const driftedPaths = [];
6372
6648
  for (const slug of FABRIC_SKILL_SLUGS) {
6373
- const claudeRef = join17(projectRoot, ".claude", "skills", slug, "ref");
6374
- const codexRef = join17(projectRoot, ".codex", "skills", slug, "ref");
6649
+ const claudeRef = join18(projectRoot, ".claude", "skills", slug, "ref");
6650
+ const codexRef = join18(projectRoot, ".codex", "skills", slug, "ref");
6375
6651
  const [claudeFiles, codexFiles] = await Promise.all([listMarkdownFiles(claudeRef), listMarkdownFiles(codexRef)]);
6376
6652
  if (claudeFiles === null || codexFiles === null) continue;
6377
6653
  const claudeSet = new Set(claudeFiles);
@@ -6388,8 +6664,8 @@ async function inspectSkillRefMirror(projectRoot) {
6388
6664
  let codexBody;
6389
6665
  try {
6390
6666
  [claudeBody, codexBody] = await Promise.all([
6391
- readFile10(join17(claudeRef, fname), "utf8"),
6392
- readFile10(join17(codexRef, fname), "utf8")
6667
+ readFile11(join18(claudeRef, fname), "utf8"),
6668
+ readFile11(join18(codexRef, fname), "utf8")
6393
6669
  ]);
6394
6670
  } catch {
6395
6671
  continue;
@@ -6408,10 +6684,10 @@ async function inspectSkillTokenBudget(projectRoot) {
6408
6684
  const overSize = [];
6409
6685
  let highestSeverity = "ok";
6410
6686
  for (const slug of FABRIC_SKILL_SLUGS) {
6411
- const skillMdPath = join17(projectRoot, ".claude", "skills", slug, "SKILL.md");
6687
+ const skillMdPath = join18(projectRoot, ".claude", "skills", slug, "SKILL.md");
6412
6688
  let body;
6413
6689
  try {
6414
- body = await readFile10(skillMdPath, "utf8");
6690
+ body = await readFile11(skillMdPath, "utf8");
6415
6691
  } catch {
6416
6692
  continue;
6417
6693
  }
@@ -6432,10 +6708,10 @@ async function inspectSkillDescription(projectRoot) {
6432
6708
  const CJK_PATTERN = /[\u3400-\u4dbf\u4e00-\u9fff]/u;
6433
6709
  const ASCII_PATTERN = /[a-zA-Z]{2,}/u;
6434
6710
  for (const slug of FABRIC_SKILL_SLUGS) {
6435
- const skillMdPath = join17(projectRoot, ".claude", "skills", slug, "SKILL.md");
6711
+ const skillMdPath = join18(projectRoot, ".claude", "skills", slug, "SKILL.md");
6436
6712
  let body;
6437
6713
  try {
6438
- body = await readFile10(skillMdPath, "utf8");
6714
+ body = await readFile11(skillMdPath, "utf8");
6439
6715
  } catch {
6440
6716
  continue;
6441
6717
  }
@@ -6466,7 +6742,7 @@ async function inspectSkillDescription(projectRoot) {
6466
6742
  async function inspectSkillMdYamlInvalid(projectRoot) {
6467
6743
  const candidates = [];
6468
6744
  for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
6469
- const rootAbs = join17(projectRoot, rootRel);
6745
+ const rootAbs = join18(projectRoot, rootRel);
6470
6746
  let dirEntries;
6471
6747
  try {
6472
6748
  dirEntries = await readdir3(rootAbs, { withFileTypes: true });
@@ -6475,10 +6751,10 @@ async function inspectSkillMdYamlInvalid(projectRoot) {
6475
6751
  }
6476
6752
  for (const dirEntry of dirEntries) {
6477
6753
  if (!dirEntry.isDirectory()) continue;
6478
- const skillFile = join17(rootAbs, dirEntry.name, "SKILL.md");
6754
+ const skillFile = join18(rootAbs, dirEntry.name, "SKILL.md");
6479
6755
  let raw;
6480
6756
  try {
6481
- raw = await readFile10(skillFile, "utf8");
6757
+ raw = await readFile11(skillFile, "utf8");
6482
6758
  } catch {
6483
6759
  continue;
6484
6760
  }
@@ -6601,8 +6877,8 @@ function createSkillMdYamlInvalidCheck(t, inspection) {
6601
6877
  }
6602
6878
 
6603
6879
  // src/services/doctor-retired-references-lint.ts
6604
- import { readdir as readdir4, readFile as readFile11, stat as stat3 } from "fs/promises";
6605
- import { join as join18, posix as posix3, relative as relative2 } from "path";
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";
6606
6882
  var RETIRED_TOKENS = [
6607
6883
  { token: "fab_plan_context", replacement: "fab_recall", reason: "retrieval collapsed to one lean fab_recall (KT-DEC-0026)" },
6608
6884
  { token: "fab_get_knowledge_sections", replacement: "fab_recall", reason: "two-step fetch retired (KT-DEC-0026)" },
@@ -6625,7 +6901,7 @@ var RETIRED_TOKENS = [
6625
6901
  ];
6626
6902
  var HOOK_DIRS = [".claude/hooks", ".codex/hooks"];
6627
6903
  var SKILL_DIRS = [".claude/skills", ".codex/skills"];
6628
- var BOOTSTRAP_FILES = ["AGENTS.md", "CLAUDE.md", join18(".fabric", "AGENTS.md")];
6904
+ var BOOTSTRAP_FILES = ["AGENTS.md", "CLAUDE.md", join19(".fabric", "AGENTS.md")];
6629
6905
  function isCommentLine(line) {
6630
6906
  const t = line.trim();
6631
6907
  return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*");
@@ -6639,7 +6915,7 @@ async function walkFiles(dir, exts) {
6639
6915
  }
6640
6916
  const out = [];
6641
6917
  for (const e of entries) {
6642
- const full = join18(dir, e.name);
6918
+ const full = join19(dir, e.name);
6643
6919
  if (e.isDirectory()) {
6644
6920
  out.push(...await walkFiles(full, exts));
6645
6921
  } else if (exts.some((ext) => e.name.endsWith(ext))) {
@@ -6665,29 +6941,29 @@ async function inspectRetiredReferences(projectRoot) {
6665
6941
  let scannedFiles = 0;
6666
6942
  const toRel = (p) => posix3.normalize(relative2(projectRoot, p).split("\\").join("/"));
6667
6943
  for (const rel of BOOTSTRAP_FILES) {
6668
- const abs = join18(projectRoot, rel);
6944
+ const abs = join19(projectRoot, rel);
6669
6945
  try {
6670
6946
  if ((await stat3(abs)).isFile()) {
6671
6947
  scannedFiles += 1;
6672
- scanText(toRel(abs), await readFile11(abs, "utf8"), false, hits);
6948
+ scanText(toRel(abs), await readFile12(abs, "utf8"), false, hits);
6673
6949
  }
6674
6950
  } catch {
6675
6951
  }
6676
6952
  }
6677
6953
  for (const dir of SKILL_DIRS) {
6678
- for (const file of await walkFiles(join18(projectRoot, dir), [".md"])) {
6954
+ for (const file of await walkFiles(join19(projectRoot, dir), [".md"])) {
6679
6955
  scannedFiles += 1;
6680
6956
  try {
6681
- scanText(toRel(file), await readFile11(file, "utf8"), false, hits);
6957
+ scanText(toRel(file), await readFile12(file, "utf8"), false, hits);
6682
6958
  } catch {
6683
6959
  }
6684
6960
  }
6685
6961
  }
6686
6962
  for (const dir of HOOK_DIRS) {
6687
- for (const file of await walkFiles(join18(projectRoot, dir), [".cjs"])) {
6963
+ for (const file of await walkFiles(join19(projectRoot, dir), [".cjs"])) {
6688
6964
  scannedFiles += 1;
6689
6965
  try {
6690
- scanText(toRel(file), await readFile11(file, "utf8"), true, hits);
6966
+ scanText(toRel(file), await readFile12(file, "utf8"), true, hits);
6691
6967
  } catch {
6692
6968
  }
6693
6969
  }
@@ -6722,8 +6998,8 @@ function createRetiredReferenceCheck(t, inspection) {
6722
6998
 
6723
6999
  // src/services/doctor-hooks-lints.ts
6724
7000
  import { constants } from "fs";
6725
- import { access, readdir as readdir5, readFile as readFile12, stat as stat4 } from "fs/promises";
6726
- import { join as join19, posix as posix4 } from "path";
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";
6727
7003
  import { Script } from "vm";
6728
7004
  var HOOKS_RUNTIME_CLIENT_DIRS = [
6729
7005
  { client: "claude", dir: ".claude/hooks" },
@@ -6785,14 +7061,14 @@ async function isFile(absPath) {
6785
7061
  }
6786
7062
  }
6787
7063
  async function inspectHooksWired(projectRoot) {
6788
- const claudeEntries = await readDirectoryFileNames(join19(projectRoot, ".claude"));
7064
+ const claudeEntries = await readDirectoryFileNames(join20(projectRoot, ".claude"));
6789
7065
  if (claudeEntries === null) {
6790
7066
  return { status: "skipped", missingHooks: [] };
6791
7067
  }
6792
- const settingsPath = join19(projectRoot, ".claude", "settings.json");
7068
+ const settingsPath = join20(projectRoot, ".claude", "settings.json");
6793
7069
  let parsed;
6794
7070
  try {
6795
- parsed = JSON.parse(await readFile12(settingsPath, "utf8"));
7071
+ parsed = JSON.parse(await readFile13(settingsPath, "utf8"));
6796
7072
  } catch {
6797
7073
  return { status: "missing-settings", missingHooks: [] };
6798
7074
  }
@@ -6815,8 +7091,8 @@ async function inspectHooksWired(projectRoot) {
6815
7091
  }
6816
7092
  async function inspectHookCacheWritability(projectRoot) {
6817
7093
  const relPath = posix4.join(".fabric", ".cache");
6818
- const fabricDir = join19(projectRoot, ".fabric");
6819
- const cacheDir = join19(projectRoot, ".fabric", ".cache");
7094
+ const fabricDir = join20(projectRoot, ".fabric");
7095
+ const cacheDir = join20(projectRoot, ".fabric", ".cache");
6820
7096
  try {
6821
7097
  try {
6822
7098
  const cacheStats = await stat4(cacheDir);
@@ -6865,12 +7141,12 @@ async function inspectHookCacheWritability(projectRoot) {
6865
7141
  async function inspectHooksContentDrift(projectRoot) {
6866
7142
  const hookFilesByBasename = /* @__PURE__ */ new Map();
6867
7143
  for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
6868
- const absDir = join19(projectRoot, dir);
7144
+ const absDir = join20(projectRoot, dir);
6869
7145
  const entries = await readDirectoryFileNames(absDir);
6870
7146
  if (entries === null) continue;
6871
7147
  for (const name of entries) {
6872
7148
  if (!name.endsWith(".cjs")) continue;
6873
- const abs = join19(absDir, name);
7149
+ const abs = join20(absDir, name);
6874
7150
  if (!await isFile(abs)) continue;
6875
7151
  const arr = hookFilesByBasename.get(name) ?? [];
6876
7152
  arr.push({ client, abs });
@@ -6885,7 +7161,7 @@ async function inspectHooksContentDrift(projectRoot) {
6885
7161
  const hashes = [];
6886
7162
  for (const { client, abs } of copies) {
6887
7163
  try {
6888
- const body = await readFile12(abs, "utf8");
7164
+ const body = await readFile13(abs, "utf8");
6889
7165
  hashes.push({ client, sha: sha256(body) });
6890
7166
  } catch {
6891
7167
  }
@@ -6907,18 +7183,18 @@ async function inspectHooksRuntime(projectRoot) {
6907
7183
  const issues = [];
6908
7184
  let scanned = 0;
6909
7185
  for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
6910
- const absDir = join19(projectRoot, dir);
7186
+ const absDir = join20(projectRoot, dir);
6911
7187
  const entries = await readDirectoryFileNames(absDir);
6912
7188
  if (entries === null) continue;
6913
7189
  for (const name of entries) {
6914
7190
  if (!name.endsWith(".cjs")) continue;
6915
- const abs = join19(absDir, name);
7191
+ const abs = join20(absDir, name);
6916
7192
  const displayPath = `${dir}/${name}`;
6917
7193
  if (!await isFile(abs)) continue;
6918
7194
  scanned += 1;
6919
7195
  let body;
6920
7196
  try {
6921
- body = await readFile12(abs, "utf8");
7197
+ body = await readFile13(abs, "utf8");
6922
7198
  } catch (err) {
6923
7199
  issues.push({
6924
7200
  path: displayPath,
@@ -7055,8 +7331,8 @@ function createHookCacheWritabilityCheck(t, inspection) {
7055
7331
  }
7056
7332
 
7057
7333
  // src/services/doctor-bootstrap-lints.ts
7058
- import { access as access2, readFile as readFile13 } from "fs/promises";
7059
- import { join as join20 } from "path";
7334
+ import { access as access2, readFile as readFile14 } from "fs/promises";
7335
+ import { join as join21 } from "path";
7060
7336
  import {
7061
7337
  BOOTSTRAP_MARKER_BEGIN,
7062
7338
  BOOTSTRAP_MARKER_END,
@@ -7088,17 +7364,17 @@ async function fileExists(path) {
7088
7364
  }
7089
7365
  async function inspectBootstrapAnchor(projectRoot) {
7090
7366
  const [hasAgentsMd, hasClaudeMd] = await Promise.all([
7091
- fileExists(join20(projectRoot, "AGENTS.md")),
7092
- fileExists(join20(projectRoot, "CLAUDE.md"))
7367
+ fileExists(join21(projectRoot, "AGENTS.md")),
7368
+ fileExists(join21(projectRoot, "CLAUDE.md"))
7093
7369
  ]);
7094
7370
  return { hasAgentsMd, hasClaudeMd };
7095
7371
  }
7096
7372
  async function inspectL1BootstrapSnapshotDrift(target) {
7097
- const abs = join20(target, ".fabric", "AGENTS.md");
7373
+ const abs = join21(target, ".fabric", "AGENTS.md");
7098
7374
  const canonical = resolveBootstrapCanonical();
7099
7375
  let onDisk;
7100
7376
  try {
7101
- onDisk = await readFile13(abs, "utf8");
7377
+ onDisk = await readFile14(abs, "utf8");
7102
7378
  } catch {
7103
7379
  return { status: "missing", canonical, onDisk: null };
7104
7380
  }
@@ -7124,17 +7400,17 @@ function createL1BootstrapSnapshotDriftCheck(t, inspection) {
7124
7400
  );
7125
7401
  }
7126
7402
  async function inspectL2ManagedBlockDrift(target) {
7127
- const snapshotPath = join20(target, ".fabric", "AGENTS.md");
7403
+ const snapshotPath = join21(target, ".fabric", "AGENTS.md");
7128
7404
  let snapshot;
7129
7405
  try {
7130
- snapshot = await readFile13(snapshotPath, "utf8");
7406
+ snapshot = await readFile14(snapshotPath, "utf8");
7131
7407
  } catch {
7132
7408
  return { status: "ok", drifted: [] };
7133
7409
  }
7134
- const projectRulesPath = join20(target, ".fabric", "project-rules.md");
7410
+ const projectRulesPath = join21(target, ".fabric", "project-rules.md");
7135
7411
  let expectedBody = snapshot;
7136
7412
  try {
7137
- const projectRules = await readFile13(projectRulesPath, "utf8");
7413
+ const projectRules = await readFile14(projectRulesPath, "utf8");
7138
7414
  expectedBody = `${snapshot}
7139
7415
  ---
7140
7416
  ${projectRules}`;
@@ -7143,12 +7419,12 @@ ${projectRules}`;
7143
7419
  const drifted = [];
7144
7420
  let anyManagedBlockFound = false;
7145
7421
  const blockTargets = [
7146
- join20(target, "AGENTS.md")
7422
+ join21(target, "AGENTS.md")
7147
7423
  ];
7148
7424
  for (const abs of blockTargets) {
7149
7425
  let content;
7150
7426
  try {
7151
- content = await readFile13(abs, "utf8");
7427
+ content = await readFile14(abs, "utf8");
7152
7428
  } catch {
7153
7429
  continue;
7154
7430
  }
@@ -7171,9 +7447,9 @@ ${projectRules}`;
7171
7447
  drifted.push({ path: abs, expected: expectedBody, actual: body });
7172
7448
  }
7173
7449
  }
7174
- const claudeMdPath = join20(target, "CLAUDE.md");
7450
+ const claudeMdPath = join21(target, "CLAUDE.md");
7175
7451
  try {
7176
- const claudeContent = await readFile13(claudeMdPath, "utf8");
7452
+ const claudeContent = await readFile14(claudeMdPath, "utf8");
7177
7453
  anyManagedBlockFound = true;
7178
7454
  const lines = claudeContent.split(/\r?\n/u);
7179
7455
  const hasAtImport = lines.some((line) => line.trim() === "@.fabric/AGENTS.md");
@@ -7241,7 +7517,7 @@ import { appendFile as appendFile3 } from "fs/promises";
7241
7517
  import { minimatch as minimatch2 } from "minimatch";
7242
7518
 
7243
7519
  // src/services/cite-rollup.ts
7244
- import { readFile as readFile14 } from "fs/promises";
7520
+ import { readFile as readFile15 } from "fs/promises";
7245
7521
  import { createLedgerWriteQueue as createLedgerWriteQueue3 } from "@fenglimg/fabric-shared/node/atomic-write";
7246
7522
  var citeRollupQueue = createLedgerWriteQueue3();
7247
7523
  async function appendCiteRollupRow(projectRoot, row) {
@@ -7253,7 +7529,7 @@ async function readCiteRollup(projectRoot) {
7253
7529
  const path = getCiteRollupPath(projectRoot);
7254
7530
  let raw;
7255
7531
  try {
7256
- raw = await readFile14(path, "utf8");
7532
+ raw = await readFile15(path, "utf8");
7257
7533
  } catch (error) {
7258
7534
  if (isNodeError(error) && error.code === "ENOENT") return [];
7259
7535
  throw error;
@@ -8383,7 +8659,7 @@ async function runDoctorReport(target) {
8383
8659
  const globalCliVersion = process.env.VITEST === "true" ? { status: "ok", version: "test-skipped" } : inspectGlobalCliVersion();
8384
8660
  const targetFiles = Object.fromEntries(
8385
8661
  await Promise.all(
8386
- TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(join21(projectRoot, path))])
8662
+ TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(join22(projectRoot, path))])
8387
8663
  )
8388
8664
  );
8389
8665
  const checks = [
@@ -8587,7 +8863,7 @@ async function runDoctorFix(target) {
8587
8863
  const fixed = [];
8588
8864
  const ledgerWarnings = [];
8589
8865
  if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
8590
- const snapshotPath = join21(projectRoot, ".fabric", "AGENTS.md");
8866
+ const snapshotPath = join22(projectRoot, ".fabric", "AGENTS.md");
8591
8867
  await ensureParentDirectory(snapshotPath);
8592
8868
  await atomicWriteText4(snapshotPath, resolveBootstrapCanonical2());
8593
8869
  fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
@@ -8660,7 +8936,7 @@ async function runDoctorFix(target) {
8660
8936
  if (before.infos.some((issue) => issue.code === "stale_serve_lock")) {
8661
8937
  const lockInspection = inspectStaleServeLock(projectRoot, Date.now());
8662
8938
  if (lockInspection.present && !lockInspection.pidAlive) {
8663
- const lockFilePath = join21(projectRoot, ".fabric", ".serve.lock");
8939
+ const lockFilePath = join22(projectRoot, ".fabric", ".serve.lock");
8664
8940
  try {
8665
8941
  await unlink4(lockFilePath);
8666
8942
  } catch (err) {
@@ -8777,7 +9053,7 @@ function createApplyLintMessage(succeeded, failed, manualErrorCount) {
8777
9053
  }
8778
9054
  async function applySessionHintsStaleCleanup(projectRoot, candidate) {
8779
9055
  const detail = `deleted (${candidate.age_days}d old)`;
8780
- const absPath = join21(projectRoot, candidate.path);
9056
+ const absPath = join22(projectRoot, candidate.path);
8781
9057
  try {
8782
9058
  const { unlink: unlink5 } = await import("fs/promises");
8783
9059
  await unlink5(absPath);
@@ -8802,9 +9078,9 @@ function truncateErrorMessage(error) {
8802
9078
  return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
8803
9079
  }
8804
9080
  async function inspectForensic(projectRoot) {
8805
- const path = join21(projectRoot, ".fabric", "forensic.json");
9081
+ const path = join22(projectRoot, ".fabric", "forensic.json");
8806
9082
  try {
8807
- const parsed = forensicReportSchema.parse(JSON.parse(await readFile16(path, "utf8")));
9083
+ const parsed = forensicReportSchema.parse(JSON.parse(await readFile17(path, "utf8")));
8808
9084
  return { present: true, valid: true, report: parsed };
8809
9085
  } catch (error) {
8810
9086
  if (isMissingFileError(error)) {
@@ -8834,7 +9110,7 @@ async function inspectEventLedger(projectRoot) {
8834
9110
  try {
8835
9111
  await access4(path, constants2.W_OK);
8836
9112
  const { warnings } = await readEventLedger(projectRoot);
8837
- const raw = await readFile16(path, "utf8");
9113
+ const raw = await readFile17(path, "utf8");
8838
9114
  const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
8839
9115
  const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
8840
9116
  const schemaVersionSamples = [];
@@ -9379,7 +9655,7 @@ async function inspectPreexistingRootFiles(projectRoot) {
9379
9655
  const candidates = ["CLAUDE.md", "AGENTS.md"];
9380
9656
  const detected = [];
9381
9657
  for (const name of candidates) {
9382
- if (await pathExists(join21(projectRoot, name))) {
9658
+ if (await pathExists(join22(projectRoot, name))) {
9383
9659
  detected.push(name);
9384
9660
  }
9385
9661
  }
@@ -9464,7 +9740,7 @@ async function buildLastActiveIndex(projectRoot) {
9464
9740
  return map;
9465
9741
  }
9466
9742
  async function inspectSessionHintsStale(projectRoot, now) {
9467
- const cacheDir = join21(projectRoot, ".fabric", ".cache");
9743
+ const cacheDir = join22(projectRoot, ".fabric", ".cache");
9468
9744
  let entries;
9469
9745
  try {
9470
9746
  entries = await readdirAsync(cacheDir, { withFileTypes: true });
@@ -9476,7 +9752,7 @@ async function inspectSessionHintsStale(projectRoot, now) {
9476
9752
  if (!entry.isFile()) continue;
9477
9753
  if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
9478
9754
  if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
9479
- const absPath = join21(cacheDir, entry.name);
9755
+ const absPath = join22(cacheDir, entry.name);
9480
9756
  let mtimeMs = 0;
9481
9757
  try {
9482
9758
  mtimeMs = (await statAsync(absPath)).mtimeMs;
@@ -9508,9 +9784,9 @@ function inspectStaleServeLock(projectRoot, now) {
9508
9784
  };
9509
9785
  }
9510
9786
  async function readUnderseedThresholdFromConfig(projectRoot) {
9511
- const configPath = join21(projectRoot, ".fabric", "fabric-config.json");
9787
+ const configPath = join22(projectRoot, ".fabric", "fabric-config.json");
9512
9788
  try {
9513
- const raw = await readFile16(configPath, "utf8");
9789
+ const raw = await readFile17(configPath, "utf8");
9514
9790
  const parsed = JSON.parse(raw);
9515
9791
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
9516
9792
  const v = parsed.underseed_node_threshold;
@@ -9626,10 +9902,10 @@ async function inspectOnboardCoverage(projectRoot) {
9626
9902
  return { filled, missing, opted_out: optedOut };
9627
9903
  }
9628
9904
  async function readOnboardOptedOut(projectRoot) {
9629
- const path = join21(projectRoot, ".fabric", "fabric-config.json");
9905
+ const path = join22(projectRoot, ".fabric", "fabric-config.json");
9630
9906
  let raw;
9631
9907
  try {
9632
- raw = await readFile16(path, "utf8");
9908
+ raw = await readFile17(path, "utf8");
9633
9909
  } catch {
9634
9910
  return [];
9635
9911
  }
@@ -9731,22 +10007,22 @@ async function* iterateCanonicalFilenames(projectRoot) {
9731
10007
  }
9732
10008
  }
9733
10009
  async function rewriteThreeEndManagedBlocks(projectRoot) {
9734
- const snapshotPath = join21(projectRoot, ".fabric", "AGENTS.md");
10010
+ const snapshotPath = join22(projectRoot, ".fabric", "AGENTS.md");
9735
10011
  if (!await pathExists(snapshotPath)) {
9736
10012
  return;
9737
10013
  }
9738
10014
  let snapshot;
9739
10015
  try {
9740
- snapshot = await readFile16(snapshotPath, "utf8");
10016
+ snapshot = await readFile17(snapshotPath, "utf8");
9741
10017
  } catch {
9742
10018
  return;
9743
10019
  }
9744
- const projectRulesPath = join21(projectRoot, ".fabric", "project-rules.md");
10020
+ const projectRulesPath = join22(projectRoot, ".fabric", "project-rules.md");
9745
10021
  const hasProjectRules = await pathExists(projectRulesPath);
9746
10022
  let expectedBody = snapshot;
9747
10023
  if (hasProjectRules) {
9748
10024
  try {
9749
- const projectRules = await readFile16(projectRulesPath, "utf8");
10025
+ const projectRules = await readFile17(projectRulesPath, "utf8");
9750
10026
  expectedBody = `${snapshot}
9751
10027
  ---
9752
10028
  ${projectRules}`;
@@ -9757,7 +10033,7 @@ ${projectRules}`;
9757
10033
  ${expectedBody}
9758
10034
  ${BOOTSTRAP_MARKER_END2}`;
9759
10035
  const blockTargets = [
9760
- join21(projectRoot, "AGENTS.md")
10036
+ join22(projectRoot, "AGENTS.md")
9761
10037
  ];
9762
10038
  for (const abs of blockTargets) {
9763
10039
  if (!await pathExists(abs)) {
@@ -9765,7 +10041,7 @@ ${BOOTSTRAP_MARKER_END2}`;
9765
10041
  }
9766
10042
  let existing;
9767
10043
  try {
9768
- existing = await readFile16(abs, "utf8");
10044
+ existing = await readFile17(abs, "utf8");
9769
10045
  } catch {
9770
10046
  continue;
9771
10047
  }
@@ -9790,11 +10066,11 @@ ${managedBlock}
9790
10066
  }
9791
10067
  await atomicWriteText4(abs, next);
9792
10068
  }
9793
- const claudeMdPath = join21(projectRoot, "CLAUDE.md");
10069
+ const claudeMdPath = join22(projectRoot, "CLAUDE.md");
9794
10070
  if (await pathExists(claudeMdPath)) {
9795
10071
  let claudeContent;
9796
10072
  try {
9797
- claudeContent = await readFile16(claudeMdPath, "utf8");
10073
+ claudeContent = await readFile17(claudeMdPath, "utf8");
9798
10074
  } catch {
9799
10075
  return;
9800
10076
  }
@@ -9821,7 +10097,7 @@ ${managedBlock}
9821
10097
  async function ensureEventLedger(projectRoot) {
9822
10098
  const path = getEventLedgerPath(projectRoot);
9823
10099
  await ensureParentDirectory(path);
9824
- await writeFile3(path, "", { encoding: "utf8", flag: "a" });
10100
+ await writeFile4(path, "", { encoding: "utf8", flag: "a" });
9825
10101
  }
9826
10102
  function createFixMessage(fixed, report) {
9827
10103
  const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
@@ -9860,7 +10136,7 @@ async function collectEntryPoints(root) {
9860
10136
  continue;
9861
10137
  }
9862
10138
  for (const entry of await readdirAsync(current, { withFileTypes: true })) {
9863
- const absolutePath = join21(current, entry.name);
10139
+ const absolutePath = join22(current, entry.name);
9864
10140
  const relativePath = normalizePath2(absolutePath.slice(root.length + 1));
9865
10141
  if (relativePath.length === 0) {
9866
10142
  continue;
@@ -9936,7 +10212,7 @@ async function enrichDescriptions(projectRoot, opts = {}) {
9936
10212
  scanned += 1;
9937
10213
  let source;
9938
10214
  try {
9939
- source = await readFile16(absPath, "utf8");
10215
+ source = await readFile17(absPath, "utf8");
9940
10216
  } catch {
9941
10217
  continue;
9942
10218
  }
@@ -10074,8 +10350,8 @@ async function runDoctorConflictLint(projectRoot, opts = {}) {
10074
10350
  }
10075
10351
 
10076
10352
  // src/services/why-not-surfaced.ts
10077
- import { readFile as readFile17 } from "fs/promises";
10078
- import { basename as basename3, join as join22 } from "path";
10353
+ import { readFile as readFile18 } from "fs/promises";
10354
+ import { basename as basename3, join as join23 } from "path";
10079
10355
  import {
10080
10356
  buildStoreResolveInput as buildStoreResolveInput7,
10081
10357
  createStoreResolver as createStoreResolver7,
@@ -10115,7 +10391,7 @@ async function explainWhyNotSurfaced(projectRoot, query) {
10115
10391
  const allStores = input.mountedStores.map((s) => ({
10116
10392
  store_uuid: s.store_uuid,
10117
10393
  alias: s.alias,
10118
- dir: join22(globalRoot, storeRelativePathForMount6(s))
10394
+ dir: join23(globalRoot, storeRelativePathForMount6(s))
10119
10395
  }));
10120
10396
  const refs = await readKnowledgeAcrossStores4(allStores);
10121
10397
  const candidate = refs.find((ref) => fileMatchesId(ref.file, localId));
@@ -10124,7 +10400,7 @@ async function explainWhyNotSurfaced(projectRoot, query) {
10124
10400
  }
10125
10401
  let source;
10126
10402
  try {
10127
- source = await readFile17(candidate.file, "utf8");
10403
+ source = await readFile18(candidate.file, "utf8");
10128
10404
  } catch {
10129
10405
  return base;
10130
10406
  }
@@ -10212,7 +10488,7 @@ import { IOFabricError as IOFabricError2, RuleError } from "@fenglimg/fabric-sha
10212
10488
 
10213
10489
  // src/services/read-ledger.ts
10214
10490
  import { randomUUID as randomUUID7 } from "crypto";
10215
- import { access as access5, copyFile, readFile as readFile18, rm } from "fs/promises";
10491
+ import { access as access5, copyFile, readFile as readFile19, rm } from "fs/promises";
10216
10492
  import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
10217
10493
  async function resolveLedgerPaths(projectRoot) {
10218
10494
  const primaryPath = getLedgerPath(projectRoot);
@@ -10240,7 +10516,7 @@ async function readLegacyLedger(projectRoot) {
10240
10516
  const { readPath } = await resolveLedgerPaths(projectRoot);
10241
10517
  let raw;
10242
10518
  try {
10243
- raw = await readFile18(readPath, "utf8");
10519
+ raw = await readFile19(readPath, "utf8");
10244
10520
  } catch (error) {
10245
10521
  if (isNodeError(error) && error.code === "ENOENT") {
10246
10522
  return [];
@@ -10495,8 +10771,8 @@ function formatError(error) {
10495
10771
  }
10496
10772
  function formatPreexistingRootMessage(projectRoot) {
10497
10773
  const preexisting = [];
10498
- if (existsSync9(join23(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
10499
- if (existsSync9(join23(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
10774
+ if (existsSync9(join24(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
10775
+ if (existsSync9(join24(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
10500
10776
  if (preexisting.length === 0) return null;
10501
10777
  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.`;
10502
10778
  }
@@ -10523,7 +10799,7 @@ function createFabricServer(tracker) {
10523
10799
  const server = new McpServer(
10524
10800
  {
10525
10801
  name: "fabric-knowledge-server",
10526
- version: "2.3.0-rc.1"
10802
+ version: "2.3.0-rc.2"
10527
10803
  },
10528
10804
  {
10529
10805
  instructions: FABRIC_SERVER_INSTRUCTIONS
@@ -10543,10 +10819,10 @@ function createFabricServer(tracker) {
10543
10819
  },
10544
10820
  async (_uri) => {
10545
10821
  const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
10546
- const path = join23(projectRoot, ".fabric", "bootstrap", "README.md");
10822
+ const path = join24(projectRoot, ".fabric", "bootstrap", "README.md");
10547
10823
  let text = "";
10548
10824
  if (existsSync9(path)) {
10549
- text = await readFile19(path, "utf8");
10825
+ text = await readFile20(path, "utf8");
10550
10826
  }
10551
10827
  return {
10552
10828
  contents: [