@fenglimg/fabric-server 2.2.0-rc.8 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/index.ts
2
- import { existsSync as existsSync8 } from "fs";
3
- import { readFile as readFile16 } from "fs/promises";
4
- import { join as join20, resolve as resolve4 } from "path";
2
+ import { existsSync as existsSync9 } from "fs";
3
+ import { readFile as readFile17 } from "fs/promises";
4
+ import { join as join21, 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";
@@ -700,13 +700,9 @@ function appendPayloadWarning(warnings, guardResult, actionHint) {
700
700
  // src/config-loader.ts
701
701
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
702
702
  import { join as join4 } from "path";
703
- import { selectionTokenTtlMsSchema, planContextTopKSchema, resolveRetrievalBudget } from "@fenglimg/fabric-shared";
704
- var RETRIEVAL_BUDGET_PROFILES = ["conservative", "balanced", "generous"];
705
- function readRetrievalBudgetProfile(config) {
706
- const raw = config.retrieval_budget_profile;
707
- return typeof raw === "string" && RETRIEVAL_BUDGET_PROFILES.includes(raw) ? raw : void 0;
708
- }
703
+ import { selectionTokenTtlMsSchema, planContextTopKSchema } from "@fenglimg/fabric-shared";
709
704
  var PLAN_CONTEXT_TOP_K_DEFAULT = 24;
705
+ var RECALL_RELEVANCE_RATIO_DEFAULT = 0.25;
710
706
  function readFabricConfig(projectRoot) {
711
707
  const configPath = join4(projectRoot, ".fabric", "fabric-config.json");
712
708
  if (!existsSync2(configPath)) {
@@ -719,18 +715,7 @@ function readFabricConfig(projectRoot) {
719
715
  return parsed;
720
716
  }
721
717
  function readPayloadLimits(projectRoot) {
722
- const config = readFabricConfig(projectRoot);
723
- const explicit = config.mcpPayloadLimits;
724
- const profile = readRetrievalBudgetProfile(config);
725
- if (profile === void 0 && explicit === void 0) {
726
- return void 0;
727
- }
728
- const resolved = resolveRetrievalBudget({
729
- profile,
730
- payloadWarnBytes: explicit?.warnBytes,
731
- payloadHardBytes: explicit?.hardBytes
732
- });
733
- return { warnBytes: resolved.payloadWarnBytes, hardBytes: resolved.payloadHardBytes };
718
+ return readFabricConfig(projectRoot).mcpPayloadLimits;
734
719
  }
735
720
  function readSelectionTokenTtlMs(projectRoot) {
736
721
  try {
@@ -776,17 +761,27 @@ function readDefaultLayerFilter(projectRoot) {
776
761
  }
777
762
  function readPlanContextTopK(projectRoot) {
778
763
  try {
779
- const config = readFabricConfig(projectRoot);
780
- const raw = config.plan_context_top_k;
764
+ const raw = readFabricConfig(projectRoot).plan_context_top_k;
781
765
  if (raw !== void 0) {
782
766
  const parsed = planContextTopKSchema.safeParse(raw);
783
767
  if (parsed.success) return parsed.data;
784
768
  }
785
- return resolveRetrievalBudget({ profile: readRetrievalBudgetProfile(config) }).topK;
769
+ return PLAN_CONTEXT_TOP_K_DEFAULT;
786
770
  } catch {
787
771
  return PLAN_CONTEXT_TOP_K_DEFAULT;
788
772
  }
789
773
  }
774
+ function readRecallRelevanceRatio(projectRoot) {
775
+ try {
776
+ const raw = readFabricConfig(projectRoot).recall_relevance_ratio;
777
+ if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0 && raw <= 1) {
778
+ return raw;
779
+ }
780
+ return RECALL_RELEVANCE_RATIO_DEFAULT;
781
+ } catch {
782
+ return RECALL_RELEVANCE_RATIO_DEFAULT;
783
+ }
784
+ }
790
785
  function readOrphanDemoteThresholdDays(projectRoot) {
791
786
  try {
792
787
  const cfg = readFabricConfig(projectRoot);
@@ -2033,6 +2028,8 @@ async function buildKnowledgeCensus(projectRoot) {
2033
2028
  const census = {
2034
2029
  by_type: {},
2035
2030
  by_layer: { team: 0, personal: 0, project: 0 },
2031
+ broad_by_type: {},
2032
+ narrow_total: 0,
2036
2033
  dropped_other_project: 0,
2037
2034
  total: 0
2038
2035
  };
@@ -2042,10 +2039,16 @@ async function buildKnowledgeCensus(projectRoot) {
2042
2039
  const kept = filterByActiveProject(all, activeProject);
2043
2040
  census.dropped_other_project = all.length - kept.length;
2044
2041
  for (const entry of kept) {
2045
- const type = extractRuleDescription(entry.source)?.knowledge_type;
2042
+ const desc = extractRuleDescription(entry.source);
2043
+ const type = desc?.knowledge_type;
2044
+ const isNarrow = desc?.relevance_scope === "narrow";
2046
2045
  if (typeof type === "string") {
2047
2046
  census.by_type[type] = (census.by_type[type] ?? 0) + 1;
2047
+ if (!isNarrow) {
2048
+ census.broad_by_type[type] = (census.broad_by_type[type] ?? 0) + 1;
2049
+ }
2048
2050
  }
2051
+ if (isNarrow) census.narrow_total += 1;
2049
2052
  if (scopeRoot(entry.semanticScope) === "project") {
2050
2053
  census.by_layer.project += 1;
2051
2054
  } else {
@@ -2069,6 +2072,7 @@ async function buildAlwaysActiveBodies(projectRoot) {
2069
2072
  if (desc === void 0) continue;
2070
2073
  const type = desc.knowledge_type;
2071
2074
  if (typeof type !== "string" || !ALWAYS_ACTIVE_TYPES.has(type)) continue;
2075
+ if (desc.relevance_scope === "narrow") continue;
2072
2076
  out.push({
2073
2077
  stable_id: entry.qualifiedId,
2074
2078
  type,
@@ -2643,11 +2647,23 @@ async function planContext(projectRoot, input) {
2643
2647
  scoringContext.vectorWeight = embedConfig.weight;
2644
2648
  }
2645
2649
  }
2646
- const builtItems = sortDescriptionItems(rawItems, scoringContext);
2647
- const rankedCandidates = dedupeDescriptionIndex(builtItems);
2650
+ const scoredSorted = sortDescriptionItems(rawItems, scoringContext);
2651
+ const seenStableIds = /* @__PURE__ */ new Set();
2652
+ const rankedScored = scoredSorted.filter(({ item }) => {
2653
+ if (seenStableIds.has(item.stable_id)) return false;
2654
+ seenStableIds.add(item.stable_id);
2655
+ return true;
2656
+ });
2657
+ const rankedCandidates = rankedScored.map((entry) => entry.item);
2648
2658
  const topK = readPlanContextTopK(projectRoot);
2649
- const omittedCandidateCount = Math.max(0, rankedCandidates.length - topK);
2650
- const topKCandidates = omittedCandidateCount > 0 ? rankedCandidates.slice(0, topK) : rankedCandidates;
2659
+ const cappedScored = rankedScored.slice(0, topK);
2660
+ const relevanceRatio = readRecallRelevanceRatio(projectRoot);
2661
+ const hasQuery = scoringContext.queryTerms.length > 0;
2662
+ const maxScore = rankedScored.length > 0 ? rankedScored[0].score : 0;
2663
+ const relevanceFloor = maxScore * relevanceRatio;
2664
+ const survivingScored = hasQuery && maxScore > 0 && relevanceRatio > 0 ? cappedScored.filter((entry) => entry.score >= relevanceFloor) : cappedScored;
2665
+ const topKCandidates = survivingScored.map((entry) => entry.item);
2666
+ const omittedCandidateCount = Math.max(0, rankedCandidates.length - topKCandidates.length);
2651
2667
  let candidates = topKCandidates;
2652
2668
  const relatedAppended = {};
2653
2669
  if (input.include_related === true) {
@@ -2889,7 +2905,7 @@ function compareScopeThenId(left, right, scopeRank) {
2889
2905
  }
2890
2906
  function sortDescriptionItems(rawItems, scoringContext) {
2891
2907
  if (scoringContext === void 0) {
2892
- return [...rawItems].sort((left, right) => compareStableIds(left.stable_id, right.stable_id));
2908
+ return [...rawItems].sort((left, right) => compareStableIds(left.stable_id, right.stable_id)).map((item) => ({ item, score: 0 }));
2893
2909
  }
2894
2910
  const scored = rawItems.map((item) => ({
2895
2911
  item,
@@ -2899,7 +2915,7 @@ function sortDescriptionItems(rawItems, scoringContext) {
2899
2915
  (left, right) => left.score !== right.score ? right.score - left.score : compareScopeThenId(left.item, right.item, scoringContext.scopeRank)
2900
2916
  // W2/A4 scope tie-break
2901
2917
  );
2902
- return scored.map((entry) => entry.item);
2918
+ return scored;
2903
2919
  }
2904
2920
  function documentTextForItem(description) {
2905
2921
  return [
@@ -2930,16 +2946,6 @@ function buildPreflightDiagnostics(suppressedStableIds) {
2930
2946
  function dedupeStableIds(stableIds) {
2931
2947
  return Array.from(new Set(stableIds));
2932
2948
  }
2933
- function dedupeDescriptionIndex(items) {
2934
- const seenStableIds = /* @__PURE__ */ new Set();
2935
- return items.filter((item) => {
2936
- if (seenStableIds.has(item.stable_id)) {
2937
- return false;
2938
- }
2939
- seenStableIds.add(item.stable_id);
2940
- return true;
2941
- });
2942
- }
2943
2949
  var RECENCY_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
2944
2950
  var RECENCY_BOOST = 25;
2945
2951
  var LOCALITY_SAME_FILE = 100;
@@ -3091,7 +3097,7 @@ function buildNextSteps(planResult, paths, candidateById, candidateLookup) {
3091
3097
  const omitted = planResult.omitted_candidate_count ?? 0;
3092
3098
  if (omitted > 0) {
3093
3099
  nextSteps.push(
3094
- `${omitted} lower-ranked candidate(s) were omitted by the retrieval budget \u2014 pass a narrower intent (or raise plan_context_top_k / the retrieval_budget_profile) to surface them.`
3100
+ `${omitted} lower-ranked candidate(s) were omitted by the retrieval budget \u2014 pass a narrower intent (or raise plan_context_top_k) to surface them.`
3095
3101
  );
3096
3102
  }
3097
3103
  const surfacedPaths = new Set(paths.map((p) => p.stable_id));
@@ -3409,11 +3415,155 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
3409
3415
 
3410
3416
  // src/services/review.ts
3411
3417
  import { execFileSync } from "child_process";
3412
- import { existsSync as existsSync4 } from "fs";
3413
- import { readFile as readFile5, readdir, stat as stat2, unlink } from "fs/promises";
3418
+ import { existsSync as existsSync5 } from "fs";
3419
+ import { readFile as readFile6, readdir as readdir2, stat as stat2, unlink as unlink2 } from "fs/promises";
3414
3420
  import { homedir } from "os";
3415
- import { basename, isAbsolute, join as join9, relative, resolve as resolve2, sep as sep2 } from "path";
3421
+ import { basename, isAbsolute, join as join10, relative, resolve as resolve2, sep as sep2 } from "path";
3416
3422
  import { allocateStoreKnowledgeId, isPersonalScope as isPersonalScope2 } from "@fenglimg/fabric-shared";
3423
+
3424
+ // src/services/pending-dedupe.ts
3425
+ import { existsSync as existsSync4 } from "fs";
3426
+ import { readdir, readFile as readFile5, unlink } from "fs/promises";
3427
+ import { join as join9 } from "path";
3428
+ var PENDING_TYPES = ["decisions", "pitfalls", "guidelines", "models", "processes"];
3429
+ var DISAMBIGUATION_SUFFIX = /^(.+)-([2-9])\.md$/u;
3430
+ var SOURCE_SESSIONS_LINE = /^source_sessions:\s*\[(.*)\]\s*$/mu;
3431
+ var CREATED_AT_LINE = /^created_at:\s*(.+)$/mu;
3432
+ function parseSourceSessions(content) {
3433
+ const m = SOURCE_SESSIONS_LINE.exec(content);
3434
+ if (!m) return [];
3435
+ try {
3436
+ const arr = JSON.parse(`[${m[1]}]`);
3437
+ return Array.isArray(arr) ? arr.filter((s) => typeof s === "string") : [];
3438
+ } catch {
3439
+ return [];
3440
+ }
3441
+ }
3442
+ function parseCreatedAt(content) {
3443
+ const m = CREATED_AT_LINE.exec(content);
3444
+ return m ? m[1].trim() : "";
3445
+ }
3446
+ function resolveBaseSlug(name, present) {
3447
+ const m = DISAMBIGUATION_SUFFIX.exec(name);
3448
+ if (m && present.has(`${m[1]}.md`)) return m[1];
3449
+ return name.replace(/\.md$/u, "");
3450
+ }
3451
+ function unionSessions(survivor, twins) {
3452
+ const seen = /* @__PURE__ */ new Set();
3453
+ const out = [];
3454
+ for (const s of [survivor, ...twins]) {
3455
+ for (const sid of s.sourceSessions) {
3456
+ if (sid.length > 0 && !seen.has(sid)) {
3457
+ seen.add(sid);
3458
+ out.push(sid);
3459
+ }
3460
+ }
3461
+ }
3462
+ return out;
3463
+ }
3464
+ function buildMergedContent(survivor, twins) {
3465
+ const union = unionSessions(survivor, twins);
3466
+ let merged = survivor.content.replace(
3467
+ SOURCE_SESSIONS_LINE,
3468
+ `source_sessions: [${union.map((s) => JSON.stringify(s)).join(", ")}]`
3469
+ );
3470
+ if (!merged.endsWith("\n")) merged += "\n";
3471
+ for (const twin of twins) {
3472
+ const body = extractBody(twin.content).trim();
3473
+ if (body.length === 0) continue;
3474
+ merged += `
3475
+ ## Evidence (merged from session ${twin.primarySession || "unknown"})
3476
+
3477
+ ${body}
3478
+ `;
3479
+ }
3480
+ return merged;
3481
+ }
3482
+ function chooseSurvivor(baseSlug, group) {
3483
+ const exact = group.find((p) => p.name === `${baseSlug}.md`);
3484
+ if (exact) return exact;
3485
+ return [...group].sort((a, b) => {
3486
+ if (a.createdAt !== b.createdAt) return a.createdAt < b.createdAt ? -1 : 1;
3487
+ return a.name < b.name ? -1 : 1;
3488
+ })[0];
3489
+ }
3490
+ async function mergePendingTwins(projectRoot) {
3491
+ const merged = [];
3492
+ for (const layer of ["team", "personal"]) {
3493
+ let pendingBase2;
3494
+ try {
3495
+ pendingBase2 = resolveStorePendingBase(layer, projectRoot);
3496
+ } catch {
3497
+ continue;
3498
+ }
3499
+ for (const type of PENDING_TYPES) {
3500
+ const dir = join9(pendingBase2, type);
3501
+ if (!existsSync4(dir)) continue;
3502
+ let names;
3503
+ try {
3504
+ names = (await readdir(dir)).filter((n) => n.endsWith(".md"));
3505
+ } catch {
3506
+ continue;
3507
+ }
3508
+ if (names.length < 2) continue;
3509
+ const present = new Set(names);
3510
+ const groups = /* @__PURE__ */ new Map();
3511
+ for (const name of names) {
3512
+ const base = resolveBaseSlug(name, present);
3513
+ const arr = groups.get(base);
3514
+ if (arr) arr.push(name);
3515
+ else groups.set(base, [name]);
3516
+ }
3517
+ for (const [baseSlug, groupNames] of groups) {
3518
+ if (groupNames.length < 2) continue;
3519
+ const parsed = [];
3520
+ for (const name of groupNames) {
3521
+ const abs = join9(dir, name);
3522
+ let content;
3523
+ try {
3524
+ content = await readFile5(abs, "utf8");
3525
+ } catch {
3526
+ continue;
3527
+ }
3528
+ const sourceSessions = parseSourceSessions(content);
3529
+ parsed.push({
3530
+ name,
3531
+ abs,
3532
+ content,
3533
+ sourceSessions,
3534
+ primarySession: sourceSessions[0] ?? "",
3535
+ createdAt: parseCreatedAt(content)
3536
+ });
3537
+ }
3538
+ if (parsed.length < 2) continue;
3539
+ const distinctPrimaries = new Set(parsed.map((p) => p.primarySession).filter((s) => s.length > 0));
3540
+ if (distinctPrimaries.size < 2) continue;
3541
+ const survivor = chooseSurvivor(baseSlug, parsed);
3542
+ const twins = parsed.filter((p) => p.abs !== survivor.abs);
3543
+ const mergedContent = buildMergedContent(survivor, twins);
3544
+ try {
3545
+ await atomicWriteText(survivor.abs, mergedContent);
3546
+ for (const twin of twins) {
3547
+ await unlink(twin.abs);
3548
+ }
3549
+ } catch {
3550
+ continue;
3551
+ }
3552
+ merged.push({
3553
+ layer,
3554
+ type,
3555
+ base_slug: baseSlug,
3556
+ survivor: survivor.abs,
3557
+ removed: twins.map((t) => t.abs),
3558
+ source_sessions: unionSessions(survivor, twins)
3559
+ });
3560
+ }
3561
+ }
3562
+ }
3563
+ return { merged };
3564
+ }
3565
+
3566
+ // src/services/review.ts
3417
3567
  var PLURAL_TYPES = [
3418
3568
  "decisions",
3419
3569
  "pitfalls",
@@ -3520,6 +3670,10 @@ function isVisibleByLifecycle(fm, filters) {
3520
3670
  return true;
3521
3671
  }
3522
3672
  async function listPending(projectRoot, filters) {
3673
+ try {
3674
+ await mergePendingTwins(projectRoot);
3675
+ } catch {
3676
+ }
3523
3677
  const items = [];
3524
3678
  const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
3525
3679
  const sources = [];
@@ -3539,22 +3693,22 @@ async function listPending(projectRoot, filters) {
3539
3693
  }
3540
3694
  for (const source of sources) {
3541
3695
  for (const type of typesToScan) {
3542
- const dir = join9(source.root, type);
3543
- if (!existsSync4(dir)) {
3696
+ const dir = join10(source.root, type);
3697
+ if (!existsSync5(dir)) {
3544
3698
  continue;
3545
3699
  }
3546
3700
  let entries;
3547
3701
  try {
3548
- entries = await readdir(dir);
3702
+ entries = await readdir2(dir);
3549
3703
  } catch {
3550
3704
  continue;
3551
3705
  }
3552
3706
  for (const name of entries) {
3553
3707
  if (!name.endsWith(".md")) continue;
3554
- const absolutePath = join9(dir, name);
3708
+ const absolutePath = join10(dir, name);
3555
3709
  let content;
3556
3710
  try {
3557
- content = await readFile5(absolutePath, "utf8");
3711
+ content = await readFile6(absolutePath, "utf8");
3558
3712
  } catch {
3559
3713
  continue;
3560
3714
  }
@@ -3664,7 +3818,7 @@ async function approveOne(projectRoot, pendingPath) {
3664
3818
  let targetAbs;
3665
3819
  let writtenTarget = false;
3666
3820
  try {
3667
- const content = await readFile5(sourceAbs, "utf8");
3821
+ const content = await readFile6(sourceAbs, "utf8");
3668
3822
  const fm = parseFrontmatter(content);
3669
3823
  const pluralType = fm.type;
3670
3824
  if (pluralType === void 0 || !PLURAL_TYPES.includes(pluralType)) {
@@ -3678,14 +3832,14 @@ async function approveOne(projectRoot, pendingPath) {
3678
3832
  );
3679
3833
  allocatedId = stableId;
3680
3834
  const newFilename = `${stableId}--${slug}.md`;
3681
- targetAbs = join9(resolveStoreCanonicalBase(layer, projectRoot), pluralType, newFilename);
3835
+ targetAbs = join10(resolveStoreCanonicalBase(layer, projectRoot), pluralType, newFilename);
3682
3836
  await ensureParentDirectory(targetAbs);
3683
3837
  const rewritten = rewriteFrontmatterForPromote(content, stableId);
3684
3838
  await atomicWriteText(targetAbs, rewritten);
3685
3839
  writtenTarget = true;
3686
3840
  if (sourceIsStore) {
3687
- if (existsSync4(sourceAbs)) {
3688
- await unlink(sourceAbs);
3841
+ if (existsSync5(sourceAbs)) {
3842
+ await unlink2(sourceAbs);
3689
3843
  }
3690
3844
  } else if (sourceOrigin === "team") {
3691
3845
  try {
@@ -3694,13 +3848,13 @@ async function approveOne(projectRoot, pendingPath) {
3694
3848
  stdio: ["ignore", "pipe", "pipe"]
3695
3849
  });
3696
3850
  } catch {
3697
- if (existsSync4(sourceAbs)) {
3698
- await unlink(sourceAbs);
3851
+ if (existsSync5(sourceAbs)) {
3852
+ await unlink2(sourceAbs);
3699
3853
  }
3700
3854
  }
3701
3855
  } else {
3702
- if (existsSync4(sourceAbs)) {
3703
- await unlink(sourceAbs);
3856
+ if (existsSync5(sourceAbs)) {
3857
+ await unlink2(sourceAbs);
3704
3858
  }
3705
3859
  }
3706
3860
  await emitEventBestEffort2(projectRoot, {
@@ -3711,9 +3865,9 @@ async function approveOne(projectRoot, pendingPath) {
3711
3865
  });
3712
3866
  return { pending_path: pendingPath, stable_id: stableId };
3713
3867
  } catch (err) {
3714
- if (writtenTarget && targetAbs !== void 0 && existsSync4(targetAbs)) {
3868
+ if (writtenTarget && targetAbs !== void 0 && existsSync5(targetAbs)) {
3715
3869
  try {
3716
- await unlink(targetAbs);
3870
+ await unlink2(targetAbs);
3717
3871
  } catch {
3718
3872
  }
3719
3873
  }
@@ -3732,14 +3886,14 @@ async function rejectAll(projectRoot, pendingPaths, reason) {
3732
3886
  for (const pendingPath of pendingPaths) {
3733
3887
  try {
3734
3888
  const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
3735
- if (existsSync4(sandboxed.abs)) {
3736
- const content = await readFile5(sandboxed.abs, "utf8");
3889
+ if (existsSync5(sandboxed.abs)) {
3890
+ const content = await readFile6(sandboxed.abs, "utf8");
3737
3891
  const merged = rewriteFrontmatterMerge(content, { status: "rejected" });
3738
3892
  const rejectedAbs = sandboxed.abs.includes(`${sep2}pending${sep2}`) ? sandboxed.abs.replace(`${sep2}pending${sep2}`, `${sep2}rejected${sep2}`) : null;
3739
3893
  if (rejectedAbs !== null) {
3740
3894
  await ensureParentDirectory(rejectedAbs);
3741
3895
  await atomicWriteText(rejectedAbs, merged);
3742
- await unlink(sandboxed.abs);
3896
+ await unlink2(sandboxed.abs);
3743
3897
  } else if (merged !== content) {
3744
3898
  await atomicWriteText(sandboxed.abs, merged);
3745
3899
  }
@@ -3760,7 +3914,7 @@ async function modifyEntry(projectRoot, pendingPath, changes) {
3760
3914
  if (target === null) {
3761
3915
  throw new Error(`modify target not found: ${pendingPath}`);
3762
3916
  }
3763
- const content = await readFile5(target.absPath, "utf8");
3917
+ const content = await readFile6(target.absPath, "utf8");
3764
3918
  const fm = parseFrontmatter(content);
3765
3919
  const currentLayer = fm.layer ?? "team";
3766
3920
  if (changes.semantic_scope !== void 0 && isPersonalScope2(changes.semantic_scope)) {
@@ -3805,7 +3959,7 @@ function resolveModifyTarget(projectRoot, pendingPath) {
3805
3959
  } catch {
3806
3960
  return null;
3807
3961
  }
3808
- if (existsSync4(sandboxed.abs)) {
3962
+ if (existsSync5(sandboxed.abs)) {
3809
3963
  return {
3810
3964
  absPath: sandboxed.abs,
3811
3965
  isInProjectTree: sandboxed.isInProjectTree,
@@ -3852,7 +4006,7 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
3852
4006
  pluralType,
3853
4007
  resolveWriteTargetStoreDir(toLayer, projectRoot)
3854
4008
  );
3855
- const toAbs = join9(
4009
+ const toAbs = join10(
3856
4010
  resolveStoreCanonicalBase(toLayer, projectRoot),
3857
4011
  pluralType,
3858
4012
  `${newStableId}--${slug}.md`
@@ -3880,12 +4034,12 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
3880
4034
  stdio: ["ignore", "pipe", "pipe"]
3881
4035
  });
3882
4036
  } catch {
3883
- if (existsSync4(target.absPath)) {
3884
- await unlink(target.absPath);
4037
+ if (existsSync5(target.absPath)) {
4038
+ await unlink2(target.absPath);
3885
4039
  }
3886
4040
  }
3887
- } else if (existsSync4(target.absPath)) {
3888
- await unlink(target.absPath);
4041
+ } else if (existsSync5(target.absPath)) {
4042
+ await unlink2(target.absPath);
3889
4043
  }
3890
4044
  const flipReason = `layer_flip:${priorStableId ?? "<unassigned>"}->${newStableId}`;
3891
4045
  const flipTimestamp = (/* @__PURE__ */ new Date()).toISOString();
@@ -3948,10 +4102,10 @@ function getSearchDirectoryCache(cacheKey) {
3948
4102
  return created;
3949
4103
  }
3950
4104
  async function listIndexedSearchEntries(source, type) {
3951
- const dir = join9(source.root, type);
4105
+ const dir = join10(source.root, type);
3952
4106
  let entries;
3953
4107
  try {
3954
- entries = await readdir(dir);
4108
+ entries = await readdir2(dir);
3955
4109
  } catch {
3956
4110
  return [];
3957
4111
  }
@@ -3961,7 +4115,7 @@ async function listIndexedSearchEntries(source, type) {
3961
4115
  const indexed = [];
3962
4116
  for (const name of entries) {
3963
4117
  if (!name.endsWith(".md")) continue;
3964
- const absolutePath = join9(dir, name);
4118
+ const absolutePath = join10(dir, name);
3965
4119
  let fingerprint;
3966
4120
  try {
3967
4121
  const st = await stat2(absolutePath);
@@ -3978,7 +4132,7 @@ async function listIndexedSearchEntries(source, type) {
3978
4132
  }
3979
4133
  let content;
3980
4134
  try {
3981
- content = await readFile5(absolutePath, "utf8");
4135
+ content = await readFile6(absolutePath, "utf8");
3982
4136
  searchEntryIndexContentReads += 1;
3983
4137
  } catch {
3984
4138
  directoryCache.files.delete(absolutePath);
@@ -4098,8 +4252,8 @@ async function deferAll(projectRoot, pendingPaths, until, reason) {
4098
4252
  let stableId;
4099
4253
  try {
4100
4254
  const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
4101
- if (existsSync4(sandboxed.abs)) {
4102
- const content = await readFile5(sandboxed.abs, "utf8");
4255
+ if (existsSync5(sandboxed.abs)) {
4256
+ const content = await readFile6(sandboxed.abs, "utf8");
4103
4257
  stableId = parseFrontmatter(content).id;
4104
4258
  const patch = {
4105
4259
  status: "deferred",
@@ -4371,9 +4525,9 @@ function registerReview(server, tracker) {
4371
4525
  }
4372
4526
 
4373
4527
  // src/services/doctor.ts
4374
- import { access as access4, readFile as readFile14, readdir as readdirAsync, stat as statAsync, unlink as unlink3, writeFile as writeFile3 } from "fs/promises";
4528
+ import { access as access4, readFile as readFile15, readdir as readdirAsync, stat as statAsync, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
4375
4529
  import { constants as constants2 } from "fs";
4376
- import { isAbsolute as isAbsolute2, join as join19, posix as posix4, resolve as resolve3 } from "path";
4530
+ import { isAbsolute as isAbsolute2, join as join20, posix as posix4, resolve as resolve3 } from "path";
4377
4531
  import {
4378
4532
  createTranslator,
4379
4533
  forensicReportSchema,
@@ -4587,8 +4741,8 @@ function createKnowledgeSummaryOpaqueCheck(t, inspection) {
4587
4741
  }
4588
4742
 
4589
4743
  // src/services/doctor-stable-id-collision.ts
4590
- import { readFile as readFile6 } from "fs/promises";
4591
- import { basename as basename2, join as join10 } from "path";
4744
+ import { readFile as readFile7 } from "fs/promises";
4745
+ import { basename as basename2, join as join11 } from "path";
4592
4746
  import {
4593
4747
  buildStoreResolveInput as buildStoreResolveInput4,
4594
4748
  createStoreResolver as createStoreResolver4,
@@ -4615,7 +4769,7 @@ function resolveIntegrityStores(projectRoot) {
4615
4769
  return {
4616
4770
  store_uuid: entry.store_uuid,
4617
4771
  alias: entry.alias,
4618
- dir: join10(globalRoot, storeRelativePathForMount3(mounted ?? { store_uuid: entry.store_uuid }))
4772
+ dir: join11(globalRoot, storeRelativePathForMount3(mounted ?? { store_uuid: entry.store_uuid }))
4619
4773
  };
4620
4774
  });
4621
4775
  return { dirs, personalUuids };
@@ -4634,7 +4788,7 @@ async function inspectStoreStableIdIntegrity(projectRoot) {
4634
4788
  for (const ref of await readKnowledgeAcrossStores2(resolved.dirs)) {
4635
4789
  let source;
4636
4790
  try {
4637
- source = await readFile6(ref.file, "utf8");
4791
+ source = await readFile7(ref.file, "utf8");
4638
4792
  } catch {
4639
4793
  continue;
4640
4794
  }
@@ -4718,8 +4872,8 @@ function createLayerMismatchCheck(t, inspection) {
4718
4872
 
4719
4873
  // src/services/doctor-relevance-paths.ts
4720
4874
  import { execFileSync as execFileSync2 } from "child_process";
4721
- import { existsSync as existsSync5, readdirSync, statSync as statSync3 } from "fs";
4722
- import { join as join11, sep as sep3 } from "path";
4875
+ import { existsSync as existsSync6, readdirSync, statSync as statSync3 } from "fs";
4876
+ import { join as join12, sep as sep3 } from "path";
4723
4877
  import { minimatch } from "minimatch";
4724
4878
  var MS_PER_DAY = 24 * 60 * 60 * 1e3;
4725
4879
  var RELEVANCE_PATHS_DRIFT_WINDOW_DAYS = 90;
@@ -4740,7 +4894,7 @@ function expandGlob(rawGlob) {
4740
4894
  return rawGlob.endsWith("/") ? `${rawGlob}**` : rawGlob;
4741
4895
  }
4742
4896
  function collectWorkspacePaths(projectRoot) {
4743
- if (!existsSync5(projectRoot)) {
4897
+ if (!existsSync6(projectRoot)) {
4744
4898
  return [];
4745
4899
  }
4746
4900
  try {
@@ -4762,7 +4916,7 @@ function collectWorkspacePaths(projectRoot) {
4762
4916
  continue;
4763
4917
  }
4764
4918
  for (const entry of entries) {
4765
- const abs = join11(current, entry.name);
4919
+ const abs = join12(current, entry.name);
4766
4920
  const rel = toPosix(abs.slice(projectRoot.length + 1));
4767
4921
  if (rel.length === 0) continue;
4768
4922
  if (entry.isDirectory()) {
@@ -4955,16 +5109,16 @@ function createNarrowNoPathsCheck(t, inspection) {
4955
5109
  }
4956
5110
 
4957
5111
  // src/services/doctor-broad-index.ts
4958
- import { readFile as readFile7 } from "fs/promises";
4959
- import { join as join12 } from "path";
5112
+ import { readFile as readFile8 } from "fs/promises";
5113
+ import { join as join13 } from "path";
4960
5114
  var DEFAULT_BROAD_INDEX_BACKSTOP = 50;
4961
5115
  var BROAD_INDEX_BACKSTOP_MIN = 20;
4962
5116
  var BROAD_INDEX_BACKSTOP_MAX = 500;
4963
5117
  var BROAD_INDEX_DRIFT_RATIO = 0.8;
4964
5118
  async function readBroadIndexBackstop(projectRoot) {
4965
- const configPath = join12(projectRoot, ".fabric", "fabric-config.json");
5119
+ const configPath = join13(projectRoot, ".fabric", "fabric-config.json");
4966
5120
  try {
4967
- const raw = await readFile7(configPath, "utf8");
5121
+ const raw = await readFile8(configPath, "utf8");
4968
5122
  const parsed = JSON.parse(raw);
4969
5123
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
4970
5124
  const v = parsed.broad_index_backstop;
@@ -5145,8 +5299,8 @@ function createStaleArchiveCheck(t, inspection) {
5145
5299
  }
5146
5300
 
5147
5301
  // src/services/doctor-scope-lint.ts
5148
- import { readFile as readFile8 } from "fs/promises";
5149
- import { join as join13 } from "path";
5302
+ import { readFile as readFile9 } from "fs/promises";
5303
+ import { join as join14 } from "path";
5150
5304
  import {
5151
5305
  buildStoreResolveInput as buildStoreResolveInput5,
5152
5306
  createStoreResolver as createStoreResolver5,
@@ -5181,7 +5335,7 @@ async function resolveLintStores(projectRoot) {
5181
5335
  const globalRoot = resolveGlobalRoot4();
5182
5336
  return Promise.all(readSet.stores.map(async (entry) => {
5183
5337
  const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
5184
- const dir = join13(
5338
+ const dir = join14(
5185
5339
  globalRoot,
5186
5340
  storeRelativePathForMount4(mounted ?? { store_uuid: entry.store_uuid })
5187
5341
  );
@@ -5213,7 +5367,7 @@ async function lintStoreScopes(projectRoot) {
5213
5367
  }
5214
5368
  let source;
5215
5369
  try {
5216
- source = await readFile8(ref.file, "utf8");
5370
+ source = await readFile9(ref.file, "utf8");
5217
5371
  } catch {
5218
5372
  continue;
5219
5373
  }
@@ -5335,8 +5489,8 @@ function createUnboundProjectCheck(t, violation) {
5335
5489
  }
5336
5490
 
5337
5491
  // src/services/doctor-store-counters.ts
5338
- import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
5339
- import { join as join14 } from "path";
5492
+ import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
5493
+ import { join as join15 } from "path";
5340
5494
  import {
5341
5495
  buildStoreResolveInput as buildStoreResolveInput6,
5342
5496
  createStoreResolver as createStoreResolver6,
@@ -5363,7 +5517,7 @@ function resolveCounterStores(projectRoot) {
5363
5517
  return readSet.stores.map((entry) => ({
5364
5518
  uuid: entry.store_uuid,
5365
5519
  alias: entry.alias,
5366
- dir: join14(
5520
+ dir: join15(
5367
5521
  globalRoot,
5368
5522
  storeRelativePathForMount5(
5369
5523
  input.mountedStores.find((s) => s.store_uuid === entry.store_uuid) ?? {
@@ -5391,8 +5545,8 @@ function readEntryId(file) {
5391
5545
  function computeStoreDiskMax(storeDir) {
5392
5546
  const max = defaultAgentsMetaCounters();
5393
5547
  for (const type of STORE_KNOWLEDGE_TYPE_DIRS) {
5394
- const dir = join14(storeDir, STORE_LAYOUT2.knowledgeDir, type);
5395
- if (!existsSync6(dir)) {
5548
+ const dir = join15(storeDir, STORE_LAYOUT2.knowledgeDir, type);
5549
+ if (!existsSync7(dir)) {
5396
5550
  continue;
5397
5551
  }
5398
5552
  let names;
@@ -5405,7 +5559,7 @@ function computeStoreDiskMax(storeDir) {
5405
5559
  if (!name.endsWith(".md")) {
5406
5560
  continue;
5407
5561
  }
5408
- const parsed = parseKnowledgeId2(readEntryId(join14(dir, name)) ?? "");
5562
+ const parsed = parseKnowledgeId2(readEntryId(join15(dir, name)) ?? "");
5409
5563
  if (parsed === null) {
5410
5564
  continue;
5411
5565
  }
@@ -5488,7 +5642,7 @@ function createStoreCounterCheck(t, drifts) {
5488
5642
 
5489
5643
  // src/services/doctor-store-orphan.ts
5490
5644
  import { readdirSync as readdirSync3 } from "fs";
5491
- import { join as join15 } from "path";
5645
+ import { join as join16 } from "path";
5492
5646
  import {
5493
5647
  STORES_ROOT_DIR,
5494
5648
  addMountedStore,
@@ -5512,15 +5666,15 @@ function inspectStoreOrphans(globalRoot = resolveGlobalRoot6()) {
5512
5666
  return [];
5513
5667
  }
5514
5668
  const registered = new Set(config.stores.map((s) => s.store_uuid));
5515
- const storesRoot = join15(globalRoot, STORES_ROOT_DIR);
5669
+ const storesRoot = join16(globalRoot, STORES_ROOT_DIR);
5516
5670
  const orphans = [];
5517
5671
  for (const group of listDir(storesRoot)) {
5518
5672
  if (group === STORE_BY_ALIAS_DIR) {
5519
5673
  continue;
5520
5674
  }
5521
- const groupDir = join15(storesRoot, group);
5675
+ const groupDir = join16(storesRoot, group);
5522
5676
  for (const mount of listDir(groupDir)) {
5523
- const dir = join15(groupDir, mount);
5677
+ const dir = join16(groupDir, mount);
5524
5678
  const identity = readStoreIdentity(dir);
5525
5679
  if (identity === null || registered.has(identity.store_uuid)) {
5526
5680
  continue;
@@ -5593,7 +5747,7 @@ import {
5593
5747
 
5594
5748
  // src/services/events-jsonl-gates.ts
5595
5749
  import { promises as fs } from "fs";
5596
- import { existsSync as existsSync7 } from "fs";
5750
+ import { existsSync as existsSync8 } from "fs";
5597
5751
  var EVENTS_JSONL_SIZE_WARN_BYTES = 10 * 1024 * 1024;
5598
5752
  var METRICS_STALE_WARN_MS = 10 * 60 * 1e3;
5599
5753
  var ROTATION_OVERDUE_WARN_MS = 90 * 24 * 60 * 60 * 1e3;
@@ -5615,7 +5769,7 @@ async function inspectEventsJsonlGates(projectRoot, options = {}) {
5615
5769
  if (!(isNodeError(error) && error.code === "ENOENT")) throw error;
5616
5770
  }
5617
5771
  let metricsStalenessMs = null;
5618
- if (existsSync7(metricsPath)) {
5772
+ if (existsSync8(metricsPath)) {
5619
5773
  try {
5620
5774
  const stat4 = await fs.stat(metricsPath);
5621
5775
  metricsStalenessMs = Math.max(0, now.getTime() - stat4.mtimeMs);
@@ -5657,9 +5811,18 @@ async function inspectEventsJsonlGates(projectRoot, options = {}) {
5657
5811
  }
5658
5812
 
5659
5813
  // src/services/doctor-skill-lints.ts
5660
- import { readdir as readdir2, readFile as readFile9 } from "fs/promises";
5661
- import { join as join16, posix as posix2 } from "path";
5814
+ import { readdir as readdir3, readFile as readFile10 } from "fs/promises";
5815
+ import { join as join17, posix as posix2 } from "path";
5662
5816
  var FABRIC_SKILL_SLUGS = ["fabric-archive", "fabric-review", "fabric-import"];
5817
+ var ROUTER_VALID_LEAF_SLUGS = /* @__PURE__ */ new Set([
5818
+ "fabric-archive",
5819
+ "fabric-review",
5820
+ "fabric-import",
5821
+ "fabric-sync",
5822
+ "fabric-store",
5823
+ "fabric-audit",
5824
+ "fabric-connect"
5825
+ ]);
5663
5826
  var SKILL_MD_FRONTMATTER_ROOTS = [".claude/skills", ".codex/skills"];
5664
5827
  var SKILL_FRONTMATTER_KEY_PATTERN = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]+(.+?)[ \t]*$/u;
5665
5828
  var SKILL_QUOTED_VALUE_LEADS = /* @__PURE__ */ new Set(['"', "'", "[", "{", ">", "|"]);
@@ -5680,7 +5843,7 @@ function issueCheck(name, status, kind, code, message, actionHint, audience) {
5680
5843
  }
5681
5844
  async function listMarkdownFiles(dir) {
5682
5845
  try {
5683
- return (await readdir2(dir)).filter((name) => name.endsWith(".md"));
5846
+ return (await readdir3(dir)).filter((name) => name.endsWith(".md"));
5684
5847
  } catch {
5685
5848
  return null;
5686
5849
  }
@@ -5688,8 +5851,8 @@ async function listMarkdownFiles(dir) {
5688
5851
  async function inspectSkillRefMirror(projectRoot) {
5689
5852
  const driftedPaths = [];
5690
5853
  for (const slug of FABRIC_SKILL_SLUGS) {
5691
- const claudeRef = join16(projectRoot, ".claude", "skills", slug, "ref");
5692
- const codexRef = join16(projectRoot, ".codex", "skills", slug, "ref");
5854
+ const claudeRef = join17(projectRoot, ".claude", "skills", slug, "ref");
5855
+ const codexRef = join17(projectRoot, ".codex", "skills", slug, "ref");
5693
5856
  const [claudeFiles, codexFiles] = await Promise.all([listMarkdownFiles(claudeRef), listMarkdownFiles(codexRef)]);
5694
5857
  if (claudeFiles === null || codexFiles === null) continue;
5695
5858
  const claudeSet = new Set(claudeFiles);
@@ -5706,8 +5869,8 @@ async function inspectSkillRefMirror(projectRoot) {
5706
5869
  let codexBody;
5707
5870
  try {
5708
5871
  [claudeBody, codexBody] = await Promise.all([
5709
- readFile9(join16(claudeRef, fname), "utf8"),
5710
- readFile9(join16(codexRef, fname), "utf8")
5872
+ readFile10(join17(claudeRef, fname), "utf8"),
5873
+ readFile10(join17(codexRef, fname), "utf8")
5711
5874
  ]);
5712
5875
  } catch {
5713
5876
  continue;
@@ -5726,10 +5889,10 @@ async function inspectSkillTokenBudget(projectRoot) {
5726
5889
  const overSize = [];
5727
5890
  let highestSeverity = "ok";
5728
5891
  for (const slug of FABRIC_SKILL_SLUGS) {
5729
- const skillMdPath = join16(projectRoot, ".claude", "skills", slug, "SKILL.md");
5892
+ const skillMdPath = join17(projectRoot, ".claude", "skills", slug, "SKILL.md");
5730
5893
  let body;
5731
5894
  try {
5732
- body = await readFile9(skillMdPath, "utf8");
5895
+ body = await readFile10(skillMdPath, "utf8");
5733
5896
  } catch {
5734
5897
  continue;
5735
5898
  }
@@ -5750,10 +5913,10 @@ async function inspectSkillDescription(projectRoot) {
5750
5913
  const CJK_PATTERN = /[\u3400-\u4dbf\u4e00-\u9fff]/u;
5751
5914
  const ASCII_PATTERN = /[a-zA-Z]{2,}/u;
5752
5915
  for (const slug of FABRIC_SKILL_SLUGS) {
5753
- const skillMdPath = join16(projectRoot, ".claude", "skills", slug, "SKILL.md");
5916
+ const skillMdPath = join17(projectRoot, ".claude", "skills", slug, "SKILL.md");
5754
5917
  let body;
5755
5918
  try {
5756
- body = await readFile9(skillMdPath, "utf8");
5919
+ body = await readFile10(skillMdPath, "utf8");
5757
5920
  } catch {
5758
5921
  continue;
5759
5922
  }
@@ -5784,19 +5947,19 @@ async function inspectSkillDescription(projectRoot) {
5784
5947
  async function inspectSkillMdYamlInvalid(projectRoot) {
5785
5948
  const candidates = [];
5786
5949
  for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
5787
- const rootAbs = join16(projectRoot, rootRel);
5950
+ const rootAbs = join17(projectRoot, rootRel);
5788
5951
  let dirEntries;
5789
5952
  try {
5790
- dirEntries = await readdir2(rootAbs, { withFileTypes: true });
5953
+ dirEntries = await readdir3(rootAbs, { withFileTypes: true });
5791
5954
  } catch {
5792
5955
  continue;
5793
5956
  }
5794
5957
  for (const dirEntry of dirEntries) {
5795
5958
  if (!dirEntry.isDirectory()) continue;
5796
- const skillFile = join16(rootAbs, dirEntry.name, "SKILL.md");
5959
+ const skillFile = join17(rootAbs, dirEntry.name, "SKILL.md");
5797
5960
  let raw;
5798
5961
  try {
5799
- raw = await readFile9(skillFile, "utf8");
5962
+ raw = await readFile10(skillFile, "utf8");
5800
5963
  } catch {
5801
5964
  continue;
5802
5965
  }
@@ -5844,6 +6007,68 @@ function extractSkillFrontmatterLines(raw) {
5844
6007
  }
5845
6008
  return null;
5846
6009
  }
6010
+ function extractMarkdownSectionBody(markdown, sectionName) {
6011
+ const lines = markdown.split(/\r?\n/u);
6012
+ const headingRe = /^(#{2,3})\s+(.+?)\s*$/u;
6013
+ let start = -1;
6014
+ for (let i = 0; i < lines.length; i++) {
6015
+ const h = headingRe.exec(lines[i]);
6016
+ if (h && h[2] === sectionName) {
6017
+ start = i + 1;
6018
+ break;
6019
+ }
6020
+ }
6021
+ if (start === -1) return null;
6022
+ const out = [];
6023
+ for (let i = start; i < lines.length; i++) {
6024
+ if (headingRe.test(lines[i])) break;
6025
+ out.push(lines[i]);
6026
+ }
6027
+ return out.join("\n");
6028
+ }
6029
+ async function inspectRouterChainRef(projectRoot) {
6030
+ const candidatePaths = [
6031
+ join17(projectRoot, ".claude", "skills", "fabric", "SKILL.md"),
6032
+ join17(projectRoot, ".codex", "skills", "fabric", "SKILL.md")
6033
+ ];
6034
+ let body = null;
6035
+ for (const candidate of candidatePaths) {
6036
+ try {
6037
+ body = await readFile10(candidate, "utf8");
6038
+ break;
6039
+ } catch {
6040
+ }
6041
+ }
6042
+ if (body === null) return { status: "ok", unknownRefs: [] };
6043
+ const chainSection = extractMarkdownSectionBody(body, "S_CHAIN");
6044
+ if (chainSection === null) return { status: "ok", unknownRefs: [] };
6045
+ const refs = /* @__PURE__ */ new Set();
6046
+ const tokenRe = /`(fabric-[a-z]+)`/gu;
6047
+ let match;
6048
+ while ((match = tokenRe.exec(chainSection)) !== null) {
6049
+ refs.add(match[1]);
6050
+ }
6051
+ const unknownRefs = [...refs].filter((slug) => !ROUTER_VALID_LEAF_SLUGS.has(slug)).sort();
6052
+ return unknownRefs.length === 0 ? { status: "ok", unknownRefs: [] } : { status: "drift", unknownRefs };
6053
+ }
6054
+ function createRouterChainRefCheck(t, inspection) {
6055
+ if (inspection.status === "ok") {
6056
+ return okCheck(t("doctor.check.router_chain_ref.name"), t("doctor.check.router_chain_ref.ok"));
6057
+ }
6058
+ const count = inspection.unknownRefs.length;
6059
+ return issueCheck(
6060
+ t("doctor.check.router_chain_ref.name"),
6061
+ "warn",
6062
+ "warning",
6063
+ "router_chain_ref_drift",
6064
+ t(`doctor.check.router_chain_ref.message.${count === 1 ? "singular" : "plural"}`, {
6065
+ count: String(count),
6066
+ list: inspection.unknownRefs.join(", ")
6067
+ }),
6068
+ t("doctor.check.router_chain_ref.remediation"),
6069
+ "maintainer"
6070
+ );
6071
+ }
5847
6072
  function createSkillRefMirrorCheck(t, inspection) {
5848
6073
  if (inspection.status === "ok") {
5849
6074
  return okCheck(t("doctor.check.skill_ref_mirror.name"), t("doctor.check.skill_ref_mirror.ok"));
@@ -5920,8 +6145,8 @@ function createSkillMdYamlInvalidCheck(t, inspection) {
5920
6145
 
5921
6146
  // src/services/doctor-hooks-lints.ts
5922
6147
  import { constants } from "fs";
5923
- import { access, readdir as readdir3, readFile as readFile10, stat as stat3 } from "fs/promises";
5924
- import { join as join17, posix as posix3 } from "path";
6148
+ import { access, readdir as readdir4, readFile as readFile11, stat as stat3 } from "fs/promises";
6149
+ import { join as join18, posix as posix3 } from "path";
5925
6150
  import { Script } from "vm";
5926
6151
  var HOOKS_RUNTIME_CLIENT_DIRS = [
5927
6152
  { client: "claude", dir: ".claude/hooks" },
@@ -5970,7 +6195,7 @@ function isHookWiredForEvent(hooks, event, hookFile) {
5970
6195
  }
5971
6196
  async function readDirectoryFileNames(dir) {
5972
6197
  try {
5973
- return await readdir3(dir);
6198
+ return await readdir4(dir);
5974
6199
  } catch {
5975
6200
  return null;
5976
6201
  }
@@ -5983,14 +6208,14 @@ async function isFile(absPath) {
5983
6208
  }
5984
6209
  }
5985
6210
  async function inspectHooksWired(projectRoot) {
5986
- const claudeEntries = await readDirectoryFileNames(join17(projectRoot, ".claude"));
6211
+ const claudeEntries = await readDirectoryFileNames(join18(projectRoot, ".claude"));
5987
6212
  if (claudeEntries === null) {
5988
6213
  return { status: "skipped", missingHooks: [] };
5989
6214
  }
5990
- const settingsPath = join17(projectRoot, ".claude", "settings.json");
6215
+ const settingsPath = join18(projectRoot, ".claude", "settings.json");
5991
6216
  let parsed;
5992
6217
  try {
5993
- parsed = JSON.parse(await readFile10(settingsPath, "utf8"));
6218
+ parsed = JSON.parse(await readFile11(settingsPath, "utf8"));
5994
6219
  } catch {
5995
6220
  return { status: "missing-settings", missingHooks: [] };
5996
6221
  }
@@ -6013,8 +6238,8 @@ async function inspectHooksWired(projectRoot) {
6013
6238
  }
6014
6239
  async function inspectHookCacheWritability(projectRoot) {
6015
6240
  const relPath = posix3.join(".fabric", ".cache");
6016
- const fabricDir = join17(projectRoot, ".fabric");
6017
- const cacheDir = join17(projectRoot, ".fabric", ".cache");
6241
+ const fabricDir = join18(projectRoot, ".fabric");
6242
+ const cacheDir = join18(projectRoot, ".fabric", ".cache");
6018
6243
  try {
6019
6244
  try {
6020
6245
  const cacheStats = await stat3(cacheDir);
@@ -6063,12 +6288,12 @@ async function inspectHookCacheWritability(projectRoot) {
6063
6288
  async function inspectHooksContentDrift(projectRoot) {
6064
6289
  const hookFilesByBasename = /* @__PURE__ */ new Map();
6065
6290
  for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
6066
- const absDir = join17(projectRoot, dir);
6291
+ const absDir = join18(projectRoot, dir);
6067
6292
  const entries = await readDirectoryFileNames(absDir);
6068
6293
  if (entries === null) continue;
6069
6294
  for (const name of entries) {
6070
6295
  if (!name.endsWith(".cjs")) continue;
6071
- const abs = join17(absDir, name);
6296
+ const abs = join18(absDir, name);
6072
6297
  if (!await isFile(abs)) continue;
6073
6298
  const arr = hookFilesByBasename.get(name) ?? [];
6074
6299
  arr.push({ client, abs });
@@ -6083,7 +6308,7 @@ async function inspectHooksContentDrift(projectRoot) {
6083
6308
  const hashes = [];
6084
6309
  for (const { client, abs } of copies) {
6085
6310
  try {
6086
- const body = await readFile10(abs, "utf8");
6311
+ const body = await readFile11(abs, "utf8");
6087
6312
  hashes.push({ client, sha: sha256(body) });
6088
6313
  } catch {
6089
6314
  }
@@ -6105,18 +6330,18 @@ async function inspectHooksRuntime(projectRoot) {
6105
6330
  const issues = [];
6106
6331
  let scanned = 0;
6107
6332
  for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
6108
- const absDir = join17(projectRoot, dir);
6333
+ const absDir = join18(projectRoot, dir);
6109
6334
  const entries = await readDirectoryFileNames(absDir);
6110
6335
  if (entries === null) continue;
6111
6336
  for (const name of entries) {
6112
6337
  if (!name.endsWith(".cjs")) continue;
6113
- const abs = join17(absDir, name);
6338
+ const abs = join18(absDir, name);
6114
6339
  const displayPath = `${dir}/${name}`;
6115
6340
  if (!await isFile(abs)) continue;
6116
6341
  scanned += 1;
6117
6342
  let body;
6118
6343
  try {
6119
- body = await readFile10(abs, "utf8");
6344
+ body = await readFile11(abs, "utf8");
6120
6345
  } catch (err) {
6121
6346
  issues.push({
6122
6347
  path: displayPath,
@@ -6253,8 +6478,8 @@ function createHookCacheWritabilityCheck(t, inspection) {
6253
6478
  }
6254
6479
 
6255
6480
  // src/services/doctor-bootstrap-lints.ts
6256
- import { access as access2, readFile as readFile11 } from "fs/promises";
6257
- import { join as join18 } from "path";
6481
+ import { access as access2, readFile as readFile12 } from "fs/promises";
6482
+ import { join as join19 } from "path";
6258
6483
  import {
6259
6484
  BOOTSTRAP_MARKER_BEGIN,
6260
6485
  BOOTSTRAP_MARKER_END,
@@ -6286,17 +6511,17 @@ async function fileExists(path) {
6286
6511
  }
6287
6512
  async function inspectBootstrapAnchor(projectRoot) {
6288
6513
  const [hasAgentsMd, hasClaudeMd] = await Promise.all([
6289
- fileExists(join18(projectRoot, "AGENTS.md")),
6290
- fileExists(join18(projectRoot, "CLAUDE.md"))
6514
+ fileExists(join19(projectRoot, "AGENTS.md")),
6515
+ fileExists(join19(projectRoot, "CLAUDE.md"))
6291
6516
  ]);
6292
6517
  return { hasAgentsMd, hasClaudeMd };
6293
6518
  }
6294
6519
  async function inspectL1BootstrapSnapshotDrift(target) {
6295
- const abs = join18(target, ".fabric", "AGENTS.md");
6520
+ const abs = join19(target, ".fabric", "AGENTS.md");
6296
6521
  const canonical = resolveBootstrapCanonical();
6297
6522
  let onDisk;
6298
6523
  try {
6299
- onDisk = await readFile11(abs, "utf8");
6524
+ onDisk = await readFile12(abs, "utf8");
6300
6525
  } catch {
6301
6526
  return { status: "missing", canonical, onDisk: null };
6302
6527
  }
@@ -6322,17 +6547,17 @@ function createL1BootstrapSnapshotDriftCheck(t, inspection) {
6322
6547
  );
6323
6548
  }
6324
6549
  async function inspectL2ManagedBlockDrift(target) {
6325
- const snapshotPath = join18(target, ".fabric", "AGENTS.md");
6550
+ const snapshotPath = join19(target, ".fabric", "AGENTS.md");
6326
6551
  let snapshot;
6327
6552
  try {
6328
- snapshot = await readFile11(snapshotPath, "utf8");
6553
+ snapshot = await readFile12(snapshotPath, "utf8");
6329
6554
  } catch {
6330
6555
  return { status: "ok", drifted: [] };
6331
6556
  }
6332
- const projectRulesPath = join18(target, ".fabric", "project-rules.md");
6557
+ const projectRulesPath = join19(target, ".fabric", "project-rules.md");
6333
6558
  let expectedBody = snapshot;
6334
6559
  try {
6335
- const projectRules = await readFile11(projectRulesPath, "utf8");
6560
+ const projectRules = await readFile12(projectRulesPath, "utf8");
6336
6561
  expectedBody = `${snapshot}
6337
6562
  ---
6338
6563
  ${projectRules}`;
@@ -6341,12 +6566,12 @@ ${projectRules}`;
6341
6566
  const drifted = [];
6342
6567
  let anyManagedBlockFound = false;
6343
6568
  const blockTargets = [
6344
- join18(target, "AGENTS.md")
6569
+ join19(target, "AGENTS.md")
6345
6570
  ];
6346
6571
  for (const abs of blockTargets) {
6347
6572
  let content;
6348
6573
  try {
6349
- content = await readFile11(abs, "utf8");
6574
+ content = await readFile12(abs, "utf8");
6350
6575
  } catch {
6351
6576
  continue;
6352
6577
  }
@@ -6369,9 +6594,9 @@ ${projectRules}`;
6369
6594
  drifted.push({ path: abs, expected: expectedBody, actual: body });
6370
6595
  }
6371
6596
  }
6372
- const claudeMdPath = join18(target, "CLAUDE.md");
6597
+ const claudeMdPath = join19(target, "CLAUDE.md");
6373
6598
  try {
6374
- const claudeContent = await readFile11(claudeMdPath, "utf8");
6599
+ const claudeContent = await readFile12(claudeMdPath, "utf8");
6375
6600
  anyManagedBlockFound = true;
6376
6601
  const lines = claudeContent.split(/\r?\n/u);
6377
6602
  const hasAtImport = lines.some((line) => line.trim() === "@.fabric/AGENTS.md");
@@ -6439,7 +6664,7 @@ import { appendFile as appendFile3 } from "fs/promises";
6439
6664
  import { minimatch as minimatch2 } from "minimatch";
6440
6665
 
6441
6666
  // src/services/cite-rollup.ts
6442
- import { readFile as readFile12 } from "fs/promises";
6667
+ import { readFile as readFile13 } from "fs/promises";
6443
6668
  import { createLedgerWriteQueue as createLedgerWriteQueue3 } from "@fenglimg/fabric-shared/node/atomic-write";
6444
6669
  var citeRollupQueue = createLedgerWriteQueue3();
6445
6670
  async function appendCiteRollupRow(projectRoot, row) {
@@ -6451,7 +6676,7 @@ async function readCiteRollup(projectRoot) {
6451
6676
  const path = getCiteRollupPath(projectRoot);
6452
6677
  let raw;
6453
6678
  try {
6454
- raw = await readFile12(path, "utf8");
6679
+ raw = await readFile13(path, "utf8");
6455
6680
  } catch (error) {
6456
6681
  if (isNodeError(error) && error.code === "ENOENT") return [];
6457
6682
  throw error;
@@ -7565,6 +7790,7 @@ async function runDoctorReport(target) {
7565
7790
  const hookCacheWritability = await inspectHookCacheWritability(projectRoot);
7566
7791
  const staleServeLock = inspectStaleServeLock(projectRoot, lintNow);
7567
7792
  const skillMdYamlInvalid = await inspectSkillMdYamlInvalid(projectRoot);
7793
+ const routerChainRef = await inspectRouterChainRef(projectRoot);
7568
7794
  const onboardCoverage = await inspectOnboardCoverage(projectRoot);
7569
7795
  const [hooksWired, hooksRuntime, hooksContentDrift] = await Promise.all([
7570
7796
  inspectHooksWired(projectRoot),
@@ -7575,7 +7801,7 @@ async function runDoctorReport(target) {
7575
7801
  const globalCliVersion = process.env.VITEST === "true" ? { status: "ok", version: "test-skipped" } : inspectGlobalCliVersion();
7576
7802
  const targetFiles = Object.fromEntries(
7577
7803
  await Promise.all(
7578
- TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(join19(projectRoot, path))])
7804
+ TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(join20(projectRoot, path))])
7579
7805
  )
7580
7806
  );
7581
7807
  const checks = [
@@ -7641,6 +7867,9 @@ async function runDoctorReport(target) {
7641
7867
  // rc.12 lint #29: skill_md_yaml_invalid. Warning kind — surfaces
7642
7868
  // SKILL.md frontmatter that Codex CLI silently drops at load.
7643
7869
  createSkillMdYamlInvalidCheck(t, skillMdYamlInvalid),
7870
+ // B2 skill-router (A4): S_CHAIN reference backstop. Warning kind — flags an
7871
+ // S_CHAIN `fabric-*` reference to a leaf no longer in the install set.
7872
+ createRouterChainRefCheck(t, routerChainRef),
7644
7873
  // v2.0.0-rc.23 TASK-014 (F8c): Onboard coverage advisory. Info kind.
7645
7874
  // Surfaces uncovered S5 onboard slots and recommends /fabric-archive
7646
7875
  // first-run phase. Sits adjacent to Skill markdown YAML — both are
@@ -7772,7 +8001,7 @@ async function runDoctorFix(target) {
7772
8001
  const fixed = [];
7773
8002
  const ledgerWarnings = [];
7774
8003
  if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
7775
- const snapshotPath = join19(projectRoot, ".fabric", "AGENTS.md");
8004
+ const snapshotPath = join20(projectRoot, ".fabric", "AGENTS.md");
7776
8005
  await ensureParentDirectory(snapshotPath);
7777
8006
  await atomicWriteText4(snapshotPath, resolveBootstrapCanonical2());
7778
8007
  fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
@@ -7845,9 +8074,9 @@ async function runDoctorFix(target) {
7845
8074
  if (before.infos.some((issue) => issue.code === "stale_serve_lock")) {
7846
8075
  const lockInspection = inspectStaleServeLock(projectRoot, Date.now());
7847
8076
  if (lockInspection.present && !lockInspection.pidAlive) {
7848
- const lockFilePath = join19(projectRoot, ".fabric", ".serve.lock");
8077
+ const lockFilePath = join20(projectRoot, ".fabric", ".serve.lock");
7849
8078
  try {
7850
- await unlink3(lockFilePath);
8079
+ await unlink4(lockFilePath);
7851
8080
  } catch (err) {
7852
8081
  const errno = err;
7853
8082
  if (errno.code !== "ENOENT") throw err;
@@ -7962,10 +8191,10 @@ function createApplyLintMessage(succeeded, failed, manualErrorCount) {
7962
8191
  }
7963
8192
  async function applySessionHintsStaleCleanup(projectRoot, candidate) {
7964
8193
  const detail = `deleted (${candidate.age_days}d old)`;
7965
- const absPath = join19(projectRoot, candidate.path);
8194
+ const absPath = join20(projectRoot, candidate.path);
7966
8195
  try {
7967
- const { unlink: unlink4 } = await import("fs/promises");
7968
- await unlink4(absPath);
8196
+ const { unlink: unlink5 } = await import("fs/promises");
8197
+ await unlink5(absPath);
7969
8198
  return {
7970
8199
  kind: "knowledge_session_hints_stale_cleanup",
7971
8200
  path: candidate.path,
@@ -7987,9 +8216,9 @@ function truncateErrorMessage(error) {
7987
8216
  return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
7988
8217
  }
7989
8218
  async function inspectForensic(projectRoot) {
7990
- const path = join19(projectRoot, ".fabric", "forensic.json");
8219
+ const path = join20(projectRoot, ".fabric", "forensic.json");
7991
8220
  try {
7992
- const parsed = forensicReportSchema.parse(JSON.parse(await readFile14(path, "utf8")));
8221
+ const parsed = forensicReportSchema.parse(JSON.parse(await readFile15(path, "utf8")));
7993
8222
  return { present: true, valid: true, report: parsed };
7994
8223
  } catch (error) {
7995
8224
  if (isMissingFileError(error)) {
@@ -8019,7 +8248,7 @@ async function inspectEventLedger(projectRoot) {
8019
8248
  try {
8020
8249
  await access4(path, constants2.W_OK);
8021
8250
  const { warnings } = await readEventLedger(projectRoot);
8022
- const raw = await readFile14(path, "utf8");
8251
+ const raw = await readFile15(path, "utf8");
8023
8252
  const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
8024
8253
  const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
8025
8254
  const schemaVersionSamples = [];
@@ -8564,7 +8793,7 @@ async function inspectPreexistingRootFiles(projectRoot) {
8564
8793
  const candidates = ["CLAUDE.md", "AGENTS.md"];
8565
8794
  const detected = [];
8566
8795
  for (const name of candidates) {
8567
- if (await pathExists(join19(projectRoot, name))) {
8796
+ if (await pathExists(join20(projectRoot, name))) {
8568
8797
  detected.push(name);
8569
8798
  }
8570
8799
  }
@@ -8649,7 +8878,7 @@ async function buildLastActiveIndex(projectRoot) {
8649
8878
  return map;
8650
8879
  }
8651
8880
  async function inspectSessionHintsStale(projectRoot, now) {
8652
- const cacheDir = join19(projectRoot, ".fabric", ".cache");
8881
+ const cacheDir = join20(projectRoot, ".fabric", ".cache");
8653
8882
  let entries;
8654
8883
  try {
8655
8884
  entries = await readdirAsync(cacheDir, { withFileTypes: true });
@@ -8661,7 +8890,7 @@ async function inspectSessionHintsStale(projectRoot, now) {
8661
8890
  if (!entry.isFile()) continue;
8662
8891
  if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
8663
8892
  if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
8664
- const absPath = join19(cacheDir, entry.name);
8893
+ const absPath = join20(cacheDir, entry.name);
8665
8894
  let mtimeMs = 0;
8666
8895
  try {
8667
8896
  mtimeMs = (await statAsync(absPath)).mtimeMs;
@@ -8693,9 +8922,9 @@ function inspectStaleServeLock(projectRoot, now) {
8693
8922
  };
8694
8923
  }
8695
8924
  async function readUnderseedThresholdFromConfig(projectRoot) {
8696
- const configPath = join19(projectRoot, ".fabric", "fabric-config.json");
8925
+ const configPath = join20(projectRoot, ".fabric", "fabric-config.json");
8697
8926
  try {
8698
- const raw = await readFile14(configPath, "utf8");
8927
+ const raw = await readFile15(configPath, "utf8");
8699
8928
  const parsed = JSON.parse(raw);
8700
8929
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
8701
8930
  const v = parsed.underseed_node_threshold;
@@ -8811,10 +9040,10 @@ async function inspectOnboardCoverage(projectRoot) {
8811
9040
  return { filled, missing, opted_out: optedOut };
8812
9041
  }
8813
9042
  async function readOnboardOptedOut(projectRoot) {
8814
- const path = join19(projectRoot, ".fabric", "fabric-config.json");
9043
+ const path = join20(projectRoot, ".fabric", "fabric-config.json");
8815
9044
  let raw;
8816
9045
  try {
8817
- raw = await readFile14(path, "utf8");
9046
+ raw = await readFile15(path, "utf8");
8818
9047
  } catch {
8819
9048
  return [];
8820
9049
  }
@@ -8916,22 +9145,22 @@ async function* iterateCanonicalFilenames(projectRoot) {
8916
9145
  }
8917
9146
  }
8918
9147
  async function rewriteThreeEndManagedBlocks(projectRoot) {
8919
- const snapshotPath = join19(projectRoot, ".fabric", "AGENTS.md");
9148
+ const snapshotPath = join20(projectRoot, ".fabric", "AGENTS.md");
8920
9149
  if (!await pathExists(snapshotPath)) {
8921
9150
  return;
8922
9151
  }
8923
9152
  let snapshot;
8924
9153
  try {
8925
- snapshot = await readFile14(snapshotPath, "utf8");
9154
+ snapshot = await readFile15(snapshotPath, "utf8");
8926
9155
  } catch {
8927
9156
  return;
8928
9157
  }
8929
- const projectRulesPath = join19(projectRoot, ".fabric", "project-rules.md");
9158
+ const projectRulesPath = join20(projectRoot, ".fabric", "project-rules.md");
8930
9159
  const hasProjectRules = await pathExists(projectRulesPath);
8931
9160
  let expectedBody = snapshot;
8932
9161
  if (hasProjectRules) {
8933
9162
  try {
8934
- const projectRules = await readFile14(projectRulesPath, "utf8");
9163
+ const projectRules = await readFile15(projectRulesPath, "utf8");
8935
9164
  expectedBody = `${snapshot}
8936
9165
  ---
8937
9166
  ${projectRules}`;
@@ -8942,7 +9171,7 @@ ${projectRules}`;
8942
9171
  ${expectedBody}
8943
9172
  ${BOOTSTRAP_MARKER_END2}`;
8944
9173
  const blockTargets = [
8945
- join19(projectRoot, "AGENTS.md")
9174
+ join20(projectRoot, "AGENTS.md")
8946
9175
  ];
8947
9176
  for (const abs of blockTargets) {
8948
9177
  if (!await pathExists(abs)) {
@@ -8950,7 +9179,7 @@ ${BOOTSTRAP_MARKER_END2}`;
8950
9179
  }
8951
9180
  let existing;
8952
9181
  try {
8953
- existing = await readFile14(abs, "utf8");
9182
+ existing = await readFile15(abs, "utf8");
8954
9183
  } catch {
8955
9184
  continue;
8956
9185
  }
@@ -8975,11 +9204,11 @@ ${managedBlock}
8975
9204
  }
8976
9205
  await atomicWriteText4(abs, next);
8977
9206
  }
8978
- const claudeMdPath = join19(projectRoot, "CLAUDE.md");
9207
+ const claudeMdPath = join20(projectRoot, "CLAUDE.md");
8979
9208
  if (await pathExists(claudeMdPath)) {
8980
9209
  let claudeContent;
8981
9210
  try {
8982
- claudeContent = await readFile14(claudeMdPath, "utf8");
9211
+ claudeContent = await readFile15(claudeMdPath, "utf8");
8983
9212
  } catch {
8984
9213
  return;
8985
9214
  }
@@ -9045,7 +9274,7 @@ async function collectEntryPoints(root) {
9045
9274
  continue;
9046
9275
  }
9047
9276
  for (const entry of await readdirAsync(current, { withFileTypes: true })) {
9048
- const absolutePath = join19(current, entry.name);
9277
+ const absolutePath = join20(current, entry.name);
9049
9278
  const relativePath = normalizePath2(absolutePath.slice(root.length + 1));
9050
9279
  if (relativePath.length === 0) {
9051
9280
  continue;
@@ -9121,7 +9350,7 @@ async function enrichDescriptions(projectRoot, opts = {}) {
9121
9350
  scanned += 1;
9122
9351
  let source;
9123
9352
  try {
9124
- source = await readFile14(absPath, "utf8");
9353
+ source = await readFile15(absPath, "utf8");
9125
9354
  } catch {
9126
9355
  continue;
9127
9356
  }
@@ -9340,6 +9569,25 @@ async function runDoctorConflictLint(projectRoot, opts = {}) {
9340
9569
  };
9341
9570
  }
9342
9571
 
9572
+ // src/services/summary-cold-eval.ts
9573
+ var COLD_EVAL_RUBRIC = [
9574
+ "You are a ZERO-CONTEXT judge. You are shown ONLY a one-line knowledge summary \u2014",
9575
+ "never the full entry body. For each summary decide: could a reader who has NOT",
9576
+ "seen the body ACT on this line alone (apply the decision / avoid the pitfall /",
9577
+ "follow the rule)?",
9578
+ "",
9579
+ "PASS (self_sufficient=true): the line states the thesis \u2014 the what + the",
9580
+ "operative so-what. FAIL (self_sufficient=false): the line only POINTS at the",
9581
+ "body ('explains the approach', 'covers the edge cases') without stating it.",
9582
+ "When you FAIL one, return a suggested_summary that states the thesis in one line."
9583
+ ].join("\n");
9584
+ function buildColdEvalBatch(candidates) {
9585
+ const judgeable = candidates.filter(
9586
+ (c) => typeof c.summary === "string" && c.summary.trim().length > 0
9587
+ );
9588
+ return { rubric: COLD_EVAL_RUBRIC, candidates: judgeable };
9589
+ }
9590
+
9343
9591
  // src/services/rotation-tick.ts
9344
9592
  var DEFAULT_TICK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
9345
9593
  var tickTimers = /* @__PURE__ */ new Map();
@@ -9375,7 +9623,7 @@ import { IOFabricError as IOFabricError2, RuleError } from "@fenglimg/fabric-sha
9375
9623
 
9376
9624
  // src/services/read-ledger.ts
9377
9625
  import { randomUUID as randomUUID6 } from "crypto";
9378
- import { access as access5, copyFile, readFile as readFile15, rm } from "fs/promises";
9626
+ import { access as access5, copyFile, readFile as readFile16, rm } from "fs/promises";
9379
9627
  import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
9380
9628
  async function resolveLedgerPaths(projectRoot) {
9381
9629
  const primaryPath = getLedgerPath(projectRoot);
@@ -9403,7 +9651,7 @@ async function readLegacyLedger(projectRoot) {
9403
9651
  const { readPath } = await resolveLedgerPaths(projectRoot);
9404
9652
  let raw;
9405
9653
  try {
9406
- raw = await readFile15(readPath, "utf8");
9654
+ raw = await readFile16(readPath, "utf8");
9407
9655
  } catch (error) {
9408
9656
  if (isNodeError(error) && error.code === "ENOENT") {
9409
9657
  return [];
@@ -9658,8 +9906,8 @@ function formatError(error) {
9658
9906
  }
9659
9907
  function formatPreexistingRootMessage(projectRoot) {
9660
9908
  const preexisting = [];
9661
- if (existsSync8(join20(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
9662
- if (existsSync8(join20(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
9909
+ if (existsSync9(join21(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
9910
+ if (existsSync9(join21(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
9663
9911
  if (preexisting.length === 0) return null;
9664
9912
  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.`;
9665
9913
  }
@@ -9685,7 +9933,7 @@ function createFabricServer(tracker) {
9685
9933
  const server = new McpServer(
9686
9934
  {
9687
9935
  name: "fabric-knowledge-server",
9688
- version: "2.2.0-rc.8"
9936
+ version: "2.2.0"
9689
9937
  },
9690
9938
  {
9691
9939
  instructions: FABRIC_SERVER_INSTRUCTIONS
@@ -9704,10 +9952,10 @@ function createFabricServer(tracker) {
9704
9952
  },
9705
9953
  async (_uri) => {
9706
9954
  const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
9707
- const path = join20(projectRoot, ".fabric", "bootstrap", "README.md");
9955
+ const path = join21(projectRoot, ".fabric", "bootstrap", "README.md");
9708
9956
  let text = "";
9709
- if (existsSync8(path)) {
9710
- text = await readFile16(path, "utf8");
9957
+ if (existsSync9(path)) {
9958
+ text = await readFile17(path, "utf8");
9711
9959
  }
9712
9960
  return {
9713
9961
  contents: [
@@ -9797,6 +10045,7 @@ if (isMainModule) {
9797
10045
  }
9798
10046
  export {
9799
10047
  AGENTS_MD_RESOURCE_URI,
10048
+ COLD_EVAL_RUBRIC,
9800
10049
  DEFAULT_CONFLICT_SIMILARITY_THRESHOLD,
9801
10050
  EVENT_LEDGER_PATH,
9802
10051
  FABRIC_SERVER_INSTRUCTIONS,
@@ -9806,6 +10055,7 @@ export {
9806
10055
  METRIC_COUNTER_NAMES,
9807
10056
  appendEventLedgerEvent,
9808
10057
  buildAlwaysActiveBodies,
10058
+ buildColdEvalBatch,
9809
10059
  buildKnowledgeCensus,
9810
10060
  bumpCounter,
9811
10061
  contextCache,