@fenglimg/fabric-server 2.0.0-rc.29 → 2.0.0-rc.33

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.
@@ -851,8 +851,9 @@ function createDefaultNodeMeta(contentRef) {
851
851
  scope_glob: deriveScopeGlob(contentRef),
852
852
  deps: layer === "L0" ? [] : ["L0"],
853
853
  priority: layer === "L0" ? "high" : "medium",
854
+ // v2.0.0-rc.30 TASK-004: dropped duplicate `layer:` write — was always
855
+ // identical to `level:`; AgentsMetaNode no longer carries the field.
854
856
  level: layer,
855
- layer,
856
857
  topology_type: topologyType,
857
858
  hash: ""
858
859
  };
@@ -1690,6 +1691,27 @@ function readSelectionTokenTtlMs(projectRoot) {
1690
1691
  return void 0;
1691
1692
  }
1692
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
+ }
1693
1715
 
1694
1716
  // src/services/doctor.ts
1695
1717
  import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText4 } from "@fenglimg/fabric-shared/node/atomic-write";
@@ -1803,6 +1825,10 @@ async function runDoctorReport(target) {
1803
1825
  ]);
1804
1826
  const mcpConfigInWrongFile = inspectMcpConfigInWrongFile(projectRoot);
1805
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);
1806
1832
  const metaManuallyDiverged = await inspectMetaManuallyDiverged(projectRoot);
1807
1833
  const knowledgeDirUnindexed = inspectKnowledgeDirUnindexed(projectRoot, meta);
1808
1834
  const knowledgeDirMissing = inspectKnowledgeDirMissing(projectRoot);
