@fenglimg/fabric-server 2.0.0-rc.30 → 2.0.0-rc.34

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.
@@ -1691,6 +1691,27 @@ function readSelectionTokenTtlMs(projectRoot) {
1691
1691
  return void 0;
1692
1692
  }
1693
1693
  }
1694
+ function readOrphanDemoteThresholdDays(projectRoot) {
1695
+ try {
1696
+ const cfg = readFabricConfig(projectRoot);
1697
+ const out = {};
1698
+ const validate = (v) => {
1699
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 1 || v > 3650 || !Number.isInteger(v)) {
1700
+ return void 0;
1701
+ }
1702
+ return v;
1703
+ };
1704
+ const s = validate(cfg.orphan_demote_stable_days);
1705
+ if (s !== void 0) out.stable = s;
1706
+ const e = validate(cfg.orphan_demote_endorsed_days);
1707
+ if (e !== void 0) out.endorsed = e;
1708
+ const d = validate(cfg.orphan_demote_draft_days);
1709
+ if (d !== void 0) out.draft = d;
1710
+ return out;
1711
+ } catch {
1712
+ return {};
1713
+ }
1714
+ }
1694
1715
 
1695
1716
  // src/services/doctor.ts
1696
1717
  import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText4 } from "@fenglimg/fabric-shared/node/atomic-write";
@@ -1804,6 +1825,10 @@ async function runDoctorReport(target) {
1804
1825
  ]);
1805
1826
  const mcpConfigInWrongFile = inspectMcpConfigInWrongFile(projectRoot);
1806
1827
  const skillRefMirror = inspectSkillRefMirror(projectRoot);
1828
+ const skillTokenBudget = inspectSkillTokenBudget(projectRoot);
1829
+ const skillDescription = inspectSkillDescription(projectRoot);
1830
+ const citeGoodhart = await inspectCiteGoodhart(projectRoot);
1831
+ const draftBacklog = inspectDraftBacklog(projectRoot);
1807
1832
  const metaManuallyDiverged = await inspectMetaManuallyDiverged(projectRoot);
1808
1833
  const knowledgeDirUnindexed = inspectKnowledgeDirUnindexed(projectRoot, meta);
1809
1834
  const knowledgeDirMissing = inspectKnowledgeDirMissing(projectRoot);
