@fenglimg/fabric-server 2.3.0-rc.4 → 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 (2) hide show
  1. package/dist/index.js +498 -195
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/index.ts
2
2
  import { existsSync as existsSync10 } from "fs";
3
- import { readFile as readFile21 } from "fs/promises";
4
- import { join as join26, resolve as resolve4 } from "path";
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";
@@ -905,8 +905,10 @@ import { createHash as createHash2 } from "crypto";
905
905
  import { readFile as readFile3 } from "fs/promises";
906
906
  import { join as join5 } from "path";
907
907
  import {
908
+ STORE_KNOWLEDGE_TYPE_DIRS,
908
909
  STORE_LAYOUT,
909
910
  STORE_PENDING_DIR,
911
+ STORE_PROJECT_ID_PATTERN,
910
912
  buildStoreResolveInput,
911
913
  createStoreResolver,
912
914
  isPersonalLeakIntoSharedStore,
@@ -955,8 +957,25 @@ function resolveWriteTargetStoreDir(layer, projectRoot, semanticScope) {
955
957
  function resolveStorePendingBase(layer, projectRoot, semanticScope) {
956
958
  return join5(resolveWriteTargetStoreDir(layer, projectRoot, semanticScope), STORE_LAYOUT.knowledgeDir, STORE_PENDING_DIR);
957
959
  }
958
- function resolveStoreCanonicalBase(layer, projectRoot) {
959
- 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;
960
979
  }
961
980
  function resolveWriteScopeMeta(layer, projectRoot, semanticScope) {
962
981
  const input = buildStoreResolveInput(projectRoot);
@@ -998,7 +1017,6 @@ import {
998
1017
  deriveAgentsMetaStableId,
999
1018
  isKnowledgeStableId,
1000
1019
  KnowledgeTypeSchema,
1001
- LayerSchema,
1002
1020
  MaturitySchema,
1003
1021
  parseKnowledgeId,
1004
1022
  StableIdSchema
@@ -1081,8 +1099,7 @@ function extractRuleDescription(source) {
1081
1099
  id: knowledge?.id,
1082
1100
  knowledge_type: knowledge?.knowledge_type,
1083
1101
  maturity: knowledge?.maturity,
1084
- knowledge_layer: knowledge?.knowledge_layer,
1085
- layer_reason: knowledge?.layer_reason,
1102
+ // W4/Track1 (D1): no `knowledge_layer` — layer derives from the id prefix.
1086
1103
  created_at: knowledge?.created_at,
1087
1104
  tags: knowledge?.tags,
1088
1105
  // v2.0-rc.5 (C1): default-safe values when there is no frontmatter at all;
@@ -1107,12 +1124,10 @@ function extractDescriptionFromFrontmatter(frontmatter) {
1107
1124
  tech_stack: extractInlineArray(frontmatter, "tech_stack"),
1108
1125
  impact: extractInlineArray(frontmatter, "impact"),
1109
1126
  must_read_if: extractScalar(frontmatter, "must_read_if") ?? summary,
1110
- entities: extractInlineArray(frontmatter, "entities"),
1111
1127
  id: knowledge.id,
1112
1128
  knowledge_type: knowledge.knowledge_type,
1113
1129
  maturity: knowledge.maturity,
1114
- knowledge_layer: knowledge.knowledge_layer,
1115
- layer_reason: knowledge.layer_reason,
1130
+ // W4/Track1 (D1): no `knowledge_layer` — layer derives from the id prefix.
1116
1131
  created_at: knowledge.created_at,
1117
1132
  tags: knowledge.tags,
1118
1133
  relevance_scope: knowledge.relevance_scope,
@@ -1143,8 +1158,6 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
1143
1158
  const rawId = extractScalar(frontmatter, "id");
1144
1159
  const rawType = extractScalar(frontmatter, "type");
1145
1160
  const rawMaturity = extractScalar(frontmatter, "maturity");
1146
- const rawLayer = extractScalar(frontmatter, "layer");
1147
- const rawLayerReason = extractScalar(frontmatter, "layer_reason");
1148
1161
  const rawCreatedAt = extractScalar(frontmatter, "created_at");
1149
1162
  let id;
1150
1163
  if (rawId !== void 0) {
@@ -1181,16 +1194,6 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
1181
1194
  maturity = parsed.data;
1182
1195
  } else {
1183
1196
  process.stderr.write(`[fabric] frontmatter: unknown maturity ${JSON.stringify(rawMaturity)}; skipping
1184
- `);
1185
- }
1186
- }
1187
- let knowledge_layer;
1188
- if (rawLayer !== void 0) {
1189
- const parsed = LayerSchema.safeParse(rawLayer);
1190
- if (parsed.success) {
1191
- knowledge_layer = parsed.data;
1192
- } else {
1193
- process.stderr.write(`[fabric] frontmatter: unknown layer ${JSON.stringify(rawLayer)}; skipping
1194
1197
  `);
1195
1198
  }
1196
1199
  }
@@ -1203,23 +1206,12 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
1203
1206
  `);
1204
1207
  }
1205
1208
  }
1206
- if (id !== void 0 && knowledge_layer !== void 0) {
1207
- const decoded = parseKnowledgeId(id);
1208
- if (decoded !== null && decoded.layer !== knowledge_layer) {
1209
- process.stderr.write(
1210
- `[fabric] frontmatter: id ${id} encodes layer ${decoded.layer} but layer field says ${knowledge_layer}; dropping both
1211
- `
1212
- );
1213
- id = void 0;
1214
- knowledge_layer = void 0;
1215
- }
1216
- }
1217
1209
  const tags = extractInlineArray(frontmatter, "tags");
1218
1210
  const rawRelevanceScope = extractScalar(frontmatter, "relevance_scope");
1219
1211
  const relevance_scope = rawRelevanceScope === "narrow" || rawRelevanceScope === "broad" ? rawRelevanceScope : "broad";
1220
1212
  const relevance_paths = extractInlineArray(frontmatter, "relevance_paths");
1221
1213
  const rawRelated = extractInlineArray(frontmatter, "related");
1222
- const sourceLayer = knowledge_layer ?? (id !== void 0 ? parseKnowledgeId(id)?.layer ?? "team" : "team");
1214
+ const sourceLayer = id !== void 0 ? parseKnowledgeId(id)?.layer ?? "team" : "team";
1223
1215
  const related = rawRelated.filter((targetId) => {
1224
1216
  if (isForbiddenCrossLayerEdge(sourceLayer, targetId)) {
1225
1217
  process.stderr.write(
@@ -1234,8 +1226,6 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
1234
1226
  id,
1235
1227
  knowledge_type,
1236
1228
  maturity,
1237
- knowledge_layer,
1238
- layer_reason: rawLayerReason,
1239
1229
  created_at,
1240
1230
  tags: tags.length > 0 ? tags : void 0,
1241
1231
  relevance_scope,
@@ -1286,7 +1276,13 @@ async function readSetFingerprint(refs) {
1286
1276
  return parts.sort().join("\n");
1287
1277
  }
1288
1278
  var SEMANTIC_SCOPE_LINE = /^semantic_scope:\s*"?([^"\n]+?)"?\s*$/mu;
1289
- 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
+ }
1290
1286
  const match = SEMANTIC_SCOPE_LINE.exec(source);
1291
1287
  return match?.[1] ?? layer;
1292
1288
  }
@@ -1365,7 +1361,7 @@ async function walkReadSetStoresUncached(snapshot) {
1365
1361
  type: ref.type,
1366
1362
  alias: ref.alias,
1367
1363
  layer,
1368
- semanticScope: readSemanticScope(source, layer),
1364
+ semanticScope: readSemanticScope(source, layer, ref.project),
1369
1365
  source
1370
1366
  };
1371
1367
  }));
@@ -1383,7 +1379,9 @@ async function buildCrossStoreRawItems(projectRoot) {
1383
1379
  stable_id: entry.qualifiedId,
1384
1380
  description: {
1385
1381
  ...baseDescription,
1386
- 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).
1387
1385
  semantic_scope: entry.semanticScope
1388
1386
  }
1389
1387
  });
@@ -2893,11 +2891,12 @@ var LEDGER_DUAL_WRITE_METRIC_NAMES = {
2893
2891
  };
2894
2892
 
2895
2893
  // src/services/plan-context.ts
2896
- import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
2894
+ import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
2897
2895
  import { join as join10 } from "path";
2898
2896
 
2899
2897
  // src/services/vector-retrieval.ts
2900
2898
  import { mkdirSync } from "fs";
2899
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
2901
2900
  import { createRequire } from "module";
2902
2901
  import { join as join9 } from "path";
2903
2902
  import { resolveGlobalRoot as resolveGlobalRoot3 } from "@fenglimg/fabric-shared";
@@ -3008,11 +3007,63 @@ function cacheDocVector(text, vector) {
3008
3007
  docVectorCache.delete(lru);
3009
3008
  }
3010
3009
  }
3011
- 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) {
3012
3055
  if (embedder === null || queryText.trim().length === 0 || items.length === 0) {
3013
3056
  return null;
3014
3057
  }
3015
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
+ }
3016
3067
  const missTexts = [];
3017
3068
  for (const item of items) {
3018
3069
  if (!docVectorCache.has(item.text)) {
@@ -3028,6 +3079,24 @@ async function buildVectorScores(embedder, queryText, items) {
3028
3079
  for (let m = 0; m < missTexts.length; m += 1) {
3029
3080
  cacheDocVector(missTexts[m], embedded[m + 1]);
3030
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;
3031
3100
  const scores = /* @__PURE__ */ new Map();
3032
3101
  for (const item of items) {
3033
3102
  const docVec = docVectorCache.get(item.text);
@@ -3036,6 +3105,20 @@ async function buildVectorScores(embedder, queryText, items) {
3036
3105
  }
3037
3106
  scores.set(item.stable_id, cosineSimilarity(queryVec, docVec));
3038
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
+ }
3039
3122
  return scores;
3040
3123
  } catch {
3041
3124
  return null;
@@ -3043,6 +3126,11 @@ async function buildVectorScores(embedder, queryText, items) {
3043
3126
  }
3044
3127
 
3045
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
+ }
3046
3134
  var SELECTION_TOKEN_TTL_DEFAULT_MS = 30 * 60 * 1e3;
3047
3135
  var selectionTokenCache = /* @__PURE__ */ new Map();
3048
3136
  var SELECTION_TOKEN_CACHE_MAX = 1e3;
@@ -3099,7 +3187,7 @@ async function planContext(projectRoot, input) {
3099
3187
  const storeRawItems = await buildCrossStoreRawItems(projectRoot).catch(() => []);
3100
3188
  const { rawItems: allRawItems, suppressedStableIds } = partitionEmptyShells(storeRawItems);
3101
3189
  const effectiveLayerFilter = input.layer_filter ?? readDefaultLayerFilter(projectRoot);
3102
- 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);
3103
3191
  const scoringContext = await buildScoringContext(projectRoot, revision, rawItems, {
3104
3192
  queryText,
3105
3193
  targetPaths: input.target_paths ?? dedupePaths(input.paths)
@@ -3320,7 +3408,7 @@ function bm25CachePath(projectRoot, revision) {
3320
3408
  }
3321
3409
  async function loadBm25ModelFromDisk(projectRoot, revision) {
3322
3410
  try {
3323
- const raw = await readFile6(bm25CachePath(projectRoot, revision), "utf8");
3411
+ const raw = await readFile7(bm25CachePath(projectRoot, revision), "utf8");
3324
3412
  const parsed = JSON.parse(raw);
3325
3413
  if (parsed.version !== 1) return null;
3326
3414
  return rehydrateBm25Model(parsed);
@@ -3331,8 +3419,8 @@ async function loadBm25ModelFromDisk(projectRoot, revision) {
3331
3419
  async function saveBm25ModelToDisk(projectRoot, revision, model) {
3332
3420
  try {
3333
3421
  const path = bm25CachePath(projectRoot, revision);
3334
- await mkdir4(join10(projectRoot, BM25_CACHE_DIR), { recursive: true });
3335
- 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");
3336
3424
  } catch {
3337
3425
  }
3338
3426
  }
@@ -3373,7 +3461,7 @@ function buildScopeRankMap(items, projectRoot) {
3373
3461
  const colon = it.stable_id.indexOf(":");
3374
3462
  const alias = colon === -1 ? "" : it.stable_id.slice(0, colon);
3375
3463
  const localId = colon === -1 ? it.stable_id : it.stable_id.slice(colon + 1);
3376
- const semanticScope = it.description.semantic_scope ?? it.description.knowledge_layer ?? "team";
3464
+ const semanticScope = it.description.semantic_scope ?? layerFromStableId(it.stable_id);
3377
3465
  return {
3378
3466
  global_ref: it.stable_id,
3379
3467
  store_uuid: aliasToUuid.get(alias) ?? alias,
@@ -3423,7 +3511,8 @@ async function buildScoringContext(projectRoot, revision, rawItems, opts) {
3423
3511
  rawItems.map((item) => ({
3424
3512
  stable_id: item.stable_id,
3425
3513
  text: docTexts.get(item.stable_id) ?? documentTextForItem(item.description)
3426
- }))
3514
+ })),
3515
+ { projectRoot, corpusRevision: revision, embeddingModel: embedConfig.model }
3427
3516
  );
3428
3517
  if (vectorScores !== null) {
3429
3518
  scoringContext.vectorScores = vectorScores;
@@ -3531,16 +3620,13 @@ function documentTextForItem(description) {
3531
3620
  ...description.intent_clues,
3532
3621
  ...description.tech_stack,
3533
3622
  ...description.impact,
3534
- ...description.entities ?? [],
3535
3623
  ...description.tags ?? []
3536
3624
  ].join(" ");
3537
3625
  }
3538
3626
  function documentFieldsForItem(description) {
3539
3627
  return {
3540
3628
  title: tokenize2(description.summary),
3541
- tags: tokenize2(
3542
- [...description.tags ?? [], ...description.tech_stack, ...description.entities ?? []].join(" ")
3543
- ),
3629
+ tags: tokenize2([...description.tags ?? [], ...description.tech_stack].join(" ")),
3544
3630
  summary: tokenize2([description.must_read_if, ...description.intent_clues].join(" ")),
3545
3631
  body: tokenize2(description.impact.join(" "))
3546
3632
  };
@@ -4116,7 +4202,7 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
4116
4202
  // src/services/review.ts
4117
4203
  import { execFileSync } from "child_process";
4118
4204
  import { existsSync as existsSync5 } from "fs";
4119
- import { readFile as readFile8, 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";
4120
4206
  import { homedir } from "os";
4121
4207
  import { basename, isAbsolute, join as join12, relative, resolve as resolve2, sep as sep2 } from "path";
4122
4208
 
@@ -4154,11 +4240,11 @@ async function hasUnresolvedDismissal(projectRoot, id) {
4154
4240
  }
4155
4241
 
4156
4242
  // src/services/review.ts
4157
- import { allocateStoreKnowledgeId, isPersonalScope as isPersonalScope2 } from "@fenglimg/fabric-shared";
4243
+ import { allocateStoreKnowledgeId, isPersonalScope as isPersonalScope2, loadProjectConfig as loadProjectConfig3 } from "@fenglimg/fabric-shared";
4158
4244
 
4159
4245
  // src/services/pending-dedupe.ts
4160
4246
  import { existsSync as existsSync4 } from "fs";
4161
- import { readdir, readFile as readFile7, unlink } from "fs/promises";
4247
+ import { readdir, readFile as readFile8, unlink } from "fs/promises";
4162
4248
  import { join as join11 } from "path";
4163
4249
  var PENDING_TYPES = ["decisions", "pitfalls", "guidelines", "models", "processes"];
4164
4250
  var DISAMBIGUATION_SUFFIX = /^(.+)-([2-9])\.md$/u;
@@ -4256,7 +4342,7 @@ async function mergePendingTwins(projectRoot) {
4256
4342
  const abs = join11(dir, name);
4257
4343
  let content;
4258
4344
  try {
4259
- content = await readFile7(abs, "utf8");
4345
+ content = await readFile8(abs, "utf8");
4260
4346
  } catch {
4261
4347
  continue;
4262
4348
  }
@@ -4451,7 +4537,7 @@ async function listPending(projectRoot, filters) {
4451
4537
  const absolutePath = join12(dir, name);
4452
4538
  let content;
4453
4539
  try {
4454
- content = await readFile8(absolutePath, "utf8");
4540
+ content = await readFile9(absolutePath, "utf8");
4455
4541
  } catch {
4456
4542
  continue;
4457
4543
  }
@@ -4561,7 +4647,7 @@ async function approveOne(projectRoot, pendingPath) {
4561
4647
  let targetAbs;
4562
4648
  let writtenTarget = false;
4563
4649
  try {
4564
- const content = await readFile8(sourceAbs, "utf8");
4650
+ const content = await readFile9(sourceAbs, "utf8");
4565
4651
  const fm = parseFrontmatter(content);
4566
4652
  const pluralType = fm.type;
4567
4653
  if (pluralType === void 0 || !PLURAL_TYPES.includes(pluralType)) {
@@ -4575,7 +4661,12 @@ async function approveOne(projectRoot, pendingPath) {
4575
4661
  );
4576
4662
  allocatedId = stableId;
4577
4663
  const newFilename = `${stableId}--${slug}.md`;
4578
- 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
+ );
4579
4670
  await ensureParentDirectory(targetAbs);
4580
4671
  const rewritten = rewriteFrontmatterMerge(
4581
4672
  rewriteFrontmatterForPromote(content, stableId),
@@ -4633,7 +4724,7 @@ async function rejectAll(projectRoot, pendingPaths, reason) {
4633
4724
  try {
4634
4725
  const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
4635
4726
  if (existsSync5(sandboxed.abs)) {
4636
- const content = await readFile8(sandboxed.abs, "utf8");
4727
+ const content = await readFile9(sandboxed.abs, "utf8");
4637
4728
  const merged = rewriteFrontmatterMerge(content, { status: "rejected" });
4638
4729
  const rejectedAbs = sandboxed.abs.includes(`${sep2}pending${sep2}`) ? sandboxed.abs.replace(`${sep2}pending${sep2}`, `${sep2}rejected${sep2}`) : null;
4639
4730
  if (rejectedAbs !== null) {
@@ -4660,7 +4751,7 @@ async function modifyEntry(projectRoot, pendingPath, changes) {
4660
4751
  if (target === null) {
4661
4752
  throw new Error(`modify target not found: ${pendingPath}`);
4662
4753
  }
4663
- const content = await readFile8(target.absPath, "utf8");
4754
+ const content = await readFile9(target.absPath, "utf8");
4664
4755
  const fm = parseFrontmatter(content);
4665
4756
  const currentLayer = fm.layer ?? "team";
4666
4757
  if (fm.maturity === "verified" && changes.maturity === "proven" && fm.id !== void 0) {
@@ -4762,8 +4853,9 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
4762
4853
  pluralType,
4763
4854
  resolveWriteTargetStoreDir(toLayer, projectRoot)
4764
4855
  );
4856
+ const flipProject = toLayer === "team" ? loadProjectConfig3(projectRoot)?.active_project : void 0;
4765
4857
  const toAbs = join12(
4766
- resolveStoreCanonicalBase(toLayer, projectRoot),
4858
+ resolveStoreCanonicalBase(toLayer, projectRoot, flipProject),
4767
4859
  pluralType,
4768
4860
  `${newStableId}--${slug}.md`
4769
4861
  );
@@ -4785,20 +4877,22 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
4785
4877
  { ...effectivePatch, last_review_confirmed_at: (/* @__PURE__ */ new Date()).toISOString() },
4786
4878
  { id: newStableId }
4787
4879
  );
4788
- await atomicWriteText(toAbs, rewritten);
4880
+ let moved = false;
4789
4881
  if (target.isInProjectTree) {
4790
4882
  const relSource = relative(projectRoot, target.absPath);
4883
+ const relDest = relative(projectRoot, toAbs);
4791
4884
  try {
4792
- execFileSync("git", ["rm", "--quiet", "-f", relSource], {
4885
+ execFileSync("git", ["mv", "-f", relSource, relDest], {
4793
4886
  cwd: projectRoot,
4794
4887
  stdio: ["ignore", "pipe", "pipe"]
4795
4888
  });
4889
+ moved = true;
4796
4890
  } catch {
4797
- if (existsSync5(target.absPath)) {
4798
- await unlink2(target.absPath);
4799
- }
4891
+ moved = false;
4800
4892
  }
4801
- } else if (existsSync5(target.absPath)) {
4893
+ }
4894
+ await atomicWriteText(toAbs, rewritten);
4895
+ if (!moved && existsSync5(target.absPath) && target.absPath !== toAbs) {
4802
4896
  await unlink2(target.absPath);
4803
4897
  }
4804
4898
  const flipReason = `layer_flip:${priorStableId ?? "<unassigned>"}->${newStableId}`;
@@ -4892,7 +4986,7 @@ async function listIndexedSearchEntries(source, type) {
4892
4986
  }
4893
4987
  let content;
4894
4988
  try {
4895
- content = await readFile8(absolutePath, "utf8");
4989
+ content = await readFile9(absolutePath, "utf8");
4896
4990
  searchEntryIndexContentReads += 1;
4897
4991
  } catch {
4898
4992
  directoryCache.files.delete(absolutePath);
@@ -4942,7 +5036,8 @@ function pendingEntryToRankerItem(indexed) {
4942
5036
  ...fm.id !== void 0 ? { id: fm.id } : {},
4943
5037
  ...fm.type !== void 0 ? { knowledge_type: fm.type } : {},
4944
5038
  maturity: fm.maturity ?? "draft",
4945
- 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.
4946
5041
  ...fm.semantic_scope !== void 0 ? { semantic_scope: fm.semantic_scope } : {},
4947
5042
  ...fm.created_at !== void 0 ? { created_at: fm.created_at } : {},
4948
5043
  tags: fm.tags ?? [],
@@ -5061,7 +5156,7 @@ async function deferAll(projectRoot, pendingPaths, until, reason) {
5061
5156
  try {
5062
5157
  const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
5063
5158
  if (existsSync5(sandboxed.abs)) {
5064
- const content = await readFile8(sandboxed.abs, "utf8");
5159
+ const content = await readFile9(sandboxed.abs, "utf8");
5065
5160
  stableId = parseFrontmatter(content).id;
5066
5161
  const patch = {
5067
5162
  status: "deferred",
@@ -5402,9 +5497,9 @@ function registerPending(server, tracker) {
5402
5497
  }
5403
5498
 
5404
5499
  // src/services/doctor.ts
5405
- import { access as access4, readFile as readFile18, 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";
5406
5501
  import { constants as constants2 } from "fs";
5407
- 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";
5408
5503
  import {
5409
5504
  createTranslator,
5410
5505
  forensicReportSchema,
@@ -5618,7 +5713,7 @@ function createKnowledgeSummaryOpaqueCheck(t, inspection) {
5618
5713
  }
5619
5714
 
5620
5715
  // src/services/doctor-stable-id-collision.ts
5621
- import { readFile as readFile9 } from "fs/promises";
5716
+ import { readFile as readFile10 } from "fs/promises";
5622
5717
  import { basename as basename2, join as join13 } from "path";
5623
5718
  import {
5624
5719
  buildStoreResolveInput as buildStoreResolveInput4,
@@ -5665,7 +5760,7 @@ async function inspectStoreStableIdIntegrity(projectRoot) {
5665
5760
  for (const ref of await readKnowledgeAcrossStores2(resolved.dirs)) {
5666
5761
  let source;
5667
5762
  try {
5668
- source = await readFile9(ref.file, "utf8");
5763
+ source = await readFile10(ref.file, "utf8");
5669
5764
  } catch {
5670
5765
  continue;
5671
5766
  }
@@ -5986,7 +6081,7 @@ function createNarrowNoPathsCheck(t, inspection) {
5986
6081
  }
5987
6082
 
5988
6083
  // src/services/doctor-broad-index.ts
5989
- import { readFile as readFile10 } from "fs/promises";
6084
+ import { readFile as readFile11 } from "fs/promises";
5990
6085
  import { join as join15 } from "path";
5991
6086
  var DEFAULT_BROAD_INDEX_BACKSTOP = 50;
5992
6087
  var BROAD_INDEX_BACKSTOP_MIN = 20;
@@ -5995,7 +6090,7 @@ var BROAD_INDEX_DRIFT_RATIO = 0.8;
5995
6090
  async function readBroadIndexBackstop(projectRoot) {
5996
6091
  const configPath = join15(projectRoot, ".fabric", "fabric-config.json");
5997
6092
  try {
5998
- const raw = await readFile10(configPath, "utf8");
6093
+ const raw = await readFile11(configPath, "utf8");
5999
6094
  const parsed = JSON.parse(raw);
6000
6095
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
6001
6096
  const v = parsed.broad_index_backstop;
@@ -6312,7 +6407,7 @@ function createBroadReviewRecheckCheck(t, inspection) {
6312
6407
  }
6313
6408
 
6314
6409
  // src/services/doctor-scope-lint.ts
6315
- import { readFile as readFile11 } from "fs/promises";
6410
+ import { readFile as readFile12 } from "fs/promises";
6316
6411
  import { join as join16 } from "path";
6317
6412
  import {
6318
6413
  buildStoreResolveInput as buildStoreResolveInput5,
@@ -6380,7 +6475,7 @@ async function lintStoreScopes(projectRoot) {
6380
6475
  }
6381
6476
  let source;
6382
6477
  try {
6383
- source = await readFile11(ref.file, "utf8");
6478
+ source = await readFile12(ref.file, "utf8");
6384
6479
  } catch {
6385
6480
  continue;
6386
6481
  }
@@ -6463,9 +6558,9 @@ function createScopeLintCheck(t, violations) {
6463
6558
  }
6464
6559
 
6465
6560
  // src/services/doctor-unbound-project.ts
6466
- import { loadProjectConfig as loadProjectConfig3 } from "@fenglimg/fabric-shared";
6561
+ import { loadProjectConfig as loadProjectConfig4 } from "@fenglimg/fabric-shared";
6467
6562
  function detectUnboundProject(projectRoot) {
6468
- const config = loadProjectConfig3(projectRoot);
6563
+ const config = loadProjectConfig4(projectRoot);
6469
6564
  const alias = config?.active_write_store;
6470
6565
  if (typeof alias !== "string" || alias.length === 0) {
6471
6566
  return null;
@@ -6513,7 +6608,7 @@ import {
6513
6608
  readStoreCounters,
6514
6609
  reconcileStoreCounters,
6515
6610
  resolveGlobalRoot as resolveGlobalRoot6,
6516
- STORE_KNOWLEDGE_TYPE_DIRS,
6611
+ STORE_KNOWLEDGE_TYPE_DIRS as STORE_KNOWLEDGE_TYPE_DIRS2,
6517
6612
  STORE_LAYOUT as STORE_LAYOUT2,
6518
6613
  storeRelativePathForMount as storeRelativePathForMount5
6519
6614
  } from "@fenglimg/fabric-shared";
@@ -6557,7 +6652,7 @@ function readEntryId(file) {
6557
6652
  }
6558
6653
  function computeStoreDiskMax(storeDir) {
6559
6654
  const max = defaultAgentsMetaCounters();
6560
- for (const type of STORE_KNOWLEDGE_TYPE_DIRS) {
6655
+ for (const type of STORE_KNOWLEDGE_TYPE_DIRS2) {
6561
6656
  const dir = join17(storeDir, STORE_LAYOUT2.knowledgeDir, type);
6562
6657
  if (!existsSync7(dir)) {
6563
6658
  continue;
@@ -6752,6 +6847,195 @@ function createStoreOrphanCheck(t, orphans) {
6752
6847
  };
6753
6848
  }
6754
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
+
6755
7039
  // src/services/legacy-serve-lock-probe.ts
6756
7040
  import {
6757
7041
  isAlive,
@@ -6824,8 +7108,8 @@ async function inspectEventsJsonlGates(projectRoot, options = {}) {
6824
7108
  }
6825
7109
 
6826
7110
  // src/services/doctor-skill-lints.ts
6827
- import { readdir as readdir3, readFile as readFile12 } from "fs/promises";
6828
- 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";
6829
7113
  var FABRIC_SKILL_SLUGS = ["fabric-archive", "fabric-review"];
6830
7114
  var SKILL_MD_FRONTMATTER_ROOTS = [".claude/skills", ".codex/skills"];
6831
7115
  var SKILL_FRONTMATTER_KEY_PATTERN = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]+(.+?)[ \t]*$/u;
@@ -6847,7 +7131,7 @@ function issueCheck(name, status, kind, code, message, actionHint, audience) {
6847
7131
  }
6848
7132
  async function listMarkdownFiles(dir) {
6849
7133
  try {
6850
- return (await readdir3(dir)).filter((name) => name.endsWith(".md"));
7134
+ return (await readdir4(dir)).filter((name) => name.endsWith(".md"));
6851
7135
  } catch {
6852
7136
  return null;
6853
7137
  }
@@ -6855,8 +7139,8 @@ async function listMarkdownFiles(dir) {
6855
7139
  async function inspectSkillRefMirror(projectRoot) {
6856
7140
  const driftedPaths = [];
6857
7141
  for (const slug of FABRIC_SKILL_SLUGS) {
6858
- const claudeRef = join19(projectRoot, ".claude", "skills", slug, "ref");
6859
- 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");
6860
7144
  const [claudeFiles, codexFiles] = await Promise.all([listMarkdownFiles(claudeRef), listMarkdownFiles(codexRef)]);
6861
7145
  if (claudeFiles === null || codexFiles === null) continue;
6862
7146
  const claudeSet = new Set(claudeFiles);
@@ -6873,8 +7157,8 @@ async function inspectSkillRefMirror(projectRoot) {
6873
7157
  let codexBody;
6874
7158
  try {
6875
7159
  [claudeBody, codexBody] = await Promise.all([
6876
- readFile12(join19(claudeRef, fname), "utf8"),
6877
- readFile12(join19(codexRef, fname), "utf8")
7160
+ readFile13(join20(claudeRef, fname), "utf8"),
7161
+ readFile13(join20(codexRef, fname), "utf8")
6878
7162
  ]);
6879
7163
  } catch {
6880
7164
  continue;
@@ -6893,10 +7177,10 @@ async function inspectSkillTokenBudget(projectRoot) {
6893
7177
  const overSize = [];
6894
7178
  let highestSeverity = "ok";
6895
7179
  for (const slug of FABRIC_SKILL_SLUGS) {
6896
- const skillMdPath = join19(projectRoot, ".claude", "skills", slug, "SKILL.md");
7180
+ const skillMdPath = join20(projectRoot, ".claude", "skills", slug, "SKILL.md");
6897
7181
  let body;
6898
7182
  try {
6899
- body = await readFile12(skillMdPath, "utf8");
7183
+ body = await readFile13(skillMdPath, "utf8");
6900
7184
  } catch {
6901
7185
  continue;
6902
7186
  }
@@ -6917,10 +7201,10 @@ async function inspectSkillDescription(projectRoot) {
6917
7201
  const CJK_PATTERN = /[\u3400-\u4dbf\u4e00-\u9fff]/u;
6918
7202
  const ASCII_PATTERN = /[a-zA-Z]{2,}/u;
6919
7203
  for (const slug of FABRIC_SKILL_SLUGS) {
6920
- const skillMdPath = join19(projectRoot, ".claude", "skills", slug, "SKILL.md");
7204
+ const skillMdPath = join20(projectRoot, ".claude", "skills", slug, "SKILL.md");
6921
7205
  let body;
6922
7206
  try {
6923
- body = await readFile12(skillMdPath, "utf8");
7207
+ body = await readFile13(skillMdPath, "utf8");
6924
7208
  } catch {
6925
7209
  continue;
6926
7210
  }
@@ -6951,19 +7235,19 @@ async function inspectSkillDescription(projectRoot) {
6951
7235
  async function inspectSkillMdYamlInvalid(projectRoot) {
6952
7236
  const candidates = [];
6953
7237
  for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
6954
- const rootAbs = join19(projectRoot, rootRel);
7238
+ const rootAbs = join20(projectRoot, rootRel);
6955
7239
  let dirEntries;
6956
7240
  try {
6957
- dirEntries = await readdir3(rootAbs, { withFileTypes: true });
7241
+ dirEntries = await readdir4(rootAbs, { withFileTypes: true });
6958
7242
  } catch {
6959
7243
  continue;
6960
7244
  }
6961
7245
  for (const dirEntry of dirEntries) {
6962
7246
  if (!dirEntry.isDirectory()) continue;
6963
- const skillFile = join19(rootAbs, dirEntry.name, "SKILL.md");
7247
+ const skillFile = join20(rootAbs, dirEntry.name, "SKILL.md");
6964
7248
  let raw;
6965
7249
  try {
6966
- raw = await readFile12(skillFile, "utf8");
7250
+ raw = await readFile13(skillFile, "utf8");
6967
7251
  } catch {
6968
7252
  continue;
6969
7253
  }
@@ -7086,8 +7370,8 @@ function createSkillMdYamlInvalidCheck(t, inspection) {
7086
7370
  }
7087
7371
 
7088
7372
  // src/services/doctor-retired-references-lint.ts
7089
- import { readdir as readdir4, readFile as readFile13, stat as stat3 } from "fs/promises";
7090
- import { join as join20, posix as posix3, relative as relative2 } from "path";
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";
7091
7375
  var RETIRED_TOKENS = [
7092
7376
  { token: "fab_plan_context", replacement: "fab_recall", reason: "retrieval collapsed to one lean fab_recall (KT-DEC-0026)" },
7093
7377
  { token: "fab_get_knowledge_sections", replacement: "fab_recall", reason: "two-step fetch retired (KT-DEC-0026)" },
@@ -7110,7 +7394,7 @@ var RETIRED_TOKENS = [
7110
7394
  ];
7111
7395
  var HOOK_DIRS = [".claude/hooks", ".codex/hooks"];
7112
7396
  var SKILL_DIRS = [".claude/skills", ".codex/skills"];
7113
- var BOOTSTRAP_FILES = ["AGENTS.md", "CLAUDE.md", join20(".fabric", "AGENTS.md")];
7397
+ var BOOTSTRAP_FILES = ["AGENTS.md", "CLAUDE.md", join21(".fabric", "AGENTS.md")];
7114
7398
  function isCommentLine(line) {
7115
7399
  const t = line.trim();
7116
7400
  return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*");
@@ -7118,13 +7402,13 @@ function isCommentLine(line) {
7118
7402
  async function walkFiles(dir, exts) {
7119
7403
  let entries;
7120
7404
  try {
7121
- entries = await readdir4(dir, { withFileTypes: true });
7405
+ entries = await readdir5(dir, { withFileTypes: true });
7122
7406
  } catch {
7123
7407
  return [];
7124
7408
  }
7125
7409
  const out = [];
7126
7410
  for (const e of entries) {
7127
- const full = join20(dir, e.name);
7411
+ const full = join21(dir, e.name);
7128
7412
  if (e.isDirectory()) {
7129
7413
  out.push(...await walkFiles(full, exts));
7130
7414
  } else if (exts.some((ext) => e.name.endsWith(ext))) {
@@ -7150,29 +7434,29 @@ async function inspectRetiredReferences(projectRoot) {
7150
7434
  let scannedFiles = 0;
7151
7435
  const toRel = (p) => posix3.normalize(relative2(projectRoot, p).split("\\").join("/"));
7152
7436
  for (const rel of BOOTSTRAP_FILES) {
7153
- const abs = join20(projectRoot, rel);
7437
+ const abs = join21(projectRoot, rel);
7154
7438
  try {
7155
7439
  if ((await stat3(abs)).isFile()) {
7156
7440
  scannedFiles += 1;
7157
- scanText(toRel(abs), await readFile13(abs, "utf8"), false, hits);
7441
+ scanText(toRel(abs), await readFile14(abs, "utf8"), false, hits);
7158
7442
  }
7159
7443
  } catch {
7160
7444
  }
7161
7445
  }
7162
7446
  for (const dir of SKILL_DIRS) {
7163
- for (const file of await walkFiles(join20(projectRoot, dir), [".md"])) {
7447
+ for (const file of await walkFiles(join21(projectRoot, dir), [".md"])) {
7164
7448
  scannedFiles += 1;
7165
7449
  try {
7166
- scanText(toRel(file), await readFile13(file, "utf8"), false, hits);
7450
+ scanText(toRel(file), await readFile14(file, "utf8"), false, hits);
7167
7451
  } catch {
7168
7452
  }
7169
7453
  }
7170
7454
  }
7171
7455
  for (const dir of HOOK_DIRS) {
7172
- for (const file of await walkFiles(join20(projectRoot, dir), [".cjs"])) {
7456
+ for (const file of await walkFiles(join21(projectRoot, dir), [".cjs"])) {
7173
7457
  scannedFiles += 1;
7174
7458
  try {
7175
- scanText(toRel(file), await readFile13(file, "utf8"), true, hits);
7459
+ scanText(toRel(file), await readFile14(file, "utf8"), true, hits);
7176
7460
  } catch {
7177
7461
  }
7178
7462
  }
@@ -7207,8 +7491,8 @@ function createRetiredReferenceCheck(t, inspection) {
7207
7491
 
7208
7492
  // src/services/doctor-hooks-lints.ts
7209
7493
  import { constants } from "fs";
7210
- import { access, readdir as readdir5, readFile as readFile14, stat as stat4 } from "fs/promises";
7211
- import { join as join21, posix as posix4 } from "path";
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";
7212
7496
  import { Script } from "vm";
7213
7497
  var HOOKS_RUNTIME_CLIENT_DIRS = [
7214
7498
  { client: "claude", dir: ".claude/hooks" },
@@ -7257,7 +7541,7 @@ function isHookWiredForEvent(hooks, event, hookFile) {
7257
7541
  }
7258
7542
  async function readDirectoryFileNames(dir) {
7259
7543
  try {
7260
- return await readdir5(dir);
7544
+ return await readdir6(dir);
7261
7545
  } catch {
7262
7546
  return null;
7263
7547
  }
@@ -7270,14 +7554,14 @@ async function isFile(absPath) {
7270
7554
  }
7271
7555
  }
7272
7556
  async function inspectHooksWired(projectRoot) {
7273
- const claudeEntries = await readDirectoryFileNames(join21(projectRoot, ".claude"));
7557
+ const claudeEntries = await readDirectoryFileNames(join22(projectRoot, ".claude"));
7274
7558
  if (claudeEntries === null) {
7275
7559
  return { status: "skipped", missingHooks: [] };
7276
7560
  }
7277
- const settingsPath = join21(projectRoot, ".claude", "settings.json");
7561
+ const settingsPath = join22(projectRoot, ".claude", "settings.json");
7278
7562
  let parsed;
7279
7563
  try {
7280
- parsed = JSON.parse(await readFile14(settingsPath, "utf8"));
7564
+ parsed = JSON.parse(await readFile15(settingsPath, "utf8"));
7281
7565
  } catch {
7282
7566
  return { status: "missing-settings", missingHooks: [] };
7283
7567
  }
@@ -7300,8 +7584,8 @@ async function inspectHooksWired(projectRoot) {
7300
7584
  }
7301
7585
  async function inspectHookCacheWritability(projectRoot) {
7302
7586
  const relPath = posix4.join(".fabric", ".cache");
7303
- const fabricDir = join21(projectRoot, ".fabric");
7304
- const cacheDir = join21(projectRoot, ".fabric", ".cache");
7587
+ const fabricDir = join22(projectRoot, ".fabric");
7588
+ const cacheDir = join22(projectRoot, ".fabric", ".cache");
7305
7589
  try {
7306
7590
  try {
7307
7591
  const cacheStats = await stat4(cacheDir);
@@ -7350,12 +7634,12 @@ async function inspectHookCacheWritability(projectRoot) {
7350
7634
  async function inspectHooksContentDrift(projectRoot) {
7351
7635
  const hookFilesByBasename = /* @__PURE__ */ new Map();
7352
7636
  for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
7353
- const absDir = join21(projectRoot, dir);
7637
+ const absDir = join22(projectRoot, dir);
7354
7638
  const entries = await readDirectoryFileNames(absDir);
7355
7639
  if (entries === null) continue;
7356
7640
  for (const name of entries) {
7357
7641
  if (!name.endsWith(".cjs")) continue;
7358
- const abs = join21(absDir, name);
7642
+ const abs = join22(absDir, name);
7359
7643
  if (!await isFile(abs)) continue;
7360
7644
  const arr = hookFilesByBasename.get(name) ?? [];
7361
7645
  arr.push({ client, abs });
@@ -7370,7 +7654,7 @@ async function inspectHooksContentDrift(projectRoot) {
7370
7654
  const hashes = [];
7371
7655
  for (const { client, abs } of copies) {
7372
7656
  try {
7373
- const body = await readFile14(abs, "utf8");
7657
+ const body = await readFile15(abs, "utf8");
7374
7658
  hashes.push({ client, sha: sha256(body) });
7375
7659
  } catch {
7376
7660
  }
@@ -7392,18 +7676,18 @@ async function inspectHooksRuntime(projectRoot) {
7392
7676
  const issues = [];
7393
7677
  let scanned = 0;
7394
7678
  for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
7395
- const absDir = join21(projectRoot, dir);
7679
+ const absDir = join22(projectRoot, dir);
7396
7680
  const entries = await readDirectoryFileNames(absDir);
7397
7681
  if (entries === null) continue;
7398
7682
  for (const name of entries) {
7399
7683
  if (!name.endsWith(".cjs")) continue;
7400
- const abs = join21(absDir, name);
7684
+ const abs = join22(absDir, name);
7401
7685
  const displayPath = `${dir}/${name}`;
7402
7686
  if (!await isFile(abs)) continue;
7403
7687
  scanned += 1;
7404
7688
  let body;
7405
7689
  try {
7406
- body = await readFile14(abs, "utf8");
7690
+ body = await readFile15(abs, "utf8");
7407
7691
  } catch (err) {
7408
7692
  issues.push({
7409
7693
  path: displayPath,
@@ -7540,8 +7824,8 @@ function createHookCacheWritabilityCheck(t, inspection) {
7540
7824
  }
7541
7825
 
7542
7826
  // src/services/doctor-bootstrap-lints.ts
7543
- import { access as access2, readFile as readFile15 } from "fs/promises";
7544
- import { join as join22 } from "path";
7827
+ import { access as access2, readFile as readFile16 } from "fs/promises";
7828
+ import { join as join23 } from "path";
7545
7829
  import {
7546
7830
  BOOTSTRAP_MARKER_BEGIN,
7547
7831
  BOOTSTRAP_MARKER_END,
@@ -7573,17 +7857,17 @@ async function fileExists(path) {
7573
7857
  }
7574
7858
  async function inspectBootstrapAnchor(projectRoot) {
7575
7859
  const [hasAgentsMd, hasClaudeMd] = await Promise.all([
7576
- fileExists(join22(projectRoot, "AGENTS.md")),
7577
- fileExists(join22(projectRoot, "CLAUDE.md"))
7860
+ fileExists(join23(projectRoot, "AGENTS.md")),
7861
+ fileExists(join23(projectRoot, "CLAUDE.md"))
7578
7862
  ]);
7579
7863
  return { hasAgentsMd, hasClaudeMd };
7580
7864
  }
7581
7865
  async function inspectL1BootstrapSnapshotDrift(target) {
7582
- const abs = join22(target, ".fabric", "AGENTS.md");
7866
+ const abs = join23(target, ".fabric", "AGENTS.md");
7583
7867
  const canonical = resolveBootstrapCanonical();
7584
7868
  let onDisk;
7585
7869
  try {
7586
- onDisk = await readFile15(abs, "utf8");
7870
+ onDisk = await readFile16(abs, "utf8");
7587
7871
  } catch {
7588
7872
  return { status: "missing", canonical, onDisk: null };
7589
7873
  }
@@ -7609,17 +7893,17 @@ function createL1BootstrapSnapshotDriftCheck(t, inspection) {
7609
7893
  );
7610
7894
  }
7611
7895
  async function inspectL2ManagedBlockDrift(target) {
7612
- const snapshotPath = join22(target, ".fabric", "AGENTS.md");
7896
+ const snapshotPath = join23(target, ".fabric", "AGENTS.md");
7613
7897
  let snapshot;
7614
7898
  try {
7615
- snapshot = await readFile15(snapshotPath, "utf8");
7899
+ snapshot = await readFile16(snapshotPath, "utf8");
7616
7900
  } catch {
7617
7901
  return { status: "ok", drifted: [] };
7618
7902
  }
7619
- const projectRulesPath = join22(target, ".fabric", "project-rules.md");
7903
+ const projectRulesPath = join23(target, ".fabric", "project-rules.md");
7620
7904
  let expectedBody = snapshot;
7621
7905
  try {
7622
- const projectRules = await readFile15(projectRulesPath, "utf8");
7906
+ const projectRules = await readFile16(projectRulesPath, "utf8");
7623
7907
  expectedBody = `${snapshot}
7624
7908
  ---
7625
7909
  ${projectRules}`;
@@ -7628,12 +7912,12 @@ ${projectRules}`;
7628
7912
  const drifted = [];
7629
7913
  let anyManagedBlockFound = false;
7630
7914
  const blockTargets = [
7631
- join22(target, "AGENTS.md")
7915
+ join23(target, "AGENTS.md")
7632
7916
  ];
7633
7917
  for (const abs of blockTargets) {
7634
7918
  let content;
7635
7919
  try {
7636
- content = await readFile15(abs, "utf8");
7920
+ content = await readFile16(abs, "utf8");
7637
7921
  } catch {
7638
7922
  continue;
7639
7923
  }
@@ -7656,9 +7940,9 @@ ${projectRules}`;
7656
7940
  drifted.push({ path: abs, expected: expectedBody, actual: body });
7657
7941
  }
7658
7942
  }
7659
- const claudeMdPath = join22(target, "CLAUDE.md");
7943
+ const claudeMdPath = join23(target, "CLAUDE.md");
7660
7944
  try {
7661
- const claudeContent = await readFile15(claudeMdPath, "utf8");
7945
+ const claudeContent = await readFile16(claudeMdPath, "utf8");
7662
7946
  anyManagedBlockFound = true;
7663
7947
  const lines = claudeContent.split(/\r?\n/u);
7664
7948
  const hasAtImport = lines.some((line) => line.trim() === "@.fabric/AGENTS.md");
@@ -7726,7 +8010,7 @@ import { appendFile as appendFile3 } from "fs/promises";
7726
8010
  import { minimatch as minimatch2 } from "minimatch";
7727
8011
 
7728
8012
  // src/services/cite-rollup.ts
7729
- import { readFile as readFile16 } from "fs/promises";
8013
+ import { readFile as readFile17 } from "fs/promises";
7730
8014
  import { createLedgerWriteQueue as createLedgerWriteQueue3 } from "@fenglimg/fabric-shared/node/atomic-write";
7731
8015
  var citeRollupQueue = createLedgerWriteQueue3();
7732
8016
  async function appendCiteRollupRow(projectRoot, row) {
@@ -7738,7 +8022,7 @@ async function readCiteRollup(projectRoot) {
7738
8022
  const path = getCiteRollupPath(projectRoot);
7739
8023
  let raw;
7740
8024
  try {
7741
- raw = await readFile16(path, "utf8");
8025
+ raw = await readFile17(path, "utf8");
7742
8026
  } catch (error) {
7743
8027
  if (isNodeError(error) && error.code === "ENOENT") return [];
7744
8028
  throw error;
@@ -8856,6 +9140,7 @@ async function runDoctorReport(target) {
8856
9140
  const driftUnconsumed = await inspectDriftUnconsumed(projectRoot);
8857
9141
  const storeCounterDrift = inspectStoreCounters(projectRoot);
8858
9142
  const storeOrphans = inspectStoreOrphans();
9143
+ const projectRegistryDrift = await inspectProjectRegistryDrift(projectRoot);
8859
9144
  const stableIdIntegrity = await inspectStoreStableIdIntegrity(projectRoot);
8860
9145
  const relevancePaths = await inspectStoreRelevancePaths(projectRoot);
8861
9146
  const broadIndexDrift = await inspectBroadIndexDrift(projectRoot);
@@ -8885,7 +9170,7 @@ async function runDoctorReport(target) {
8885
9170
  const globalCliVersion = process.env.VITEST === "true" ? { status: "ok", version: "test-skipped" } : inspectGlobalCliVersion();
8886
9171
  const targetFiles = Object.fromEntries(
8887
9172
  await Promise.all(
8888
- 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))])
8889
9174
  )
8890
9175
  );
8891
9176
  const checks = [
@@ -8939,6 +9224,12 @@ async function runDoctorReport(target) {
8939
9224
  // (warning). `--fix` adopts it (rescue-before-delete — re-register, never
8940
9225
  // an on-disk delete).
8941
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),
8942
9233
  // rc.5 TASK-010: read-side underseeded-corpus check (#22). Info kind —
8943
9234
  // does not bump report status. Recommends running the fabric-import skill
8944
9235
  // to backfill knowledge when the corpus is below the threshold floor.
@@ -9089,7 +9380,7 @@ async function runDoctorFix(target) {
9089
9380
  const fixed = [];
9090
9381
  const ledgerWarnings = [];
9091
9382
  if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
9092
- const snapshotPath = join23(projectRoot, ".fabric", "AGENTS.md");
9383
+ const snapshotPath = join24(projectRoot, ".fabric", "AGENTS.md");
9093
9384
  await ensureParentDirectory(snapshotPath);
9094
9385
  await atomicWriteText4(snapshotPath, resolveBootstrapCanonical2());
9095
9386
  fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
@@ -9113,6 +9404,18 @@ async function runDoctorFix(target) {
9113
9404
  fixed.push(findIssue(before.warnings, "store_orphan"));
9114
9405
  }
9115
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
+ }
9116
9419
  if (before.fixable_errors.some((issue) => issue.code === "event_ledger_partial_write")) {
9117
9420
  const ledgerPath = getEventLedgerPath(projectRoot);
9118
9421
  const truncResult = await truncateLedgerToLastNewline(ledgerPath);
@@ -9162,7 +9465,7 @@ async function runDoctorFix(target) {
9162
9465
  if (before.infos.some((issue) => issue.code === "stale_serve_lock")) {
9163
9466
  const lockInspection = inspectStaleServeLock(projectRoot, Date.now());
9164
9467
  if (lockInspection.present && !lockInspection.pidAlive) {
9165
- const lockFilePath = join23(projectRoot, ".fabric", ".serve.lock");
9468
+ const lockFilePath = join24(projectRoot, ".fabric", ".serve.lock");
9166
9469
  try {
9167
9470
  await unlink4(lockFilePath);
9168
9471
  } catch (err) {
@@ -9279,7 +9582,7 @@ function createApplyLintMessage(succeeded, failed, manualErrorCount) {
9279
9582
  }
9280
9583
  async function applySessionHintsStaleCleanup(projectRoot, candidate) {
9281
9584
  const detail = `deleted (${candidate.age_days}d old)`;
9282
- const absPath = join23(projectRoot, candidate.path);
9585
+ const absPath = join24(projectRoot, candidate.path);
9283
9586
  try {
9284
9587
  const { unlink: unlink5 } = await import("fs/promises");
9285
9588
  await unlink5(absPath);
@@ -9304,9 +9607,9 @@ function truncateErrorMessage(error) {
9304
9607
  return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
9305
9608
  }
9306
9609
  async function inspectForensic(projectRoot) {
9307
- const path = join23(projectRoot, ".fabric", "forensic.json");
9610
+ const path = join24(projectRoot, ".fabric", "forensic.json");
9308
9611
  try {
9309
- const parsed = forensicReportSchema.parse(JSON.parse(await readFile18(path, "utf8")));
9612
+ const parsed = forensicReportSchema.parse(JSON.parse(await readFile19(path, "utf8")));
9310
9613
  return { present: true, valid: true, report: parsed };
9311
9614
  } catch (error) {
9312
9615
  if (isMissingFileError(error)) {
@@ -9336,7 +9639,7 @@ async function inspectEventLedger(projectRoot) {
9336
9639
  try {
9337
9640
  await access4(path, constants2.W_OK);
9338
9641
  const { warnings } = await readEventLedger(projectRoot);
9339
- const raw = await readFile18(path, "utf8");
9642
+ const raw = await readFile19(path, "utf8");
9340
9643
  const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
9341
9644
  const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
9342
9645
  const schemaVersionSamples = [];
@@ -9881,7 +10184,7 @@ async function inspectPreexistingRootFiles(projectRoot) {
9881
10184
  const candidates = ["CLAUDE.md", "AGENTS.md"];
9882
10185
  const detected = [];
9883
10186
  for (const name of candidates) {
9884
- if (await pathExists(join23(projectRoot, name))) {
10187
+ if (await pathExists(join24(projectRoot, name))) {
9885
10188
  detected.push(name);
9886
10189
  }
9887
10190
  }
@@ -9966,7 +10269,7 @@ async function buildLastActiveIndex(projectRoot) {
9966
10269
  return map;
9967
10270
  }
9968
10271
  async function inspectSessionHintsStale(projectRoot, now) {
9969
- const cacheDir = join23(projectRoot, ".fabric", ".cache");
10272
+ const cacheDir = join24(projectRoot, ".fabric", ".cache");
9970
10273
  let entries;
9971
10274
  try {
9972
10275
  entries = await readdirAsync(cacheDir, { withFileTypes: true });
@@ -9978,7 +10281,7 @@ async function inspectSessionHintsStale(projectRoot, now) {
9978
10281
  if (!entry.isFile()) continue;
9979
10282
  if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
9980
10283
  if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
9981
- const absPath = join23(cacheDir, entry.name);
10284
+ const absPath = join24(cacheDir, entry.name);
9982
10285
  let mtimeMs = 0;
9983
10286
  try {
9984
10287
  mtimeMs = (await statAsync(absPath)).mtimeMs;
@@ -10010,9 +10313,9 @@ function inspectStaleServeLock(projectRoot, now) {
10010
10313
  };
10011
10314
  }
10012
10315
  async function readUnderseedThresholdFromConfig(projectRoot) {
10013
- const configPath = join23(projectRoot, ".fabric", "fabric-config.json");
10316
+ const configPath = join24(projectRoot, ".fabric", "fabric-config.json");
10014
10317
  try {
10015
- const raw = await readFile18(configPath, "utf8");
10318
+ const raw = await readFile19(configPath, "utf8");
10016
10319
  const parsed = JSON.parse(raw);
10017
10320
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
10018
10321
  const v = parsed.underseed_node_threshold;
@@ -10128,10 +10431,10 @@ async function inspectOnboardCoverage(projectRoot) {
10128
10431
  return { filled, missing, opted_out: optedOut };
10129
10432
  }
10130
10433
  async function readOnboardOptedOut(projectRoot) {
10131
- const path = join23(projectRoot, ".fabric", "fabric-config.json");
10434
+ const path = join24(projectRoot, ".fabric", "fabric-config.json");
10132
10435
  let raw;
10133
10436
  try {
10134
- raw = await readFile18(path, "utf8");
10437
+ raw = await readFile19(path, "utf8");
10135
10438
  } catch {
10136
10439
  return [];
10137
10440
  }
@@ -10233,22 +10536,22 @@ async function* iterateCanonicalFilenames(projectRoot) {
10233
10536
  }
10234
10537
  }
10235
10538
  async function rewriteThreeEndManagedBlocks(projectRoot) {
10236
- const snapshotPath = join23(projectRoot, ".fabric", "AGENTS.md");
10539
+ const snapshotPath = join24(projectRoot, ".fabric", "AGENTS.md");
10237
10540
  if (!await pathExists(snapshotPath)) {
10238
10541
  return;
10239
10542
  }
10240
10543
  let snapshot;
10241
10544
  try {
10242
- snapshot = await readFile18(snapshotPath, "utf8");
10545
+ snapshot = await readFile19(snapshotPath, "utf8");
10243
10546
  } catch {
10244
10547
  return;
10245
10548
  }
10246
- const projectRulesPath = join23(projectRoot, ".fabric", "project-rules.md");
10549
+ const projectRulesPath = join24(projectRoot, ".fabric", "project-rules.md");
10247
10550
  const hasProjectRules = await pathExists(projectRulesPath);
10248
10551
  let expectedBody = snapshot;
10249
10552
  if (hasProjectRules) {
10250
10553
  try {
10251
- const projectRules = await readFile18(projectRulesPath, "utf8");
10554
+ const projectRules = await readFile19(projectRulesPath, "utf8");
10252
10555
  expectedBody = `${snapshot}
10253
10556
  ---
10254
10557
  ${projectRules}`;
@@ -10259,7 +10562,7 @@ ${projectRules}`;
10259
10562
  ${expectedBody}
10260
10563
  ${BOOTSTRAP_MARKER_END2}`;
10261
10564
  const blockTargets = [
10262
- join23(projectRoot, "AGENTS.md")
10565
+ join24(projectRoot, "AGENTS.md")
10263
10566
  ];
10264
10567
  for (const abs of blockTargets) {
10265
10568
  if (!await pathExists(abs)) {
@@ -10267,7 +10570,7 @@ ${BOOTSTRAP_MARKER_END2}`;
10267
10570
  }
10268
10571
  let existing;
10269
10572
  try {
10270
- existing = await readFile18(abs, "utf8");
10573
+ existing = await readFile19(abs, "utf8");
10271
10574
  } catch {
10272
10575
  continue;
10273
10576
  }
@@ -10292,11 +10595,11 @@ ${managedBlock}
10292
10595
  }
10293
10596
  await atomicWriteText4(abs, next);
10294
10597
  }
10295
- const claudeMdPath = join23(projectRoot, "CLAUDE.md");
10598
+ const claudeMdPath = join24(projectRoot, "CLAUDE.md");
10296
10599
  if (await pathExists(claudeMdPath)) {
10297
10600
  let claudeContent;
10298
10601
  try {
10299
- claudeContent = await readFile18(claudeMdPath, "utf8");
10602
+ claudeContent = await readFile19(claudeMdPath, "utf8");
10300
10603
  } catch {
10301
10604
  return;
10302
10605
  }
@@ -10323,7 +10626,7 @@ ${managedBlock}
10323
10626
  async function ensureEventLedger(projectRoot) {
10324
10627
  const path = getEventLedgerPath(projectRoot);
10325
10628
  await ensureParentDirectory(path);
10326
- await writeFile4(path, "", { encoding: "utf8", flag: "a" });
10629
+ await writeFile5(path, "", { encoding: "utf8", flag: "a" });
10327
10630
  }
10328
10631
  function createFixMessage(fixed, report) {
10329
10632
  const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
@@ -10362,7 +10665,7 @@ async function collectEntryPoints(root) {
10362
10665
  continue;
10363
10666
  }
10364
10667
  for (const entry of await readdirAsync(current, { withFileTypes: true })) {
10365
- const absolutePath = join23(current, entry.name);
10668
+ const absolutePath = join24(current, entry.name);
10366
10669
  const relativePath = normalizePath2(absolutePath.slice(root.length + 1));
10367
10670
  if (relativePath.length === 0) {
10368
10671
  continue;
@@ -10438,7 +10741,7 @@ async function enrichDescriptions(projectRoot, opts = {}) {
10438
10741
  scanned += 1;
10439
10742
  let source;
10440
10743
  try {
10441
- source = await readFile18(absPath, "utf8");
10744
+ source = await readFile19(absPath, "utf8");
10442
10745
  } catch {
10443
10746
  continue;
10444
10747
  }
@@ -10576,16 +10879,16 @@ async function runDoctorConflictLint(projectRoot, opts = {}) {
10576
10879
  }
10577
10880
 
10578
10881
  // src/services/why-not-surfaced.ts
10579
- import { readFile as readFile19 } from "fs/promises";
10580
- 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";
10581
10884
  import {
10582
- buildStoreResolveInput as buildStoreResolveInput7,
10583
- createStoreResolver as createStoreResolver7,
10584
- loadProjectConfig as loadProjectConfig4,
10885
+ buildStoreResolveInput as buildStoreResolveInput8,
10886
+ createStoreResolver as createStoreResolver8,
10887
+ loadProjectConfig as loadProjectConfig5,
10585
10888
  readKnowledgeAcrossStores as readKnowledgeAcrossStores4,
10586
- resolveGlobalRoot as resolveGlobalRoot8,
10889
+ resolveGlobalRoot as resolveGlobalRoot9,
10587
10890
  scopeRoot as scopeRoot3,
10588
- storeRelativePathForMount as storeRelativePathForMount6
10891
+ storeRelativePathForMount as storeRelativePathForMount7
10589
10892
  } from "@fenglimg/fabric-shared";
10590
10893
  var SEMANTIC_SCOPE_LINE3 = /^semantic_scope:\s*"?([^"\n]+?)"?\s*$/mu;
10591
10894
  var RELEVANCE_SCOPE_LINE = /^relevance_scope:\s*"?(broad|narrow)"?\s*$/mu;
@@ -10609,15 +10912,15 @@ async function explainWhyNotSurfaced(projectRoot, query) {
10609
10912
  activeProject: null,
10610
10913
  relevanceScope: null
10611
10914
  };
10612
- const input = buildStoreResolveInput7(projectRoot);
10915
+ const input = buildStoreResolveInput8(projectRoot);
10613
10916
  if (input === null) {
10614
10917
  return base;
10615
10918
  }
10616
- const globalRoot = resolveGlobalRoot8();
10919
+ const globalRoot = resolveGlobalRoot9();
10617
10920
  const allStores = input.mountedStores.map((s) => ({
10618
10921
  store_uuid: s.store_uuid,
10619
10922
  alias: s.alias,
10620
- dir: join24(globalRoot, storeRelativePathForMount6(s))
10923
+ dir: join25(globalRoot, storeRelativePathForMount7(s))
10621
10924
  }));
10622
10925
  const refs = await readKnowledgeAcrossStores4(allStores);
10623
10926
  const candidate = refs.find((ref) => fileMatchesId(ref.file, localId));
@@ -10626,7 +10929,7 @@ async function explainWhyNotSurfaced(projectRoot, query) {
10626
10929
  }
10627
10930
  let source;
10628
10931
  try {
10629
- source = await readFile19(candidate.file, "utf8");
10932
+ source = await readFile20(candidate.file, "utf8");
10630
10933
  } catch {
10631
10934
  return base;
10632
10935
  }
@@ -10635,9 +10938,9 @@ async function explainWhyNotSurfaced(projectRoot, query) {
10635
10938
  }
10636
10939
  const semanticScope = SEMANTIC_SCOPE_LINE3.exec(source)?.[1] ?? null;
10637
10940
  const relevanceScope = RELEVANCE_SCOPE_LINE.exec(source)?.[1] ?? "broad";
10638
- const activeProject = loadProjectConfig4(projectRoot)?.active_project ?? null;
10941
+ const activeProject = loadProjectConfig5(projectRoot)?.active_project ?? null;
10639
10942
  const boundUuids = new Set(
10640
- createStoreResolver7().resolveReadSet(input).stores.map((s) => s.store_uuid)
10943
+ createStoreResolver8().resolveReadSet(input).stores.map((s) => s.store_uuid)
10641
10944
  );
10642
10945
  const storeBound = boundUuids.has(candidate.store_uuid);
10643
10946
  const found = {
@@ -10732,12 +11035,12 @@ async function inspectRelatedGraph(projectRoot) {
10732
11035
 
10733
11036
  // src/services/store-precheck.ts
10734
11037
  import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
10735
- import { join as join25 } from "path";
11038
+ import { join as join26 } from "path";
10736
11039
  import {
10737
- buildStoreResolveInput as buildStoreResolveInput8,
10738
- createStoreResolver as createStoreResolver8,
10739
- resolveGlobalRoot as resolveGlobalRoot9,
10740
- storeRelativePathForMount as storeRelativePathForMount7
11040
+ buildStoreResolveInput as buildStoreResolveInput9,
11041
+ createStoreResolver as createStoreResolver9,
11042
+ resolveGlobalRoot as resolveGlobalRoot10,
11043
+ storeRelativePathForMount as storeRelativePathForMount8
10741
11044
  } from "@fenglimg/fabric-shared";
10742
11045
  var CACHE_TTL_MS = 6e4;
10743
11046
  var cache = /* @__PURE__ */ new Map();
@@ -10753,8 +11056,8 @@ function evaluateStoreDir(storeDir, identity) {
10753
11056
  reason: `directory not found at ${storeDir}`
10754
11057
  };
10755
11058
  }
10756
- const storeJsonPath = join25(storeDir, "store.json");
10757
- const gitDirPath = join25(storeDir, ".git");
11059
+ const storeJsonPath = join26(storeDir, "store.json");
11060
+ const gitDirPath = join26(storeDir, ".git");
10758
11061
  const hasStoreJson = existsSync9(storeJsonPath) && isValidStoreJson(storeJsonPath);
10759
11062
  const hasGitDir = existsSync9(gitDirPath);
10760
11063
  if (!hasStoreJson && !hasGitDir) {
@@ -10775,23 +11078,23 @@ function isValidStoreJson(path) {
10775
11078
  return false;
10776
11079
  }
10777
11080
  }
10778
- async function precheckStoreReachability(projectRoot, globalRoot = resolveGlobalRoot9(), now = Date.now()) {
11081
+ async function precheckStoreReachability(projectRoot, globalRoot = resolveGlobalRoot10(), now = Date.now()) {
10779
11082
  const cached = cache.get(projectRoot);
10780
11083
  if (cached !== void 0 && cached.expiresAt > now) {
10781
11084
  return cached.result;
10782
11085
  }
10783
- const input = buildStoreResolveInput8(projectRoot, globalRoot);
11086
+ const input = buildStoreResolveInput9(projectRoot, globalRoot);
10784
11087
  if (input === null) {
10785
11088
  const result2 = { stores: [], allReachable: true };
10786
11089
  cache.set(projectRoot, { result: result2, expiresAt: now + CACHE_TTL_MS });
10787
11090
  return result2;
10788
11091
  }
10789
- const readSet = createStoreResolver8().resolveReadSet(input);
11092
+ const readSet = createStoreResolver9().resolveReadSet(input);
10790
11093
  const stores = readSet.stores.map((entry) => {
10791
11094
  const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
10792
- const storeDir = join25(
11095
+ const storeDir = join26(
10793
11096
  globalRoot,
10794
- storeRelativePathForMount7(mounted ?? { store_uuid: entry.store_uuid })
11097
+ storeRelativePathForMount8(mounted ?? { store_uuid: entry.store_uuid })
10795
11098
  );
10796
11099
  return evaluateStoreDir(storeDir, { uuid: entry.store_uuid, alias: entry.alias });
10797
11100
  });
@@ -10899,7 +11202,7 @@ import { IOFabricError as IOFabricError2, RuleError } from "@fenglimg/fabric-sha
10899
11202
 
10900
11203
  // src/services/read-ledger.ts
10901
11204
  import { randomUUID as randomUUID7 } from "crypto";
10902
- import { access as access5, copyFile, readFile as readFile20, rm } from "fs/promises";
11205
+ import { access as access5, copyFile, readFile as readFile21, rm } from "fs/promises";
10903
11206
  import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
10904
11207
  async function resolveLedgerPaths(projectRoot) {
10905
11208
  const primaryPath = getLedgerPath(projectRoot);
@@ -10927,7 +11230,7 @@ async function readLegacyLedger(projectRoot) {
10927
11230
  const { readPath } = await resolveLedgerPaths(projectRoot);
10928
11231
  let raw;
10929
11232
  try {
10930
- raw = await readFile20(readPath, "utf8");
11233
+ raw = await readFile21(readPath, "utf8");
10931
11234
  } catch (error) {
10932
11235
  if (isNodeError(error) && error.code === "ENOENT") {
10933
11236
  return [];
@@ -11182,8 +11485,8 @@ function formatError(error) {
11182
11485
  }
11183
11486
  function formatPreexistingRootMessage(projectRoot) {
11184
11487
  const preexisting = [];
11185
- if (existsSync10(join26(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
11186
- if (existsSync10(join26(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");
11187
11490
  if (preexisting.length === 0) return null;
11188
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.`;
11189
11492
  }
@@ -11210,7 +11513,7 @@ function createFabricServer(tracker) {
11210
11513
  const server = new McpServer(
11211
11514
  {
11212
11515
  name: "fabric-knowledge-server",
11213
- version: "2.3.0-rc.4"
11516
+ version: "2.3.0-rc.5"
11214
11517
  },
11215
11518
  {
11216
11519
  instructions: FABRIC_SERVER_INSTRUCTIONS
@@ -11230,10 +11533,10 @@ function createFabricServer(tracker) {
11230
11533
  },
11231
11534
  async (_uri) => {
11232
11535
  const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
11233
- const path = join26(projectRoot, ".fabric", "bootstrap", "README.md");
11536
+ const path = join27(projectRoot, ".fabric", "bootstrap", "README.md");
11234
11537
  let text = "";
11235
11538
  if (existsSync10(path)) {
11236
- text = await readFile21(path, "utf8");
11539
+ text = await readFile22(path, "utf8");
11237
11540
  }
11238
11541
  return {
11239
11542
  contents: [