@fenglimg/fabric-server 2.3.0-rc.3 → 2.3.0-rc.5

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 +175 -2
  2. package/dist/index.js +903 -191
  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 join25, resolve as resolve4 } from "path";
2
+ import { existsSync as existsSync10 } from "fs";
3
+ import { readFile as readFile22 } from "fs/promises";
4
+ import { join as join27, 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";
@@ -893,7 +893,7 @@ async function awaitFirstReconcileGate(timeoutMs = 5e3) {
893
893
 
894
894
  // src/services/extract-knowledge.ts
895
895
  import { existsSync as existsSync3 } from "fs";
896
- import { readFile as readFile4 } from "fs/promises";
896
+ import { readFile as readFile5 } from "fs/promises";
897
897
  import { join as join7 } from "path";
898
898
  import {
899
899
  PROPOSED_REASON_DESCRIPTIONS_BY_LOCALE
@@ -901,10 +901,14 @@ import {
901
901
  import { hasSecrets, isPersonalScope, redactPii, resolveGlobalLocale } from "@fenglimg/fabric-shared";
902
902
 
903
903
  // src/services/cross-store-write.ts
904
+ import { createHash as createHash2 } from "crypto";
905
+ import { readFile as readFile3 } from "fs/promises";
904
906
  import { join as join5 } from "path";
905
907
  import {
908
+ STORE_KNOWLEDGE_TYPE_DIRS,
906
909
  STORE_LAYOUT,
907
910
  STORE_PENDING_DIR,
911
+ STORE_PROJECT_ID_PATTERN,
908
912
  buildStoreResolveInput,
909
913
  createStoreResolver,
910
914
  isPersonalLeakIntoSharedStore,
@@ -916,6 +920,7 @@ import {
916
920
  PersonalScopeLeakError,
917
921
  StoreWriteTargetUnresolvedError
918
922
  } from "@fenglimg/fabric-shared/errors";
923
+ import { withFileLock as withFileLock2 } from "@fenglimg/fabric-shared/node/atomic-write";
919
924
  function writeTargetUnresolved(scope, layer) {
920
925
  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}\``;
921
926
  return new StoreWriteTargetUnresolvedError(
@@ -952,8 +957,25 @@ function resolveWriteTargetStoreDir(layer, projectRoot, semanticScope) {
952
957
  function resolveStorePendingBase(layer, projectRoot, semanticScope) {
953
958
  return join5(resolveWriteTargetStoreDir(layer, projectRoot, semanticScope), STORE_LAYOUT.knowledgeDir, STORE_PENDING_DIR);
954
959
  }
955
- function resolveStoreCanonicalBase(layer, projectRoot) {
956
- return join5(resolveWriteTargetStoreDir(layer, projectRoot), STORE_LAYOUT.knowledgeDir);
960
+ var STORE_PROJECTS_DIR = "projects";
961
+ function isValidWriteProjectSegment(project) {
962
+ if (project.length === 0) {
963
+ return false;
964
+ }
965
+ if (project === STORE_PROJECTS_DIR) {
966
+ return false;
967
+ }
968
+ if (STORE_KNOWLEDGE_TYPE_DIRS.includes(project)) {
969
+ return false;
970
+ }
971
+ return STORE_PROJECT_ID_PATTERN.test(project);
972
+ }
973
+ function resolveStoreCanonicalBase(layer, projectRoot, project) {
974
+ const base = join5(resolveWriteTargetStoreDir(layer, projectRoot), STORE_LAYOUT.knowledgeDir);
975
+ if (layer === "team" && project !== void 0 && isValidWriteProjectSegment(project)) {
976
+ return join5(base, STORE_PROJECTS_DIR, project);
977
+ }
978
+ return base;
957
979
  }
958
980
  function resolveWriteScopeMeta(layer, projectRoot, semanticScope) {
959
981
  const input = buildStoreResolveInput(projectRoot);
@@ -978,7 +1000,7 @@ function resolveWriteScopeMeta(layer, projectRoot, semanticScope) {
978
1000
  }
979
1001
 
980
1002
  // src/services/cross-store-recall.ts
981
- import { readFile as readFile3, stat } from "fs/promises";
1003
+ import { readFile as readFile4, stat } from "fs/promises";
982
1004
  import { join as join6 } from "path";
983
1005
  import {
984
1006
  buildStoreResolveInput as buildStoreResolveInput2,
@@ -995,7 +1017,6 @@ import {
995
1017
  deriveAgentsMetaStableId,
996
1018
  isKnowledgeStableId,
997
1019
  KnowledgeTypeSchema,
998
- LayerSchema,
999
1020
  MaturitySchema,
1000
1021
  parseKnowledgeId,
1001
1022
  StableIdSchema
@@ -1078,8 +1099,7 @@ function extractRuleDescription(source) {
1078
1099
  id: knowledge?.id,
1079
1100
  knowledge_type: knowledge?.knowledge_type,
1080
1101
  maturity: knowledge?.maturity,
1081
- knowledge_layer: knowledge?.knowledge_layer,
1082
- layer_reason: knowledge?.layer_reason,
1102
+ // W4/Track1 (D1): no `knowledge_layer` — layer derives from the id prefix.
1083
1103
  created_at: knowledge?.created_at,
1084
1104
  tags: knowledge?.tags,
1085
1105
  // v2.0-rc.5 (C1): default-safe values when there is no frontmatter at all;
@@ -1104,12 +1124,10 @@ function extractDescriptionFromFrontmatter(frontmatter) {
1104
1124
  tech_stack: extractInlineArray(frontmatter, "tech_stack"),
1105
1125
  impact: extractInlineArray(frontmatter, "impact"),
1106
1126
  must_read_if: extractScalar(frontmatter, "must_read_if") ?? summary,
1107
- entities: extractInlineArray(frontmatter, "entities"),
1108
1127
  id: knowledge.id,
1109
1128
  knowledge_type: knowledge.knowledge_type,
1110
1129
  maturity: knowledge.maturity,
1111
- knowledge_layer: knowledge.knowledge_layer,
1112
- layer_reason: knowledge.layer_reason,
1130
+ // W4/Track1 (D1): no `knowledge_layer` — layer derives from the id prefix.
1113
1131
  created_at: knowledge.created_at,
1114
1132
  tags: knowledge.tags,
1115
1133
  relevance_scope: knowledge.relevance_scope,
@@ -1140,8 +1158,6 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
1140
1158
  const rawId = extractScalar(frontmatter, "id");
1141
1159
  const rawType = extractScalar(frontmatter, "type");
1142
1160
  const rawMaturity = extractScalar(frontmatter, "maturity");
1143
- const rawLayer = extractScalar(frontmatter, "layer");
1144
- const rawLayerReason = extractScalar(frontmatter, "layer_reason");
1145
1161
  const rawCreatedAt = extractScalar(frontmatter, "created_at");
1146
1162
  let id;
1147
1163
  if (rawId !== void 0) {
@@ -1178,16 +1194,6 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
1178
1194
  maturity = parsed.data;
1179
1195
  } else {
1180
1196
  process.stderr.write(`[fabric] frontmatter: unknown maturity ${JSON.stringify(rawMaturity)}; skipping
1181
- `);
1182
- }
1183
- }
1184
- let knowledge_layer;
1185
- if (rawLayer !== void 0) {
1186
- const parsed = LayerSchema.safeParse(rawLayer);
1187
- if (parsed.success) {
1188
- knowledge_layer = parsed.data;
1189
- } else {
1190
- process.stderr.write(`[fabric] frontmatter: unknown layer ${JSON.stringify(rawLayer)}; skipping
1191
1197
  `);
1192
1198
  }
1193
1199
  }
@@ -1200,23 +1206,12 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
1200
1206
  `);
1201
1207
  }
1202
1208
  }
1203
- if (id !== void 0 && knowledge_layer !== void 0) {
1204
- const decoded = parseKnowledgeId(id);
1205
- if (decoded !== null && decoded.layer !== knowledge_layer) {
1206
- process.stderr.write(
1207
- `[fabric] frontmatter: id ${id} encodes layer ${decoded.layer} but layer field says ${knowledge_layer}; dropping both
1208
- `
1209
- );
1210
- id = void 0;
1211
- knowledge_layer = void 0;
1212
- }
1213
- }
1214
1209
  const tags = extractInlineArray(frontmatter, "tags");
1215
1210
  const rawRelevanceScope = extractScalar(frontmatter, "relevance_scope");
1216
1211
  const relevance_scope = rawRelevanceScope === "narrow" || rawRelevanceScope === "broad" ? rawRelevanceScope : "broad";
1217
1212
  const relevance_paths = extractInlineArray(frontmatter, "relevance_paths");
1218
1213
  const rawRelated = extractInlineArray(frontmatter, "related");
1219
- const sourceLayer = knowledge_layer ?? (id !== void 0 ? parseKnowledgeId(id)?.layer ?? "team" : "team");
1214
+ const sourceLayer = id !== void 0 ? parseKnowledgeId(id)?.layer ?? "team" : "team";
1220
1215
  const related = rawRelated.filter((targetId) => {
1221
1216
  if (isForbiddenCrossLayerEdge(sourceLayer, targetId)) {
1222
1217
  process.stderr.write(
@@ -1231,8 +1226,6 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
1231
1226
  id,
1232
1227
  knowledge_type,
1233
1228
  maturity,
1234
- knowledge_layer,
1235
- layer_reason: rawLayerReason,
1236
1229
  created_at,
1237
1230
  tags: tags.length > 0 ? tags : void 0,
1238
1231
  relevance_scope,
@@ -1283,7 +1276,13 @@ async function readSetFingerprint(refs) {
1283
1276
  return parts.sort().join("\n");
1284
1277
  }
1285
1278
  var SEMANTIC_SCOPE_LINE = /^semantic_scope:\s*"?([^"\n]+?)"?\s*$/mu;
1286
- function readSemanticScope(source, layer) {
1279
+ function readSemanticScope(source, layer, project) {
1280
+ if (layer === "personal") {
1281
+ return "personal";
1282
+ }
1283
+ if (typeof project === "string" && project.length > 0) {
1284
+ return `project:${project}`;
1285
+ }
1287
1286
  const match = SEMANTIC_SCOPE_LINE.exec(source);
1288
1287
  return match?.[1] ?? layer;
1289
1288
  }
@@ -1350,7 +1349,7 @@ async function walkReadSetStoresUncached(snapshot) {
1350
1349
  const entries = await Promise.all(snapshot.refs.map(async (ref) => {
1351
1350
  let source;
1352
1351
  try {
1353
- source = await readFile3(ref.file, "utf8");
1352
+ source = await readFile4(ref.file, "utf8");
1354
1353
  } catch {
1355
1354
  return null;
1356
1355
  }
@@ -1362,7 +1361,7 @@ async function walkReadSetStoresUncached(snapshot) {
1362
1361
  type: ref.type,
1363
1362
  alias: ref.alias,
1364
1363
  layer,
1365
- semanticScope: readSemanticScope(source, layer),
1364
+ semanticScope: readSemanticScope(source, layer, ref.project),
1366
1365
  source
1367
1366
  };
1368
1367
  }));
@@ -1380,7 +1379,9 @@ async function buildCrossStoreRawItems(projectRoot) {
1380
1379
  stable_id: entry.qualifiedId,
1381
1380
  description: {
1382
1381
  ...baseDescription,
1383
- knowledge_layer: entry.layer,
1382
+ // W4/Track1 (D1): no `knowledge_layer` backfill — a candidate's layer is
1383
+ // derived from its stable_id prefix (layerFromStableId in plan-context),
1384
+ // the single source of truth (KT-DEC-0004).
1384
1385
  semantic_scope: entry.semanticScope
1385
1386
  }
1386
1387
  });
@@ -1622,6 +1623,144 @@ function serializeBm25Model(model) {
1622
1623
  function rehydrateBm25Model(serialized) {
1623
1624
  return modelFromStats(serialized);
1624
1625
  }
1626
+ var SYNONYM_PAIRS = {
1627
+ // Code / engineering actions
1628
+ refactor: ["restructure", "rewrite", "redesign", "reorganize", "clean", "rework"],
1629
+ optimize: ["improve", "speed-up", "tune", "accelerate", "perf", "performance"],
1630
+ debug: ["fix", "troubleshoot", "diagnose", "investigate", "resolve", "correct"],
1631
+ migrate: ["port", "move", "transfer", "upgrade", "transition", "convert"],
1632
+ "set up": ["init", "initialize", "configure", "bootstrap", "install", "onboard"],
1633
+ implement: ["add", "build", "create", "develop", "write", "introduce", "introduce"],
1634
+ // Architecture / design
1635
+ architecture: ["design", "structure", "layout", "organization", "pattern", "system"],
1636
+ "data model": ["schema", "entity", "type", "structure", "contract", "shape"],
1637
+ // Quality / correctness
1638
+ test: ["verify", "validate", "assert", "check", "spec", "coverage"],
1639
+ lint: ["check", "validate", "audit", "inspect", "analyze"],
1640
+ // Communication / impact
1641
+ documentation: ["docs", "readme", "guide", "spec", "explanation", "reference"],
1642
+ decision: ["adr", "rationale", "why", "motivation", "reason", "trade-off"],
1643
+ // Change management
1644
+ release: ["deploy", "ship", "publish", "cut", "version", "tag"],
1645
+ rollback: ["revert", "undo", "back-out", "backout", "restore"],
1646
+ // Containers / infra
1647
+ container: ["docker", "image", "oci", "cri-o"],
1648
+ deploy: ["release", "rollout", "ship", "publish", "promote"],
1649
+ // Project / process
1650
+ on_boarding: ["getting-started", "quickstart", "newcomer", "new-hire", "first-time"],
1651
+ best_practice: ["convention", "guideline", "standard", "rule", "recommendation"],
1652
+ // Tech stacks
1653
+ api: ["endpoint", "route", "service", "interface", "rpc", "rest"],
1654
+ typescript: ["ts", "types", "type-safe"],
1655
+ react: ["jsx", "tsx", "component", "ui"],
1656
+ node: ["nodejs", "runtime", "backend", "server"],
1657
+ database: ["db", "sql", "nosql", "storage", "persistence"]
1658
+ };
1659
+ var STEMMING_PATTERNS = [
1660
+ { suffix: "e", alternatives: ["es", "ed", "ing", "ation"] },
1661
+ { suffix: "y", alternatives: ["ies", "ied", "ying"] }
1662
+ // Catch-all for non-verb / already-stemmed query terms: no-op by default.
1663
+ ];
1664
+ var DEFAULT_IDF_WEIGHT = 1;
1665
+ var COMMON_TERMS = /* @__PURE__ */ new Set([
1666
+ "a",
1667
+ "an",
1668
+ "the",
1669
+ "and",
1670
+ "or",
1671
+ "but",
1672
+ "in",
1673
+ "on",
1674
+ "at",
1675
+ "to",
1676
+ "for",
1677
+ "of",
1678
+ "with",
1679
+ "by",
1680
+ "from",
1681
+ "as",
1682
+ "is",
1683
+ "it",
1684
+ "at",
1685
+ "be",
1686
+ "do",
1687
+ "has",
1688
+ "have",
1689
+ "was",
1690
+ "are",
1691
+ "been",
1692
+ "this",
1693
+ "that",
1694
+ "these",
1695
+ "those",
1696
+ "will",
1697
+ "can",
1698
+ "may",
1699
+ "would",
1700
+ "could",
1701
+ "should",
1702
+ "does",
1703
+ "not",
1704
+ "no",
1705
+ "if",
1706
+ "so",
1707
+ "up",
1708
+ "out",
1709
+ "all",
1710
+ "each",
1711
+ "every",
1712
+ "both",
1713
+ "some",
1714
+ "any",
1715
+ "such",
1716
+ "only",
1717
+ "own",
1718
+ "same",
1719
+ "too",
1720
+ "very",
1721
+ "just",
1722
+ "about",
1723
+ "over",
1724
+ "than",
1725
+ "then",
1726
+ "also"
1727
+ ]);
1728
+ function idfWeight(term) {
1729
+ return COMMON_TERMS.has(term) ? 0.3 : DEFAULT_IDF_WEIGHT;
1730
+ }
1731
+ function expandQueryTerms(text) {
1732
+ const baseTerms = tokenize(text);
1733
+ const weighted = /* @__PURE__ */ new Map();
1734
+ for (const term of baseTerms) {
1735
+ const lower = term.toLowerCase();
1736
+ const aliases = /* @__PURE__ */ new Set();
1737
+ const syns = SYNONYM_PAIRS[lower];
1738
+ if (syns !== void 0) {
1739
+ for (const s of syns) aliases.add(s);
1740
+ }
1741
+ for (const [key, values] of Object.entries(SYNONYM_PAIRS)) {
1742
+ if (values.includes(lower)) {
1743
+ aliases.add(key);
1744
+ }
1745
+ }
1746
+ for (const { suffix, alternatives } of STEMMING_PATTERNS) {
1747
+ if (lower.endsWith(suffix) && lower.length > suffix.length) {
1748
+ const stem = lower.slice(0, -suffix.length);
1749
+ for (const alt of alternatives) {
1750
+ aliases.add(stem + alt);
1751
+ }
1752
+ }
1753
+ }
1754
+ const originalWeight = idfWeight(lower);
1755
+ weighted.set(term, originalWeight);
1756
+ for (const alias of aliases) {
1757
+ if (!weighted.has(alias)) {
1758
+ weighted.set(alias, originalWeight * 0.5);
1759
+ }
1760
+ }
1761
+ }
1762
+ return weighted;
1763
+ }
1625
1764
  function buildQueryTerms(text) {
1626
1765
  return tokenize(text);
1627
1766
  }
@@ -2021,7 +2160,7 @@ async function extractKnowledge(projectRoot, input) {
2021
2160
  const writeScopeMeta = resolveWriteScopeMeta(layer, projectRoot, semanticScope);
2022
2161
  await ensureParentDirectory(absolutePath);
2023
2162
  if (existsSync3(absolutePath)) {
2024
- const existing = await readFile4(absolutePath, "utf8");
2163
+ const existing = await readFile5(absolutePath, "utf8");
2025
2164
  const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
2026
2165
  if (existingKey === effectiveIdempotencyKey) {
2027
2166
  const fresh2 = renderFreshEntry({
@@ -2154,7 +2293,7 @@ async function resolveDisambiguatedSlugPath(args) {
2154
2293
  idempotencyKey: candidateKey
2155
2294
  };
2156
2295
  }
2157
- const existing = await readFile4(candidatePath, "utf8");
2296
+ const existing = await readFile5(candidatePath, "utf8");
2158
2297
  const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
2159
2298
  if (existingKey === candidateKey) {
2160
2299
  return {
@@ -2752,15 +2891,25 @@ var LEDGER_DUAL_WRITE_METRIC_NAMES = {
2752
2891
  };
2753
2892
 
2754
2893
  // src/services/plan-context.ts
2755
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
2894
+ import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
2756
2895
  import { join as join10 } from "path";
2757
2896
 
2758
2897
  // src/services/vector-retrieval.ts
2759
2898
  import { mkdirSync } from "fs";
2899
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
2900
+ import { createRequire } from "module";
2760
2901
  import { join as join9 } from "path";
2761
2902
  import { resolveGlobalRoot as resolveGlobalRoot3 } from "@fenglimg/fabric-shared";
2762
2903
  var embedderLoad;
2763
2904
  var OPTIONAL_EMBED_PACKAGE = "fastembed";
2905
+ function isEmbedderResolvable() {
2906
+ try {
2907
+ createRequire(import.meta.url).resolve(OPTIONAL_EMBED_PACKAGE);
2908
+ return true;
2909
+ } catch {
2910
+ return false;
2911
+ }
2912
+ }
2764
2913
  var embedderModuleLoader = (name) => import(name);
2765
2914
  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";
2766
2915
  var defaultMissingEmbedderHint = () => {
@@ -2858,11 +3007,63 @@ function cacheDocVector(text, vector) {
2858
3007
  docVectorCache.delete(lru);
2859
3008
  }
2860
3009
  }
2861
- async function buildVectorScores(embedder, queryText, items) {
3010
+ var VECTOR_CACHE_VERSION = 1;
3011
+ var VECTOR_CACHE_DIR = ".fabric/cache/vectors";
3012
+ function vectorCachePath(projectRoot, revision) {
3013
+ const safe = revision.replace(/[^A-Za-z0-9_-]/g, "_");
3014
+ return join9(projectRoot, VECTOR_CACHE_DIR, `${safe}.json`);
3015
+ }
3016
+ async function loadVectorCacheFromDisk(ctx) {
3017
+ try {
3018
+ const raw = await readFile6(vectorCachePath(ctx.projectRoot, ctx.corpusRevision), "utf8");
3019
+ const parsed = JSON.parse(raw);
3020
+ if (parsed.version !== VECTOR_CACHE_VERSION) return null;
3021
+ if (parsed.embedding_model !== ctx.embeddingModel) return null;
3022
+ if (parsed.corpus_revision !== ctx.corpusRevision) return null;
3023
+ if (!Array.isArray(parsed.vectors) || parsed.vectors.length === 0) return null;
3024
+ const dimension = parsed.dimension;
3025
+ if (!Number.isInteger(dimension) || dimension <= 0) return null;
3026
+ const out = /* @__PURE__ */ new Map();
3027
+ for (const entry of parsed.vectors) {
3028
+ if (!Array.isArray(entry) || entry.length !== 2) return null;
3029
+ const [text, vector] = entry;
3030
+ if (typeof text !== "string" || !Array.isArray(vector)) return null;
3031
+ if (vector.length !== dimension) return null;
3032
+ out.set(text, vector);
3033
+ }
3034
+ return out;
3035
+ } catch {
3036
+ return null;
3037
+ }
3038
+ }
3039
+ async function saveVectorCacheToDisk(ctx, vectors, dimension) {
3040
+ try {
3041
+ const payload = {
3042
+ version: VECTOR_CACHE_VERSION,
3043
+ embedding_model: ctx.embeddingModel,
3044
+ dimension,
3045
+ corpus_revision: ctx.corpusRevision,
3046
+ vectors
3047
+ };
3048
+ const path = vectorCachePath(ctx.projectRoot, ctx.corpusRevision);
3049
+ await mkdir4(join9(ctx.projectRoot, VECTOR_CACHE_DIR), { recursive: true });
3050
+ await writeFile2(path, JSON.stringify(payload), "utf8");
3051
+ } catch {
3052
+ }
3053
+ }
3054
+ async function buildVectorScores(embedder, queryText, items, cache2) {
2862
3055
  if (embedder === null || queryText.trim().length === 0 || items.length === 0) {
2863
3056
  return null;
2864
3057
  }
2865
3058
  try {
3059
+ if (cache2 !== void 0) {
3060
+ const fromDisk = await loadVectorCacheFromDisk(cache2);
3061
+ if (fromDisk !== null) {
3062
+ for (const [text, vector] of fromDisk) {
3063
+ cacheDocVector(text, vector);
3064
+ }
3065
+ }
3066
+ }
2866
3067
  const missTexts = [];
2867
3068
  for (const item of items) {
2868
3069
  if (!docVectorCache.has(item.text)) {
@@ -2878,6 +3079,24 @@ async function buildVectorScores(embedder, queryText, items) {
2878
3079
  for (let m = 0; m < missTexts.length; m += 1) {
2879
3080
  cacheDocVector(missTexts[m], embedded[m + 1]);
2880
3081
  }
3082
+ const staleTexts = [];
3083
+ for (const item of items) {
3084
+ const cached = docVectorCache.get(item.text);
3085
+ if (cached !== void 0 && cached.length !== queryVec.length) {
3086
+ docVectorCache.delete(item.text);
3087
+ staleTexts.push(item.text);
3088
+ }
3089
+ }
3090
+ if (staleTexts.length > 0) {
3091
+ const reEmbedded = await embedder.embed(staleTexts);
3092
+ if (reEmbedded.length !== staleTexts.length) {
3093
+ return null;
3094
+ }
3095
+ for (let s = 0; s < staleTexts.length; s += 1) {
3096
+ cacheDocVector(staleTexts[s], reEmbedded[s]);
3097
+ }
3098
+ }
3099
+ const embeddedNewDoc = missTexts.length > 0 || staleTexts.length > 0;
2881
3100
  const scores = /* @__PURE__ */ new Map();
2882
3101
  for (const item of items) {
2883
3102
  const docVec = docVectorCache.get(item.text);
@@ -2886,6 +3105,20 @@ async function buildVectorScores(embedder, queryText, items) {
2886
3105
  }
2887
3106
  scores.set(item.stable_id, cosineSimilarity(queryVec, docVec));
2888
3107
  }
3108
+ if (cache2 !== void 0 && embeddedNewDoc) {
3109
+ const snapshot = [];
3110
+ for (const item of items) {
3111
+ const vec = docVectorCache.get(item.text);
3112
+ if (vec !== void 0) {
3113
+ snapshot.push([item.text, vec]);
3114
+ }
3115
+ }
3116
+ const dimension = snapshot.length > 0 ? snapshot[0][1].length : 0;
3117
+ const uniform = dimension > 0 && snapshot.every(([, v]) => v.length === dimension);
3118
+ if (uniform) {
3119
+ await saveVectorCacheToDisk(cache2, snapshot, dimension);
3120
+ }
3121
+ }
2889
3122
  return scores;
2890
3123
  } catch {
2891
3124
  return null;
@@ -2893,6 +3126,11 @@ async function buildVectorScores(embedder, queryText, items) {
2893
3126
  }
2894
3127
 
2895
3128
  // src/services/plan-context.ts
3129
+ function layerFromStableId(qualifiedId) {
3130
+ const colon = qualifiedId.lastIndexOf(":");
3131
+ const localId = colon === -1 ? qualifiedId : qualifiedId.slice(colon + 1);
3132
+ return localId.startsWith("KP-") ? "personal" : "team";
3133
+ }
2896
3134
  var SELECTION_TOKEN_TTL_DEFAULT_MS = 30 * 60 * 1e3;
2897
3135
  var selectionTokenCache = /* @__PURE__ */ new Map();
2898
3136
  var SELECTION_TOKEN_CACHE_MAX = 1e3;
@@ -2949,7 +3187,7 @@ async function planContext(projectRoot, input) {
2949
3187
  const storeRawItems = await buildCrossStoreRawItems(projectRoot).catch(() => []);
2950
3188
  const { rawItems: allRawItems, suppressedStableIds } = partitionEmptyShells(storeRawItems);
2951
3189
  const effectiveLayerFilter = input.layer_filter ?? readDefaultLayerFilter(projectRoot);
2952
- const rawItems = effectiveLayerFilter === "both" ? allRawItems : allRawItems.filter((item) => item.description.knowledge_layer === effectiveLayerFilter);
3190
+ const rawItems = effectiveLayerFilter === "both" ? allRawItems : allRawItems.filter((item) => layerFromStableId(item.stable_id) === effectiveLayerFilter);
2953
3191
  const scoringContext = await buildScoringContext(projectRoot, revision, rawItems, {
2954
3192
  queryText,
2955
3193
  targetPaths: input.target_paths ?? dedupePaths(input.paths)
@@ -3170,7 +3408,7 @@ function bm25CachePath(projectRoot, revision) {
3170
3408
  }
3171
3409
  async function loadBm25ModelFromDisk(projectRoot, revision) {
3172
3410
  try {
3173
- const raw = await readFile5(bm25CachePath(projectRoot, revision), "utf8");
3411
+ const raw = await readFile7(bm25CachePath(projectRoot, revision), "utf8");
3174
3412
  const parsed = JSON.parse(raw);
3175
3413
  if (parsed.version !== 1) return null;
3176
3414
  return rehydrateBm25Model(parsed);
@@ -3181,8 +3419,8 @@ async function loadBm25ModelFromDisk(projectRoot, revision) {
3181
3419
  async function saveBm25ModelToDisk(projectRoot, revision, model) {
3182
3420
  try {
3183
3421
  const path = bm25CachePath(projectRoot, revision);
3184
- await mkdir4(join10(projectRoot, BM25_CACHE_DIR), { recursive: true });
3185
- await writeFile2(path, JSON.stringify(serializeBm25Model(model)), "utf8");
3422
+ await mkdir5(join10(projectRoot, BM25_CACHE_DIR), { recursive: true });
3423
+ await writeFile3(path, JSON.stringify(serializeBm25Model(model)), "utf8");
3186
3424
  } catch {
3187
3425
  }
3188
3426
  }
@@ -3223,7 +3461,7 @@ function buildScopeRankMap(items, projectRoot) {
3223
3461
  const colon = it.stable_id.indexOf(":");
3224
3462
  const alias = colon === -1 ? "" : it.stable_id.slice(0, colon);
3225
3463
  const localId = colon === -1 ? it.stable_id : it.stable_id.slice(colon + 1);
3226
- const semanticScope = it.description.semantic_scope ?? it.description.knowledge_layer ?? "team";
3464
+ const semanticScope = it.description.semantic_scope ?? layerFromStableId(it.stable_id);
3227
3465
  return {
3228
3466
  global_ref: it.stable_id,
3229
3467
  store_uuid: aliasToUuid.get(alias) ?? alias,
@@ -3273,7 +3511,8 @@ async function buildScoringContext(projectRoot, revision, rawItems, opts) {
3273
3511
  rawItems.map((item) => ({
3274
3512
  stable_id: item.stable_id,
3275
3513
  text: docTexts.get(item.stable_id) ?? documentTextForItem(item.description)
3276
- }))
3514
+ })),
3515
+ { projectRoot, corpusRevision: revision, embeddingModel: embedConfig.model }
3277
3516
  );
3278
3517
  if (vectorScores !== null) {
3279
3518
  scoringContext.vectorScores = vectorScores;
@@ -3283,6 +3522,10 @@ async function buildScoringContext(projectRoot, revision, rawItems, opts) {
3283
3522
  const configuredFusion = readFusion(projectRoot);
3284
3523
  const vectorActive = scoringContext.vectorScores !== void 0 && scoringContext.vectorScores.size > 0;
3285
3524
  scoringContext.fusion = configuredFusion === "auto" ? vectorActive ? "rrf" : "additive" : configuredFusion;
3525
+ let queryTermWeights;
3526
+ if (scoringContext.queryTerms.length > 0) {
3527
+ queryTermWeights = expandQueryTerms(opts.queryText);
3528
+ }
3286
3529
  if (scoringContext.fusion === "rrf" && scoringContext.queryTerms.length > 0 && rawItems.length > 0) {
3287
3530
  const rankIds = rawItems.map((item) => item.stable_id).sort((a, b) => compareStableIds(a, b));
3288
3531
  if (scoringContext.bm25 !== void 0) {
@@ -3294,6 +3537,49 @@ async function buildScoringContext(projectRoot, revision, rawItems, opts) {
3294
3537
  }
3295
3538
  return scoringContext;
3296
3539
  }
3540
+ var PROXIMITY_WINDOW = 6;
3541
+ var PROXIMITY_BOOST_CAP = 0.15;
3542
+ function proximityBoost(item, context, contentScore2) {
3543
+ if (contentScore2 <= 0) return 0;
3544
+ const text = context.docTexts?.get(item.stable_id);
3545
+ if (text === void 0 || text.length === 0) return 0;
3546
+ const tokens = text.toLowerCase().split(/[^a-z0-9_$#]+/u).filter(Boolean);
3547
+ const queryTerms = context.queryTerms;
3548
+ if (queryTerms.length < 2) return 0;
3549
+ const positions = /* @__PURE__ */ new Map();
3550
+ for (const qt of queryTerms) {
3551
+ const qtLower = qt.toLowerCase();
3552
+ const pos = [];
3553
+ for (let i = 0; i < tokens.length; i++) {
3554
+ if (tokens[i] === qtLower) {
3555
+ pos.push(i);
3556
+ }
3557
+ }
3558
+ if (pos.length > 0) {
3559
+ positions.set(qtLower, pos);
3560
+ }
3561
+ }
3562
+ if (positions.size < 2) return 0;
3563
+ const termList = [...positions.entries()];
3564
+ let minDist = Infinity;
3565
+ for (let i = 0; i < termList.length; i++) {
3566
+ const [, posI] = termList[i];
3567
+ for (let j = i + 1; j < termList.length; j++) {
3568
+ const [, posJ] = termList[j];
3569
+ for (const pi of posI) {
3570
+ for (const pj of posJ) {
3571
+ const dist = Math.abs(pi - pj);
3572
+ if (dist < minDist) minDist = dist;
3573
+ }
3574
+ }
3575
+ }
3576
+ }
3577
+ if (!Number.isFinite(minDist)) return 0;
3578
+ if (minDist >= PROXIMITY_WINDOW) return 0;
3579
+ const ratio = 1 - minDist / PROXIMITY_WINDOW;
3580
+ const boost = contentScore2 * PROXIMITY_BOOST_CAP * ratio;
3581
+ return boost;
3582
+ }
3297
3583
  function sortDescriptionItems(rawItems, scoringContext) {
3298
3584
  if (scoringContext === void 0) {
3299
3585
  return [...rawItems].sort((left, right) => compareStableIds(left.stable_id, right.stable_id)).map((item) => ({ item, score: 0 }));
@@ -3334,16 +3620,13 @@ function documentTextForItem(description) {
3334
3620
  ...description.intent_clues,
3335
3621
  ...description.tech_stack,
3336
3622
  ...description.impact,
3337
- ...description.entities ?? [],
3338
3623
  ...description.tags ?? []
3339
3624
  ].join(" ");
3340
3625
  }
3341
3626
  function documentFieldsForItem(description) {
3342
3627
  return {
3343
3628
  title: tokenize2(description.summary),
3344
- tags: tokenize2(
3345
- [...description.tags ?? [], ...description.tech_stack, ...description.entities ?? []].join(" ")
3346
- ),
3629
+ tags: tokenize2([...description.tags ?? [], ...description.tech_stack].join(" ")),
3347
3630
  summary: tokenize2([description.must_read_if, ...description.intent_clues].join(" ")),
3348
3631
  body: tokenize2(description.impact.join(" "))
3349
3632
  };
@@ -3436,7 +3719,8 @@ function structuralScaleFor(context) {
3436
3719
  function scoreDescriptionItem(item, context) {
3437
3720
  const content = contentScore(item, context);
3438
3721
  const structural = salienceScore(item) + recencyBoost(item, context) + localityBoost(item, context);
3439
- return content + structuralScaleFor(context) * structural;
3722
+ const proximity = proximityBoost(item, context, content);
3723
+ return content + structuralScaleFor(context) * structural + proximity;
3440
3724
  }
3441
3725
  function scoreBreakdownForItem(item, context) {
3442
3726
  const hasQuery = context.queryTerms.length > 0;
@@ -3918,7 +4202,7 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
3918
4202
  // src/services/review.ts
3919
4203
  import { execFileSync } from "child_process";
3920
4204
  import { existsSync as existsSync5 } from "fs";
3921
- import { readFile as readFile7, readdir as readdir2, stat as stat2, unlink as unlink2 } from "fs/promises";
4205
+ import { readFile as readFile9, readdir as readdir2, stat as stat2, unlink as unlink2 } from "fs/promises";
3922
4206
  import { homedir } from "os";
3923
4207
  import { basename, isAbsolute, join as join12, relative, resolve as resolve2, sep as sep2 } from "path";
3924
4208
 
@@ -3956,11 +4240,11 @@ async function hasUnresolvedDismissal(projectRoot, id) {
3956
4240
  }
3957
4241
 
3958
4242
  // src/services/review.ts
3959
- import { allocateStoreKnowledgeId, isPersonalScope as isPersonalScope2 } from "@fenglimg/fabric-shared";
4243
+ import { allocateStoreKnowledgeId, isPersonalScope as isPersonalScope2, loadProjectConfig as loadProjectConfig3 } from "@fenglimg/fabric-shared";
3960
4244
 
3961
4245
  // src/services/pending-dedupe.ts
3962
4246
  import { existsSync as existsSync4 } from "fs";
3963
- import { readdir, readFile as readFile6, unlink } from "fs/promises";
4247
+ import { readdir, readFile as readFile8, unlink } from "fs/promises";
3964
4248
  import { join as join11 } from "path";
3965
4249
  var PENDING_TYPES = ["decisions", "pitfalls", "guidelines", "models", "processes"];
3966
4250
  var DISAMBIGUATION_SUFFIX = /^(.+)-([2-9])\.md$/u;
@@ -4058,7 +4342,7 @@ async function mergePendingTwins(projectRoot) {
4058
4342
  const abs = join11(dir, name);
4059
4343
  let content;
4060
4344
  try {
4061
- content = await readFile6(abs, "utf8");
4345
+ content = await readFile8(abs, "utf8");
4062
4346
  } catch {
4063
4347
  continue;
4064
4348
  }
@@ -4253,7 +4537,7 @@ async function listPending(projectRoot, filters) {
4253
4537
  const absolutePath = join12(dir, name);
4254
4538
  let content;
4255
4539
  try {
4256
- content = await readFile7(absolutePath, "utf8");
4540
+ content = await readFile9(absolutePath, "utf8");
4257
4541
  } catch {
4258
4542
  continue;
4259
4543
  }
@@ -4363,7 +4647,7 @@ async function approveOne(projectRoot, pendingPath) {
4363
4647
  let targetAbs;
4364
4648
  let writtenTarget = false;
4365
4649
  try {
4366
- const content = await readFile7(sourceAbs, "utf8");
4650
+ const content = await readFile9(sourceAbs, "utf8");
4367
4651
  const fm = parseFrontmatter(content);
4368
4652
  const pluralType = fm.type;
4369
4653
  if (pluralType === void 0 || !PLURAL_TYPES.includes(pluralType)) {
@@ -4377,7 +4661,12 @@ async function approveOne(projectRoot, pendingPath) {
4377
4661
  );
4378
4662
  allocatedId = stableId;
4379
4663
  const newFilename = `${stableId}--${slug}.md`;
4380
- targetAbs = join12(resolveStoreCanonicalBase(layer, projectRoot), pluralType, newFilename);
4664
+ const promoteProject = layer === "team" ? loadProjectConfig3(projectRoot)?.active_project : void 0;
4665
+ targetAbs = join12(
4666
+ resolveStoreCanonicalBase(layer, projectRoot, promoteProject),
4667
+ pluralType,
4668
+ newFilename
4669
+ );
4381
4670
  await ensureParentDirectory(targetAbs);
4382
4671
  const rewritten = rewriteFrontmatterMerge(
4383
4672
  rewriteFrontmatterForPromote(content, stableId),
@@ -4435,7 +4724,7 @@ async function rejectAll(projectRoot, pendingPaths, reason) {
4435
4724
  try {
4436
4725
  const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
4437
4726
  if (existsSync5(sandboxed.abs)) {
4438
- const content = await readFile7(sandboxed.abs, "utf8");
4727
+ const content = await readFile9(sandboxed.abs, "utf8");
4439
4728
  const merged = rewriteFrontmatterMerge(content, { status: "rejected" });
4440
4729
  const rejectedAbs = sandboxed.abs.includes(`${sep2}pending${sep2}`) ? sandboxed.abs.replace(`${sep2}pending${sep2}`, `${sep2}rejected${sep2}`) : null;
4441
4730
  if (rejectedAbs !== null) {
@@ -4462,7 +4751,7 @@ async function modifyEntry(projectRoot, pendingPath, changes) {
4462
4751
  if (target === null) {
4463
4752
  throw new Error(`modify target not found: ${pendingPath}`);
4464
4753
  }
4465
- const content = await readFile7(target.absPath, "utf8");
4754
+ const content = await readFile9(target.absPath, "utf8");
4466
4755
  const fm = parseFrontmatter(content);
4467
4756
  const currentLayer = fm.layer ?? "team";
4468
4757
  if (fm.maturity === "verified" && changes.maturity === "proven" && fm.id !== void 0) {
@@ -4564,8 +4853,9 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
4564
4853
  pluralType,
4565
4854
  resolveWriteTargetStoreDir(toLayer, projectRoot)
4566
4855
  );
4856
+ const flipProject = toLayer === "team" ? loadProjectConfig3(projectRoot)?.active_project : void 0;
4567
4857
  const toAbs = join12(
4568
- resolveStoreCanonicalBase(toLayer, projectRoot),
4858
+ resolveStoreCanonicalBase(toLayer, projectRoot, flipProject),
4569
4859
  pluralType,
4570
4860
  `${newStableId}--${slug}.md`
4571
4861
  );
@@ -4587,20 +4877,22 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
4587
4877
  { ...effectivePatch, last_review_confirmed_at: (/* @__PURE__ */ new Date()).toISOString() },
4588
4878
  { id: newStableId }
4589
4879
  );
4590
- await atomicWriteText(toAbs, rewritten);
4880
+ let moved = false;
4591
4881
  if (target.isInProjectTree) {
4592
4882
  const relSource = relative(projectRoot, target.absPath);
4883
+ const relDest = relative(projectRoot, toAbs);
4593
4884
  try {
4594
- execFileSync("git", ["rm", "--quiet", "-f", relSource], {
4885
+ execFileSync("git", ["mv", "-f", relSource, relDest], {
4595
4886
  cwd: projectRoot,
4596
4887
  stdio: ["ignore", "pipe", "pipe"]
4597
4888
  });
4889
+ moved = true;
4598
4890
  } catch {
4599
- if (existsSync5(target.absPath)) {
4600
- await unlink2(target.absPath);
4601
- }
4891
+ moved = false;
4602
4892
  }
4603
- } else if (existsSync5(target.absPath)) {
4893
+ }
4894
+ await atomicWriteText(toAbs, rewritten);
4895
+ if (!moved && existsSync5(target.absPath) && target.absPath !== toAbs) {
4604
4896
  await unlink2(target.absPath);
4605
4897
  }
4606
4898
  const flipReason = `layer_flip:${priorStableId ?? "<unassigned>"}->${newStableId}`;
@@ -4694,7 +4986,7 @@ async function listIndexedSearchEntries(source, type) {
4694
4986
  }
4695
4987
  let content;
4696
4988
  try {
4697
- content = await readFile7(absolutePath, "utf8");
4989
+ content = await readFile9(absolutePath, "utf8");
4698
4990
  searchEntryIndexContentReads += 1;
4699
4991
  } catch {
4700
4992
  directoryCache.files.delete(absolutePath);
@@ -4744,7 +5036,8 @@ function pendingEntryToRankerItem(indexed) {
4744
5036
  ...fm.id !== void 0 ? { id: fm.id } : {},
4745
5037
  ...fm.type !== void 0 ? { knowledge_type: fm.type } : {},
4746
5038
  maturity: fm.maturity ?? "draft",
4747
- knowledge_layer: indexed.layer,
5039
+ // W4/Track1 (D1): no `knowledge_layer` field — layer is derived from the
5040
+ // stable_id prefix (KT-DEC-0004), never carried on the description.
4748
5041
  ...fm.semantic_scope !== void 0 ? { semantic_scope: fm.semantic_scope } : {},
4749
5042
  ...fm.created_at !== void 0 ? { created_at: fm.created_at } : {},
4750
5043
  tags: fm.tags ?? [],
@@ -4863,7 +5156,7 @@ async function deferAll(projectRoot, pendingPaths, until, reason) {
4863
5156
  try {
4864
5157
  const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
4865
5158
  if (existsSync5(sandboxed.abs)) {
4866
- const content = await readFile7(sandboxed.abs, "utf8");
5159
+ const content = await readFile9(sandboxed.abs, "utf8");
4867
5160
  stableId = parseFrontmatter(content).id;
4868
5161
  const patch = {
4869
5162
  status: "deferred",
@@ -5204,9 +5497,9 @@ function registerPending(server, tracker) {
5204
5497
  }
5205
5498
 
5206
5499
  // src/services/doctor.ts
5207
- import { access as access4, readFile as readFile17, readdir as readdirAsync, stat as statAsync, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
5500
+ import { access as access4, readFile as readFile19, readdir as readdirAsync, stat as statAsync, unlink as unlink4, writeFile as writeFile5 } from "fs/promises";
5208
5501
  import { constants as constants2 } from "fs";
5209
- import { isAbsolute as isAbsolute2, join as join23, posix as posix5, resolve as resolve3 } from "path";
5502
+ import { isAbsolute as isAbsolute2, join as join24, posix as posix5, resolve as resolve3 } from "path";
5210
5503
  import {
5211
5504
  createTranslator,
5212
5505
  forensicReportSchema,
@@ -5420,7 +5713,7 @@ function createKnowledgeSummaryOpaqueCheck(t, inspection) {
5420
5713
  }
5421
5714
 
5422
5715
  // src/services/doctor-stable-id-collision.ts
5423
- import { readFile as readFile8 } from "fs/promises";
5716
+ import { readFile as readFile10 } from "fs/promises";
5424
5717
  import { basename as basename2, join as join13 } from "path";
5425
5718
  import {
5426
5719
  buildStoreResolveInput as buildStoreResolveInput4,
@@ -5467,7 +5760,7 @@ async function inspectStoreStableIdIntegrity(projectRoot) {
5467
5760
  for (const ref of await readKnowledgeAcrossStores2(resolved.dirs)) {
5468
5761
  let source;
5469
5762
  try {
5470
- source = await readFile8(ref.file, "utf8");
5763
+ source = await readFile10(ref.file, "utf8");
5471
5764
  } catch {
5472
5765
  continue;
5473
5766
  }
@@ -5788,7 +6081,7 @@ function createNarrowNoPathsCheck(t, inspection) {
5788
6081
  }
5789
6082
 
5790
6083
  // src/services/doctor-broad-index.ts
5791
- import { readFile as readFile9 } from "fs/promises";
6084
+ import { readFile as readFile11 } from "fs/promises";
5792
6085
  import { join as join15 } from "path";
5793
6086
  var DEFAULT_BROAD_INDEX_BACKSTOP = 50;
5794
6087
  var BROAD_INDEX_BACKSTOP_MIN = 20;
@@ -5797,7 +6090,7 @@ var BROAD_INDEX_DRIFT_RATIO = 0.8;
5797
6090
  async function readBroadIndexBackstop(projectRoot) {
5798
6091
  const configPath = join15(projectRoot, ".fabric", "fabric-config.json");
5799
6092
  try {
5800
- const raw = await readFile9(configPath, "utf8");
6093
+ const raw = await readFile11(configPath, "utf8");
5801
6094
  const parsed = JSON.parse(raw);
5802
6095
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
5803
6096
  const v = parsed.broad_index_backstop;
@@ -6114,7 +6407,7 @@ function createBroadReviewRecheckCheck(t, inspection) {
6114
6407
  }
6115
6408
 
6116
6409
  // src/services/doctor-scope-lint.ts
6117
- import { readFile as readFile10 } from "fs/promises";
6410
+ import { readFile as readFile12 } from "fs/promises";
6118
6411
  import { join as join16 } from "path";
6119
6412
  import {
6120
6413
  buildStoreResolveInput as buildStoreResolveInput5,
@@ -6182,7 +6475,7 @@ async function lintStoreScopes(projectRoot) {
6182
6475
  }
6183
6476
  let source;
6184
6477
  try {
6185
- source = await readFile10(ref.file, "utf8");
6478
+ source = await readFile12(ref.file, "utf8");
6186
6479
  } catch {
6187
6480
  continue;
6188
6481
  }
@@ -6265,9 +6558,9 @@ function createScopeLintCheck(t, violations) {
6265
6558
  }
6266
6559
 
6267
6560
  // src/services/doctor-unbound-project.ts
6268
- import { loadProjectConfig as loadProjectConfig3 } from "@fenglimg/fabric-shared";
6561
+ import { loadProjectConfig as loadProjectConfig4 } from "@fenglimg/fabric-shared";
6269
6562
  function detectUnboundProject(projectRoot) {
6270
- const config = loadProjectConfig3(projectRoot);
6563
+ const config = loadProjectConfig4(projectRoot);
6271
6564
  const alias = config?.active_write_store;
6272
6565
  if (typeof alias !== "string" || alias.length === 0) {
6273
6566
  return null;
@@ -6315,7 +6608,7 @@ import {
6315
6608
  readStoreCounters,
6316
6609
  reconcileStoreCounters,
6317
6610
  resolveGlobalRoot as resolveGlobalRoot6,
6318
- STORE_KNOWLEDGE_TYPE_DIRS,
6611
+ STORE_KNOWLEDGE_TYPE_DIRS as STORE_KNOWLEDGE_TYPE_DIRS2,
6319
6612
  STORE_LAYOUT as STORE_LAYOUT2,
6320
6613
  storeRelativePathForMount as storeRelativePathForMount5
6321
6614
  } from "@fenglimg/fabric-shared";
@@ -6359,7 +6652,7 @@ function readEntryId(file) {
6359
6652
  }
6360
6653
  function computeStoreDiskMax(storeDir) {
6361
6654
  const max = defaultAgentsMetaCounters();
6362
- for (const type of STORE_KNOWLEDGE_TYPE_DIRS) {
6655
+ for (const type of STORE_KNOWLEDGE_TYPE_DIRS2) {
6363
6656
  const dir = join17(storeDir, STORE_LAYOUT2.knowledgeDir, type);
6364
6657
  if (!existsSync7(dir)) {
6365
6658
  continue;
@@ -6554,6 +6847,195 @@ function createStoreOrphanCheck(t, orphans) {
6554
6847
  };
6555
6848
  }
6556
6849
 
6850
+ // src/services/doctor-project-registry-drift.ts
6851
+ import { readdir as readdir3, rmdir } from "fs/promises";
6852
+ import { join as join19 } from "path";
6853
+ import {
6854
+ addStoreProject,
6855
+ buildStoreResolveInput as buildStoreResolveInput7,
6856
+ createStoreResolver as createStoreResolver7,
6857
+ readStoreProjects as readStoreProjects2,
6858
+ resolveGlobalRoot as resolveGlobalRoot8,
6859
+ STORE_KNOWLEDGE_TYPE_DIRS as STORE_KNOWLEDGE_TYPE_DIRS3,
6860
+ STORE_LAYOUT as STORE_LAYOUT3,
6861
+ STORE_PROJECT_ID_PATTERN as STORE_PROJECT_ID_PATTERN2,
6862
+ storeRelativePathForMount as storeRelativePathForMount6
6863
+ } from "@fenglimg/fabric-shared";
6864
+ var PROJECTS_DIR = "projects";
6865
+ var EMPTY_INSPECTION = { findings: [] };
6866
+ function resolveDriftStores(projectRoot) {
6867
+ const input = buildStoreResolveInput7(projectRoot);
6868
+ if (input === null) {
6869
+ return [];
6870
+ }
6871
+ const readSet = createStoreResolver7().resolveReadSet(input);
6872
+ if (readSet.stores.length === 0) {
6873
+ return [];
6874
+ }
6875
+ const globalRoot = resolveGlobalRoot8();
6876
+ return readSet.stores.map((entry) => {
6877
+ const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
6878
+ return {
6879
+ uuid: entry.store_uuid,
6880
+ alias: entry.alias,
6881
+ dir: join19(globalRoot, storeRelativePathForMount6(mounted ?? { store_uuid: entry.store_uuid }))
6882
+ };
6883
+ });
6884
+ }
6885
+ async function listDir2(dir) {
6886
+ try {
6887
+ return await readdir3(dir);
6888
+ } catch {
6889
+ return [];
6890
+ }
6891
+ }
6892
+ async function listProjectFolders(storeDir) {
6893
+ const projectsRoot = join19(storeDir, STORE_LAYOUT3.knowledgeDir, PROJECTS_DIR);
6894
+ const reserved = /* @__PURE__ */ new Set([PROJECTS_DIR, ...STORE_KNOWLEDGE_TYPE_DIRS3]);
6895
+ return (await listDir2(projectsRoot)).filter((id) => STORE_PROJECT_ID_PATTERN2.test(id) && !reserved.has(id)).sort();
6896
+ }
6897
+ async function folderHasEntries(storeDir, projectId) {
6898
+ const base = join19(storeDir, STORE_LAYOUT3.knowledgeDir, PROJECTS_DIR, projectId);
6899
+ for (const type of STORE_KNOWLEDGE_TYPE_DIRS3) {
6900
+ const names = await listDir2(join19(base, type));
6901
+ if (names.some((name) => name.endsWith(".md"))) {
6902
+ return true;
6903
+ }
6904
+ }
6905
+ return false;
6906
+ }
6907
+ async function inspectProjectRegistryDrift(projectRoot) {
6908
+ const stores = resolveDriftStores(projectRoot);
6909
+ if (stores.length === 0) {
6910
+ return EMPTY_INSPECTION;
6911
+ }
6912
+ const findings = [];
6913
+ for (const store of stores) {
6914
+ const registered = new Set((await readStoreProjects2(store.dir)).map((p) => p.id));
6915
+ const folders = await listProjectFolders(store.dir);
6916
+ const folderSet = new Set(folders);
6917
+ const base = { store_alias: store.alias, store_uuid: store.uuid, store_dir: store.dir };
6918
+ for (const projectId of folders) {
6919
+ if (registered.has(projectId)) {
6920
+ if (!await folderHasEntries(store.dir, projectId)) {
6921
+ findings.push({ ...base, project_id: projectId, kind: "empty_folder" });
6922
+ }
6923
+ continue;
6924
+ }
6925
+ const kind = await folderHasEntries(store.dir, projectId) ? "unregistered_write" : "orphan_folder";
6926
+ findings.push({ ...base, project_id: projectId, kind });
6927
+ }
6928
+ void folderSet;
6929
+ }
6930
+ findings.sort(
6931
+ (a, b) => a.store_alias.localeCompare(b.store_alias) || a.project_id.localeCompare(b.project_id)
6932
+ );
6933
+ return { findings };
6934
+ }
6935
+ async function fixProjectRegistryDrift(projectRoot) {
6936
+ const registered = [];
6937
+ const pruned = [];
6938
+ const { findings } = await inspectProjectRegistryDrift(projectRoot);
6939
+ for (const finding of findings) {
6940
+ if (finding.kind === "empty_folder") {
6941
+ if (await folderHasEntries(finding.store_dir, finding.project_id)) {
6942
+ continue;
6943
+ }
6944
+ try {
6945
+ const base = join19(
6946
+ finding.store_dir,
6947
+ STORE_LAYOUT3.knowledgeDir,
6948
+ PROJECTS_DIR,
6949
+ finding.project_id
6950
+ );
6951
+ for (const type of STORE_KNOWLEDGE_TYPE_DIRS3) {
6952
+ await rmdir(join19(base, type)).catch(() => void 0);
6953
+ }
6954
+ await rmdir(base);
6955
+ pruned.push(finding);
6956
+ } catch {
6957
+ }
6958
+ continue;
6959
+ }
6960
+ try {
6961
+ await addStoreProject(finding.store_dir, {
6962
+ id: finding.project_id,
6963
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
6964
+ });
6965
+ registered.push(finding);
6966
+ } catch {
6967
+ }
6968
+ }
6969
+ return { registered, pruned };
6970
+ }
6971
+ function createProjectRegistryDriftCheck(t, inspection) {
6972
+ const { findings } = inspection;
6973
+ if (findings.length === 0) {
6974
+ return {
6975
+ name: t("doctor.check.project_registry_drift.name"),
6976
+ status: "ok",
6977
+ message: t("doctor.check.project_registry_drift.ok")
6978
+ };
6979
+ }
6980
+ const unregistered = findings.filter((f) => f.kind === "unregistered_write").length;
6981
+ const orphans = findings.filter((f) => f.kind === "orphan_folder").length;
6982
+ const empties = findings.filter((f) => f.kind === "empty_folder").length;
6983
+ const breakdown = [
6984
+ unregistered > 0 ? `${unregistered} unregistered-write` : null,
6985
+ orphans > 0 ? `${orphans} orphan-folder` : null,
6986
+ empties > 0 ? `${empties} empty-folder` : null
6987
+ ].filter((part) => part !== null).join(", ");
6988
+ if (unregistered > 0) {
6989
+ const first2 = findings.find((f) => f.kind === "unregistered_write");
6990
+ return {
6991
+ name: t("doctor.check.project_registry_drift.name"),
6992
+ status: "error",
6993
+ kind: "manual_error",
6994
+ code: "project_registry_drift",
6995
+ fixable: true,
6996
+ message: t("doctor.check.project_registry_drift.message.unregistered", {
6997
+ total: String(findings.length),
6998
+ breakdown,
6999
+ projectId: first2.project_id,
7000
+ storeAlias: first2.store_alias
7001
+ }),
7002
+ actionHint: t("doctor.check.project_registry_drift.remediation")
7003
+ };
7004
+ }
7005
+ if (orphans > 0) {
7006
+ const first2 = findings.find((f) => f.kind === "orphan_folder");
7007
+ return {
7008
+ name: t("doctor.check.project_registry_drift.name"),
7009
+ status: "warn",
7010
+ kind: "warning",
7011
+ code: "project_registry_drift",
7012
+ fixable: true,
7013
+ message: t("doctor.check.project_registry_drift.message.orphan", {
7014
+ total: String(findings.length),
7015
+ breakdown,
7016
+ projectId: first2.project_id,
7017
+ storeAlias: first2.store_alias
7018
+ }),
7019
+ actionHint: t("doctor.check.project_registry_drift.remediation")
7020
+ };
7021
+ }
7022
+ const first = findings.find((f) => f.kind === "empty_folder");
7023
+ return {
7024
+ name: t("doctor.check.project_registry_drift.name"),
7025
+ status: "warn",
7026
+ kind: "info",
7027
+ code: "project_registry_drift",
7028
+ fixable: true,
7029
+ message: t("doctor.check.project_registry_drift.message.empty", {
7030
+ total: String(findings.length),
7031
+ breakdown,
7032
+ projectId: first.project_id,
7033
+ storeAlias: first.store_alias
7034
+ }),
7035
+ actionHint: t("doctor.check.project_registry_drift.remediation")
7036
+ };
7037
+ }
7038
+
6557
7039
  // src/services/legacy-serve-lock-probe.ts
6558
7040
  import {
6559
7041
  isAlive,
@@ -6626,8 +7108,8 @@ async function inspectEventsJsonlGates(projectRoot, options = {}) {
6626
7108
  }
6627
7109
 
6628
7110
  // src/services/doctor-skill-lints.ts
6629
- import { readdir as readdir3, readFile as readFile11 } from "fs/promises";
6630
- import { join as join19, posix as posix2 } from "path";
7111
+ import { readdir as readdir4, readFile as readFile13 } from "fs/promises";
7112
+ import { join as join20, posix as posix2 } from "path";
6631
7113
  var FABRIC_SKILL_SLUGS = ["fabric-archive", "fabric-review"];
6632
7114
  var SKILL_MD_FRONTMATTER_ROOTS = [".claude/skills", ".codex/skills"];
6633
7115
  var SKILL_FRONTMATTER_KEY_PATTERN = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]+(.+?)[ \t]*$/u;
@@ -6649,7 +7131,7 @@ function issueCheck(name, status, kind, code, message, actionHint, audience) {
6649
7131
  }
6650
7132
  async function listMarkdownFiles(dir) {
6651
7133
  try {
6652
- return (await readdir3(dir)).filter((name) => name.endsWith(".md"));
7134
+ return (await readdir4(dir)).filter((name) => name.endsWith(".md"));
6653
7135
  } catch {
6654
7136
  return null;
6655
7137
  }
@@ -6657,8 +7139,8 @@ async function listMarkdownFiles(dir) {
6657
7139
  async function inspectSkillRefMirror(projectRoot) {
6658
7140
  const driftedPaths = [];
6659
7141
  for (const slug of FABRIC_SKILL_SLUGS) {
6660
- const claudeRef = join19(projectRoot, ".claude", "skills", slug, "ref");
6661
- const codexRef = join19(projectRoot, ".codex", "skills", slug, "ref");
7142
+ const claudeRef = join20(projectRoot, ".claude", "skills", slug, "ref");
7143
+ const codexRef = join20(projectRoot, ".codex", "skills", slug, "ref");
6662
7144
  const [claudeFiles, codexFiles] = await Promise.all([listMarkdownFiles(claudeRef), listMarkdownFiles(codexRef)]);
6663
7145
  if (claudeFiles === null || codexFiles === null) continue;
6664
7146
  const claudeSet = new Set(claudeFiles);
@@ -6675,8 +7157,8 @@ async function inspectSkillRefMirror(projectRoot) {
6675
7157
  let codexBody;
6676
7158
  try {
6677
7159
  [claudeBody, codexBody] = await Promise.all([
6678
- readFile11(join19(claudeRef, fname), "utf8"),
6679
- readFile11(join19(codexRef, fname), "utf8")
7160
+ readFile13(join20(claudeRef, fname), "utf8"),
7161
+ readFile13(join20(codexRef, fname), "utf8")
6680
7162
  ]);
6681
7163
  } catch {
6682
7164
  continue;
@@ -6695,10 +7177,10 @@ async function inspectSkillTokenBudget(projectRoot) {
6695
7177
  const overSize = [];
6696
7178
  let highestSeverity = "ok";
6697
7179
  for (const slug of FABRIC_SKILL_SLUGS) {
6698
- const skillMdPath = join19(projectRoot, ".claude", "skills", slug, "SKILL.md");
7180
+ const skillMdPath = join20(projectRoot, ".claude", "skills", slug, "SKILL.md");
6699
7181
  let body;
6700
7182
  try {
6701
- body = await readFile11(skillMdPath, "utf8");
7183
+ body = await readFile13(skillMdPath, "utf8");
6702
7184
  } catch {
6703
7185
  continue;
6704
7186
  }
@@ -6719,10 +7201,10 @@ async function inspectSkillDescription(projectRoot) {
6719
7201
  const CJK_PATTERN = /[\u3400-\u4dbf\u4e00-\u9fff]/u;
6720
7202
  const ASCII_PATTERN = /[a-zA-Z]{2,}/u;
6721
7203
  for (const slug of FABRIC_SKILL_SLUGS) {
6722
- const skillMdPath = join19(projectRoot, ".claude", "skills", slug, "SKILL.md");
7204
+ const skillMdPath = join20(projectRoot, ".claude", "skills", slug, "SKILL.md");
6723
7205
  let body;
6724
7206
  try {
6725
- body = await readFile11(skillMdPath, "utf8");
7207
+ body = await readFile13(skillMdPath, "utf8");
6726
7208
  } catch {
6727
7209
  continue;
6728
7210
  }
@@ -6753,19 +7235,19 @@ async function inspectSkillDescription(projectRoot) {
6753
7235
  async function inspectSkillMdYamlInvalid(projectRoot) {
6754
7236
  const candidates = [];
6755
7237
  for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
6756
- const rootAbs = join19(projectRoot, rootRel);
7238
+ const rootAbs = join20(projectRoot, rootRel);
6757
7239
  let dirEntries;
6758
7240
  try {
6759
- dirEntries = await readdir3(rootAbs, { withFileTypes: true });
7241
+ dirEntries = await readdir4(rootAbs, { withFileTypes: true });
6760
7242
  } catch {
6761
7243
  continue;
6762
7244
  }
6763
7245
  for (const dirEntry of dirEntries) {
6764
7246
  if (!dirEntry.isDirectory()) continue;
6765
- const skillFile = join19(rootAbs, dirEntry.name, "SKILL.md");
7247
+ const skillFile = join20(rootAbs, dirEntry.name, "SKILL.md");
6766
7248
  let raw;
6767
7249
  try {
6768
- raw = await readFile11(skillFile, "utf8");
7250
+ raw = await readFile13(skillFile, "utf8");
6769
7251
  } catch {
6770
7252
  continue;
6771
7253
  }
@@ -6888,8 +7370,8 @@ function createSkillMdYamlInvalidCheck(t, inspection) {
6888
7370
  }
6889
7371
 
6890
7372
  // src/services/doctor-retired-references-lint.ts
6891
- import { readdir as readdir4, readFile as readFile12, stat as stat3 } from "fs/promises";
6892
- import { join as join20, posix as posix3, relative as relative2 } from "path";
7373
+ import { readdir as readdir5, readFile as readFile14, stat as stat3 } from "fs/promises";
7374
+ import { join as join21, posix as posix3, relative as relative2 } from "path";
6893
7375
  var RETIRED_TOKENS = [
6894
7376
  { token: "fab_plan_context", replacement: "fab_recall", reason: "retrieval collapsed to one lean fab_recall (KT-DEC-0026)" },
6895
7377
  { token: "fab_get_knowledge_sections", replacement: "fab_recall", reason: "two-step fetch retired (KT-DEC-0026)" },
@@ -6912,7 +7394,7 @@ var RETIRED_TOKENS = [
6912
7394
  ];
6913
7395
  var HOOK_DIRS = [".claude/hooks", ".codex/hooks"];
6914
7396
  var SKILL_DIRS = [".claude/skills", ".codex/skills"];
6915
- var BOOTSTRAP_FILES = ["AGENTS.md", "CLAUDE.md", join20(".fabric", "AGENTS.md")];
7397
+ var BOOTSTRAP_FILES = ["AGENTS.md", "CLAUDE.md", join21(".fabric", "AGENTS.md")];
6916
7398
  function isCommentLine(line) {
6917
7399
  const t = line.trim();
6918
7400
  return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*");
@@ -6920,13 +7402,13 @@ function isCommentLine(line) {
6920
7402
  async function walkFiles(dir, exts) {
6921
7403
  let entries;
6922
7404
  try {
6923
- entries = await readdir4(dir, { withFileTypes: true });
7405
+ entries = await readdir5(dir, { withFileTypes: true });
6924
7406
  } catch {
6925
7407
  return [];
6926
7408
  }
6927
7409
  const out = [];
6928
7410
  for (const e of entries) {
6929
- const full = join20(dir, e.name);
7411
+ const full = join21(dir, e.name);
6930
7412
  if (e.isDirectory()) {
6931
7413
  out.push(...await walkFiles(full, exts));
6932
7414
  } else if (exts.some((ext) => e.name.endsWith(ext))) {
@@ -6952,29 +7434,29 @@ async function inspectRetiredReferences(projectRoot) {
6952
7434
  let scannedFiles = 0;
6953
7435
  const toRel = (p) => posix3.normalize(relative2(projectRoot, p).split("\\").join("/"));
6954
7436
  for (const rel of BOOTSTRAP_FILES) {
6955
- const abs = join20(projectRoot, rel);
7437
+ const abs = join21(projectRoot, rel);
6956
7438
  try {
6957
7439
  if ((await stat3(abs)).isFile()) {
6958
7440
  scannedFiles += 1;
6959
- scanText(toRel(abs), await readFile12(abs, "utf8"), false, hits);
7441
+ scanText(toRel(abs), await readFile14(abs, "utf8"), false, hits);
6960
7442
  }
6961
7443
  } catch {
6962
7444
  }
6963
7445
  }
6964
7446
  for (const dir of SKILL_DIRS) {
6965
- for (const file of await walkFiles(join20(projectRoot, dir), [".md"])) {
7447
+ for (const file of await walkFiles(join21(projectRoot, dir), [".md"])) {
6966
7448
  scannedFiles += 1;
6967
7449
  try {
6968
- scanText(toRel(file), await readFile12(file, "utf8"), false, hits);
7450
+ scanText(toRel(file), await readFile14(file, "utf8"), false, hits);
6969
7451
  } catch {
6970
7452
  }
6971
7453
  }
6972
7454
  }
6973
7455
  for (const dir of HOOK_DIRS) {
6974
- for (const file of await walkFiles(join20(projectRoot, dir), [".cjs"])) {
7456
+ for (const file of await walkFiles(join21(projectRoot, dir), [".cjs"])) {
6975
7457
  scannedFiles += 1;
6976
7458
  try {
6977
- scanText(toRel(file), await readFile12(file, "utf8"), true, hits);
7459
+ scanText(toRel(file), await readFile14(file, "utf8"), true, hits);
6978
7460
  } catch {
6979
7461
  }
6980
7462
  }
@@ -7009,8 +7491,8 @@ function createRetiredReferenceCheck(t, inspection) {
7009
7491
 
7010
7492
  // src/services/doctor-hooks-lints.ts
7011
7493
  import { constants } from "fs";
7012
- import { access, readdir as readdir5, readFile as readFile13, stat as stat4 } from "fs/promises";
7013
- import { join as join21, posix as posix4 } from "path";
7494
+ import { access, readdir as readdir6, readFile as readFile15, stat as stat4 } from "fs/promises";
7495
+ import { join as join22, posix as posix4 } from "path";
7014
7496
  import { Script } from "vm";
7015
7497
  var HOOKS_RUNTIME_CLIENT_DIRS = [
7016
7498
  { client: "claude", dir: ".claude/hooks" },
@@ -7059,7 +7541,7 @@ function isHookWiredForEvent(hooks, event, hookFile) {
7059
7541
  }
7060
7542
  async function readDirectoryFileNames(dir) {
7061
7543
  try {
7062
- return await readdir5(dir);
7544
+ return await readdir6(dir);
7063
7545
  } catch {
7064
7546
  return null;
7065
7547
  }
@@ -7072,14 +7554,14 @@ async function isFile(absPath) {
7072
7554
  }
7073
7555
  }
7074
7556
  async function inspectHooksWired(projectRoot) {
7075
- const claudeEntries = await readDirectoryFileNames(join21(projectRoot, ".claude"));
7557
+ const claudeEntries = await readDirectoryFileNames(join22(projectRoot, ".claude"));
7076
7558
  if (claudeEntries === null) {
7077
7559
  return { status: "skipped", missingHooks: [] };
7078
7560
  }
7079
- const settingsPath = join21(projectRoot, ".claude", "settings.json");
7561
+ const settingsPath = join22(projectRoot, ".claude", "settings.json");
7080
7562
  let parsed;
7081
7563
  try {
7082
- parsed = JSON.parse(await readFile13(settingsPath, "utf8"));
7564
+ parsed = JSON.parse(await readFile15(settingsPath, "utf8"));
7083
7565
  } catch {
7084
7566
  return { status: "missing-settings", missingHooks: [] };
7085
7567
  }
@@ -7102,8 +7584,8 @@ async function inspectHooksWired(projectRoot) {
7102
7584
  }
7103
7585
  async function inspectHookCacheWritability(projectRoot) {
7104
7586
  const relPath = posix4.join(".fabric", ".cache");
7105
- const fabricDir = join21(projectRoot, ".fabric");
7106
- const cacheDir = join21(projectRoot, ".fabric", ".cache");
7587
+ const fabricDir = join22(projectRoot, ".fabric");
7588
+ const cacheDir = join22(projectRoot, ".fabric", ".cache");
7107
7589
  try {
7108
7590
  try {
7109
7591
  const cacheStats = await stat4(cacheDir);
@@ -7152,12 +7634,12 @@ async function inspectHookCacheWritability(projectRoot) {
7152
7634
  async function inspectHooksContentDrift(projectRoot) {
7153
7635
  const hookFilesByBasename = /* @__PURE__ */ new Map();
7154
7636
  for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
7155
- const absDir = join21(projectRoot, dir);
7637
+ const absDir = join22(projectRoot, dir);
7156
7638
  const entries = await readDirectoryFileNames(absDir);
7157
7639
  if (entries === null) continue;
7158
7640
  for (const name of entries) {
7159
7641
  if (!name.endsWith(".cjs")) continue;
7160
- const abs = join21(absDir, name);
7642
+ const abs = join22(absDir, name);
7161
7643
  if (!await isFile(abs)) continue;
7162
7644
  const arr = hookFilesByBasename.get(name) ?? [];
7163
7645
  arr.push({ client, abs });
@@ -7172,7 +7654,7 @@ async function inspectHooksContentDrift(projectRoot) {
7172
7654
  const hashes = [];
7173
7655
  for (const { client, abs } of copies) {
7174
7656
  try {
7175
- const body = await readFile13(abs, "utf8");
7657
+ const body = await readFile15(abs, "utf8");
7176
7658
  hashes.push({ client, sha: sha256(body) });
7177
7659
  } catch {
7178
7660
  }
@@ -7194,18 +7676,18 @@ async function inspectHooksRuntime(projectRoot) {
7194
7676
  const issues = [];
7195
7677
  let scanned = 0;
7196
7678
  for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
7197
- const absDir = join21(projectRoot, dir);
7679
+ const absDir = join22(projectRoot, dir);
7198
7680
  const entries = await readDirectoryFileNames(absDir);
7199
7681
  if (entries === null) continue;
7200
7682
  for (const name of entries) {
7201
7683
  if (!name.endsWith(".cjs")) continue;
7202
- const abs = join21(absDir, name);
7684
+ const abs = join22(absDir, name);
7203
7685
  const displayPath = `${dir}/${name}`;
7204
7686
  if (!await isFile(abs)) continue;
7205
7687
  scanned += 1;
7206
7688
  let body;
7207
7689
  try {
7208
- body = await readFile13(abs, "utf8");
7690
+ body = await readFile15(abs, "utf8");
7209
7691
  } catch (err) {
7210
7692
  issues.push({
7211
7693
  path: displayPath,
@@ -7342,8 +7824,8 @@ function createHookCacheWritabilityCheck(t, inspection) {
7342
7824
  }
7343
7825
 
7344
7826
  // src/services/doctor-bootstrap-lints.ts
7345
- import { access as access2, readFile as readFile14 } from "fs/promises";
7346
- import { join as join22 } from "path";
7827
+ import { access as access2, readFile as readFile16 } from "fs/promises";
7828
+ import { join as join23 } from "path";
7347
7829
  import {
7348
7830
  BOOTSTRAP_MARKER_BEGIN,
7349
7831
  BOOTSTRAP_MARKER_END,
@@ -7375,17 +7857,17 @@ async function fileExists(path) {
7375
7857
  }
7376
7858
  async function inspectBootstrapAnchor(projectRoot) {
7377
7859
  const [hasAgentsMd, hasClaudeMd] = await Promise.all([
7378
- fileExists(join22(projectRoot, "AGENTS.md")),
7379
- fileExists(join22(projectRoot, "CLAUDE.md"))
7860
+ fileExists(join23(projectRoot, "AGENTS.md")),
7861
+ fileExists(join23(projectRoot, "CLAUDE.md"))
7380
7862
  ]);
7381
7863
  return { hasAgentsMd, hasClaudeMd };
7382
7864
  }
7383
7865
  async function inspectL1BootstrapSnapshotDrift(target) {
7384
- const abs = join22(target, ".fabric", "AGENTS.md");
7866
+ const abs = join23(target, ".fabric", "AGENTS.md");
7385
7867
  const canonical = resolveBootstrapCanonical();
7386
7868
  let onDisk;
7387
7869
  try {
7388
- onDisk = await readFile14(abs, "utf8");
7870
+ onDisk = await readFile16(abs, "utf8");
7389
7871
  } catch {
7390
7872
  return { status: "missing", canonical, onDisk: null };
7391
7873
  }
@@ -7411,17 +7893,17 @@ function createL1BootstrapSnapshotDriftCheck(t, inspection) {
7411
7893
  );
7412
7894
  }
7413
7895
  async function inspectL2ManagedBlockDrift(target) {
7414
- const snapshotPath = join22(target, ".fabric", "AGENTS.md");
7896
+ const snapshotPath = join23(target, ".fabric", "AGENTS.md");
7415
7897
  let snapshot;
7416
7898
  try {
7417
- snapshot = await readFile14(snapshotPath, "utf8");
7899
+ snapshot = await readFile16(snapshotPath, "utf8");
7418
7900
  } catch {
7419
7901
  return { status: "ok", drifted: [] };
7420
7902
  }
7421
- const projectRulesPath = join22(target, ".fabric", "project-rules.md");
7903
+ const projectRulesPath = join23(target, ".fabric", "project-rules.md");
7422
7904
  let expectedBody = snapshot;
7423
7905
  try {
7424
- const projectRules = await readFile14(projectRulesPath, "utf8");
7906
+ const projectRules = await readFile16(projectRulesPath, "utf8");
7425
7907
  expectedBody = `${snapshot}
7426
7908
  ---
7427
7909
  ${projectRules}`;
@@ -7430,12 +7912,12 @@ ${projectRules}`;
7430
7912
  const drifted = [];
7431
7913
  let anyManagedBlockFound = false;
7432
7914
  const blockTargets = [
7433
- join22(target, "AGENTS.md")
7915
+ join23(target, "AGENTS.md")
7434
7916
  ];
7435
7917
  for (const abs of blockTargets) {
7436
7918
  let content;
7437
7919
  try {
7438
- content = await readFile14(abs, "utf8");
7920
+ content = await readFile16(abs, "utf8");
7439
7921
  } catch {
7440
7922
  continue;
7441
7923
  }
@@ -7458,9 +7940,9 @@ ${projectRules}`;
7458
7940
  drifted.push({ path: abs, expected: expectedBody, actual: body });
7459
7941
  }
7460
7942
  }
7461
- const claudeMdPath = join22(target, "CLAUDE.md");
7943
+ const claudeMdPath = join23(target, "CLAUDE.md");
7462
7944
  try {
7463
- const claudeContent = await readFile14(claudeMdPath, "utf8");
7945
+ const claudeContent = await readFile16(claudeMdPath, "utf8");
7464
7946
  anyManagedBlockFound = true;
7465
7947
  const lines = claudeContent.split(/\r?\n/u);
7466
7948
  const hasAtImport = lines.some((line) => line.trim() === "@.fabric/AGENTS.md");
@@ -7528,7 +8010,7 @@ import { appendFile as appendFile3 } from "fs/promises";
7528
8010
  import { minimatch as minimatch2 } from "minimatch";
7529
8011
 
7530
8012
  // src/services/cite-rollup.ts
7531
- import { readFile as readFile15 } from "fs/promises";
8013
+ import { readFile as readFile17 } from "fs/promises";
7532
8014
  import { createLedgerWriteQueue as createLedgerWriteQueue3 } from "@fenglimg/fabric-shared/node/atomic-write";
7533
8015
  var citeRollupQueue = createLedgerWriteQueue3();
7534
8016
  async function appendCiteRollupRow(projectRoot, row) {
@@ -7540,7 +8022,7 @@ async function readCiteRollup(projectRoot) {
7540
8022
  const path = getCiteRollupPath(projectRoot);
7541
8023
  let raw;
7542
8024
  try {
7543
- raw = await readFile15(path, "utf8");
8025
+ raw = await readFile17(path, "utf8");
7544
8026
  } catch (error) {
7545
8027
  if (isNodeError(error) && error.code === "ENOENT") return [];
7546
8028
  throw error;
@@ -8131,6 +8613,18 @@ async function runDoctorCiteCoverage(projectRoot, options) {
8131
8613
  }
8132
8614
  }
8133
8615
  }
8616
+ const editSessionIds = /* @__PURE__ */ new Set();
8617
+ for (const edit of editEvents) {
8618
+ if (typeof edit.session_id === "string" && edit.session_id.length > 0) {
8619
+ editSessionIds.add(edit.session_id);
8620
+ }
8621
+ }
8622
+ let recallsInWindow = 0;
8623
+ for (const list of plannedBySession.values()) recallsInWindow += list.length;
8624
+ let recallSessionsCorrelated = 0;
8625
+ for (const sid of plannedBySession.keys()) {
8626
+ if (editSessionIds.has(sid)) recallSessionsCorrelated += 1;
8627
+ }
8134
8628
  const noneTotal = Object.values(noneHistogram).reduce((a, b) => a + b, 0);
8135
8629
  const compliantCites = qualifyingCites + noneTotal;
8136
8630
  const noncompliantCites = expectedButMissed;
@@ -8198,6 +8692,11 @@ async function runDoctorCiteCoverage(projectRoot, options) {
8198
8692
  uncorrelatable_edits: uncorrelatableEdits,
8199
8693
  recall_backed_edits: recallBackedEdits,
8200
8694
  recall_coverage_rate: recallCoverageRate,
8695
+ recall_diagnostics: {
8696
+ recalls_in_window: recallsInWindow,
8697
+ recall_sessions: plannedBySession.size,
8698
+ recall_sessions_correlated: recallSessionsCorrelated
8699
+ },
8201
8700
  exposed_and_mutated: {
8202
8701
  count: exposedAndMutated.count,
8203
8702
  ...exposedAndMutated.ids.length > 0 ? { ids: exposedAndMutated.ids } : {}
@@ -8641,6 +9140,7 @@ async function runDoctorReport(target) {
8641
9140
  const driftUnconsumed = await inspectDriftUnconsumed(projectRoot);
8642
9141
  const storeCounterDrift = inspectStoreCounters(projectRoot);
8643
9142
  const storeOrphans = inspectStoreOrphans();
9143
+ const projectRegistryDrift = await inspectProjectRegistryDrift(projectRoot);
8644
9144
  const stableIdIntegrity = await inspectStoreStableIdIntegrity(projectRoot);
8645
9145
  const relevancePaths = await inspectStoreRelevancePaths(projectRoot);
8646
9146
  const broadIndexDrift = await inspectBroadIndexDrift(projectRoot);
@@ -8670,7 +9170,7 @@ async function runDoctorReport(target) {
8670
9170
  const globalCliVersion = process.env.VITEST === "true" ? { status: "ok", version: "test-skipped" } : inspectGlobalCliVersion();
8671
9171
  const targetFiles = Object.fromEntries(
8672
9172
  await Promise.all(
8673
- TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(join23(projectRoot, path))])
9173
+ TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(join24(projectRoot, path))])
8674
9174
  )
8675
9175
  );
8676
9176
  const checks = [
@@ -8724,6 +9224,12 @@ async function runDoctorReport(target) {
8724
9224
  // (warning). `--fix` adopts it (rescue-before-delete — re-register, never
8725
9225
  // an on-disk delete).
8726
9226
  createStoreOrphanCheck(t, storeOrphans),
9227
+ // W2 (F-003): project-registry drift over projects.json ↔ projects/ folder
9228
+ // tree. unregistered-write (data unrouted) → manual_error; orphan-folder →
9229
+ // warning; empty-folder → info; ghost-registration emits nothing (DA-05).
9230
+ // `--fix` rescue-registers orphans/unregistered (addStoreProject — never a
9231
+ // non-empty delete) and prunes only genuinely-empty registered folders.
9232
+ createProjectRegistryDriftCheck(t, projectRegistryDrift),
8727
9233
  // rc.5 TASK-010: read-side underseeded-corpus check (#22). Info kind —
8728
9234
  // does not bump report status. Recommends running the fabric-import skill
8729
9235
  // to backfill knowledge when the corpus is below the threshold floor.
@@ -8874,7 +9380,7 @@ async function runDoctorFix(target) {
8874
9380
  const fixed = [];
8875
9381
  const ledgerWarnings = [];
8876
9382
  if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
8877
- const snapshotPath = join23(projectRoot, ".fabric", "AGENTS.md");
9383
+ const snapshotPath = join24(projectRoot, ".fabric", "AGENTS.md");
8878
9384
  await ensureParentDirectory(snapshotPath);
8879
9385
  await atomicWriteText4(snapshotPath, resolveBootstrapCanonical2());
8880
9386
  fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
@@ -8898,6 +9404,18 @@ async function runDoctorFix(target) {
8898
9404
  fixed.push(findIssue(before.warnings, "store_orphan"));
8899
9405
  }
8900
9406
  }
9407
+ const registryDriftFound = before.manual_errors.some((issue) => issue.code === "project_registry_drift") || before.warnings.some((issue) => issue.code === "project_registry_drift") || before.infos.some((issue) => issue.code === "project_registry_drift");
9408
+ if (registryDriftFound) {
9409
+ const result = await fixProjectRegistryDrift(projectRoot);
9410
+ if (result.registered.length > 0 || result.pruned.length > 0) {
9411
+ fixed.push(
9412
+ findIssue(
9413
+ [...before.manual_errors, ...before.warnings, ...before.infos],
9414
+ "project_registry_drift"
9415
+ )
9416
+ );
9417
+ }
9418
+ }
8901
9419
  if (before.fixable_errors.some((issue) => issue.code === "event_ledger_partial_write")) {
8902
9420
  const ledgerPath = getEventLedgerPath(projectRoot);
8903
9421
  const truncResult = await truncateLedgerToLastNewline(ledgerPath);
@@ -8947,7 +9465,7 @@ async function runDoctorFix(target) {
8947
9465
  if (before.infos.some((issue) => issue.code === "stale_serve_lock")) {
8948
9466
  const lockInspection = inspectStaleServeLock(projectRoot, Date.now());
8949
9467
  if (lockInspection.present && !lockInspection.pidAlive) {
8950
- const lockFilePath = join23(projectRoot, ".fabric", ".serve.lock");
9468
+ const lockFilePath = join24(projectRoot, ".fabric", ".serve.lock");
8951
9469
  try {
8952
9470
  await unlink4(lockFilePath);
8953
9471
  } catch (err) {
@@ -9064,7 +9582,7 @@ function createApplyLintMessage(succeeded, failed, manualErrorCount) {
9064
9582
  }
9065
9583
  async function applySessionHintsStaleCleanup(projectRoot, candidate) {
9066
9584
  const detail = `deleted (${candidate.age_days}d old)`;
9067
- const absPath = join23(projectRoot, candidate.path);
9585
+ const absPath = join24(projectRoot, candidate.path);
9068
9586
  try {
9069
9587
  const { unlink: unlink5 } = await import("fs/promises");
9070
9588
  await unlink5(absPath);
@@ -9089,9 +9607,9 @@ function truncateErrorMessage(error) {
9089
9607
  return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
9090
9608
  }
9091
9609
  async function inspectForensic(projectRoot) {
9092
- const path = join23(projectRoot, ".fabric", "forensic.json");
9610
+ const path = join24(projectRoot, ".fabric", "forensic.json");
9093
9611
  try {
9094
- const parsed = forensicReportSchema.parse(JSON.parse(await readFile17(path, "utf8")));
9612
+ const parsed = forensicReportSchema.parse(JSON.parse(await readFile19(path, "utf8")));
9095
9613
  return { present: true, valid: true, report: parsed };
9096
9614
  } catch (error) {
9097
9615
  if (isMissingFileError(error)) {
@@ -9121,7 +9639,7 @@ async function inspectEventLedger(projectRoot) {
9121
9639
  try {
9122
9640
  await access4(path, constants2.W_OK);
9123
9641
  const { warnings } = await readEventLedger(projectRoot);
9124
- const raw = await readFile17(path, "utf8");
9642
+ const raw = await readFile19(path, "utf8");
9125
9643
  const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
9126
9644
  const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
9127
9645
  const schemaVersionSamples = [];
@@ -9666,7 +10184,7 @@ async function inspectPreexistingRootFiles(projectRoot) {
9666
10184
  const candidates = ["CLAUDE.md", "AGENTS.md"];
9667
10185
  const detected = [];
9668
10186
  for (const name of candidates) {
9669
- if (await pathExists(join23(projectRoot, name))) {
10187
+ if (await pathExists(join24(projectRoot, name))) {
9670
10188
  detected.push(name);
9671
10189
  }
9672
10190
  }
@@ -9751,7 +10269,7 @@ async function buildLastActiveIndex(projectRoot) {
9751
10269
  return map;
9752
10270
  }
9753
10271
  async function inspectSessionHintsStale(projectRoot, now) {
9754
- const cacheDir = join23(projectRoot, ".fabric", ".cache");
10272
+ const cacheDir = join24(projectRoot, ".fabric", ".cache");
9755
10273
  let entries;
9756
10274
  try {
9757
10275
  entries = await readdirAsync(cacheDir, { withFileTypes: true });
@@ -9763,7 +10281,7 @@ async function inspectSessionHintsStale(projectRoot, now) {
9763
10281
  if (!entry.isFile()) continue;
9764
10282
  if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
9765
10283
  if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
9766
- const absPath = join23(cacheDir, entry.name);
10284
+ const absPath = join24(cacheDir, entry.name);
9767
10285
  let mtimeMs = 0;
9768
10286
  try {
9769
10287
  mtimeMs = (await statAsync(absPath)).mtimeMs;
@@ -9795,9 +10313,9 @@ function inspectStaleServeLock(projectRoot, now) {
9795
10313
  };
9796
10314
  }
9797
10315
  async function readUnderseedThresholdFromConfig(projectRoot) {
9798
- const configPath = join23(projectRoot, ".fabric", "fabric-config.json");
10316
+ const configPath = join24(projectRoot, ".fabric", "fabric-config.json");
9799
10317
  try {
9800
- const raw = await readFile17(configPath, "utf8");
10318
+ const raw = await readFile19(configPath, "utf8");
9801
10319
  const parsed = JSON.parse(raw);
9802
10320
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
9803
10321
  const v = parsed.underseed_node_threshold;
@@ -9913,10 +10431,10 @@ async function inspectOnboardCoverage(projectRoot) {
9913
10431
  return { filled, missing, opted_out: optedOut };
9914
10432
  }
9915
10433
  async function readOnboardOptedOut(projectRoot) {
9916
- const path = join23(projectRoot, ".fabric", "fabric-config.json");
10434
+ const path = join24(projectRoot, ".fabric", "fabric-config.json");
9917
10435
  let raw;
9918
10436
  try {
9919
- raw = await readFile17(path, "utf8");
10437
+ raw = await readFile19(path, "utf8");
9920
10438
  } catch {
9921
10439
  return [];
9922
10440
  }
@@ -10018,22 +10536,22 @@ async function* iterateCanonicalFilenames(projectRoot) {
10018
10536
  }
10019
10537
  }
10020
10538
  async function rewriteThreeEndManagedBlocks(projectRoot) {
10021
- const snapshotPath = join23(projectRoot, ".fabric", "AGENTS.md");
10539
+ const snapshotPath = join24(projectRoot, ".fabric", "AGENTS.md");
10022
10540
  if (!await pathExists(snapshotPath)) {
10023
10541
  return;
10024
10542
  }
10025
10543
  let snapshot;
10026
10544
  try {
10027
- snapshot = await readFile17(snapshotPath, "utf8");
10545
+ snapshot = await readFile19(snapshotPath, "utf8");
10028
10546
  } catch {
10029
10547
  return;
10030
10548
  }
10031
- const projectRulesPath = join23(projectRoot, ".fabric", "project-rules.md");
10549
+ const projectRulesPath = join24(projectRoot, ".fabric", "project-rules.md");
10032
10550
  const hasProjectRules = await pathExists(projectRulesPath);
10033
10551
  let expectedBody = snapshot;
10034
10552
  if (hasProjectRules) {
10035
10553
  try {
10036
- const projectRules = await readFile17(projectRulesPath, "utf8");
10554
+ const projectRules = await readFile19(projectRulesPath, "utf8");
10037
10555
  expectedBody = `${snapshot}
10038
10556
  ---
10039
10557
  ${projectRules}`;
@@ -10044,7 +10562,7 @@ ${projectRules}`;
10044
10562
  ${expectedBody}
10045
10563
  ${BOOTSTRAP_MARKER_END2}`;
10046
10564
  const blockTargets = [
10047
- join23(projectRoot, "AGENTS.md")
10565
+ join24(projectRoot, "AGENTS.md")
10048
10566
  ];
10049
10567
  for (const abs of blockTargets) {
10050
10568
  if (!await pathExists(abs)) {
@@ -10052,7 +10570,7 @@ ${BOOTSTRAP_MARKER_END2}`;
10052
10570
  }
10053
10571
  let existing;
10054
10572
  try {
10055
- existing = await readFile17(abs, "utf8");
10573
+ existing = await readFile19(abs, "utf8");
10056
10574
  } catch {
10057
10575
  continue;
10058
10576
  }
@@ -10077,11 +10595,11 @@ ${managedBlock}
10077
10595
  }
10078
10596
  await atomicWriteText4(abs, next);
10079
10597
  }
10080
- const claudeMdPath = join23(projectRoot, "CLAUDE.md");
10598
+ const claudeMdPath = join24(projectRoot, "CLAUDE.md");
10081
10599
  if (await pathExists(claudeMdPath)) {
10082
10600
  let claudeContent;
10083
10601
  try {
10084
- claudeContent = await readFile17(claudeMdPath, "utf8");
10602
+ claudeContent = await readFile19(claudeMdPath, "utf8");
10085
10603
  } catch {
10086
10604
  return;
10087
10605
  }
@@ -10108,7 +10626,7 @@ ${managedBlock}
10108
10626
  async function ensureEventLedger(projectRoot) {
10109
10627
  const path = getEventLedgerPath(projectRoot);
10110
10628
  await ensureParentDirectory(path);
10111
- await writeFile4(path, "", { encoding: "utf8", flag: "a" });
10629
+ await writeFile5(path, "", { encoding: "utf8", flag: "a" });
10112
10630
  }
10113
10631
  function createFixMessage(fixed, report) {
10114
10632
  const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
@@ -10147,7 +10665,7 @@ async function collectEntryPoints(root) {
10147
10665
  continue;
10148
10666
  }
10149
10667
  for (const entry of await readdirAsync(current, { withFileTypes: true })) {
10150
- const absolutePath = join23(current, entry.name);
10668
+ const absolutePath = join24(current, entry.name);
10151
10669
  const relativePath = normalizePath2(absolutePath.slice(root.length + 1));
10152
10670
  if (relativePath.length === 0) {
10153
10671
  continue;
@@ -10223,7 +10741,7 @@ async function enrichDescriptions(projectRoot, opts = {}) {
10223
10741
  scanned += 1;
10224
10742
  let source;
10225
10743
  try {
10226
- source = await readFile17(absPath, "utf8");
10744
+ source = await readFile19(absPath, "utf8");
10227
10745
  } catch {
10228
10746
  continue;
10229
10747
  }
@@ -10361,16 +10879,16 @@ async function runDoctorConflictLint(projectRoot, opts = {}) {
10361
10879
  }
10362
10880
 
10363
10881
  // src/services/why-not-surfaced.ts
10364
- import { readFile as readFile18 } from "fs/promises";
10365
- import { basename as basename3, join as join24 } from "path";
10882
+ import { readFile as readFile20 } from "fs/promises";
10883
+ import { basename as basename3, join as join25 } from "path";
10366
10884
  import {
10367
- buildStoreResolveInput as buildStoreResolveInput7,
10368
- createStoreResolver as createStoreResolver7,
10369
- loadProjectConfig as loadProjectConfig4,
10885
+ buildStoreResolveInput as buildStoreResolveInput8,
10886
+ createStoreResolver as createStoreResolver8,
10887
+ loadProjectConfig as loadProjectConfig5,
10370
10888
  readKnowledgeAcrossStores as readKnowledgeAcrossStores4,
10371
- resolveGlobalRoot as resolveGlobalRoot8,
10889
+ resolveGlobalRoot as resolveGlobalRoot9,
10372
10890
  scopeRoot as scopeRoot3,
10373
- storeRelativePathForMount as storeRelativePathForMount6
10891
+ storeRelativePathForMount as storeRelativePathForMount7
10374
10892
  } from "@fenglimg/fabric-shared";
10375
10893
  var SEMANTIC_SCOPE_LINE3 = /^semantic_scope:\s*"?([^"\n]+?)"?\s*$/mu;
10376
10894
  var RELEVANCE_SCOPE_LINE = /^relevance_scope:\s*"?(broad|narrow)"?\s*$/mu;
@@ -10394,15 +10912,15 @@ async function explainWhyNotSurfaced(projectRoot, query) {
10394
10912
  activeProject: null,
10395
10913
  relevanceScope: null
10396
10914
  };
10397
- const input = buildStoreResolveInput7(projectRoot);
10915
+ const input = buildStoreResolveInput8(projectRoot);
10398
10916
  if (input === null) {
10399
10917
  return base;
10400
10918
  }
10401
- const globalRoot = resolveGlobalRoot8();
10919
+ const globalRoot = resolveGlobalRoot9();
10402
10920
  const allStores = input.mountedStores.map((s) => ({
10403
10921
  store_uuid: s.store_uuid,
10404
10922
  alias: s.alias,
10405
- dir: join24(globalRoot, storeRelativePathForMount6(s))
10923
+ dir: join25(globalRoot, storeRelativePathForMount7(s))
10406
10924
  }));
10407
10925
  const refs = await readKnowledgeAcrossStores4(allStores);
10408
10926
  const candidate = refs.find((ref) => fileMatchesId(ref.file, localId));
@@ -10411,7 +10929,7 @@ async function explainWhyNotSurfaced(projectRoot, query) {
10411
10929
  }
10412
10930
  let source;
10413
10931
  try {
10414
- source = await readFile18(candidate.file, "utf8");
10932
+ source = await readFile20(candidate.file, "utf8");
10415
10933
  } catch {
10416
10934
  return base;
10417
10935
  }
@@ -10420,9 +10938,9 @@ async function explainWhyNotSurfaced(projectRoot, query) {
10420
10938
  }
10421
10939
  const semanticScope = SEMANTIC_SCOPE_LINE3.exec(source)?.[1] ?? null;
10422
10940
  const relevanceScope = RELEVANCE_SCOPE_LINE.exec(source)?.[1] ?? "broad";
10423
- const activeProject = loadProjectConfig4(projectRoot)?.active_project ?? null;
10941
+ const activeProject = loadProjectConfig5(projectRoot)?.active_project ?? null;
10424
10942
  const boundUuids = new Set(
10425
- createStoreResolver7().resolveReadSet(input).stores.map((s) => s.store_uuid)
10943
+ createStoreResolver8().resolveReadSet(input).stores.map((s) => s.store_uuid)
10426
10944
  );
10427
10945
  const storeBound = boundUuids.has(candidate.store_uuid);
10428
10946
  const found = {
@@ -10464,6 +10982,191 @@ function buildColdEvalBatch(candidates) {
10464
10982
  return { rubric: COLD_EVAL_RUBRIC, candidates: judgeable };
10465
10983
  }
10466
10984
 
10985
+ // src/services/doctor-related-graph.ts
10986
+ function buildRelatedGraph(nodes) {
10987
+ const brokenLinks = [];
10988
+ const inDegree = /* @__PURE__ */ new Map();
10989
+ const allIds = /* @__PURE__ */ new Set();
10990
+ const edgeSources = [];
10991
+ for (const node of nodes) {
10992
+ const bareId = extractBareStableId(node.qualifiedId);
10993
+ allIds.add(node.qualifiedId);
10994
+ if (bareId !== null) {
10995
+ allIds.add(bareId);
10996
+ }
10997
+ const related = node.related;
10998
+ if (related !== void 0 && related.length > 0) {
10999
+ edgeSources.push({ source: node.qualifiedId, related });
11000
+ for (const rel of related) {
11001
+ inDegree.set(rel, (inDegree.get(rel) ?? 0) + 1);
11002
+ }
11003
+ }
11004
+ }
11005
+ for (const { source, related } of edgeSources) {
11006
+ for (const rel of related) {
11007
+ if (!allIds.has(rel)) {
11008
+ brokenLinks.push({ source, target: rel });
11009
+ }
11010
+ }
11011
+ }
11012
+ const hubEntries = [...inDegree.entries()].map(([stableId, degree]) => ({ stableId, inDegree: degree })).sort((a, b) => b.inDegree - a.inDegree || a.stableId.localeCompare(b.stableId));
11013
+ return {
11014
+ brokenLinks,
11015
+ hubEntries,
11016
+ totalEntries: nodes.length
11017
+ };
11018
+ }
11019
+ function extractBareStableId(qualifiedId) {
11020
+ const colonIdx = qualifiedId.indexOf(":");
11021
+ if (colonIdx > 0 && colonIdx < qualifiedId.length - 1) {
11022
+ return qualifiedId.slice(colonIdx + 1);
11023
+ }
11024
+ return null;
11025
+ }
11026
+ async function inspectRelatedGraph(projectRoot) {
11027
+ const entries = await collectStoreCanonicalEntries(projectRoot);
11028
+ return buildRelatedGraph(
11029
+ entries.map((entry) => ({
11030
+ qualifiedId: entry.qualifiedId,
11031
+ related: entry.description.related
11032
+ }))
11033
+ );
11034
+ }
11035
+
11036
+ // src/services/store-precheck.ts
11037
+ import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
11038
+ import { join as join26 } from "path";
11039
+ import {
11040
+ buildStoreResolveInput as buildStoreResolveInput9,
11041
+ createStoreResolver as createStoreResolver9,
11042
+ resolveGlobalRoot as resolveGlobalRoot10,
11043
+ storeRelativePathForMount as storeRelativePathForMount8
11044
+ } from "@fenglimg/fabric-shared";
11045
+ var CACHE_TTL_MS = 6e4;
11046
+ var cache = /* @__PURE__ */ new Map();
11047
+ function clearPrecheckCache() {
11048
+ cache.clear();
11049
+ }
11050
+ function evaluateStoreDir(storeDir, identity) {
11051
+ if (!existsSync9(storeDir)) {
11052
+ return {
11053
+ uuid: identity.uuid,
11054
+ alias: identity.alias,
11055
+ reachable: false,
11056
+ reason: `directory not found at ${storeDir}`
11057
+ };
11058
+ }
11059
+ const storeJsonPath = join26(storeDir, "store.json");
11060
+ const gitDirPath = join26(storeDir, ".git");
11061
+ const hasStoreJson = existsSync9(storeJsonPath) && isValidStoreJson(storeJsonPath);
11062
+ const hasGitDir = existsSync9(gitDirPath);
11063
+ if (!hasStoreJson && !hasGitDir) {
11064
+ return {
11065
+ uuid: identity.uuid,
11066
+ alias: identity.alias,
11067
+ reachable: false,
11068
+ reason: `no store.json or .git found in ${storeDir}`
11069
+ };
11070
+ }
11071
+ return { uuid: identity.uuid, alias: identity.alias, reachable: true };
11072
+ }
11073
+ function isValidStoreJson(path) {
11074
+ try {
11075
+ JSON.parse(readFileSync4(path, "utf8"));
11076
+ return true;
11077
+ } catch {
11078
+ return false;
11079
+ }
11080
+ }
11081
+ async function precheckStoreReachability(projectRoot, globalRoot = resolveGlobalRoot10(), now = Date.now()) {
11082
+ const cached = cache.get(projectRoot);
11083
+ if (cached !== void 0 && cached.expiresAt > now) {
11084
+ return cached.result;
11085
+ }
11086
+ const input = buildStoreResolveInput9(projectRoot, globalRoot);
11087
+ if (input === null) {
11088
+ const result2 = { stores: [], allReachable: true };
11089
+ cache.set(projectRoot, { result: result2, expiresAt: now + CACHE_TTL_MS });
11090
+ return result2;
11091
+ }
11092
+ const readSet = createStoreResolver9().resolveReadSet(input);
11093
+ const stores = readSet.stores.map((entry) => {
11094
+ const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
11095
+ const storeDir = join26(
11096
+ globalRoot,
11097
+ storeRelativePathForMount8(mounted ?? { store_uuid: entry.store_uuid })
11098
+ );
11099
+ return evaluateStoreDir(storeDir, { uuid: entry.store_uuid, alias: entry.alias });
11100
+ });
11101
+ const result = {
11102
+ stores,
11103
+ allReachable: stores.every((s) => s.reachable)
11104
+ };
11105
+ cache.set(projectRoot, { result, expiresAt: now + CACHE_TTL_MS });
11106
+ return result;
11107
+ }
11108
+
11109
+ // src/services/doctor-consumption-lint.ts
11110
+ var DEFAULT_WINDOW_DAYS = 30;
11111
+ var DEFAULT_TOP_N = 10;
11112
+ var DEFAULT_MIN_TOTAL_ENTRIES = 20;
11113
+ var DEFAULT_MIN_CONSUMED_WINDOWS = 30;
11114
+ var DEFAULT_MIN_CONSUMED_EVENTS = 50;
11115
+ var CONSUMED_PREFIX = "knowledge_consumed:";
11116
+ var MS_PER_DAY5 = 24 * 60 * 60 * 1e3;
11117
+ function resolveConfig(config) {
11118
+ return {
11119
+ windowDays: config?.windowDays ?? DEFAULT_WINDOW_DAYS,
11120
+ topN: config?.topN ?? DEFAULT_TOP_N,
11121
+ minTotalEntries: config?.minTotalEntries ?? DEFAULT_MIN_TOTAL_ENTRIES,
11122
+ minConsumedWindows: config?.minConsumedWindows ?? DEFAULT_MIN_CONSUMED_WINDOWS,
11123
+ minConsumedEvents: config?.minConsumedEvents ?? DEFAULT_MIN_CONSUMED_EVENTS
11124
+ };
11125
+ }
11126
+ function aggregateConsumption(rows, allQualifiedIds, now, config) {
11127
+ const cfg = resolveConfig(config);
11128
+ const cutoffMs = now - cfg.windowDays * MS_PER_DAY5;
11129
+ const consumed = /* @__PURE__ */ new Map();
11130
+ let consumedWindows = 0;
11131
+ let totalConsumedEvents = 0;
11132
+ for (const row of rows) {
11133
+ const rowTs = Date.parse(row.timestamp);
11134
+ if (!Number.isFinite(rowTs) || rowTs < cutoffMs) continue;
11135
+ if (row.counters === null || typeof row.counters !== "object") continue;
11136
+ let windowHadConsumption = false;
11137
+ for (const [counterName, rawCount] of Object.entries(row.counters)) {
11138
+ if (!counterName.startsWith(CONSUMED_PREFIX)) continue;
11139
+ const count = typeof rawCount === "number" ? rawCount : 0;
11140
+ if (count <= 0) continue;
11141
+ const id = counterName.slice(CONSUMED_PREFIX.length);
11142
+ if (id.length === 0) continue;
11143
+ consumed.set(id, (consumed.get(id) ?? 0) + count);
11144
+ totalConsumedEvents += count;
11145
+ windowHadConsumption = true;
11146
+ }
11147
+ if (windowHadConsumption) consumedWindows += 1;
11148
+ }
11149
+ 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);
11150
+ const dataMature = consumedWindows >= cfg.minConsumedWindows && totalConsumedEvents >= cfg.minConsumedEvents && allQualifiedIds.length >= cfg.minTotalEntries;
11151
+ const zeroConsumed = dataMature ? allQualifiedIds.filter((id) => !consumed.has(id)).sort((a, b) => a.localeCompare(b)) : [];
11152
+ return {
11153
+ topConsumed,
11154
+ zeroConsumed,
11155
+ totalEntries: allQualifiedIds.length,
11156
+ consumedEntries: consumed.size,
11157
+ consumedWindows,
11158
+ totalConsumedEvents,
11159
+ dataMature,
11160
+ windowDays: cfg.windowDays
11161
+ };
11162
+ }
11163
+ async function inspectConsumption(projectRoot, config, now = Date.now()) {
11164
+ const rows = await readMetrics(projectRoot);
11165
+ const entries = await collectStoreCanonicalEntries(projectRoot);
11166
+ const allQualifiedIds = entries.map((entry) => entry.qualifiedId);
11167
+ return aggregateConsumption(rows, allQualifiedIds, now, config);
11168
+ }
11169
+
10467
11170
  // src/services/rotation-tick.ts
10468
11171
  var DEFAULT_TICK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
10469
11172
  var tickTimers = /* @__PURE__ */ new Map();
@@ -10499,7 +11202,7 @@ import { IOFabricError as IOFabricError2, RuleError } from "@fenglimg/fabric-sha
10499
11202
 
10500
11203
  // src/services/read-ledger.ts
10501
11204
  import { randomUUID as randomUUID7 } from "crypto";
10502
- import { access as access5, copyFile, readFile as readFile19, rm } from "fs/promises";
11205
+ import { access as access5, copyFile, readFile as readFile21, rm } from "fs/promises";
10503
11206
  import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
10504
11207
  async function resolveLedgerPaths(projectRoot) {
10505
11208
  const primaryPath = getLedgerPath(projectRoot);
@@ -10527,7 +11230,7 @@ async function readLegacyLedger(projectRoot) {
10527
11230
  const { readPath } = await resolveLedgerPaths(projectRoot);
10528
11231
  let raw;
10529
11232
  try {
10530
- raw = await readFile19(readPath, "utf8");
11233
+ raw = await readFile21(readPath, "utf8");
10531
11234
  } catch (error) {
10532
11235
  if (isNodeError(error) && error.code === "ENOENT") {
10533
11236
  return [];
@@ -10782,8 +11485,8 @@ function formatError(error) {
10782
11485
  }
10783
11486
  function formatPreexistingRootMessage(projectRoot) {
10784
11487
  const preexisting = [];
10785
- if (existsSync9(join25(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
10786
- if (existsSync9(join25(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
11488
+ if (existsSync10(join27(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
11489
+ if (existsSync10(join27(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
10787
11490
  if (preexisting.length === 0) return null;
10788
11491
  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.`;
10789
11492
  }
@@ -10810,7 +11513,7 @@ function createFabricServer(tracker) {
10810
11513
  const server = new McpServer(
10811
11514
  {
10812
11515
  name: "fabric-knowledge-server",
10813
- version: "2.3.0-rc.3"
11516
+ version: "2.3.0-rc.5"
10814
11517
  },
10815
11518
  {
10816
11519
  instructions: FABRIC_SERVER_INSTRUCTIONS
@@ -10830,10 +11533,10 @@ function createFabricServer(tracker) {
10830
11533
  },
10831
11534
  async (_uri) => {
10832
11535
  const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
10833
- const path = join25(projectRoot, ".fabric", "bootstrap", "README.md");
11536
+ const path = join27(projectRoot, ".fabric", "bootstrap", "README.md");
10834
11537
  let text = "";
10835
- if (existsSync9(path)) {
10836
- text = await readFile20(path, "utf8");
11538
+ if (existsSync10(path)) {
11539
+ text = await readFile22(path, "utf8");
10837
11540
  }
10838
11541
  return {
10839
11542
  contents: [
@@ -10933,11 +11636,15 @@ export {
10933
11636
  METRIC_COUNTER_NAMES,
10934
11637
  OPTIONAL_EMBED_PACKAGE,
10935
11638
  RETIRED_TOKENS,
11639
+ aggregateConsumption,
10936
11640
  appendEventLedgerEvent,
10937
11641
  buildAlwaysActiveBodies,
10938
11642
  buildColdEvalBatch,
10939
11643
  buildKnowledgeCensus,
11644
+ buildRelatedGraph,
10940
11645
  bumpCounter,
11646
+ clearPrecheckCache,
11647
+ collectStoreCanonicalEntries,
10941
11648
  contextCache,
10942
11649
  createFabricServer,
10943
11650
  createInFlightTracker,
@@ -10946,6 +11653,7 @@ export {
10946
11653
  detectUnboundProject,
10947
11654
  drainCounters,
10948
11655
  enrichDescriptions,
11656
+ evaluateStoreDir,
10949
11657
  explainWhyNotSurfaced,
10950
11658
  extractKnowledge,
10951
11659
  findConflictCandidates,
@@ -10956,12 +11664,16 @@ export {
10956
11664
  getLedgerPath,
10957
11665
  getLegacyLedgerPath,
10958
11666
  getMetricsLedgerPath,
11667
+ inspectConsumption,
11668
+ inspectRelatedGraph,
10959
11669
  inspectRetiredReferences,
11670
+ isEmbedderResolvable,
10960
11671
  lintConflicts,
10961
11672
  loadConflictEntries,
10962
11673
  loadEmbedder,
10963
11674
  pairSimilarity,
10964
11675
  planContext,
11676
+ precheckStoreReachability,
10965
11677
  readEmbedConfig,
10966
11678
  readEventLedger,
10967
11679
  readFusion,