@@ -1830,6 +1855,8 @@ async function runDoctorReport(target) {
1830
1855
  const relevanceFieldsMissing = inspectRelevanceFieldsMissing(projectRoot);
1831
1856
  const skillMdYamlInvalid = inspectSkillMdYamlInvalid(projectRoot);
1832
1857
  const onboardCoverage = inspectOnboardCoverage(projectRoot);
1858
+ const hooksWired = inspectHooksWired(projectRoot);
1859
+ const promoteLedgerInvariant = eventLedger.exists && eventLedger.writable && eventLedger.parseable ? await inspectPromoteLedgerInvariant(projectRoot) : null;
1833
1860
  const checks = [
1834
1861
  createBootstrapAnchorCheck(t, bootstrapAnchor),
1835
1862
  // v2.0.0-rc.19 TASK-004: bootstrap marker migration check sits adjacent to
@@ -1871,6 +1898,10 @@ async function runDoctorReport(target) {
1871
1898
  // contract between .claude/skills/<slug>/ref/ and .codex/skills/<slug>/
1872
1899
  // ref/. warning severity — fab install restores parity.
1873
1900
  createSkillRefMirrorCheck(t, skillRefMirror),
1901
+ createSkillTokenBudgetCheck(t, skillTokenBudget),
1902
+ createSkillDescriptionCheck(t, skillDescription),
1903
+ createCiteGoodhartCheck(t, citeGoodhart),
1904
+ createDraftBacklogCheck(t, draftBacklog),
1874
1905
  createMcpConfigInWrongFileCheck(t, mcpConfigInWrongFile),
1875
1906
  createMetaManuallyDivergedCheck(t, metaManuallyDiverged),
1876
1907
  createKnowledgeDirUnindexedCheck(t, knowledgeDirUnindexed),
@@ -1924,6 +1955,12 @@ async function runDoctorReport(target) {
1924
1955
  // first-run phase. Sits adjacent to Skill markdown YAML — both are
1925
1956
  // Skill-adjacent advisories. --fix never mutates onboard state.
1926
1957
  createOnboardCoverageCheck(t, onboardCoverage),
1958
+ // rc.31 BUG-M3/NEW-4: hooks_wired observability. Adjacent to onboard /
1959
+ // promote-ledger checks — all three are install/runtime-state advisories.
1960
+ createHooksWiredCheck(t, hooksWired),
1961
+ // rc.31 BUG-G2/G5: promote-ledger invariant. Sits adjacent to onboard
1962
+ // coverage — both are observability advisories built off events.jsonl.
1963
+ ...promoteLedgerInvariant === null ? [] : [createPromoteLedgerInvariantCheck(t, promoteLedgerInvariant)],
1927
1964
  createPreexistingRootFilesCheck(t, preexistingRootFiles)
1928
1965
  // v2.0 / rc.2: `createLegacyClientPathCheck` removed. The schema now
1929
1966
  // rejects retired clientPaths keys (windsurf/rooCode/geminiCLI) at Zod
@@ -2032,13 +2069,14 @@ async function runDoctorFix(target) {
2032
2069
  const reconcileCodes = [
2033
2070
  "agents_meta_missing",
2034
2071
  "agents_meta_stale",
2072
+ "agents_meta_invalid",
2035
2073
  "knowledge_test_index_missing",
2036
2074
  "knowledge_test_index_stale",
2037
2075
  "content_ref_missing",
2038
2076
  "knowledge_dir_unindexed",
2039
2077
  "meta_manually_diverged"
2040
2078
  ];
2041
- if (before.fixable_errors.some((issue) => reconcileCodes.includes(issue.code)) || before.warnings.some((issue) => reconcileCodes.includes(issue.code))) {
2079
+ if (before.fixable_errors.some((issue) => reconcileCodes.includes(issue.code)) || before.warnings.some((issue) => reconcileCodes.includes(issue.code)) || before.manual_errors.some((issue) => reconcileCodes.includes(issue.code))) {
2042
2080
  await reconcileKnowledge(projectRoot, { trigger: "doctor" });
2043
2081
  for (const issue of before.fixable_errors.filter(
2044
2082
  (candidate) => reconcileCodes.includes(candidate.code)
@@ -2050,6 +2088,11 @@ async function runDoctorFix(target) {
2050
2088
  )) {
2051
2089
  fixed.push(issue);
2052
2090
  }
2091
+ for (const issue of before.manual_errors.filter(
2092
+ (candidate) => reconcileCodes.includes(candidate.code)
2093
+ )) {
2094
+ fixed.push(issue);
2095
+ }
2053
2096
  contextCache.invalidate("meta_write", projectRoot);
2054
2097
  await fixCounterDesync(projectRoot);
2055
2098
  contextCache.invalidate("meta_write", projectRoot);
@@ -2735,6 +2778,184 @@ function inspectSkillRefMirror(projectRoot) {
2735
2778
  if (driftedPaths.length === 0) return { status: "ok" };
2736
2779
  return { status: "drift", driftedPaths };
2737
2780
  }
2781
+ function inspectSkillTokenBudget(projectRoot) {
2782
+ const skillSlugs = ["fabric-archive", "fabric-review", "fabric-import"];
2783
+ const WARN_TOKENS = 5e3;
2784
+ const ERROR_TOKENS = 1e4;
2785
+ const overSize = [];
2786
+ let highestSeverity = "ok";
2787
+ for (const slug of skillSlugs) {
2788
+ const skillMdPath = join7(projectRoot, ".claude", "skills", slug, "SKILL.md");
2789
+ let body;
2790
+ try {
2791
+ body = readFileSync3(skillMdPath, "utf8");
2792
+ } catch {
2793
+ continue;
2794
+ }
2795
+ const tokens = Math.ceil(body.length / 3);
2796
+ if (tokens > ERROR_TOKENS) {
2797
+ overSize.push({ slug, tokens, severity: "error" });
2798
+ highestSeverity = "error";
2799
+ } else if (tokens > WARN_TOKENS) {
2800
+ overSize.push({ slug, tokens, severity: "warn" });
2801
+ if (highestSeverity !== "error") highestSeverity = "warn";
2802
+ }
2803
+ }
2804
+ return { status: highestSeverity, overSize };
2805
+ }
2806
+ function inspectSkillDescription(projectRoot) {
2807
+ const skillSlugs = ["fabric-archive", "fabric-review", "fabric-import"];
2808
+ const MAX_DESCRIPTION_TOKENS = 60;
2809
+ const issues = [];
2810
+ const CJK_PATTERN = /[㐀-䶿一-鿿]/;
2811
+ const ASCII_PATTERN = /[a-zA-Z]{2,}/;
2812
+ for (const slug of skillSlugs) {
2813
+ const skillMdPath = join7(projectRoot, ".claude", "skills", slug, "SKILL.md");
2814
+ let body;
2815
+ try {
2816
+ body = readFileSync3(skillMdPath, "utf8");
2817
+ } catch {
2818
+ continue;
2819
+ }
2820
+ const fmMatch = body.match(/^---\n([\s\S]*?)\n---/);
2821
+ if (!fmMatch) {
2822
+ issues.push({ slug, problem: "missing", detail: "no YAML frontmatter" });
2823
+ continue;
2824
+ }
2825
+ const descMatch = fmMatch[1].match(/^description:\s*(.+?)\s*$/m);
2826
+ if (!descMatch || descMatch[1].trim().length === 0) {
2827
+ issues.push({ slug, problem: "missing", detail: "description field empty or absent" });
2828
+ continue;
2829
+ }
2830
+ const description = descMatch[1].replace(/^["'](.+)["']$/, "$1");
2831
+ const tokens = Math.ceil(description.length / 3);
2832
+ if (tokens > MAX_DESCRIPTION_TOKENS) {
2833
+ issues.push({ slug, problem: "too_long", detail: `${tokens} tok (max ${MAX_DESCRIPTION_TOKENS})` });
2834
+ }
2835
+ if (!CJK_PATTERN.test(description)) {
2836
+ issues.push({ slug, problem: "no_cjk", detail: "no Chinese trigger phrase" });
2837
+ }
2838
+ if (!ASCII_PATTERN.test(description)) {
2839
+ issues.push({ slug, problem: "no_ascii", detail: "no English trigger phrase" });
2840
+ }
2841
+ }
2842
+ return { status: issues.length === 0 ? "ok" : "warn", issues };
2843
+ }
2844
+ async function inspectCiteGoodhart(projectRoot) {
2845
+ const WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
2846
+ const RITUAL_REPEAT_THRESHOLD = 5;
2847
+ const DISMISSAL_ABUSE_RATIO = 0.6;
2848
+ const PLACEHOLDER_COUNT_THRESHOLD = 5;
2849
+ const cutoffMs = Date.now() - WINDOW_MS;
2850
+ const fired = [];
2851
+ let events = [];
2852
+ try {
2853
+ const result = await readEventLedger(projectRoot);
2854
+ events = result.events;
2855
+ } catch {
2856
+ return { status: "ok", fired: [] };
2857
+ }
2858
+ const turns = events.filter(
2859
+ (e) => {
2860
+ if (e.event_type !== "assistant_turn_observed") return false;
2861
+ const ts = Date.parse(e.timestamp);
2862
+ return Number.isFinite(ts) && ts >= cutoffMs;
2863
+ }
2864
+ );
2865
+ if (turns.length === 0) {
2866
+ return { status: "ok", fired: [] };
2867
+ }
2868
+ const recalledCount = /* @__PURE__ */ new Map();
2869
+ for (const turn of turns) {
2870
+ for (let i = 0; i < turn.cite_ids.length; i += 1) {
2871
+ if (turn.cite_tags[i] === "recalled") {
2872
+ const key = turn.cite_ids[i];
2873
+ recalledCount.set(key, (recalledCount.get(key) ?? 0) + 1);
2874
+ }
2875
+ }
2876
+ }
2877
+ for (const [id, n] of recalledCount.entries()) {
2878
+ if (n > RITUAL_REPEAT_THRESHOLD) {
2879
+ fired.push({ pattern: "G1", detail: `${id} repeated as [recalled] ${n}x in 7d` });
2880
+ break;
2881
+ }
2882
+ }
2883
+ let recalledTotal = 0;
2884
+ let recalledWithSkip = 0;
2885
+ for (const turn of turns) {
2886
+ for (let i = 0; i < turn.cite_ids.length; i += 1) {
2887
+ if (turn.cite_tags[i] !== "recalled") continue;
2888
+ recalledTotal += 1;
2889
+ const commitment = turn.cite_commitments[i];
2890
+ if (commitment && typeof commitment.skip_reason === "string" && commitment.skip_reason.length > 0) {
2891
+ recalledWithSkip += 1;
2892
+ }
2893
+ }
2894
+ }
2895
+ if (recalledTotal >= 5 && recalledWithSkip / recalledTotal > DISMISSAL_ABUSE_RATIO) {
2896
+ fired.push({
2897
+ pattern: "G2",
2898
+ detail: `${recalledWithSkip}/${recalledTotal} recalled cites used skip:<reason> (> ${Math.round(DISMISSAL_ABUSE_RATIO * 100)}%)`
2899
+ });
2900
+ }
2901
+ let chainedFromMisuse = 0;
2902
+ for (const turn of turns) {
2903
+ for (let i = 0; i < turn.cite_ids.length; i += 1) {
2904
+ if (turn.cite_tags[i] !== "chained-from") continue;
2905
+ const commitment = turn.cite_commitments[i];
2906
+ if (!commitment) {
2907
+ chainedFromMisuse += 1;
2908
+ continue;
2909
+ }
2910
+ const hasOps = Array.isArray(commitment.operators) && commitment.operators.length > 0;
2911
+ const hasSkip = typeof commitment.skip_reason === "string" && commitment.skip_reason.length > 0;
2912
+ if (!hasOps && !hasSkip) chainedFromMisuse += 1;
2913
+ }
2914
+ }
2915
+ if (chainedFromMisuse > RITUAL_REPEAT_THRESHOLD) {
2916
+ fired.push({
2917
+ pattern: "G3",
2918
+ detail: `${chainedFromMisuse} chained-from cites with no commitment (operators=[] + skip_reason=null) in 7d`
2919
+ });
2920
+ }
2921
+ let placeholderCount = 0;
2922
+ for (const turn of turns) {
2923
+ if (turn.cite_tags.length === 0) continue;
2924
+ const allNone = turn.cite_tags.every((t) => t === "none");
2925
+ if (!allNone) continue;
2926
+ const raw = (turn.kb_line_raw ?? "").trim();
2927
+ if (raw === "KB: none" || raw.includes("[unspecified]")) {
2928
+ placeholderCount += 1;
2929
+ }
2930
+ }
2931
+ if (placeholderCount > PLACEHOLDER_COUNT_THRESHOLD) {
2932
+ fired.push({
2933
+ pattern: "G5",
2934
+ detail: `${placeholderCount} placeholder "KB: none" / "[unspecified]" cites in 7d`
2935
+ });
2936
+ }
2937
+ return { status: fired.length === 0 ? "ok" : "warn", fired };
2938
+ }
2939
+ function inspectDraftBacklog(projectRoot) {
2940
+ const DRAFT_BACKLOG_RATIO = 0.5;
2941
+ const MIN_TOTAL_FOR_RATIO = 10;
2942
+ let draftCount = 0;
2943
+ let totalCount = 0;
2944
+ for (const entry of iterateCanonicalEntries(projectRoot, /* @__PURE__ */ new Map())) {
2945
+ totalCount += 1;
2946
+ if (entry.maturity === "draft") draftCount += 1;
2947
+ }
2948
+ if (totalCount < MIN_TOTAL_FOR_RATIO) {
2949
+ return { status: "ok", draftCount, totalCount, ratio: 0 };
2950
+ }
2951
+ const ratio = draftCount / totalCount;
2952
+ return {
2953
+ status: ratio > DRAFT_BACKLOG_RATIO ? "warn" : "ok",
2954
+ draftCount,
2955
+ totalCount,
2956
+ ratio
2957
+ };
2958
+ }
2738
2959
  async function inspectKnowledgeTestIndex(projectRoot) {
2739
2960
  const path2 = join7(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
2740
2961
  const built = await tryBuildRuleMeta(projectRoot);
@@ -3330,6 +3551,90 @@ function createSkillRefMirrorCheck(t, inspection) {
3330
3551
  t("doctor.check.skill_ref_mirror.remediation")
3331
3552
  );
3332
3553
  }
3554
+ function createSkillTokenBudgetCheck(t, inspection) {
3555
+ if (inspection.status === "ok") {
3556
+ return okCheck(
3557
+ t("doctor.check.skill_token_budget.name"),
3558
+ t("doctor.check.skill_token_budget.ok")
3559
+ );
3560
+ }
3561
+ const list = inspection.overSize.map((s) => `${s.slug}=${s.tokens} tok (${s.severity})`).join(", ");
3562
+ const count = inspection.overSize.length;
3563
+ return issueCheck(
3564
+ t("doctor.check.skill_token_budget.name"),
3565
+ inspection.status,
3566
+ inspection.status === "error" ? "manual_error" : "warning",
3567
+ "skill_token_budget_exceeded",
3568
+ t(`doctor.check.skill_token_budget.message.${count === 1 ? "singular" : "plural"}`, {
3569
+ count: String(count),
3570
+ list
3571
+ }),
3572
+ t("doctor.check.skill_token_budget.remediation")
3573
+ );
3574
+ }
3575
+ function createDraftBacklogCheck(t, inspection) {
3576
+ if (inspection.status === "ok") {
3577
+ return okCheck(
3578
+ t("doctor.check.draft_backlog.name"),
3579
+ t("doctor.check.draft_backlog.ok")
3580
+ );
3581
+ }
3582
+ const pct = Math.round(inspection.ratio * 100);
3583
+ return issueCheck(
3584
+ t("doctor.check.draft_backlog.name"),
3585
+ "warn",
3586
+ "warning",
3587
+ "knowledge_draft_backlog",
3588
+ t("doctor.check.draft_backlog.message", {
3589
+ draftCount: String(inspection.draftCount),
3590
+ totalCount: String(inspection.totalCount),
3591
+ pct: String(pct)
3592
+ }),
3593
+ t("doctor.check.draft_backlog.remediation")
3594
+ );
3595
+ }
3596
+ function createCiteGoodhartCheck(t, inspection) {
3597
+ if (inspection.status === "ok") {
3598
+ return okCheck(
3599
+ t("doctor.check.cite_goodhart.name"),
3600
+ t("doctor.check.cite_goodhart.ok")
3601
+ );
3602
+ }
3603
+ const list = inspection.fired.map((f) => `${f.pattern}: ${f.detail}`).join("; ");
3604
+ const count = inspection.fired.length;
3605
+ return issueCheck(
3606
+ t("doctor.check.cite_goodhart.name"),
3607
+ "warn",
3608
+ "warning",
3609
+ "cite_goodhart_pattern",
3610
+ t(`doctor.check.cite_goodhart.message.${count === 1 ? "singular" : "plural"}`, {
3611
+ count: String(count),
3612
+ list
3613
+ }),
3614
+ t("doctor.check.cite_goodhart.remediation")
3615
+ );
3616
+ }
3617
+ function createSkillDescriptionCheck(t, inspection) {
3618
+ if (inspection.status === "ok") {
3619
+ return okCheck(
3620
+ t("doctor.check.skill_description.name"),
3621
+ t("doctor.check.skill_description.ok")
3622
+ );
3623
+ }
3624
+ const list = inspection.issues.map((i) => `${i.slug}: ${i.problem} (${i.detail})`).join("; ");
3625
+ const count = inspection.issues.length;
3626
+ return issueCheck(
3627
+ t("doctor.check.skill_description.name"),
3628
+ "warn",
3629
+ "warning",
3630
+ "skill_description_quality",
3631
+ t(`doctor.check.skill_description.message.${count === 1 ? "singular" : "plural"}`, {
3632
+ count: String(count),
3633
+ list
3634
+ }),
3635
+ t("doctor.check.skill_description.remediation")
3636
+ );
3637
+ }
3333
3638
  function createEventLedgerPartialWriteCheck(t, ledger) {
3334
3639
  if (!ledger.exists || !ledger.writable) {
3335
3640
  return okCheck(
@@ -3358,6 +3663,137 @@ function createEventLedgerPartialWriteCheck(t, ledger) {
3358
3663
  function okCheck(name, message) {
3359
3664
  return { name, status: "ok", message };
3360
3665
  }
3666
+ function inspectHooksWired(projectRoot) {
3667
+ const claudeDir = join7(projectRoot, ".claude");
3668
+ if (!existsSync5(claudeDir)) {
3669
+ return { status: "skipped", missingHooks: [] };
3670
+ }
3671
+ const settingsPath = join7(projectRoot, ".claude", "settings.json");
3672
+ if (!existsSync5(settingsPath)) {
3673
+ return { status: "missing-settings", missingHooks: [] };
3674
+ }
3675
+ let raw;
3676
+ try {
3677
+ raw = readFileSync3(settingsPath, "utf8");
3678
+ } catch {
3679
+ return { status: "missing-settings", missingHooks: [] };
3680
+ }
3681
+ let parsed;
3682
+ try {
3683
+ parsed = JSON.parse(raw);
3684
+ } catch {
3685
+ return { status: "missing-settings", missingHooks: [] };
3686
+ }
3687
+ const required = [
3688
+ { event: "Stop", hookFile: "fabric-hint.cjs" },
3689
+ { event: "SessionStart", hookFile: "knowledge-hint-broad.cjs" },
3690
+ { event: "PreToolUse", hookFile: "knowledge-hint-narrow.cjs" }
3691
+ ];
3692
+ const missing = [];
3693
+ const hooksSection = isRecord(parsed) ? parsed.hooks : void 0;
3694
+ for (const { event, hookFile } of required) {
3695
+ if (!isHookWiredForEvent(hooksSection, event, hookFile)) {
3696
+ missing.push(`${event}:${hookFile}`);
3697
+ }
3698
+ }
3699
+ if (missing.length === 0) {
3700
+ return { status: "ok", missingHooks: [] };
3701
+ }
3702
+ return { status: "incomplete", missingHooks: missing };
3703
+ }
3704
+ function isRecord(value) {
3705
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3706
+ }
3707
+ function isHookWiredForEvent(hooks, event, hookFile) {
3708
+ if (!isRecord(hooks)) return false;
3709
+ const eventEntries = hooks[event];
3710
+ if (!Array.isArray(eventEntries)) return false;
3711
+ for (const matcherBlock of eventEntries) {
3712
+ if (!isRecord(matcherBlock)) continue;
3713
+ const inner = matcherBlock.hooks;
3714
+ if (!Array.isArray(inner)) continue;
3715
+ for (const hookEntry of inner) {
3716
+ if (!isRecord(hookEntry)) continue;
3717
+ const cmd = hookEntry.command;
3718
+ if (typeof cmd === "string" && cmd.includes(hookFile)) {
3719
+ return true;
3720
+ }
3721
+ }
3722
+ }
3723
+ return false;
3724
+ }
3725
+ function createHooksWiredCheck(t, inspection) {
3726
+ if (inspection.status === "skipped") {
3727
+ return okCheck(
3728
+ t("doctor.check.hooks_wired.name"),
3729
+ t("doctor.check.hooks_wired.ok.skipped")
3730
+ );
3731
+ }
3732
+ if (inspection.status === "ok") {
3733
+ return okCheck(
3734
+ t("doctor.check.hooks_wired.name"),
3735
+ t("doctor.check.hooks_wired.ok.wired")
3736
+ );
3737
+ }
3738
+ if (inspection.status === "missing-settings") {
3739
+ return issueCheck(
3740
+ t("doctor.check.hooks_wired.name"),
3741
+ "warn",
3742
+ "warning",
3743
+ "hooks_wired_missing_settings",
3744
+ t("doctor.check.hooks_wired.message.missing_settings"),
3745
+ t("doctor.check.hooks_wired.remediation")
3746
+ );
3747
+ }
3748
+ return issueCheck(
3749
+ t("doctor.check.hooks_wired.name"),
3750
+ "warn",
3751
+ "warning",
3752
+ "hooks_wired_incomplete",
3753
+ t("doctor.check.hooks_wired.message.incomplete", {
3754
+ missing: inspection.missingHooks.join(", ")
3755
+ }),
3756
+ t("doctor.check.hooks_wired.remediation")
3757
+ );
3758
+ }
3759
+ async function inspectPromoteLedgerInvariant(projectRoot) {
3760
+ const [proposed, started, promoted] = await Promise.all([
3761
+ readEventLedger(projectRoot, { event_type: "knowledge_proposed" }),
3762
+ readEventLedger(projectRoot, { event_type: "knowledge_promote_started" }),
3763
+ readEventLedger(projectRoot, { event_type: "knowledge_promoted" })
3764
+ ]);
3765
+ const proposedCount = proposed.events.length;
3766
+ const promoteStartedCount = started.events.length;
3767
+ const promotedCount = promoted.events.length;
3768
+ let violation = null;
3769
+ if (proposedCount < promoteStartedCount) {
3770
+ violation = "proposed-lt-started";
3771
+ } else if (promoteStartedCount < promotedCount) {
3772
+ violation = "started-lt-promoted";
3773
+ }
3774
+ return { proposedCount, promoteStartedCount, promotedCount, violation };
3775
+ }
3776
+ function createPromoteLedgerInvariantCheck(t, inspection) {
3777
+ const params = {
3778
+ proposed: String(inspection.proposedCount),
3779
+ started: String(inspection.promoteStartedCount),
3780
+ promoted: String(inspection.promotedCount)
3781
+ };
3782
+ if (inspection.violation === null) {
3783
+ return okCheck(
3784
+ t("doctor.check.promote_ledger_invariant.name"),
3785
+ t("doctor.check.promote_ledger_invariant.ok", params)
3786
+ );
3787
+ }
3788
+ return issueCheck(
3789
+ t("doctor.check.promote_ledger_invariant.name"),
3790
+ "warn",
3791
+ "warning",
3792
+ "promote_ledger_invariant_violated",
3793
+ t(`doctor.check.promote_ledger_invariant.message.${inspection.violation}`, params),
3794
+ t("doctor.check.promote_ledger_invariant.remediation")
3795
+ );
3796
+ }
3361
3797
  function issueCheck(name, status, kind, code, message, actionHint) {
3362
3798
  return {
3363
3799
  name,
@@ -3804,13 +4240,24 @@ async function buildLastConsumedIndex(projectRoot) {
3804
4240
  return map;
3805
4241
  }
3806
4242
  for (const event of events) {
3807
- if (event.event_type !== "knowledge_consumed" && event.event_type !== "knowledge_demoted" && event.event_type !== "knowledge_archived") {
3808
- continue;
3809
- }
3810
4243
  const ts = event.ts;
3811
4244
  if (typeof ts !== "number" || !Number.isFinite(ts)) {
3812
4245
  continue;
3813
4246
  }
4247
+ if (event.event_type === "knowledge_sections_fetched") {
4248
+ const ids = Array.isArray(event.final_stable_ids) ? event.final_stable_ids : [];
4249
+ for (const stableId2 of ids) {
4250
+ if (typeof stableId2 !== "string" || stableId2.length === 0) continue;
4251
+ const prev2 = map.get(stableId2);
4252
+ if (prev2 === void 0 || ts > prev2) {
4253
+ map.set(stableId2, ts);
4254
+ }
4255
+ }
4256
+ continue;
4257
+ }
4258
+ if (event.event_type !== "knowledge_consumed" && event.event_type !== "knowledge_demoted" && event.event_type !== "knowledge_archived") {
4259
+ continue;
4260
+ }
3814
4261
  const stableId = event.stable_id;
3815
4262
  if (typeof stableId !== "string" || stableId.length === 0) {
3816
4263
  continue;
@@ -3882,8 +4329,16 @@ async function buildLastActiveIndex(projectRoot) {
3882
4329
  }
3883
4330
  return map;
3884
4331
  }
3885
- function maturityThresholdDays(maturity) {
3886
- return ORPHAN_DEMOTE_THRESHOLD_DAYS[maturity];
4332
+ function resolveMaturityThresholds(projectRoot) {
4333
+ const overrides = readOrphanDemoteThresholdDays(projectRoot);
4334
+ return {
4335
+ stable: overrides.stable ?? ORPHAN_DEMOTE_THRESHOLD_DAYS.stable,
4336
+ endorsed: overrides.endorsed ?? ORPHAN_DEMOTE_THRESHOLD_DAYS.endorsed,
4337
+ draft: overrides.draft ?? ORPHAN_DEMOTE_THRESHOLD_DAYS.draft
4338
+ };
4339
+ }
4340
+ function maturityThresholdDays(maturity, thresholds) {
4341
+ return (thresholds ?? ORPHAN_DEMOTE_THRESHOLD_DAYS)[maturity];
3887
4342
  }
3888
4343
  function nextLowerMaturity(current) {
3889
4344
  if (current === "stable") return "endorsed";
@@ -3969,11 +4424,12 @@ function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
3969
4424
  }
3970
4425
  async function inspectOrphanDemote(projectRoot, now) {
3971
4426
  const lastConsumedIndex = await buildLastConsumedIndex(projectRoot);
4427
+ const thresholds = resolveMaturityThresholds(projectRoot);
3972
4428
  const candidates = [];
3973
4429
  for (const entry of iterateCanonicalEntries(projectRoot, lastConsumedIndex)) {
3974
4430
  const ageMs = entry.lastReferenceMs > 0 ? now - entry.lastReferenceMs : now;
3975
4431
  const ageDays = Math.floor(ageMs / MS_PER_DAY);
3976
- const threshold = maturityThresholdDays(entry.maturity);
4432
+ const threshold = maturityThresholdDays(entry.maturity, thresholds);
3977
4433
  if (ageDays <= threshold) {
3978
4434
  continue;
3979
4435
  }
@@ -3986,7 +4442,7 @@ async function inspectOrphanDemote(projectRoot, now) {
3986
4442
  });
3987
4443
  }
3988
4444
  candidates.sort((a, b) => a.path.localeCompare(b.path));
3989
- return { candidates };
4445
+ return { candidates, thresholds };
3990
4446
  }
3991
4447
  async function inspectStaleArchive(projectRoot, now) {
3992
4448
  const lastActiveIndex = await buildLastActiveIndex(projectRoot);
@@ -4307,9 +4763,9 @@ function createOrphanDemoteCheck(t, inspection) {
4307
4763
  "knowledge_orphan_demote_required",
4308
4764
  t(`doctor.check.orphan_demote.message.${count === 1 ? "singular" : "plural"}`, {
4309
4765
  count: String(count),
4310
- stableDays: String(ORPHAN_DEMOTE_THRESHOLD_DAYS.stable),
4311
- endorsedDays: String(ORPHAN_DEMOTE_THRESHOLD_DAYS.endorsed),
4312
- draftDays: String(ORPHAN_DEMOTE_THRESHOLD_DAYS.draft),
4766
+ stableDays: String(inspection.thresholds.stable),
4767
+ endorsedDays: String(inspection.thresholds.endorsed),
4768
+ draftDays: String(inspection.thresholds.draft),
4313
4769
  detail
4314
4770
  }),
4315
4771
  t("doctor.check.orphan_demote.remediation")
@@ -14,7 +14,7 @@ import {
14
14
  readEventLedger,
15
15
  runDoctorReport,
16
16
  sha256
17
- } from "./chunk-4DLGRSYE.js";
17
+ } from "./chunk-Z23PAA5L.js";
18
18
 
19
19
  // src/http.ts
20
20
  import { randomUUID as randomUUID2 } from "crypto";
package/dist/index.js CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  sha256,
40
40
  stableStringify,
41
41
  writeKnowledgeMeta
42
- } from "./chunk-4DLGRSYE.js";
42
+ } from "./chunk-Z23PAA5L.js";
43
43
 
44
44
  // src/index.ts
45
45
  import { existsSync as existsSync3 } from "fs";
@@ -333,6 +333,16 @@ function renderFreshEntry(args) {
333
333
  `created_at: ${createdAt}`,
334
334
  `source_sessions: [${args.sourceSessions.map((s) => JSON.stringify(s)).join(", ")}]`,
335
335
  `proposed_reason: ${args.proposedReason}`,
336
+ // rc.31 BUG-2.9/2.1: persist the caller-supplied summary in frontmatter so
337
+ // knowledge-meta-builder.extractDescriptionFromFrontmatter picks it up
338
+ // directly. Without this, the meta-builder fell back to extractRule
339
+ // Description's h1-or-stable-id-or-placeholder synthesis (line ~944),
340
+ // which made user-visible description.summary == stable_id for any
341
+ // pending file whose body started with h2-only sections (`## Summary` is
342
+ // the canonical pending shape). The frontmatter `summary:` line is the
343
+ // canonical source-of-truth: `extractDescriptionFromFrontmatter` reads it
344
+ // before extractRuleDescription's fallback kicks in.
345
+ `summary: ${quoteRelevancePath(args.summary)}`,
336
346
  "tags: []"
337
347
  ];
338
348
  if (args.relevanceScope !== void 0) {
@@ -615,7 +625,11 @@ async function planContext(projectRoot, input) {
615
625
  }
616
626
  const stale = metaResult.degraded === true || input.client_hash !== void 0 && input.client_hash !== meta.revision;
617
627
  const uniquePaths = dedupePaths(input.paths);
618
- const allDescriptions = buildDescriptionIndex(meta);
628
+ const scoringContext = {
629
+ nowMs: Date.now(),
630
+ targetPaths: input.target_paths ?? dedupePaths(input.paths)
631
+ };
632
+ const allDescriptions = buildDescriptionIndex(meta, scoringContext);
619
633
  const relevanceTargetPaths = input.target_paths ?? uniquePaths;
620
634
  const entries = uniquePaths.map((path) => {
621
635
  const profile = buildRequirementProfile(path, input);
@@ -719,7 +733,7 @@ function buildRequirementProfile(path, input) {
719
733
  detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path] ?? []
720
734
  };
721
735
  }
722
- function buildDescriptionIndex(meta) {
736
+ function buildDescriptionIndex(meta, scoringContext) {
723
737
  return Object.entries(meta.nodes).flatMap(([nodeId, node]) => {
724
738
  const level = deriveAgentsMetaLayer(node.file);
725
739
  const description = node.description ?? descriptionFromLegacyActivation(node.activation?.description);
@@ -744,7 +758,7 @@ function buildDescriptionIndex(meta) {
744
758
  relevance_scope: description.relevance_scope,
745
759
  relevance_paths: description.relevance_paths
746
760
  }];
747
- }).sort(compareDescriptionIndexItems);
761
+ }).sort((left, right) => compareDescriptionIndexItems(left, right, scoringContext));
748
762
  }
749
763
  function matchesAnyPath(globs, targetPaths) {
750
764
  if (globs.length === 0) {
@@ -827,9 +841,68 @@ function dedupeDescriptionIndex(items) {
827
841
  return true;
828
842
  });
829
843
  }
830
- function compareDescriptionIndexItems(left, right) {
844
+ function compareDescriptionIndexItems(left, right, context) {
845
+ if (context !== void 0) {
846
+ const leftScore = scoreDescriptionItem(left, context.nowMs, context.targetPaths);
847
+ const rightScore = scoreDescriptionItem(right, context.nowMs, context.targetPaths);
848
+ if (leftScore !== rightScore) {
849
+ return rightScore - leftScore;
850
+ }
851
+ }
831
852
  return left.stable_id.localeCompare(right.stable_id);
832
853
  }
854
+ var RECENCY_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
855
+ var RECENCY_BOOST = 100;
856
+ var LOCALITY_SAME_FILE = 100;
857
+ var LOCALITY_SAME_DIR = 50;
858
+ var LOCALITY_SAME_PACKAGE = 25;
859
+ function scoreDescriptionItem(item, nowMs, targetPaths) {
860
+ let score = 0;
861
+ const createdAtRaw = item.description?.created_at;
862
+ if (typeof createdAtRaw === "string" && createdAtRaw.length > 0) {
863
+ const createdMs = Date.parse(createdAtRaw);
864
+ if (Number.isFinite(createdMs) && nowMs - createdMs < RECENCY_WINDOW_MS) {
865
+ score += RECENCY_BOOST;
866
+ }
867
+ }
868
+ if (targetPaths.length > 0) {
869
+ const relevancePaths = item.relevance_paths ?? item.description?.relevance_paths ?? [];
870
+ let best = 0;
871
+ for (const rp of relevancePaths) {
872
+ for (const tp of targetPaths) {
873
+ const tier = localityTier(rp, tp);
874
+ if (tier > best) best = tier;
875
+ }
876
+ }
877
+ score += best;
878
+ }
879
+ return score;
880
+ }
881
+ function localityTier(relevancePath, targetPath) {
882
+ if (relevancePath === targetPath) return LOCALITY_SAME_FILE;
883
+ const rpDir = dirnameOfPath(relevancePath);
884
+ const tpDir = dirnameOfPath(targetPath);
885
+ if (rpDir.length > 0 && rpDir === tpDir) return LOCALITY_SAME_DIR;
886
+ const rpPkg = packageRootOfPath(relevancePath);
887
+ const tpPkg = packageRootOfPath(targetPath);
888
+ if (rpPkg.length > 0 && rpPkg === tpPkg) return LOCALITY_SAME_PACKAGE;
889
+ return 0;
890
+ }
891
+ function dirnameOfPath(p) {
892
+ const idx = p.search(/[*?[]/);
893
+ if (idx >= 0) {
894
+ return p.slice(0, idx).replace(/\/$/, "");
895
+ }
896
+ const lastSlash = p.lastIndexOf("/");
897
+ return lastSlash >= 0 ? p.slice(0, lastSlash) : "";
898
+ }
899
+ function packageRootOfPath(p) {
900
+ const idx = p.search(/[*?[]/);
901
+ const stem = idx >= 0 ? p.slice(0, idx).replace(/\/$/, "") : p;
902
+ const segments = stem.split("/").filter(Boolean);
903
+ if (segments.length < 2) return "";
904
+ return segments.slice(0, 2).join("/");
905
+ }
833
906
 
834
907
  // src/tools/plan-context.ts
835
908
  function registerPlanContext(server, tracker) {
@@ -1195,6 +1268,11 @@ async function approveOne(projectRoot, pendingPath, allocator) {
1195
1268
  return null;
1196
1269
  }
1197
1270
  const slug = basename(pendingPath).replace(/\.md$/u, "");
1271
+ await emitEventBestEffort2(projectRoot, {
1272
+ event_type: "knowledge_proposed",
1273
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1274
+ reason: `approve-synth:${slug}`
1275
+ });
1198
1276
  await emitEventBestEffort2(projectRoot, {
1199
1277
  event_type: "knowledge_promote_started",
1200
1278
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2013,7 +2091,7 @@ function formatPreexistingRootMessage(projectRoot) {
2013
2091
  function createFabricServer(tracker) {
2014
2092
  const server = new McpServer({
2015
2093
  name: "fabric-knowledge-server",
2016
- version: "2.0.0-rc.30"
2094
+ version: "2.0.0-rc.34"
2017
2095
  });
2018
2096
  registerPlanContext(server, tracker);
2019
2097
  registerKnowledgeSections(server, tracker);
@@ -2121,7 +2199,7 @@ function createShutdownHandler(deps) {
2121
2199
  };
2122
2200
  }
2123
2201
  async function startHttpServer(options) {
2124
- const { createFabricHttpApp } = await import("./http-ALTGDHLT.js");
2202
+ const { createFabricHttpApp } = await import("./http-2L2SQN7A.js");
2125
2203
  const { port, projectRoot, host = "127.0.0.1", authToken, allowLoopbackNoAuth } = options;
2126
2204
  const app = createFabricHttpApp({ projectRoot, host, authToken, allowLoopbackNoAuth });
2127
2205
  return await new Promise((resolveServer, rejectServer) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fenglimg/fabric-server",
3
- "version": "2.0.0-rc.30",
3
+ "version": "2.0.0-rc.34",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -13,7 +13,7 @@
13
13
  "express": "^5.2.1",
14
14
  "minimatch": "^10.0.1",
15
15
  "zod": "^3.25.0",
16
- "@fenglimg/fabric-shared": "2.0.0-rc.30"
16
+ "@fenglimg/fabric-shared": "2.0.0-rc.34"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/express": "^5.0.6",