@@ -1829,6 +1855,8 @@ async function runDoctorReport(target) {
1829
1855
  const relevanceFieldsMissing = inspectRelevanceFieldsMissing(projectRoot);
1830
1856
  const skillMdYamlInvalid = inspectSkillMdYamlInvalid(projectRoot);
1831
1857
  const onboardCoverage = inspectOnboardCoverage(projectRoot);
1858
+ const hooksWired = inspectHooksWired(projectRoot);
1859
+ const promoteLedgerInvariant = eventLedger.exists && eventLedger.writable && eventLedger.parseable ? await inspectPromoteLedgerInvariant(projectRoot) : null;
1832
1860
  const checks = [
1833
1861
  createBootstrapAnchorCheck(t, bootstrapAnchor),
1834
1862
  // v2.0.0-rc.19 TASK-004: bootstrap marker migration check sits adjacent to
@@ -1870,6 +1898,10 @@ async function runDoctorReport(target) {
1870
1898
  // contract between .claude/skills/<slug>/ref/ and .codex/skills/<slug>/
1871
1899
  // ref/. warning severity — fab install restores parity.
1872
1900
  createSkillRefMirrorCheck(t, skillRefMirror),
1901
+ createSkillTokenBudgetCheck(t, skillTokenBudget),
1902
+ createSkillDescriptionCheck(t, skillDescription),
1903
+ createCiteGoodhartCheck(t, citeGoodhart),
1904
+ createDraftBacklogCheck(t, draftBacklog),
1873
1905
  createMcpConfigInWrongFileCheck(t, mcpConfigInWrongFile),
1874
1906
  createMetaManuallyDivergedCheck(t, metaManuallyDiverged),
1875
1907
  createKnowledgeDirUnindexedCheck(t, knowledgeDirUnindexed),
@@ -1923,6 +1955,12 @@ async function runDoctorReport(target) {
1923
1955
  // first-run phase. Sits adjacent to Skill markdown YAML — both are
1924
1956
  // Skill-adjacent advisories. --fix never mutates onboard state.
1925
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)],
1926
1964
  createPreexistingRootFilesCheck(t, preexistingRootFiles)
1927
1965
  // v2.0 / rc.2: `createLegacyClientPathCheck` removed. The schema now
1928
1966
  // rejects retired clientPaths keys (windsurf/rooCode/geminiCLI) at Zod
@@ -2031,13 +2069,14 @@ async function runDoctorFix(target) {
2031
2069
  const reconcileCodes = [
2032
2070
  "agents_meta_missing",
2033
2071
  "agents_meta_stale",
2072
+ "agents_meta_invalid",
2034
2073
  "knowledge_test_index_missing",
2035
2074
  "knowledge_test_index_stale",
2036
2075
  "content_ref_missing",
2037
2076
  "knowledge_dir_unindexed",
2038
2077
  "meta_manually_diverged"
2039
2078
  ];
2040
- 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))) {
2041
2080
  await reconcileKnowledge(projectRoot, { trigger: "doctor" });
2042
2081
  for (const issue of before.fixable_errors.filter(
2043
2082
  (candidate) => reconcileCodes.includes(candidate.code)
@@ -2049,6 +2088,11 @@ async function runDoctorFix(target) {
2049
2088
  )) {
2050
2089
  fixed.push(issue);
2051
2090
  }
2091
+ for (const issue of before.manual_errors.filter(
2092
+ (candidate) => reconcileCodes.includes(candidate.code)
2093
+ )) {
2094
+ fixed.push(issue);
2095
+ }
2052
2096
  contextCache.invalidate("meta_write", projectRoot);
2053
2097
  await fixCounterDesync(projectRoot);
2054
2098
  contextCache.invalidate("meta_write", projectRoot);
@@ -2734,6 +2778,184 @@ function inspectSkillRefMirror(projectRoot) {
2734
2778
  if (driftedPaths.length === 0) return { status: "ok" };
2735
2779
  return { status: "drift", driftedPaths };
2736
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
+ }
2737
2959
  async function inspectKnowledgeTestIndex(projectRoot) {
2738
2960
  const path2 = join7(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
2739
2961
  const built = await tryBuildRuleMeta(projectRoot);
@@ -3329,6 +3551,90 @@ function createSkillRefMirrorCheck(t, inspection) {
3329
3551
  t("doctor.check.skill_ref_mirror.remediation")
3330
3552
  );
3331
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
+ }
3332
3638
  function createEventLedgerPartialWriteCheck(t, ledger) {
3333
3639
  if (!ledger.exists || !ledger.writable) {
3334
3640
  return okCheck(
@@ -3357,6 +3663,137 @@ function createEventLedgerPartialWriteCheck(t, ledger) {
3357
3663
  function okCheck(name, message) {
3358
3664
  return { name, status: "ok", message };
3359
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
+ }
3360
3797
  function issueCheck(name, status, kind, code, message, actionHint) {
3361
3798
  return {
3362
3799
  name,
@@ -3803,13 +4240,24 @@ async function buildLastConsumedIndex(projectRoot) {
3803
4240
  return map;
3804
4241
  }
3805
4242
  for (const event of events) {
3806
- if (event.event_type !== "knowledge_consumed" && event.event_type !== "knowledge_demoted" && event.event_type !== "knowledge_archived") {
3807
- continue;
3808
- }
3809
4243
  const ts = event.ts;
3810
4244
  if (typeof ts !== "number" || !Number.isFinite(ts)) {
3811
4245
  continue;
3812
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
+ }
3813
4261
  const stableId = event.stable_id;
3814
4262
  if (typeof stableId !== "string" || stableId.length === 0) {
3815
4263
  continue;
@@ -3881,8 +4329,16 @@ async function buildLastActiveIndex(projectRoot) {
3881
4329
  }
3882
4330
  return map;
3883
4331
  }
3884
- function maturityThresholdDays(maturity) {
3885
- 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];
3886
4342
  }
3887
4343
  function nextLowerMaturity(current) {
3888
4344
  if (current === "stable") return "endorsed";
@@ -3968,11 +4424,12 @@ function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
3968
4424
  }
3969
4425
  async function inspectOrphanDemote(projectRoot, now) {
3970
4426
  const lastConsumedIndex = await buildLastConsumedIndex(projectRoot);
4427
+ const thresholds = resolveMaturityThresholds(projectRoot);
3971
4428
  const candidates = [];
3972
4429
  for (const entry of iterateCanonicalEntries(projectRoot, lastConsumedIndex)) {
3973
4430
  const ageMs = entry.lastReferenceMs > 0 ? now - entry.lastReferenceMs : now;
3974
4431
  const ageDays = Math.floor(ageMs / MS_PER_DAY);
3975
- const threshold = maturityThresholdDays(entry.maturity);
4432
+ const threshold = maturityThresholdDays(entry.maturity, thresholds);
3976
4433
  if (ageDays <= threshold) {
3977
4434
  continue;
3978
4435
  }
@@ -3985,7 +4442,7 @@ async function inspectOrphanDemote(projectRoot, now) {
3985
4442
  });
3986
4443
  }
3987
4444
  candidates.sort((a, b) => a.path.localeCompare(b.path));
3988
- return { candidates };
4445
+ return { candidates, thresholds };
3989
4446
  }
3990
4447
  async function inspectStaleArchive(projectRoot, now) {
3991
4448
  const lastActiveIndex = await buildLastActiveIndex(projectRoot);
@@ -4306,9 +4763,9 @@ function createOrphanDemoteCheck(t, inspection) {
4306
4763
  "knowledge_orphan_demote_required",
4307
4764
  t(`doctor.check.orphan_demote.message.${count === 1 ? "singular" : "plural"}`, {
4308
4765
  count: String(count),
4309
- stableDays: String(ORPHAN_DEMOTE_THRESHOLD_DAYS.stable),
4310
- endorsedDays: String(ORPHAN_DEMOTE_THRESHOLD_DAYS.endorsed),
4311
- 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),
4312
4769
  detail
4313
4770
  }),
4314
4771
  t("doctor.check.orphan_demote.remediation")
@@ -6370,12 +6827,8 @@ async function emitAutoHealEventBestEffort(projectRoot, payload) {
6370
6827
  // src/services/get-knowledge.ts
6371
6828
  import { readFile as readFile6 } from "fs/promises";
6372
6829
  import { join as join8 } from "path";
6830
+ import { deriveAgentsMetaLayer as deriveAgentsMetaLayer2 } from "@fenglimg/fabric-shared";
6373
6831
  import { minimatch as minimatch2 } from "minimatch";
6374
- var PRIORITY_ORDER = {
6375
- high: 0,
6376
- medium: 1,
6377
- low: 2
6378
- };
6379
6832
  async function getKnowledge(projectRoot, input) {
6380
6833
  const metaResult = await loadActiveMeta(projectRoot, { caller: "getKnowledge" });
6381
6834
  if (metaResult.auto_healed) {
@@ -6432,12 +6885,7 @@ function normalizeKnowledgePath(value) {
6432
6885
  }
6433
6886
  function matchRuleNodes(meta, path2) {
6434
6887
  const requestedPath = normalizeKnowledgePath(path2);
6435
- return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
6436
- const [leftId, leftNode] = left;
6437
- const [rightId, rightNode] = right;
6438
- const priorityDelta = PRIORITY_ORDER[leftNode.priority ?? "medium"] - PRIORITY_ORDER[rightNode.priority ?? "medium"];
6439
- return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
6440
- }).map(([nodeId, node]) => ({
6888
+ return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => left[0].localeCompare(right[0])).map(([nodeId, node]) => ({
6441
6889
  node_id: nodeId,
6442
6890
  level: classifyNode(nodeId, node),
6443
6891
  stable_id: node.stable_id ?? nodeId,
@@ -6491,7 +6939,8 @@ function classifyNode(nodeId, node) {
6491
6939
  if (nodeId.startsWith("L2/")) {
6492
6940
  return "L2";
6493
6941
  }
6494
- return node.layer === "L0" ? null : node.layer ?? null;
6942
+ const layer = node.level ?? deriveAgentsMetaLayer2(node.file);
6943
+ return layer === "L0" ? null : layer;
6495
6944
  }
6496
6945
  function partitionRulesByLevel(loadedRules, dedupeByPath) {
6497
6946
  const l1 = [];
@@ -14,7 +14,7 @@ import {
14
14
  readEventLedger,
15
15
  runDoctorReport,
16
16
  sha256
17
- } from "./chunk-CTQ4UMO4.js";
17
+ } from "./chunk-Z23PAA5L.js";
18
18
 
19
19
  // src/http.ts
20
20
  import { randomUUID as randomUUID2 } from "crypto";
@@ -725,7 +725,9 @@ function buildLedgerFallbackMeta(entries) {
725
725
  scope_glob: affectedPath,
726
726
  deps: [],
727
727
  priority: "medium",
728
- layer: "L2",
728
+ // v2.0.0-rc.30 TASK-004: dropped `layer: "L2"` — use `level` only;
729
+ // AgentsMetaNode no longer carries `layer`.
730
+ level: "L2",
729
731
  topology_type: "mirror",
730
732
  hash: `replayed:${hashBase ?? entry.id}`
731
733
  };
package/dist/index.js CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  sha256,
40
40
  stableStringify,
41
41
  writeKnowledgeMeta
42
- } from "./chunk-CTQ4UMO4.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) {
@@ -848,7 +921,7 @@ function registerPlanContext(server, tracker) {
848
921
  const gateResult = await awaitFirstReconcileGate();
849
922
  const gateWarn = gateWarning(gateResult);
850
923
  const projectRoot = resolveProjectRoot();
851
- const syncReport = await ensureKnowledgeFresh(projectRoot);
924
+ const syncReport = await ensureKnowledgeFresh(projectRoot, { autoHealOnDrift: true });
852
925
  const result = await planContext(projectRoot, {
853
926
  paths,
854
927
  intent,
@@ -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(),
@@ -1774,6 +1852,7 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
1774
1852
  import { readFile as readFile4 } from "fs/promises";
1775
1853
  import { homedir as homedir3 } from "os";
1776
1854
  import { join as join3 } from "path";
1855
+ import { deriveAgentsMetaLayer as deriveAgentsMetaLayer2 } from "@fenglimg/fabric-shared";
1777
1856
  var PRIORITY_ORDER = {
1778
1857
  high: 0,
1779
1858
  medium: 1,
@@ -1896,7 +1975,7 @@ function findRuleNode(meta, stableId) {
1896
1975
  if (nodeStableId !== stableId) {
1897
1976
  continue;
1898
1977
  }
1899
- const level = node.level ?? node.layer ?? "L2";
1978
+ const level = node.level ?? deriveAgentsMetaLayer2(node.file);
1900
1979
  return {
1901
1980
  stable_id: nodeStableId,
1902
1981
  level,
@@ -1958,7 +2037,7 @@ function registerKnowledgeSections(server, tracker) {
1958
2037
  const gateResult = await awaitFirstReconcileGate();
1959
2038
  const gateWarn = gateWarning(gateResult);
1960
2039
  const projectRoot = resolveProjectRoot();
1961
- const syncReport = await ensureKnowledgeFresh(projectRoot);
2040
+ const syncReport = await ensureKnowledgeFresh(projectRoot, { autoHealOnDrift: true });
1962
2041
  const result = await getKnowledgeSections(projectRoot, input);
1963
2042
  const response = {
1964
2043
  ...result,
@@ -2012,7 +2091,7 @@ function formatPreexistingRootMessage(projectRoot) {
2012
2091
  function createFabricServer(tracker) {
2013
2092
  const server = new McpServer({
2014
2093
  name: "fabric-knowledge-server",
2015
- version: "2.0.0-rc.29"
2094
+ version: "2.0.0-rc.33"
2016
2095
  });
2017
2096
  registerPlanContext(server, tracker);
2018
2097
  registerKnowledgeSections(server, tracker);
@@ -2120,7 +2199,7 @@ function createShutdownHandler(deps) {
2120
2199
  };
2121
2200
  }
2122
2201
  async function startHttpServer(options) {
2123
- const { createFabricHttpApp } = await import("./http-TAI5X7U5.js");
2202
+ const { createFabricHttpApp } = await import("./http-UK7PIXY5.js");
2124
2203
  const { port, projectRoot, host = "127.0.0.1", authToken, allowLoopbackNoAuth } = options;
2125
2204
  const app = createFabricHttpApp({ projectRoot, host, authToken, allowLoopbackNoAuth });
2126
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.29",
3
+ "version": "2.0.0-rc.33",
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.29"
16
+ "@fenglimg/fabric-shared": "2.0.0-rc.33"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/express": "^5.0.6",