@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
|
|
3885
|
-
|
|
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(
|
|
4310
|
-
endorsedDays: String(
|
|
4311
|
-
draftDays: String(
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
|
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.
|
|
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.
|
|
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-
|
|
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.
|
|
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.
|
|
16
|
+
"@fenglimg/fabric-shared": "2.0.0-rc.33"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/express": "^5.0.6",
|