@fenglimg/fabric-server 2.3.0-rc.1 → 2.3.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +58 -5
- package/dist/index.js +542 -250
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { existsSync as existsSync9 } from "fs";
|
|
3
|
-
import { readFile as
|
|
4
|
-
import { join as
|
|
3
|
+
import { readFile as readFile20 } from "fs/promises";
|
|
4
|
+
import { join as join25, resolve as resolve4 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -742,7 +742,7 @@ var SUPPORTED_EMBED_MODELS = /* @__PURE__ */ new Set([
|
|
|
742
742
|
function readEmbedConfig(projectRoot) {
|
|
743
743
|
try {
|
|
744
744
|
const config = readFabricConfig(projectRoot);
|
|
745
|
-
const enabled = config.embed_enabled
|
|
745
|
+
const enabled = config.embed_enabled !== false;
|
|
746
746
|
const rawWeight = config.embed_weight;
|
|
747
747
|
const weight = typeof rawWeight === "number" && Number.isInteger(rawWeight) && rawWeight >= 0 && rawWeight <= 49 ? rawWeight : 30;
|
|
748
748
|
const rawModel = config.embed_model;
|
|
@@ -817,6 +817,15 @@ function readOrphanDemoteThresholdDays(projectRoot) {
|
|
|
817
817
|
return {};
|
|
818
818
|
}
|
|
819
819
|
}
|
|
820
|
+
function readFusion(projectRoot) {
|
|
821
|
+
try {
|
|
822
|
+
const raw = readFabricConfig(projectRoot).fusion;
|
|
823
|
+
if (raw === "rrf" || raw === "additive") return raw;
|
|
824
|
+
return "auto";
|
|
825
|
+
} catch {
|
|
826
|
+
return "auto";
|
|
827
|
+
}
|
|
828
|
+
}
|
|
820
829
|
function readConflictLintThreshold(projectRoot) {
|
|
821
830
|
try {
|
|
822
831
|
const cfgPath = join4(projectRoot, ".fabric", "fabric-config.json");
|
|
@@ -1534,11 +1543,44 @@ function buildBm25Model(docs) {
|
|
|
1534
1543
|
for (const field of BM25_FIELDS) {
|
|
1535
1544
|
avgFieldLength[field] = totalDocs > 0 ? totalFieldLength[field] / totalDocs : 0;
|
|
1536
1545
|
}
|
|
1546
|
+
const serialized = {
|
|
1547
|
+
version: 1,
|
|
1548
|
+
totalDocs,
|
|
1549
|
+
documentFrequency: [...documentFrequency],
|
|
1550
|
+
avgFieldLength,
|
|
1551
|
+
perDoc: [...perDoc].map(([id, stats]) => ({
|
|
1552
|
+
id,
|
|
1553
|
+
fieldTermFreq: emptyFieldRecord(() => []),
|
|
1554
|
+
fieldLength: { ...stats.fieldLength }
|
|
1555
|
+
}))
|
|
1556
|
+
};
|
|
1557
|
+
for (const entry of serialized.perDoc) {
|
|
1558
|
+
const stats = perDoc.get(entry.id);
|
|
1559
|
+
if (stats === void 0) continue;
|
|
1560
|
+
for (const field of BM25_FIELDS) {
|
|
1561
|
+
entry.fieldTermFreq[field] = [...stats.fieldTermFreq[field]];
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
return modelFromStats(serialized);
|
|
1565
|
+
}
|
|
1566
|
+
function modelFromStats(serialized) {
|
|
1567
|
+
const totalDocs = serialized.totalDocs;
|
|
1568
|
+
const documentFrequency = new Map(serialized.documentFrequency);
|
|
1569
|
+
const avgFieldLength = serialized.avgFieldLength;
|
|
1570
|
+
const perDoc = /* @__PURE__ */ new Map();
|
|
1571
|
+
for (const entry of serialized.perDoc) {
|
|
1572
|
+
const fieldTermFreq = emptyFieldRecord(() => /* @__PURE__ */ new Map());
|
|
1573
|
+
for (const field of BM25_FIELDS) {
|
|
1574
|
+
fieldTermFreq[field] = new Map(entry.fieldTermFreq[field]);
|
|
1575
|
+
}
|
|
1576
|
+
perDoc.set(entry.id, { fieldTermFreq, fieldLength: entry.fieldLength });
|
|
1577
|
+
}
|
|
1537
1578
|
const idf = (term) => {
|
|
1538
1579
|
const n = documentFrequency.get(term) ?? 0;
|
|
1539
1580
|
return Math.log(1 + (totalDocs - n + 0.5) / (n + 0.5));
|
|
1540
1581
|
};
|
|
1541
1582
|
return {
|
|
1583
|
+
__serialized: serialized,
|
|
1542
1584
|
scoreDoc(id, queryTerms) {
|
|
1543
1585
|
const data = perDoc.get(id);
|
|
1544
1586
|
if (data === void 0 || queryTerms.length === 0) {
|
|
@@ -1574,9 +1616,48 @@ function buildBm25Model(docs) {
|
|
|
1574
1616
|
}
|
|
1575
1617
|
};
|
|
1576
1618
|
}
|
|
1619
|
+
function serializeBm25Model(model) {
|
|
1620
|
+
return model.__serialized;
|
|
1621
|
+
}
|
|
1622
|
+
function rehydrateBm25Model(serialized) {
|
|
1623
|
+
return modelFromStats(serialized);
|
|
1624
|
+
}
|
|
1577
1625
|
function buildQueryTerms(text) {
|
|
1578
1626
|
return tokenize(text);
|
|
1579
1627
|
}
|
|
1628
|
+
function rankDocuments(model, ids, queryTerms) {
|
|
1629
|
+
if (queryTerms.length === 0) {
|
|
1630
|
+
return /* @__PURE__ */ new Map();
|
|
1631
|
+
}
|
|
1632
|
+
const scored = [];
|
|
1633
|
+
ids.forEach((id, order) => {
|
|
1634
|
+
const score = model.scoreDoc(id, queryTerms);
|
|
1635
|
+
if (score > 0) {
|
|
1636
|
+
scored.push({ id, score, order });
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
scored.sort((a, b) => a.score !== b.score ? b.score - a.score : a.order - b.order);
|
|
1640
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
1641
|
+
scored.forEach((entry, index) => {
|
|
1642
|
+
ranks.set(entry.id, index + 1);
|
|
1643
|
+
});
|
|
1644
|
+
return ranks;
|
|
1645
|
+
}
|
|
1646
|
+
function rankByScore(ids, scores) {
|
|
1647
|
+
const scored = [];
|
|
1648
|
+
ids.forEach((id, order) => {
|
|
1649
|
+
const score = scores.get(id) ?? 0;
|
|
1650
|
+
if (score > 0) {
|
|
1651
|
+
scored.push({ id, score, order });
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1654
|
+
scored.sort((a, b) => a.score !== b.score ? b.score - a.score : a.order - b.order);
|
|
1655
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
1656
|
+
scored.forEach((entry, index) => {
|
|
1657
|
+
ranks.set(entry.id, index + 1);
|
|
1658
|
+
});
|
|
1659
|
+
return ranks;
|
|
1660
|
+
}
|
|
1580
1661
|
|
|
1581
1662
|
// src/services/conflict-lint.ts
|
|
1582
1663
|
function buildSimilarityModel(docs) {
|
|
@@ -2670,13 +2751,37 @@ var LEDGER_DUAL_WRITE_METRIC_NAMES = {
|
|
|
2670
2751
|
edit_intent_checked: METRIC_COUNTER_NAMES.edit_intent_checked
|
|
2671
2752
|
};
|
|
2672
2753
|
|
|
2754
|
+
// src/services/plan-context.ts
|
|
2755
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
|
|
2756
|
+
import { join as join10 } from "path";
|
|
2757
|
+
|
|
2673
2758
|
// src/services/vector-retrieval.ts
|
|
2759
|
+
import { mkdirSync } from "fs";
|
|
2760
|
+
import { join as join9 } from "path";
|
|
2761
|
+
import { resolveGlobalRoot as resolveGlobalRoot3 } from "@fenglimg/fabric-shared";
|
|
2674
2762
|
var embedderLoad;
|
|
2675
2763
|
var OPTIONAL_EMBED_PACKAGE = "fastembed";
|
|
2764
|
+
var embedderModuleLoader = (name) => import(name);
|
|
2765
|
+
var MISSING_EMBEDDER_HINT = "[fabric] vector semantic recall is enabled but the optional 'fastembed' package is unavailable \u2014 falling back to text-only ranking. Install it where the server resolves modules (e.g. `npm i -g fastembed`) to enable embeddings, or set embed_enabled:false to silence this.\n";
|
|
2766
|
+
var defaultMissingEmbedderHint = () => {
|
|
2767
|
+
process.stderr.write(MISSING_EMBEDDER_HINT);
|
|
2768
|
+
};
|
|
2769
|
+
var missingEmbedderHinted = false;
|
|
2770
|
+
var emitMissingEmbedderHint = defaultMissingEmbedderHint;
|
|
2771
|
+
function hintMissingEmbedderOnce() {
|
|
2772
|
+
if (missingEmbedderHinted) {
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
missingEmbedderHinted = true;
|
|
2776
|
+
emitMissingEmbedderHint();
|
|
2777
|
+
}
|
|
2778
|
+
function defaultEmbedCacheDir() {
|
|
2779
|
+
return join9(resolveGlobalRoot3(), "cache", "embed");
|
|
2780
|
+
}
|
|
2676
2781
|
function buildEmbedInitOptions(modelName) {
|
|
2677
2782
|
return {
|
|
2678
2783
|
maxLength: 512,
|
|
2679
|
-
cacheDir: process.env.FABRIC_EMBED_CACHE_DIR,
|
|
2784
|
+
cacheDir: process.env.FABRIC_EMBED_CACHE_DIR ?? defaultEmbedCacheDir(),
|
|
2680
2785
|
...typeof modelName === "string" && modelName.length > 0 ? { model: modelName } : {}
|
|
2681
2786
|
};
|
|
2682
2787
|
}
|
|
@@ -2685,11 +2790,14 @@ async function loadEmbedder(modelName) {
|
|
|
2685
2790
|
embedderLoad = (async () => {
|
|
2686
2791
|
try {
|
|
2687
2792
|
const moduleName = OPTIONAL_EMBED_PACKAGE;
|
|
2688
|
-
const mod = await
|
|
2793
|
+
const mod = await embedderModuleLoader(moduleName);
|
|
2689
2794
|
if (mod?.FlagEmbedding?.init === void 0) {
|
|
2795
|
+
hintMissingEmbedderOnce();
|
|
2690
2796
|
return null;
|
|
2691
2797
|
}
|
|
2692
|
-
const
|
|
2798
|
+
const initOpts = buildEmbedInitOptions(modelName);
|
|
2799
|
+
mkdirSync(initOpts.cacheDir, { recursive: true });
|
|
2800
|
+
const model = await mod.FlagEmbedding.init(initOpts);
|
|
2693
2801
|
return {
|
|
2694
2802
|
async embed(texts) {
|
|
2695
2803
|
const out = [];
|
|
@@ -2702,6 +2810,7 @@ async function loadEmbedder(modelName) {
|
|
|
2702
2810
|
}
|
|
2703
2811
|
};
|
|
2704
2812
|
} catch {
|
|
2813
|
+
hintMissingEmbedderOnce();
|
|
2705
2814
|
return null;
|
|
2706
2815
|
}
|
|
2707
2816
|
})();
|
|
@@ -2732,7 +2841,7 @@ function cosineSimilarity(a, b) {
|
|
|
2732
2841
|
if (!Number.isFinite(sim)) {
|
|
2733
2842
|
return 0;
|
|
2734
2843
|
}
|
|
2735
|
-
return Math.max(
|
|
2844
|
+
return Math.max(0, Math.min(1, sim));
|
|
2736
2845
|
}
|
|
2737
2846
|
var docVectorCache = /* @__PURE__ */ new Map();
|
|
2738
2847
|
var DOC_VECTOR_CACHE_MAX = 1e4;
|
|
@@ -2837,56 +2946,28 @@ async function planContext(projectRoot, input) {
|
|
|
2837
2946
|
...input.known_tech ?? [],
|
|
2838
2947
|
...Object.values(input.detected_entities ?? {}).flat()
|
|
2839
2948
|
].join(" ");
|
|
2840
|
-
const scoringContext = {
|
|
2841
|
-
nowMs: Date.now(),
|
|
2842
|
-
targetPaths: input.target_paths ?? dedupePaths(input.paths),
|
|
2843
|
-
queryTerms: buildQueryTerms(queryText)
|
|
2844
|
-
};
|
|
2845
2949
|
const storeRawItems = await buildCrossStoreRawItems(projectRoot).catch(() => []);
|
|
2846
2950
|
const { rawItems: allRawItems, suppressedStableIds } = partitionEmptyShells(storeRawItems);
|
|
2847
2951
|
const effectiveLayerFilter = input.layer_filter ?? readDefaultLayerFilter(projectRoot);
|
|
2848
2952
|
const rawItems = effectiveLayerFilter === "both" ? allRawItems : allRawItems.filter((item) => item.description.knowledge_layer === effectiveLayerFilter);
|
|
2849
|
-
const
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
}
|
|
2853
|
-
scoringContext.docTexts = docTexts;
|
|
2854
|
-
if (scoringContext.queryTerms.length > 0 && rawItems.length > 0) {
|
|
2855
|
-
scoringContext.bm25 = getOrBuildBm25Model(revision, rawItems, docTexts);
|
|
2856
|
-
}
|
|
2857
|
-
scoringContext.scopeRank = buildScopeRankMap(rawItems, projectRoot);
|
|
2858
|
-
const embedConfig = readEmbedConfig(projectRoot);
|
|
2859
|
-
if (embedConfig.enabled && queryText.trim().length > 0 && rawItems.length > 0) {
|
|
2860
|
-
const embedder = await loadEmbedder(embedConfig.model);
|
|
2861
|
-
const vectorScores = await buildVectorScores(
|
|
2862
|
-
embedder,
|
|
2863
|
-
queryText,
|
|
2864
|
-
rawItems.map((item) => ({
|
|
2865
|
-
stable_id: item.stable_id,
|
|
2866
|
-
text: docTexts.get(item.stable_id) ?? documentTextForItem(item.description)
|
|
2867
|
-
}))
|
|
2868
|
-
);
|
|
2869
|
-
if (vectorScores !== null) {
|
|
2870
|
-
scoringContext.vectorScores = vectorScores;
|
|
2871
|
-
scoringContext.vectorWeight = embedConfig.weight;
|
|
2872
|
-
}
|
|
2873
|
-
}
|
|
2874
|
-
const scoredSorted = sortDescriptionItems(rawItems, scoringContext);
|
|
2875
|
-
const seenStableIds = /* @__PURE__ */ new Set();
|
|
2876
|
-
const rankedScored = scoredSorted.filter(({ item }) => {
|
|
2877
|
-
if (seenStableIds.has(item.stable_id)) return false;
|
|
2878
|
-
seenStableIds.add(item.stable_id);
|
|
2879
|
-
return true;
|
|
2953
|
+
const scoringContext = await buildScoringContext(projectRoot, revision, rawItems, {
|
|
2954
|
+
queryText,
|
|
2955
|
+
targetPaths: input.target_paths ?? dedupePaths(input.paths)
|
|
2880
2956
|
});
|
|
2957
|
+
const rankedScored = rankDescriptionItems(rawItems, scoringContext, "triage");
|
|
2881
2958
|
const rankedCandidates = rankedScored.map((entry) => entry.item);
|
|
2882
|
-
const
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
const maxScore = rankedScored.length > 0 ? rankedScored[0].score : 0;
|
|
2887
|
-
const relevanceFloor = maxScore * relevanceRatio;
|
|
2888
|
-
const survivingScored = hasQuery && maxScore > 0 && relevanceRatio > 0 ? cappedScored.filter((entry) => entry.score >= relevanceFloor) : cappedScored;
|
|
2959
|
+
const survivingScored = rankDescriptionItems(rawItems, scoringContext, "recall", {
|
|
2960
|
+
topK: readPlanContextTopK(projectRoot),
|
|
2961
|
+
relevanceRatio: readRecallRelevanceRatio(projectRoot)
|
|
2962
|
+
});
|
|
2889
2963
|
const topKCandidates = survivingScored.map((entry) => entry.item);
|
|
2964
|
+
const candidateScores = /* @__PURE__ */ new Map();
|
|
2965
|
+
for (const entry of survivingScored) {
|
|
2966
|
+
candidateScores.set(entry.item.stable_id, {
|
|
2967
|
+
score: entry.score,
|
|
2968
|
+
score_breakdown: scoreBreakdownForItem(entry.item, scoringContext)
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2890
2971
|
const topKIds = new Set(topKCandidates.map((item) => item.stable_id));
|
|
2891
2972
|
const retrievalDropped = rankedCandidates.filter((item) => !topKIds.has(item.stable_id)).map((item) => ({ id: item.stable_id, reason: "retrieval_budget" }));
|
|
2892
2973
|
const omittedCandidateCount = retrievalDropped.length;
|
|
@@ -2992,6 +3073,8 @@ async function planContext(projectRoot, input) {
|
|
|
2992
3073
|
// ONLY when at least one neighbour was actually appended. Empty (graph-empty
|
|
2993
3074
|
// no-op) → field omitted, steady-state wire shape unchanged.
|
|
2994
3075
|
...Object.keys(relatedAppended).length > 0 ? { related_appended: relatedAppended } : {},
|
|
3076
|
+
// P1 recall-observability: runtime-only score channel (Map → {} on the wire).
|
|
3077
|
+
...candidateScores.size > 0 ? { candidate_scores: candidateScores } : {},
|
|
2995
3078
|
...payloadTrimDropped > 0 ? { payload_trimmed: true } : {},
|
|
2996
3079
|
...payloadOverBudget ? { payload_over_budget: true } : {}
|
|
2997
3080
|
};
|
|
@@ -3080,10 +3163,38 @@ function partitionEmptyShells(items) {
|
|
|
3080
3163
|
}
|
|
3081
3164
|
var bm25ModelCache = null;
|
|
3082
3165
|
var bm25BuildCount = 0;
|
|
3083
|
-
|
|
3166
|
+
var BM25_CACHE_DIR = ".fabric/cache/bm25";
|
|
3167
|
+
function bm25CachePath(projectRoot, revision) {
|
|
3168
|
+
const safe = revision.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
3169
|
+
return join10(projectRoot, BM25_CACHE_DIR, `${safe}.json`);
|
|
3170
|
+
}
|
|
3171
|
+
async function loadBm25ModelFromDisk(projectRoot, revision) {
|
|
3172
|
+
try {
|
|
3173
|
+
const raw = await readFile5(bm25CachePath(projectRoot, revision), "utf8");
|
|
3174
|
+
const parsed = JSON.parse(raw);
|
|
3175
|
+
if (parsed.version !== 1) return null;
|
|
3176
|
+
return rehydrateBm25Model(parsed);
|
|
3177
|
+
} catch {
|
|
3178
|
+
return null;
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
async function saveBm25ModelToDisk(projectRoot, revision, model) {
|
|
3182
|
+
try {
|
|
3183
|
+
const path = bm25CachePath(projectRoot, revision);
|
|
3184
|
+
await mkdir4(join10(projectRoot, BM25_CACHE_DIR), { recursive: true });
|
|
3185
|
+
await writeFile2(path, JSON.stringify(serializeBm25Model(model)), "utf8");
|
|
3186
|
+
} catch {
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
async function getOrBuildBm25Model(projectRoot, revision, rawItems, docTexts) {
|
|
3084
3190
|
if (bm25ModelCache !== null && bm25ModelCache.revision === revision) {
|
|
3085
3191
|
return bm25ModelCache.model;
|
|
3086
3192
|
}
|
|
3193
|
+
const fromDisk = await loadBm25ModelFromDisk(projectRoot, revision);
|
|
3194
|
+
if (fromDisk !== null) {
|
|
3195
|
+
bm25ModelCache = { revision, model: fromDisk };
|
|
3196
|
+
return fromDisk;
|
|
3197
|
+
}
|
|
3087
3198
|
bm25BuildCount += 1;
|
|
3088
3199
|
const model = buildBm25Model(
|
|
3089
3200
|
rawItems.map((item) => ({
|
|
@@ -3092,6 +3203,7 @@ function getOrBuildBm25Model(revision, rawItems, docTexts) {
|
|
|
3092
3203
|
}))
|
|
3093
3204
|
);
|
|
3094
3205
|
bm25ModelCache = { revision, model };
|
|
3206
|
+
await saveBm25ModelToDisk(projectRoot, revision, model);
|
|
3095
3207
|
return model;
|
|
3096
3208
|
}
|
|
3097
3209
|
function compareStableIds(a, b) {
|
|
@@ -3137,6 +3249,51 @@ function compareScopeThenId(left, right, scopeRank) {
|
|
|
3137
3249
|
}
|
|
3138
3250
|
return compareStableIds(left.stable_id, right.stable_id);
|
|
3139
3251
|
}
|
|
3252
|
+
async function buildScoringContext(projectRoot, revision, rawItems, opts) {
|
|
3253
|
+
const scoringContext = {
|
|
3254
|
+
nowMs: Date.now(),
|
|
3255
|
+
targetPaths: opts.targetPaths,
|
|
3256
|
+
queryTerms: buildQueryTerms(opts.queryText)
|
|
3257
|
+
};
|
|
3258
|
+
const docTexts = /* @__PURE__ */ new Map();
|
|
3259
|
+
for (const item of rawItems) {
|
|
3260
|
+
docTexts.set(item.stable_id, documentTextForItem(item.description));
|
|
3261
|
+
}
|
|
3262
|
+
scoringContext.docTexts = docTexts;
|
|
3263
|
+
if (scoringContext.queryTerms.length > 0 && rawItems.length > 0) {
|
|
3264
|
+
scoringContext.bm25 = await getOrBuildBm25Model(projectRoot, revision, rawItems, docTexts);
|
|
3265
|
+
}
|
|
3266
|
+
scoringContext.scopeRank = buildScopeRankMap(rawItems, projectRoot);
|
|
3267
|
+
const embedConfig = readEmbedConfig(projectRoot);
|
|
3268
|
+
if (embedConfig.enabled && opts.queryText.trim().length > 0 && rawItems.length > 0) {
|
|
3269
|
+
const embedder = await loadEmbedder(embedConfig.model);
|
|
3270
|
+
const vectorScores = await buildVectorScores(
|
|
3271
|
+
embedder,
|
|
3272
|
+
opts.queryText,
|
|
3273
|
+
rawItems.map((item) => ({
|
|
3274
|
+
stable_id: item.stable_id,
|
|
3275
|
+
text: docTexts.get(item.stable_id) ?? documentTextForItem(item.description)
|
|
3276
|
+
}))
|
|
3277
|
+
);
|
|
3278
|
+
if (vectorScores !== null) {
|
|
3279
|
+
scoringContext.vectorScores = vectorScores;
|
|
3280
|
+
scoringContext.vectorWeight = embedConfig.weight;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
const configuredFusion = readFusion(projectRoot);
|
|
3284
|
+
const vectorActive = scoringContext.vectorScores !== void 0 && scoringContext.vectorScores.size > 0;
|
|
3285
|
+
scoringContext.fusion = configuredFusion === "auto" ? vectorActive ? "rrf" : "additive" : configuredFusion;
|
|
3286
|
+
if (scoringContext.fusion === "rrf" && scoringContext.queryTerms.length > 0 && rawItems.length > 0) {
|
|
3287
|
+
const rankIds = rawItems.map((item) => item.stable_id).sort((a, b) => compareStableIds(a, b));
|
|
3288
|
+
if (scoringContext.bm25 !== void 0) {
|
|
3289
|
+
scoringContext.bm25Ranks = rankDocuments(scoringContext.bm25, rankIds, scoringContext.queryTerms);
|
|
3290
|
+
}
|
|
3291
|
+
if (scoringContext.vectorScores !== void 0) {
|
|
3292
|
+
scoringContext.vectorRanks = rankByScore(rankIds, scoringContext.vectorScores);
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
return scoringContext;
|
|
3296
|
+
}
|
|
3140
3297
|
function sortDescriptionItems(rawItems, scoringContext) {
|
|
3141
3298
|
if (scoringContext === void 0) {
|
|
3142
3299
|
return [...rawItems].sort((left, right) => compareStableIds(left.stable_id, right.stable_id)).map((item) => ({ item, score: 0 }));
|
|
@@ -3151,6 +3308,25 @@ function sortDescriptionItems(rawItems, scoringContext) {
|
|
|
3151
3308
|
);
|
|
3152
3309
|
return scored;
|
|
3153
3310
|
}
|
|
3311
|
+
function rankDescriptionItems(items, scoringContext, mode, options = {}) {
|
|
3312
|
+
const sorted = sortDescriptionItems(items, scoringContext);
|
|
3313
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3314
|
+
const rankedScored = sorted.filter(({ item }) => {
|
|
3315
|
+
if (seen.has(item.stable_id)) return false;
|
|
3316
|
+
seen.add(item.stable_id);
|
|
3317
|
+
return true;
|
|
3318
|
+
});
|
|
3319
|
+
if (mode === "triage") {
|
|
3320
|
+
return rankedScored;
|
|
3321
|
+
}
|
|
3322
|
+
const topK = options.topK ?? rankedScored.length;
|
|
3323
|
+
const cappedScored = rankedScored.slice(0, topK);
|
|
3324
|
+
const relevanceRatio = options.relevanceRatio ?? 0;
|
|
3325
|
+
const hasQuery = scoringContext.queryTerms.length > 0;
|
|
3326
|
+
const maxScore = rankedScored.length > 0 ? rankedScored[0].score : 0;
|
|
3327
|
+
const relevanceFloor = maxScore * relevanceRatio;
|
|
3328
|
+
return hasQuery && maxScore > 0 && relevanceRatio > 0 ? cappedScored.filter((entry) => entry.score >= relevanceFloor) : cappedScored;
|
|
3329
|
+
}
|
|
3154
3330
|
function documentTextForItem(description) {
|
|
3155
3331
|
return [
|
|
3156
3332
|
description.summary,
|
|
@@ -3199,6 +3375,9 @@ var BM25_WEIGHT = 50;
|
|
|
3199
3375
|
var SALIENCE_PROVEN = 15;
|
|
3200
3376
|
var SALIENCE_VERIFIED = 8;
|
|
3201
3377
|
var SALIENCE_DRAFT = 0;
|
|
3378
|
+
var RRF_K = 10;
|
|
3379
|
+
var RRF_NORMALIZATION = 2e3;
|
|
3380
|
+
var RRF_STRUCTURAL_SCALE = 0.2;
|
|
3202
3381
|
function salienceScore(item) {
|
|
3203
3382
|
switch (item.description?.maturity) {
|
|
3204
3383
|
case "proven":
|
|
@@ -3209,35 +3388,87 @@ function salienceScore(item) {
|
|
|
3209
3388
|
return SALIENCE_DRAFT;
|
|
3210
3389
|
}
|
|
3211
3390
|
}
|
|
3212
|
-
function
|
|
3213
|
-
|
|
3214
|
-
if (context.
|
|
3215
|
-
|
|
3391
|
+
function contentScore(item, context) {
|
|
3392
|
+
const hasQuery = context.queryTerms.length > 0;
|
|
3393
|
+
if (context.fusion === "rrf" && hasQuery) {
|
|
3394
|
+
let rrf = 0;
|
|
3395
|
+
const bm25Rank = context.bm25Ranks?.get(item.stable_id);
|
|
3396
|
+
if (bm25Rank !== void 0) rrf += 1 / (RRF_K + bm25Rank);
|
|
3397
|
+
const vectorRank = context.vectorRanks?.get(item.stable_id);
|
|
3398
|
+
if (vectorRank !== void 0) rrf += 1 / (RRF_K + vectorRank);
|
|
3399
|
+
return RRF_NORMALIZATION * rrf;
|
|
3400
|
+
}
|
|
3401
|
+
let content = 0;
|
|
3402
|
+
if (context.bm25 !== void 0 && hasQuery) {
|
|
3403
|
+
content += BM25_WEIGHT * context.bm25.scoreDoc(item.stable_id, context.queryTerms);
|
|
3216
3404
|
}
|
|
3217
3405
|
if (context.vectorScores !== void 0) {
|
|
3218
|
-
|
|
3406
|
+
content += (context.vectorWeight ?? 0) * (context.vectorScores.get(item.stable_id) ?? 0);
|
|
3219
3407
|
}
|
|
3220
|
-
|
|
3408
|
+
return content;
|
|
3409
|
+
}
|
|
3410
|
+
function recencyBoost(item, context) {
|
|
3221
3411
|
const createdAtRaw = item.description?.created_at;
|
|
3222
3412
|
if (typeof createdAtRaw === "string" && createdAtRaw.length > 0) {
|
|
3223
3413
|
const createdMs = Date.parse(createdAtRaw);
|
|
3224
3414
|
if (Number.isFinite(createdMs) && context.nowMs - createdMs < RECENCY_WINDOW_MS) {
|
|
3225
|
-
|
|
3415
|
+
return RECENCY_BOOST;
|
|
3226
3416
|
}
|
|
3227
3417
|
}
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3418
|
+
return 0;
|
|
3419
|
+
}
|
|
3420
|
+
function localityBoost(item, context) {
|
|
3421
|
+
if (context.targetPaths.length === 0) return 0;
|
|
3422
|
+
const relevancePaths = item.description?.relevance_paths ?? [];
|
|
3423
|
+
let best = 0;
|
|
3424
|
+
outer: for (const rp of relevancePaths) {
|
|
3425
|
+
for (const tp of context.targetPaths) {
|
|
3426
|
+
const tier = localityTier(rp, tp);
|
|
3427
|
+
if (tier > best) best = tier;
|
|
3428
|
+
if (best === LOCALITY_SAME_FILE) break outer;
|
|
3237
3429
|
}
|
|
3238
|
-
score += best;
|
|
3239
3430
|
}
|
|
3240
|
-
return
|
|
3431
|
+
return best;
|
|
3432
|
+
}
|
|
3433
|
+
function structuralScaleFor(context) {
|
|
3434
|
+
return context.fusion === "rrf" && context.queryTerms.length > 0 ? RRF_STRUCTURAL_SCALE : 1;
|
|
3435
|
+
}
|
|
3436
|
+
function scoreDescriptionItem(item, context) {
|
|
3437
|
+
const content = contentScore(item, context);
|
|
3438
|
+
const structural = salienceScore(item) + recencyBoost(item, context) + localityBoost(item, context);
|
|
3439
|
+
return content + structuralScaleFor(context) * structural;
|
|
3440
|
+
}
|
|
3441
|
+
function scoreBreakdownForItem(item, context) {
|
|
3442
|
+
const hasQuery = context.queryTerms.length > 0;
|
|
3443
|
+
const rrfMode = context.fusion === "rrf" && hasQuery;
|
|
3444
|
+
let bm25 = 0;
|
|
3445
|
+
let vector = 0;
|
|
3446
|
+
let bm25Rank;
|
|
3447
|
+
let vectorRank;
|
|
3448
|
+
if (rrfMode) {
|
|
3449
|
+
bm25Rank = context.bm25Ranks?.get(item.stable_id);
|
|
3450
|
+
if (bm25Rank !== void 0) bm25 = RRF_NORMALIZATION * (1 / (RRF_K + bm25Rank));
|
|
3451
|
+
vectorRank = context.vectorRanks?.get(item.stable_id);
|
|
3452
|
+
if (vectorRank !== void 0) vector = RRF_NORMALIZATION * (1 / (RRF_K + vectorRank));
|
|
3453
|
+
} else {
|
|
3454
|
+
bm25 = context.bm25 !== void 0 && hasQuery ? BM25_WEIGHT * context.bm25.scoreDoc(item.stable_id, context.queryTerms) : 0;
|
|
3455
|
+
vector = context.vectorScores !== void 0 ? (context.vectorWeight ?? 0) * (context.vectorScores.get(item.stable_id) ?? 0) : 0;
|
|
3456
|
+
}
|
|
3457
|
+
const scale = structuralScaleFor(context);
|
|
3458
|
+
const salience = salienceScore(item) * scale;
|
|
3459
|
+
const recency = recencyBoost(item, context) * scale;
|
|
3460
|
+
const locality = localityBoost(item, context) * scale;
|
|
3461
|
+
const final = bm25 + vector + salience + recency + locality;
|
|
3462
|
+
return {
|
|
3463
|
+
final,
|
|
3464
|
+
...bm25 !== 0 ? { bm25 } : {},
|
|
3465
|
+
...bm25Rank !== void 0 ? { bm25_rank: bm25Rank } : {},
|
|
3466
|
+
...vector !== 0 ? { vector } : {},
|
|
3467
|
+
...vectorRank !== void 0 ? { vector_rank: vectorRank } : {},
|
|
3468
|
+
salience,
|
|
3469
|
+
recency,
|
|
3470
|
+
locality
|
|
3471
|
+
};
|
|
3241
3472
|
}
|
|
3242
3473
|
function localityTier(relevancePath, targetPath) {
|
|
3243
3474
|
if (relevancePath === targetPath) return LOCALITY_SAME_FILE;
|
|
@@ -3274,7 +3505,13 @@ function relatedLookupKeys(stableId) {
|
|
|
3274
3505
|
var RECALL_DIRECTIVE = "These entries are auto-accounted as citations for edits whose paths overlap this recall \u2014 no first-line cite needed. Speak up only to dismiss one you judge inapplicable: `dismissed: <id> (<reason>)`.";
|
|
3275
3506
|
async function recall(projectRoot, input) {
|
|
3276
3507
|
const planResult = await planContext(projectRoot, input);
|
|
3277
|
-
const {
|
|
3508
|
+
const {
|
|
3509
|
+
selection_token: _token,
|
|
3510
|
+
payload_trimmed: _pt,
|
|
3511
|
+
payload_over_budget: _pob,
|
|
3512
|
+
candidate_scores: candidateScores,
|
|
3513
|
+
...planRest
|
|
3514
|
+
} = planResult;
|
|
3278
3515
|
let bodyIndex;
|
|
3279
3516
|
try {
|
|
3280
3517
|
bodyIndex = await buildCrossStoreBodyIndex(projectRoot);
|
|
@@ -3323,13 +3560,15 @@ async function recall(projectRoot, input) {
|
|
|
3323
3560
|
const pathByStableId = new Map(paths.map((p) => [p.stable_id, p]));
|
|
3324
3561
|
const entries = planRest.candidates.map((c, index) => {
|
|
3325
3562
|
const readPath = pathByStableId.get(c.stable_id);
|
|
3563
|
+
const scored = candidateScores?.get(c.stable_id);
|
|
3326
3564
|
return {
|
|
3327
3565
|
stable_id: c.stable_id,
|
|
3328
3566
|
rank: index + 1,
|
|
3329
3567
|
description: c.description,
|
|
3330
3568
|
...readPath ? { read_path: readPath.path } : {},
|
|
3331
3569
|
...readPath?.store ? { store: readPath.store } : {},
|
|
3332
|
-
...isAlwaysActive(c) ? { body_in_context: true } : {}
|
|
3570
|
+
...isAlwaysActive(c) ? { body_in_context: true } : {},
|
|
3571
|
+
...scored ? { score: scored.score, score_breakdown: scored.score_breakdown } : {}
|
|
3333
3572
|
};
|
|
3334
3573
|
});
|
|
3335
3574
|
const { entries: _reqProfiles, candidates: _candidates, ...planRestNoLists } = planRest;
|
|
@@ -3679,9 +3918,9 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
|
|
|
3679
3918
|
// src/services/review.ts
|
|
3680
3919
|
import { execFileSync } from "child_process";
|
|
3681
3920
|
import { existsSync as existsSync5 } from "fs";
|
|
3682
|
-
import { readFile as
|
|
3921
|
+
import { readFile as readFile7, readdir as readdir2, stat as stat2, unlink as unlink2 } from "fs/promises";
|
|
3683
3922
|
import { homedir } from "os";
|
|
3684
|
-
import { basename, isAbsolute, join as
|
|
3923
|
+
import { basename, isAbsolute, join as join12, relative, resolve as resolve2, sep as sep2 } from "path";
|
|
3685
3924
|
|
|
3686
3925
|
// src/services/promotion-gate.ts
|
|
3687
3926
|
function toLocalId(id) {
|
|
@@ -3721,8 +3960,8 @@ import { allocateStoreKnowledgeId, isPersonalScope as isPersonalScope2 } from "@
|
|
|
3721
3960
|
|
|
3722
3961
|
// src/services/pending-dedupe.ts
|
|
3723
3962
|
import { existsSync as existsSync4 } from "fs";
|
|
3724
|
-
import { readdir, readFile as
|
|
3725
|
-
import { join as
|
|
3963
|
+
import { readdir, readFile as readFile6, unlink } from "fs/promises";
|
|
3964
|
+
import { join as join11 } from "path";
|
|
3726
3965
|
var PENDING_TYPES = ["decisions", "pitfalls", "guidelines", "models", "processes"];
|
|
3727
3966
|
var DISAMBIGUATION_SUFFIX = /^(.+)-([2-9])\.md$/u;
|
|
3728
3967
|
var SOURCE_SESSIONS_LINE = /^source_sessions:\s*\[(.*)\]\s*$/mu;
|
|
@@ -3795,7 +4034,7 @@ async function mergePendingTwins(projectRoot) {
|
|
|
3795
4034
|
continue;
|
|
3796
4035
|
}
|
|
3797
4036
|
for (const type of PENDING_TYPES) {
|
|
3798
|
-
const dir =
|
|
4037
|
+
const dir = join11(pendingBase2, type);
|
|
3799
4038
|
if (!existsSync4(dir)) continue;
|
|
3800
4039
|
let names;
|
|
3801
4040
|
try {
|
|
@@ -3816,10 +4055,10 @@ async function mergePendingTwins(projectRoot) {
|
|
|
3816
4055
|
if (groupNames.length < 2) continue;
|
|
3817
4056
|
const parsed = [];
|
|
3818
4057
|
for (const name of groupNames) {
|
|
3819
|
-
const abs =
|
|
4058
|
+
const abs = join11(dir, name);
|
|
3820
4059
|
let content;
|
|
3821
4060
|
try {
|
|
3822
|
-
content = await
|
|
4061
|
+
content = await readFile6(abs, "utf8");
|
|
3823
4062
|
} catch {
|
|
3824
4063
|
continue;
|
|
3825
4064
|
}
|
|
@@ -3919,7 +4158,7 @@ async function reviewPending(projectRoot, input) {
|
|
|
3919
4158
|
case "search":
|
|
3920
4159
|
return {
|
|
3921
4160
|
action: "search",
|
|
3922
|
-
items: await
|
|
4161
|
+
items: await triageSearch(projectRoot, input.query, input.filters)
|
|
3923
4162
|
};
|
|
3924
4163
|
default: {
|
|
3925
4164
|
const exhaustive = input;
|
|
@@ -3999,7 +4238,7 @@ async function listPending(projectRoot, filters) {
|
|
|
3999
4238
|
}
|
|
4000
4239
|
for (const source of sources) {
|
|
4001
4240
|
for (const type of typesToScan) {
|
|
4002
|
-
const dir =
|
|
4241
|
+
const dir = join12(source.root, type);
|
|
4003
4242
|
if (!existsSync5(dir)) {
|
|
4004
4243
|
continue;
|
|
4005
4244
|
}
|
|
@@ -4011,10 +4250,10 @@ async function listPending(projectRoot, filters) {
|
|
|
4011
4250
|
}
|
|
4012
4251
|
for (const name of entries) {
|
|
4013
4252
|
if (!name.endsWith(".md")) continue;
|
|
4014
|
-
const absolutePath =
|
|
4253
|
+
const absolutePath = join12(dir, name);
|
|
4015
4254
|
let content;
|
|
4016
4255
|
try {
|
|
4017
|
-
content = await
|
|
4256
|
+
content = await readFile7(absolutePath, "utf8");
|
|
4018
4257
|
} catch {
|
|
4019
4258
|
continue;
|
|
4020
4259
|
}
|
|
@@ -4124,7 +4363,7 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
4124
4363
|
let targetAbs;
|
|
4125
4364
|
let writtenTarget = false;
|
|
4126
4365
|
try {
|
|
4127
|
-
const content = await
|
|
4366
|
+
const content = await readFile7(sourceAbs, "utf8");
|
|
4128
4367
|
const fm = parseFrontmatter(content);
|
|
4129
4368
|
const pluralType = fm.type;
|
|
4130
4369
|
if (pluralType === void 0 || !PLURAL_TYPES.includes(pluralType)) {
|
|
@@ -4138,7 +4377,7 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
4138
4377
|
);
|
|
4139
4378
|
allocatedId = stableId;
|
|
4140
4379
|
const newFilename = `${stableId}--${slug}.md`;
|
|
4141
|
-
targetAbs =
|
|
4380
|
+
targetAbs = join12(resolveStoreCanonicalBase(layer, projectRoot), pluralType, newFilename);
|
|
4142
4381
|
await ensureParentDirectory(targetAbs);
|
|
4143
4382
|
const rewritten = rewriteFrontmatterMerge(
|
|
4144
4383
|
rewriteFrontmatterForPromote(content, stableId),
|
|
@@ -4196,7 +4435,7 @@ async function rejectAll(projectRoot, pendingPaths, reason) {
|
|
|
4196
4435
|
try {
|
|
4197
4436
|
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
4198
4437
|
if (existsSync5(sandboxed.abs)) {
|
|
4199
|
-
const content = await
|
|
4438
|
+
const content = await readFile7(sandboxed.abs, "utf8");
|
|
4200
4439
|
const merged = rewriteFrontmatterMerge(content, { status: "rejected" });
|
|
4201
4440
|
const rejectedAbs = sandboxed.abs.includes(`${sep2}pending${sep2}`) ? sandboxed.abs.replace(`${sep2}pending${sep2}`, `${sep2}rejected${sep2}`) : null;
|
|
4202
4441
|
if (rejectedAbs !== null) {
|
|
@@ -4223,7 +4462,7 @@ async function modifyEntry(projectRoot, pendingPath, changes) {
|
|
|
4223
4462
|
if (target === null) {
|
|
4224
4463
|
throw new Error(`modify target not found: ${pendingPath}`);
|
|
4225
4464
|
}
|
|
4226
|
-
const content = await
|
|
4465
|
+
const content = await readFile7(target.absPath, "utf8");
|
|
4227
4466
|
const fm = parseFrontmatter(content);
|
|
4228
4467
|
const currentLayer = fm.layer ?? "team";
|
|
4229
4468
|
if (fm.maturity === "verified" && changes.maturity === "proven" && fm.id !== void 0) {
|
|
@@ -4325,7 +4564,7 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
|
4325
4564
|
pluralType,
|
|
4326
4565
|
resolveWriteTargetStoreDir(toLayer, projectRoot)
|
|
4327
4566
|
);
|
|
4328
|
-
const toAbs =
|
|
4567
|
+
const toAbs = join12(
|
|
4329
4568
|
resolveStoreCanonicalBase(toLayer, projectRoot),
|
|
4330
4569
|
pluralType,
|
|
4331
4570
|
`${newStableId}--${slug}.md`
|
|
@@ -4425,7 +4664,7 @@ function getSearchDirectoryCache(cacheKey) {
|
|
|
4425
4664
|
return created;
|
|
4426
4665
|
}
|
|
4427
4666
|
async function listIndexedSearchEntries(source, type) {
|
|
4428
|
-
const dir =
|
|
4667
|
+
const dir = join12(source.root, type);
|
|
4429
4668
|
let entries;
|
|
4430
4669
|
try {
|
|
4431
4670
|
entries = await readdir2(dir);
|
|
@@ -4438,7 +4677,7 @@ async function listIndexedSearchEntries(source, type) {
|
|
|
4438
4677
|
const indexed = [];
|
|
4439
4678
|
for (const name of entries) {
|
|
4440
4679
|
if (!name.endsWith(".md")) continue;
|
|
4441
|
-
const absolutePath =
|
|
4680
|
+
const absolutePath = join12(dir, name);
|
|
4442
4681
|
let fingerprint;
|
|
4443
4682
|
try {
|
|
4444
4683
|
const st = await stat2(absolutePath);
|
|
@@ -4455,7 +4694,7 @@ async function listIndexedSearchEntries(source, type) {
|
|
|
4455
4694
|
}
|
|
4456
4695
|
let content;
|
|
4457
4696
|
try {
|
|
4458
|
-
content = await
|
|
4697
|
+
content = await readFile7(absolutePath, "utf8");
|
|
4459
4698
|
searchEntryIndexContentReads += 1;
|
|
4460
4699
|
} catch {
|
|
4461
4700
|
directoryCache.files.delete(absolutePath);
|
|
@@ -4482,9 +4721,41 @@ async function listIndexedSearchEntries(source, type) {
|
|
|
4482
4721
|
}
|
|
4483
4722
|
return indexed;
|
|
4484
4723
|
}
|
|
4485
|
-
|
|
4724
|
+
function matchesTriageQuery(indexed, lowerQuery, includeBody) {
|
|
4725
|
+
const haystacks = [
|
|
4726
|
+
indexed.fm.title ?? "",
|
|
4727
|
+
indexed.fm.summary ?? "",
|
|
4728
|
+
...indexed.fm.tags ?? [],
|
|
4729
|
+
indexed.name,
|
|
4730
|
+
includeBody ? indexed.body : ""
|
|
4731
|
+
].map((s) => s.toLowerCase());
|
|
4732
|
+
return haystacks.some((h) => h.includes(lowerQuery));
|
|
4733
|
+
}
|
|
4734
|
+
function pendingEntryToRankerItem(indexed) {
|
|
4735
|
+
const { fm, name } = indexed;
|
|
4736
|
+
const slug = name.replace(/\.md$/u, "");
|
|
4737
|
+
const summary = fm.summary ?? fm.title ?? slug;
|
|
4738
|
+
const description = {
|
|
4739
|
+
summary,
|
|
4740
|
+
intent_clues: [],
|
|
4741
|
+
tech_stack: [],
|
|
4742
|
+
impact: [],
|
|
4743
|
+
must_read_if: fm.title ?? summary,
|
|
4744
|
+
...fm.id !== void 0 ? { id: fm.id } : {},
|
|
4745
|
+
...fm.type !== void 0 ? { knowledge_type: fm.type } : {},
|
|
4746
|
+
maturity: fm.maturity ?? "draft",
|
|
4747
|
+
knowledge_layer: indexed.layer,
|
|
4748
|
+
...fm.semantic_scope !== void 0 ? { semantic_scope: fm.semantic_scope } : {},
|
|
4749
|
+
...fm.created_at !== void 0 ? { created_at: fm.created_at } : {},
|
|
4750
|
+
tags: fm.tags ?? [],
|
|
4751
|
+
relevance_scope: fm.relevance_scope ?? "broad",
|
|
4752
|
+
relevance_paths: fm.relevance_paths ?? []
|
|
4753
|
+
};
|
|
4754
|
+
return { stable_id: indexed.absolutePath, description };
|
|
4755
|
+
}
|
|
4756
|
+
async function triageSearch(projectRoot, query, filters) {
|
|
4486
4757
|
const lowerQuery = query.toLowerCase();
|
|
4487
|
-
const
|
|
4758
|
+
const includeBody = filters?.include_body === true;
|
|
4488
4759
|
const sources = [];
|
|
4489
4760
|
for (const layer of ["team", "personal"]) {
|
|
4490
4761
|
const isPersonal = layer === "personal";
|
|
@@ -4507,10 +4778,12 @@ async function searchEntries(projectRoot, query, filters) {
|
|
|
4507
4778
|
}
|
|
4508
4779
|
}
|
|
4509
4780
|
const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
|
|
4781
|
+
const matchedByKey = /* @__PURE__ */ new Map();
|
|
4782
|
+
const rankerItems = [];
|
|
4510
4783
|
for (const source of sources) {
|
|
4511
4784
|
for (const type of typesToScan) {
|
|
4512
4785
|
for (const indexed of await listIndexedSearchEntries(source, type)) {
|
|
4513
|
-
const {
|
|
4786
|
+
const { fm, layer, maturity } = indexed;
|
|
4514
4787
|
if (filters?.layer !== void 0 && filters.layer !== "both" && filters.layer !== layer) {
|
|
4515
4788
|
continue;
|
|
4516
4789
|
}
|
|
@@ -4531,42 +4804,56 @@ async function searchEntries(projectRoot, query, filters) {
|
|
|
4531
4804
|
if (!isVisibleByLifecycle(fm, filters)) {
|
|
4532
4805
|
continue;
|
|
4533
4806
|
}
|
|
4534
|
-
|
|
4535
|
-
const
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
...fm.tags ?? [],
|
|
4539
|
-
name,
|
|
4540
|
-
bodyForSearch
|
|
4541
|
-
].map((s) => s.toLowerCase());
|
|
4542
|
-
const matches = haystacks.some((h) => h.includes(lowerQuery));
|
|
4543
|
-
if (!matches) continue;
|
|
4544
|
-
const reportedPath = source.isStore ? absolutePath : source.isPersonal ? `~/${relative(resolvePersonalRoot(), absolutePath)}` : relative(projectRoot, absolutePath);
|
|
4545
|
-
items.push({
|
|
4546
|
-
area: source.isPending ? "pending" : "canonical",
|
|
4547
|
-
path: reportedPath,
|
|
4548
|
-
...source.isPersonal ? { path_absolute: absolutePath } : {},
|
|
4549
|
-
type,
|
|
4550
|
-
layer,
|
|
4551
|
-
maturity,
|
|
4552
|
-
// Only pending entries carry an origin tag (canonical hits live
|
|
4553
|
-
// outside the dual-pending-root convention).
|
|
4554
|
-
...source.isPending ? { origin: source.isPersonal ? "personal" : "team" } : {},
|
|
4555
|
-
...fm.tags !== void 0 && fm.tags.length > 0 ? { tags: fm.tags } : {},
|
|
4556
|
-
...fm.title !== void 0 ? { title: fm.title } : {},
|
|
4557
|
-
...fm.summary !== void 0 ? { summary: fm.summary } : {},
|
|
4558
|
-
...fm.status !== void 0 ? { status: fm.status } : {},
|
|
4559
|
-
...fm.deferred_until !== void 0 ? { deferred_until: fm.deferred_until } : {},
|
|
4560
|
-
// v2.0.0-rc.27 TASK-006 (audit §2.23): body emission when opted in.
|
|
4561
|
-
...filters?.include_body === true ? { body: bodyForSearch } : {},
|
|
4562
|
-
// Canonical hits always have an id; pending hits typically don't
|
|
4563
|
-
// yet — surface the frontmatter id when present so consumers can
|
|
4564
|
-
// dedupe across runs.
|
|
4565
|
-
...fm.id !== void 0 ? { stable_id: fm.id } : {}
|
|
4566
|
-
});
|
|
4807
|
+
if (!matchesTriageQuery(indexed, lowerQuery, includeBody)) continue;
|
|
4808
|
+
const item = pendingEntryToRankerItem(indexed);
|
|
4809
|
+
matchedByKey.set(item.stable_id, { indexed, source, type });
|
|
4810
|
+
rankerItems.push(item);
|
|
4567
4811
|
}
|
|
4568
4812
|
}
|
|
4569
4813
|
}
|
|
4814
|
+
if (rankerItems.length === 0) {
|
|
4815
|
+
return [];
|
|
4816
|
+
}
|
|
4817
|
+
let revision;
|
|
4818
|
+
try {
|
|
4819
|
+
revision = await computeReadSetRevision(projectRoot);
|
|
4820
|
+
} catch {
|
|
4821
|
+
revision = "triage-search";
|
|
4822
|
+
}
|
|
4823
|
+
const scoringContext = await buildScoringContext(projectRoot, revision, rankerItems, {
|
|
4824
|
+
queryText: query,
|
|
4825
|
+
targetPaths: []
|
|
4826
|
+
});
|
|
4827
|
+
const ranked = rankDescriptionItems(rankerItems, scoringContext, "triage");
|
|
4828
|
+
const items = [];
|
|
4829
|
+
for (const { item } of ranked) {
|
|
4830
|
+
const match = matchedByKey.get(item.stable_id);
|
|
4831
|
+
if (match === void 0) continue;
|
|
4832
|
+
const { indexed, source, type } = match;
|
|
4833
|
+
const { absolutePath, fm, layer, maturity } = indexed;
|
|
4834
|
+
const reportedPath = source.isStore ? absolutePath : source.isPersonal ? `~/${relative(resolvePersonalRoot(), absolutePath)}` : relative(projectRoot, absolutePath);
|
|
4835
|
+
items.push({
|
|
4836
|
+
area: source.isPending ? "pending" : "canonical",
|
|
4837
|
+
path: reportedPath,
|
|
4838
|
+
...source.isPersonal ? { path_absolute: absolutePath } : {},
|
|
4839
|
+
type,
|
|
4840
|
+
layer,
|
|
4841
|
+
maturity,
|
|
4842
|
+
// Only pending entries carry an origin tag (canonical hits live
|
|
4843
|
+
// outside the dual-pending-root convention).
|
|
4844
|
+
...source.isPending ? { origin: source.isPersonal ? "personal" : "team" } : {},
|
|
4845
|
+
...fm.tags !== void 0 && fm.tags.length > 0 ? { tags: fm.tags } : {},
|
|
4846
|
+
...fm.title !== void 0 ? { title: fm.title } : {},
|
|
4847
|
+
...fm.summary !== void 0 ? { summary: fm.summary } : {},
|
|
4848
|
+
...fm.status !== void 0 ? { status: fm.status } : {},
|
|
4849
|
+
...fm.deferred_until !== void 0 ? { deferred_until: fm.deferred_until } : {},
|
|
4850
|
+
// v2.0.0-rc.27 TASK-006 (audit §2.23): body emission when opted in.
|
|
4851
|
+
...includeBody ? { body: indexed.body } : {},
|
|
4852
|
+
// Canonical hits always have an id; pending hits typically don't yet —
|
|
4853
|
+
// surface the frontmatter id when present so consumers can dedupe.
|
|
4854
|
+
...fm.id !== void 0 ? { stable_id: fm.id } : {}
|
|
4855
|
+
});
|
|
4856
|
+
}
|
|
4570
4857
|
return items;
|
|
4571
4858
|
}
|
|
4572
4859
|
async function deferAll(projectRoot, pendingPaths, until, reason) {
|
|
@@ -4576,7 +4863,7 @@ async function deferAll(projectRoot, pendingPaths, until, reason) {
|
|
|
4576
4863
|
try {
|
|
4577
4864
|
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
4578
4865
|
if (existsSync5(sandboxed.abs)) {
|
|
4579
|
-
const content = await
|
|
4866
|
+
const content = await readFile7(sandboxed.abs, "utf8");
|
|
4580
4867
|
stableId = parseFrontmatter(content).id;
|
|
4581
4868
|
const patch = {
|
|
4582
4869
|
status: "deferred",
|
|
@@ -4917,9 +5204,9 @@ function registerPending(server, tracker) {
|
|
|
4917
5204
|
}
|
|
4918
5205
|
|
|
4919
5206
|
// src/services/doctor.ts
|
|
4920
|
-
import { access as access4, readFile as
|
|
5207
|
+
import { access as access4, readFile as readFile17, readdir as readdirAsync, stat as statAsync, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
|
|
4921
5208
|
import { constants as constants2 } from "fs";
|
|
4922
|
-
import { isAbsolute as isAbsolute2, join as
|
|
5209
|
+
import { isAbsolute as isAbsolute2, join as join23, posix as posix5, resolve as resolve3 } from "path";
|
|
4923
5210
|
import {
|
|
4924
5211
|
createTranslator,
|
|
4925
5212
|
forensicReportSchema,
|
|
@@ -5133,13 +5420,13 @@ function createKnowledgeSummaryOpaqueCheck(t, inspection) {
|
|
|
5133
5420
|
}
|
|
5134
5421
|
|
|
5135
5422
|
// src/services/doctor-stable-id-collision.ts
|
|
5136
|
-
import { readFile as
|
|
5137
|
-
import { basename as basename2, join as
|
|
5423
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
5424
|
+
import { basename as basename2, join as join13 } from "path";
|
|
5138
5425
|
import {
|
|
5139
5426
|
buildStoreResolveInput as buildStoreResolveInput4,
|
|
5140
5427
|
createStoreResolver as createStoreResolver4,
|
|
5141
5428
|
readKnowledgeAcrossStores as readKnowledgeAcrossStores2,
|
|
5142
|
-
resolveGlobalRoot as
|
|
5429
|
+
resolveGlobalRoot as resolveGlobalRoot4,
|
|
5143
5430
|
storeRelativePathForMount as storeRelativePathForMount3
|
|
5144
5431
|
} from "@fenglimg/fabric-shared";
|
|
5145
5432
|
var ID_LINE = /^id:\s*"?([^"\n]+?)"?\s*$/mu;
|
|
@@ -5155,13 +5442,13 @@ function resolveIntegrityStores(projectRoot) {
|
|
|
5155
5442
|
const personalUuids = new Set(
|
|
5156
5443
|
input.mountedStores.filter((s) => s.personal).map((s) => s.store_uuid)
|
|
5157
5444
|
);
|
|
5158
|
-
const globalRoot =
|
|
5445
|
+
const globalRoot = resolveGlobalRoot4();
|
|
5159
5446
|
const dirs = readSet.stores.map((entry) => {
|
|
5160
5447
|
const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
|
|
5161
5448
|
return {
|
|
5162
5449
|
store_uuid: entry.store_uuid,
|
|
5163
5450
|
alias: entry.alias,
|
|
5164
|
-
dir:
|
|
5451
|
+
dir: join13(globalRoot, storeRelativePathForMount3(mounted ?? { store_uuid: entry.store_uuid }))
|
|
5165
5452
|
};
|
|
5166
5453
|
});
|
|
5167
5454
|
return { dirs, personalUuids };
|
|
@@ -5180,7 +5467,7 @@ async function inspectStoreStableIdIntegrity(projectRoot) {
|
|
|
5180
5467
|
for (const ref of await readKnowledgeAcrossStores2(resolved.dirs)) {
|
|
5181
5468
|
let source;
|
|
5182
5469
|
try {
|
|
5183
|
-
source = await
|
|
5470
|
+
source = await readFile8(ref.file, "utf8");
|
|
5184
5471
|
} catch {
|
|
5185
5472
|
continue;
|
|
5186
5473
|
}
|
|
@@ -5265,7 +5552,7 @@ function createLayerMismatchCheck(t, inspection) {
|
|
|
5265
5552
|
// src/services/doctor-relevance-paths.ts
|
|
5266
5553
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
5267
5554
|
import { existsSync as existsSync6, readdirSync, statSync as statSync3 } from "fs";
|
|
5268
|
-
import { join as
|
|
5555
|
+
import { join as join14, sep as sep3 } from "path";
|
|
5269
5556
|
import { minimatch } from "minimatch";
|
|
5270
5557
|
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
5271
5558
|
var RELEVANCE_PATHS_DRIFT_WINDOW_DAYS = 90;
|
|
@@ -5308,7 +5595,7 @@ function collectWorkspacePaths(projectRoot) {
|
|
|
5308
5595
|
continue;
|
|
5309
5596
|
}
|
|
5310
5597
|
for (const entry of entries) {
|
|
5311
|
-
const abs =
|
|
5598
|
+
const abs = join14(current, entry.name);
|
|
5312
5599
|
const rel = toPosix(abs.slice(projectRoot.length + 1));
|
|
5313
5600
|
if (rel.length === 0) continue;
|
|
5314
5601
|
if (entry.isDirectory()) {
|
|
@@ -5501,16 +5788,16 @@ function createNarrowNoPathsCheck(t, inspection) {
|
|
|
5501
5788
|
}
|
|
5502
5789
|
|
|
5503
5790
|
// src/services/doctor-broad-index.ts
|
|
5504
|
-
import { readFile as
|
|
5505
|
-
import { join as
|
|
5791
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
5792
|
+
import { join as join15 } from "path";
|
|
5506
5793
|
var DEFAULT_BROAD_INDEX_BACKSTOP = 50;
|
|
5507
5794
|
var BROAD_INDEX_BACKSTOP_MIN = 20;
|
|
5508
5795
|
var BROAD_INDEX_BACKSTOP_MAX = 500;
|
|
5509
5796
|
var BROAD_INDEX_DRIFT_RATIO = 0.8;
|
|
5510
5797
|
async function readBroadIndexBackstop(projectRoot) {
|
|
5511
|
-
const configPath =
|
|
5798
|
+
const configPath = join15(projectRoot, ".fabric", "fabric-config.json");
|
|
5512
5799
|
try {
|
|
5513
|
-
const raw = await
|
|
5800
|
+
const raw = await readFile9(configPath, "utf8");
|
|
5514
5801
|
const parsed = JSON.parse(raw);
|
|
5515
5802
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
5516
5803
|
const v = parsed.broad_index_backstop;
|
|
@@ -5827,15 +6114,15 @@ function createBroadReviewRecheckCheck(t, inspection) {
|
|
|
5827
6114
|
}
|
|
5828
6115
|
|
|
5829
6116
|
// src/services/doctor-scope-lint.ts
|
|
5830
|
-
import { readFile as
|
|
5831
|
-
import { join as
|
|
6117
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
6118
|
+
import { join as join16 } from "path";
|
|
5832
6119
|
import {
|
|
5833
6120
|
buildStoreResolveInput as buildStoreResolveInput5,
|
|
5834
6121
|
createStoreResolver as createStoreResolver5,
|
|
5835
6122
|
isPersonalScope as isPersonalScope3,
|
|
5836
6123
|
readKnowledgeAcrossStores as readKnowledgeAcrossStores3,
|
|
5837
6124
|
readStoreProjects,
|
|
5838
|
-
resolveGlobalRoot as
|
|
6125
|
+
resolveGlobalRoot as resolveGlobalRoot5,
|
|
5839
6126
|
scopeRoot as scopeRoot2,
|
|
5840
6127
|
storeRelativePathForMount as storeRelativePathForMount4
|
|
5841
6128
|
} from "@fenglimg/fabric-shared";
|
|
@@ -5860,10 +6147,10 @@ async function resolveLintStores(projectRoot) {
|
|
|
5860
6147
|
const personalUuids = new Set(
|
|
5861
6148
|
input.mountedStores.filter((s) => s.personal).map((s) => s.store_uuid)
|
|
5862
6149
|
);
|
|
5863
|
-
const globalRoot =
|
|
6150
|
+
const globalRoot = resolveGlobalRoot5();
|
|
5864
6151
|
return Promise.all(readSet.stores.map(async (entry) => {
|
|
5865
6152
|
const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
|
|
5866
|
-
const dir =
|
|
6153
|
+
const dir = join16(
|
|
5867
6154
|
globalRoot,
|
|
5868
6155
|
storeRelativePathForMount4(mounted ?? { store_uuid: entry.store_uuid })
|
|
5869
6156
|
);
|
|
@@ -5895,7 +6182,7 @@ async function lintStoreScopes(projectRoot) {
|
|
|
5895
6182
|
}
|
|
5896
6183
|
let source;
|
|
5897
6184
|
try {
|
|
5898
|
-
source = await
|
|
6185
|
+
source = await readFile10(ref.file, "utf8");
|
|
5899
6186
|
} catch {
|
|
5900
6187
|
continue;
|
|
5901
6188
|
}
|
|
@@ -6018,7 +6305,7 @@ function createUnboundProjectCheck(t, violation) {
|
|
|
6018
6305
|
|
|
6019
6306
|
// src/services/doctor-store-counters.ts
|
|
6020
6307
|
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
6021
|
-
import { join as
|
|
6308
|
+
import { join as join17 } from "path";
|
|
6022
6309
|
import {
|
|
6023
6310
|
buildStoreResolveInput as buildStoreResolveInput6,
|
|
6024
6311
|
createStoreResolver as createStoreResolver6,
|
|
@@ -6027,7 +6314,7 @@ import {
|
|
|
6027
6314
|
parseKnowledgeId as parseKnowledgeId2,
|
|
6028
6315
|
readStoreCounters,
|
|
6029
6316
|
reconcileStoreCounters,
|
|
6030
|
-
resolveGlobalRoot as
|
|
6317
|
+
resolveGlobalRoot as resolveGlobalRoot6,
|
|
6031
6318
|
STORE_KNOWLEDGE_TYPE_DIRS,
|
|
6032
6319
|
STORE_LAYOUT as STORE_LAYOUT2,
|
|
6033
6320
|
storeRelativePathForMount as storeRelativePathForMount5
|
|
@@ -6041,11 +6328,11 @@ function resolveCounterStores(projectRoot) {
|
|
|
6041
6328
|
if (readSet.stores.length === 0) {
|
|
6042
6329
|
return [];
|
|
6043
6330
|
}
|
|
6044
|
-
const globalRoot =
|
|
6331
|
+
const globalRoot = resolveGlobalRoot6();
|
|
6045
6332
|
return readSet.stores.map((entry) => ({
|
|
6046
6333
|
uuid: entry.store_uuid,
|
|
6047
6334
|
alias: entry.alias,
|
|
6048
|
-
dir:
|
|
6335
|
+
dir: join17(
|
|
6049
6336
|
globalRoot,
|
|
6050
6337
|
storeRelativePathForMount5(
|
|
6051
6338
|
input.mountedStores.find((s) => s.store_uuid === entry.store_uuid) ?? {
|
|
@@ -6073,7 +6360,7 @@ function readEntryId(file) {
|
|
|
6073
6360
|
function computeStoreDiskMax(storeDir) {
|
|
6074
6361
|
const max = defaultAgentsMetaCounters();
|
|
6075
6362
|
for (const type of STORE_KNOWLEDGE_TYPE_DIRS) {
|
|
6076
|
-
const dir =
|
|
6363
|
+
const dir = join17(storeDir, STORE_LAYOUT2.knowledgeDir, type);
|
|
6077
6364
|
if (!existsSync7(dir)) {
|
|
6078
6365
|
continue;
|
|
6079
6366
|
}
|
|
@@ -6087,7 +6374,7 @@ function computeStoreDiskMax(storeDir) {
|
|
|
6087
6374
|
if (!name.endsWith(".md")) {
|
|
6088
6375
|
continue;
|
|
6089
6376
|
}
|
|
6090
|
-
const parsed = parseKnowledgeId2(readEntryId(
|
|
6377
|
+
const parsed = parseKnowledgeId2(readEntryId(join17(dir, name)) ?? "");
|
|
6091
6378
|
if (parsed === null) {
|
|
6092
6379
|
continue;
|
|
6093
6380
|
}
|
|
@@ -6170,14 +6457,14 @@ function createStoreCounterCheck(t, drifts) {
|
|
|
6170
6457
|
|
|
6171
6458
|
// src/services/doctor-store-orphan.ts
|
|
6172
6459
|
import { readdirSync as readdirSync3 } from "fs";
|
|
6173
|
-
import { join as
|
|
6460
|
+
import { join as join18 } from "path";
|
|
6174
6461
|
import {
|
|
6175
6462
|
STORES_ROOT_DIR,
|
|
6176
6463
|
addMountedStore,
|
|
6177
6464
|
disambiguateAlias,
|
|
6178
6465
|
loadGlobalConfig,
|
|
6179
6466
|
readStoreIdentity,
|
|
6180
|
-
resolveGlobalRoot as
|
|
6467
|
+
resolveGlobalRoot as resolveGlobalRoot7,
|
|
6181
6468
|
saveGlobalConfig
|
|
6182
6469
|
} from "@fenglimg/fabric-shared";
|
|
6183
6470
|
var STORE_BY_ALIAS_DIR = "by-alias";
|
|
@@ -6188,21 +6475,21 @@ function listDir(dir) {
|
|
|
6188
6475
|
return [];
|
|
6189
6476
|
}
|
|
6190
6477
|
}
|
|
6191
|
-
function inspectStoreOrphans(globalRoot =
|
|
6478
|
+
function inspectStoreOrphans(globalRoot = resolveGlobalRoot7()) {
|
|
6192
6479
|
const config = loadGlobalConfig(globalRoot);
|
|
6193
6480
|
if (config === null) {
|
|
6194
6481
|
return [];
|
|
6195
6482
|
}
|
|
6196
6483
|
const registered = new Set(config.stores.map((s) => s.store_uuid));
|
|
6197
|
-
const storesRoot =
|
|
6484
|
+
const storesRoot = join18(globalRoot, STORES_ROOT_DIR);
|
|
6198
6485
|
const orphans = [];
|
|
6199
6486
|
for (const group of listDir(storesRoot)) {
|
|
6200
6487
|
if (group === STORE_BY_ALIAS_DIR) {
|
|
6201
6488
|
continue;
|
|
6202
6489
|
}
|
|
6203
|
-
const groupDir =
|
|
6490
|
+
const groupDir = join18(storesRoot, group);
|
|
6204
6491
|
for (const mount of listDir(groupDir)) {
|
|
6205
|
-
const dir =
|
|
6492
|
+
const dir = join18(groupDir, mount);
|
|
6206
6493
|
const identity = readStoreIdentity(dir);
|
|
6207
6494
|
if (identity === null || registered.has(identity.store_uuid)) {
|
|
6208
6495
|
continue;
|
|
@@ -6212,7 +6499,7 @@ function inspectStoreOrphans(globalRoot = resolveGlobalRoot6()) {
|
|
|
6212
6499
|
}
|
|
6213
6500
|
return orphans;
|
|
6214
6501
|
}
|
|
6215
|
-
function fixStoreOrphans(globalRoot =
|
|
6502
|
+
function fixStoreOrphans(globalRoot = resolveGlobalRoot7()) {
|
|
6216
6503
|
let config = loadGlobalConfig(globalRoot);
|
|
6217
6504
|
if (config === null) {
|
|
6218
6505
|
return [];
|
|
@@ -6339,8 +6626,8 @@ async function inspectEventsJsonlGates(projectRoot, options = {}) {
|
|
|
6339
6626
|
}
|
|
6340
6627
|
|
|
6341
6628
|
// src/services/doctor-skill-lints.ts
|
|
6342
|
-
import { readdir as readdir3, readFile as
|
|
6343
|
-
import { join as
|
|
6629
|
+
import { readdir as readdir3, readFile as readFile11 } from "fs/promises";
|
|
6630
|
+
import { join as join19, posix as posix2 } from "path";
|
|
6344
6631
|
var FABRIC_SKILL_SLUGS = ["fabric-archive", "fabric-review"];
|
|
6345
6632
|
var SKILL_MD_FRONTMATTER_ROOTS = [".claude/skills", ".codex/skills"];
|
|
6346
6633
|
var SKILL_FRONTMATTER_KEY_PATTERN = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]+(.+?)[ \t]*$/u;
|
|
@@ -6370,8 +6657,8 @@ async function listMarkdownFiles(dir) {
|
|
|
6370
6657
|
async function inspectSkillRefMirror(projectRoot) {
|
|
6371
6658
|
const driftedPaths = [];
|
|
6372
6659
|
for (const slug of FABRIC_SKILL_SLUGS) {
|
|
6373
|
-
const claudeRef =
|
|
6374
|
-
const codexRef =
|
|
6660
|
+
const claudeRef = join19(projectRoot, ".claude", "skills", slug, "ref");
|
|
6661
|
+
const codexRef = join19(projectRoot, ".codex", "skills", slug, "ref");
|
|
6375
6662
|
const [claudeFiles, codexFiles] = await Promise.all([listMarkdownFiles(claudeRef), listMarkdownFiles(codexRef)]);
|
|
6376
6663
|
if (claudeFiles === null || codexFiles === null) continue;
|
|
6377
6664
|
const claudeSet = new Set(claudeFiles);
|
|
@@ -6388,8 +6675,8 @@ async function inspectSkillRefMirror(projectRoot) {
|
|
|
6388
6675
|
let codexBody;
|
|
6389
6676
|
try {
|
|
6390
6677
|
[claudeBody, codexBody] = await Promise.all([
|
|
6391
|
-
|
|
6392
|
-
|
|
6678
|
+
readFile11(join19(claudeRef, fname), "utf8"),
|
|
6679
|
+
readFile11(join19(codexRef, fname), "utf8")
|
|
6393
6680
|
]);
|
|
6394
6681
|
} catch {
|
|
6395
6682
|
continue;
|
|
@@ -6408,10 +6695,10 @@ async function inspectSkillTokenBudget(projectRoot) {
|
|
|
6408
6695
|
const overSize = [];
|
|
6409
6696
|
let highestSeverity = "ok";
|
|
6410
6697
|
for (const slug of FABRIC_SKILL_SLUGS) {
|
|
6411
|
-
const skillMdPath =
|
|
6698
|
+
const skillMdPath = join19(projectRoot, ".claude", "skills", slug, "SKILL.md");
|
|
6412
6699
|
let body;
|
|
6413
6700
|
try {
|
|
6414
|
-
body = await
|
|
6701
|
+
body = await readFile11(skillMdPath, "utf8");
|
|
6415
6702
|
} catch {
|
|
6416
6703
|
continue;
|
|
6417
6704
|
}
|
|
@@ -6432,10 +6719,10 @@ async function inspectSkillDescription(projectRoot) {
|
|
|
6432
6719
|
const CJK_PATTERN = /[\u3400-\u4dbf\u4e00-\u9fff]/u;
|
|
6433
6720
|
const ASCII_PATTERN = /[a-zA-Z]{2,}/u;
|
|
6434
6721
|
for (const slug of FABRIC_SKILL_SLUGS) {
|
|
6435
|
-
const skillMdPath =
|
|
6722
|
+
const skillMdPath = join19(projectRoot, ".claude", "skills", slug, "SKILL.md");
|
|
6436
6723
|
let body;
|
|
6437
6724
|
try {
|
|
6438
|
-
body = await
|
|
6725
|
+
body = await readFile11(skillMdPath, "utf8");
|
|
6439
6726
|
} catch {
|
|
6440
6727
|
continue;
|
|
6441
6728
|
}
|
|
@@ -6466,7 +6753,7 @@ async function inspectSkillDescription(projectRoot) {
|
|
|
6466
6753
|
async function inspectSkillMdYamlInvalid(projectRoot) {
|
|
6467
6754
|
const candidates = [];
|
|
6468
6755
|
for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
|
|
6469
|
-
const rootAbs =
|
|
6756
|
+
const rootAbs = join19(projectRoot, rootRel);
|
|
6470
6757
|
let dirEntries;
|
|
6471
6758
|
try {
|
|
6472
6759
|
dirEntries = await readdir3(rootAbs, { withFileTypes: true });
|
|
@@ -6475,10 +6762,10 @@ async function inspectSkillMdYamlInvalid(projectRoot) {
|
|
|
6475
6762
|
}
|
|
6476
6763
|
for (const dirEntry of dirEntries) {
|
|
6477
6764
|
if (!dirEntry.isDirectory()) continue;
|
|
6478
|
-
const skillFile =
|
|
6765
|
+
const skillFile = join19(rootAbs, dirEntry.name, "SKILL.md");
|
|
6479
6766
|
let raw;
|
|
6480
6767
|
try {
|
|
6481
|
-
raw = await
|
|
6768
|
+
raw = await readFile11(skillFile, "utf8");
|
|
6482
6769
|
} catch {
|
|
6483
6770
|
continue;
|
|
6484
6771
|
}
|
|
@@ -6601,8 +6888,8 @@ function createSkillMdYamlInvalidCheck(t, inspection) {
|
|
|
6601
6888
|
}
|
|
6602
6889
|
|
|
6603
6890
|
// src/services/doctor-retired-references-lint.ts
|
|
6604
|
-
import { readdir as readdir4, readFile as
|
|
6605
|
-
import { join as
|
|
6891
|
+
import { readdir as readdir4, readFile as readFile12, stat as stat3 } from "fs/promises";
|
|
6892
|
+
import { join as join20, posix as posix3, relative as relative2 } from "path";
|
|
6606
6893
|
var RETIRED_TOKENS = [
|
|
6607
6894
|
{ token: "fab_plan_context", replacement: "fab_recall", reason: "retrieval collapsed to one lean fab_recall (KT-DEC-0026)" },
|
|
6608
6895
|
{ token: "fab_get_knowledge_sections", replacement: "fab_recall", reason: "two-step fetch retired (KT-DEC-0026)" },
|
|
@@ -6625,7 +6912,7 @@ var RETIRED_TOKENS = [
|
|
|
6625
6912
|
];
|
|
6626
6913
|
var HOOK_DIRS = [".claude/hooks", ".codex/hooks"];
|
|
6627
6914
|
var SKILL_DIRS = [".claude/skills", ".codex/skills"];
|
|
6628
|
-
var BOOTSTRAP_FILES = ["AGENTS.md", "CLAUDE.md",
|
|
6915
|
+
var BOOTSTRAP_FILES = ["AGENTS.md", "CLAUDE.md", join20(".fabric", "AGENTS.md")];
|
|
6629
6916
|
function isCommentLine(line) {
|
|
6630
6917
|
const t = line.trim();
|
|
6631
6918
|
return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*");
|
|
@@ -6639,7 +6926,7 @@ async function walkFiles(dir, exts) {
|
|
|
6639
6926
|
}
|
|
6640
6927
|
const out = [];
|
|
6641
6928
|
for (const e of entries) {
|
|
6642
|
-
const full =
|
|
6929
|
+
const full = join20(dir, e.name);
|
|
6643
6930
|
if (e.isDirectory()) {
|
|
6644
6931
|
out.push(...await walkFiles(full, exts));
|
|
6645
6932
|
} else if (exts.some((ext) => e.name.endsWith(ext))) {
|
|
@@ -6665,29 +6952,29 @@ async function inspectRetiredReferences(projectRoot) {
|
|
|
6665
6952
|
let scannedFiles = 0;
|
|
6666
6953
|
const toRel = (p) => posix3.normalize(relative2(projectRoot, p).split("\\").join("/"));
|
|
6667
6954
|
for (const rel of BOOTSTRAP_FILES) {
|
|
6668
|
-
const abs =
|
|
6955
|
+
const abs = join20(projectRoot, rel);
|
|
6669
6956
|
try {
|
|
6670
6957
|
if ((await stat3(abs)).isFile()) {
|
|
6671
6958
|
scannedFiles += 1;
|
|
6672
|
-
scanText(toRel(abs), await
|
|
6959
|
+
scanText(toRel(abs), await readFile12(abs, "utf8"), false, hits);
|
|
6673
6960
|
}
|
|
6674
6961
|
} catch {
|
|
6675
6962
|
}
|
|
6676
6963
|
}
|
|
6677
6964
|
for (const dir of SKILL_DIRS) {
|
|
6678
|
-
for (const file of await walkFiles(
|
|
6965
|
+
for (const file of await walkFiles(join20(projectRoot, dir), [".md"])) {
|
|
6679
6966
|
scannedFiles += 1;
|
|
6680
6967
|
try {
|
|
6681
|
-
scanText(toRel(file), await
|
|
6968
|
+
scanText(toRel(file), await readFile12(file, "utf8"), false, hits);
|
|
6682
6969
|
} catch {
|
|
6683
6970
|
}
|
|
6684
6971
|
}
|
|
6685
6972
|
}
|
|
6686
6973
|
for (const dir of HOOK_DIRS) {
|
|
6687
|
-
for (const file of await walkFiles(
|
|
6974
|
+
for (const file of await walkFiles(join20(projectRoot, dir), [".cjs"])) {
|
|
6688
6975
|
scannedFiles += 1;
|
|
6689
6976
|
try {
|
|
6690
|
-
scanText(toRel(file), await
|
|
6977
|
+
scanText(toRel(file), await readFile12(file, "utf8"), true, hits);
|
|
6691
6978
|
} catch {
|
|
6692
6979
|
}
|
|
6693
6980
|
}
|
|
@@ -6722,8 +7009,8 @@ function createRetiredReferenceCheck(t, inspection) {
|
|
|
6722
7009
|
|
|
6723
7010
|
// src/services/doctor-hooks-lints.ts
|
|
6724
7011
|
import { constants } from "fs";
|
|
6725
|
-
import { access, readdir as readdir5, readFile as
|
|
6726
|
-
import { join as
|
|
7012
|
+
import { access, readdir as readdir5, readFile as readFile13, stat as stat4 } from "fs/promises";
|
|
7013
|
+
import { join as join21, posix as posix4 } from "path";
|
|
6727
7014
|
import { Script } from "vm";
|
|
6728
7015
|
var HOOKS_RUNTIME_CLIENT_DIRS = [
|
|
6729
7016
|
{ client: "claude", dir: ".claude/hooks" },
|
|
@@ -6785,14 +7072,14 @@ async function isFile(absPath) {
|
|
|
6785
7072
|
}
|
|
6786
7073
|
}
|
|
6787
7074
|
async function inspectHooksWired(projectRoot) {
|
|
6788
|
-
const claudeEntries = await readDirectoryFileNames(
|
|
7075
|
+
const claudeEntries = await readDirectoryFileNames(join21(projectRoot, ".claude"));
|
|
6789
7076
|
if (claudeEntries === null) {
|
|
6790
7077
|
return { status: "skipped", missingHooks: [] };
|
|
6791
7078
|
}
|
|
6792
|
-
const settingsPath =
|
|
7079
|
+
const settingsPath = join21(projectRoot, ".claude", "settings.json");
|
|
6793
7080
|
let parsed;
|
|
6794
7081
|
try {
|
|
6795
|
-
parsed = JSON.parse(await
|
|
7082
|
+
parsed = JSON.parse(await readFile13(settingsPath, "utf8"));
|
|
6796
7083
|
} catch {
|
|
6797
7084
|
return { status: "missing-settings", missingHooks: [] };
|
|
6798
7085
|
}
|
|
@@ -6815,8 +7102,8 @@ async function inspectHooksWired(projectRoot) {
|
|
|
6815
7102
|
}
|
|
6816
7103
|
async function inspectHookCacheWritability(projectRoot) {
|
|
6817
7104
|
const relPath = posix4.join(".fabric", ".cache");
|
|
6818
|
-
const fabricDir =
|
|
6819
|
-
const cacheDir =
|
|
7105
|
+
const fabricDir = join21(projectRoot, ".fabric");
|
|
7106
|
+
const cacheDir = join21(projectRoot, ".fabric", ".cache");
|
|
6820
7107
|
try {
|
|
6821
7108
|
try {
|
|
6822
7109
|
const cacheStats = await stat4(cacheDir);
|
|
@@ -6865,12 +7152,12 @@ async function inspectHookCacheWritability(projectRoot) {
|
|
|
6865
7152
|
async function inspectHooksContentDrift(projectRoot) {
|
|
6866
7153
|
const hookFilesByBasename = /* @__PURE__ */ new Map();
|
|
6867
7154
|
for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
|
|
6868
|
-
const absDir =
|
|
7155
|
+
const absDir = join21(projectRoot, dir);
|
|
6869
7156
|
const entries = await readDirectoryFileNames(absDir);
|
|
6870
7157
|
if (entries === null) continue;
|
|
6871
7158
|
for (const name of entries) {
|
|
6872
7159
|
if (!name.endsWith(".cjs")) continue;
|
|
6873
|
-
const abs =
|
|
7160
|
+
const abs = join21(absDir, name);
|
|
6874
7161
|
if (!await isFile(abs)) continue;
|
|
6875
7162
|
const arr = hookFilesByBasename.get(name) ?? [];
|
|
6876
7163
|
arr.push({ client, abs });
|
|
@@ -6885,7 +7172,7 @@ async function inspectHooksContentDrift(projectRoot) {
|
|
|
6885
7172
|
const hashes = [];
|
|
6886
7173
|
for (const { client, abs } of copies) {
|
|
6887
7174
|
try {
|
|
6888
|
-
const body = await
|
|
7175
|
+
const body = await readFile13(abs, "utf8");
|
|
6889
7176
|
hashes.push({ client, sha: sha256(body) });
|
|
6890
7177
|
} catch {
|
|
6891
7178
|
}
|
|
@@ -6907,18 +7194,18 @@ async function inspectHooksRuntime(projectRoot) {
|
|
|
6907
7194
|
const issues = [];
|
|
6908
7195
|
let scanned = 0;
|
|
6909
7196
|
for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
|
|
6910
|
-
const absDir =
|
|
7197
|
+
const absDir = join21(projectRoot, dir);
|
|
6911
7198
|
const entries = await readDirectoryFileNames(absDir);
|
|
6912
7199
|
if (entries === null) continue;
|
|
6913
7200
|
for (const name of entries) {
|
|
6914
7201
|
if (!name.endsWith(".cjs")) continue;
|
|
6915
|
-
const abs =
|
|
7202
|
+
const abs = join21(absDir, name);
|
|
6916
7203
|
const displayPath = `${dir}/${name}`;
|
|
6917
7204
|
if (!await isFile(abs)) continue;
|
|
6918
7205
|
scanned += 1;
|
|
6919
7206
|
let body;
|
|
6920
7207
|
try {
|
|
6921
|
-
body = await
|
|
7208
|
+
body = await readFile13(abs, "utf8");
|
|
6922
7209
|
} catch (err) {
|
|
6923
7210
|
issues.push({
|
|
6924
7211
|
path: displayPath,
|
|
@@ -7055,8 +7342,8 @@ function createHookCacheWritabilityCheck(t, inspection) {
|
|
|
7055
7342
|
}
|
|
7056
7343
|
|
|
7057
7344
|
// src/services/doctor-bootstrap-lints.ts
|
|
7058
|
-
import { access as access2, readFile as
|
|
7059
|
-
import { join as
|
|
7345
|
+
import { access as access2, readFile as readFile14 } from "fs/promises";
|
|
7346
|
+
import { join as join22 } from "path";
|
|
7060
7347
|
import {
|
|
7061
7348
|
BOOTSTRAP_MARKER_BEGIN,
|
|
7062
7349
|
BOOTSTRAP_MARKER_END,
|
|
@@ -7088,17 +7375,17 @@ async function fileExists(path) {
|
|
|
7088
7375
|
}
|
|
7089
7376
|
async function inspectBootstrapAnchor(projectRoot) {
|
|
7090
7377
|
const [hasAgentsMd, hasClaudeMd] = await Promise.all([
|
|
7091
|
-
fileExists(
|
|
7092
|
-
fileExists(
|
|
7378
|
+
fileExists(join22(projectRoot, "AGENTS.md")),
|
|
7379
|
+
fileExists(join22(projectRoot, "CLAUDE.md"))
|
|
7093
7380
|
]);
|
|
7094
7381
|
return { hasAgentsMd, hasClaudeMd };
|
|
7095
7382
|
}
|
|
7096
7383
|
async function inspectL1BootstrapSnapshotDrift(target) {
|
|
7097
|
-
const abs =
|
|
7384
|
+
const abs = join22(target, ".fabric", "AGENTS.md");
|
|
7098
7385
|
const canonical = resolveBootstrapCanonical();
|
|
7099
7386
|
let onDisk;
|
|
7100
7387
|
try {
|
|
7101
|
-
onDisk = await
|
|
7388
|
+
onDisk = await readFile14(abs, "utf8");
|
|
7102
7389
|
} catch {
|
|
7103
7390
|
return { status: "missing", canonical, onDisk: null };
|
|
7104
7391
|
}
|
|
@@ -7124,17 +7411,17 @@ function createL1BootstrapSnapshotDriftCheck(t, inspection) {
|
|
|
7124
7411
|
);
|
|
7125
7412
|
}
|
|
7126
7413
|
async function inspectL2ManagedBlockDrift(target) {
|
|
7127
|
-
const snapshotPath =
|
|
7414
|
+
const snapshotPath = join22(target, ".fabric", "AGENTS.md");
|
|
7128
7415
|
let snapshot;
|
|
7129
7416
|
try {
|
|
7130
|
-
snapshot = await
|
|
7417
|
+
snapshot = await readFile14(snapshotPath, "utf8");
|
|
7131
7418
|
} catch {
|
|
7132
7419
|
return { status: "ok", drifted: [] };
|
|
7133
7420
|
}
|
|
7134
|
-
const projectRulesPath =
|
|
7421
|
+
const projectRulesPath = join22(target, ".fabric", "project-rules.md");
|
|
7135
7422
|
let expectedBody = snapshot;
|
|
7136
7423
|
try {
|
|
7137
|
-
const projectRules = await
|
|
7424
|
+
const projectRules = await readFile14(projectRulesPath, "utf8");
|
|
7138
7425
|
expectedBody = `${snapshot}
|
|
7139
7426
|
---
|
|
7140
7427
|
${projectRules}`;
|
|
@@ -7143,12 +7430,12 @@ ${projectRules}`;
|
|
|
7143
7430
|
const drifted = [];
|
|
7144
7431
|
let anyManagedBlockFound = false;
|
|
7145
7432
|
const blockTargets = [
|
|
7146
|
-
|
|
7433
|
+
join22(target, "AGENTS.md")
|
|
7147
7434
|
];
|
|
7148
7435
|
for (const abs of blockTargets) {
|
|
7149
7436
|
let content;
|
|
7150
7437
|
try {
|
|
7151
|
-
content = await
|
|
7438
|
+
content = await readFile14(abs, "utf8");
|
|
7152
7439
|
} catch {
|
|
7153
7440
|
continue;
|
|
7154
7441
|
}
|
|
@@ -7171,9 +7458,9 @@ ${projectRules}`;
|
|
|
7171
7458
|
drifted.push({ path: abs, expected: expectedBody, actual: body });
|
|
7172
7459
|
}
|
|
7173
7460
|
}
|
|
7174
|
-
const claudeMdPath =
|
|
7461
|
+
const claudeMdPath = join22(target, "CLAUDE.md");
|
|
7175
7462
|
try {
|
|
7176
|
-
const claudeContent = await
|
|
7463
|
+
const claudeContent = await readFile14(claudeMdPath, "utf8");
|
|
7177
7464
|
anyManagedBlockFound = true;
|
|
7178
7465
|
const lines = claudeContent.split(/\r?\n/u);
|
|
7179
7466
|
const hasAtImport = lines.some((line) => line.trim() === "@.fabric/AGENTS.md");
|
|
@@ -7241,7 +7528,7 @@ import { appendFile as appendFile3 } from "fs/promises";
|
|
|
7241
7528
|
import { minimatch as minimatch2 } from "minimatch";
|
|
7242
7529
|
|
|
7243
7530
|
// src/services/cite-rollup.ts
|
|
7244
|
-
import { readFile as
|
|
7531
|
+
import { readFile as readFile15 } from "fs/promises";
|
|
7245
7532
|
import { createLedgerWriteQueue as createLedgerWriteQueue3 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
7246
7533
|
var citeRollupQueue = createLedgerWriteQueue3();
|
|
7247
7534
|
async function appendCiteRollupRow(projectRoot, row) {
|
|
@@ -7253,7 +7540,7 @@ async function readCiteRollup(projectRoot) {
|
|
|
7253
7540
|
const path = getCiteRollupPath(projectRoot);
|
|
7254
7541
|
let raw;
|
|
7255
7542
|
try {
|
|
7256
|
-
raw = await
|
|
7543
|
+
raw = await readFile15(path, "utf8");
|
|
7257
7544
|
} catch (error) {
|
|
7258
7545
|
if (isNodeError(error) && error.code === "ENOENT") return [];
|
|
7259
7546
|
throw error;
|
|
@@ -8383,7 +8670,7 @@ async function runDoctorReport(target) {
|
|
|
8383
8670
|
const globalCliVersion = process.env.VITEST === "true" ? { status: "ok", version: "test-skipped" } : inspectGlobalCliVersion();
|
|
8384
8671
|
const targetFiles = Object.fromEntries(
|
|
8385
8672
|
await Promise.all(
|
|
8386
|
-
TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(
|
|
8673
|
+
TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(join23(projectRoot, path))])
|
|
8387
8674
|
)
|
|
8388
8675
|
);
|
|
8389
8676
|
const checks = [
|
|
@@ -8587,7 +8874,7 @@ async function runDoctorFix(target) {
|
|
|
8587
8874
|
const fixed = [];
|
|
8588
8875
|
const ledgerWarnings = [];
|
|
8589
8876
|
if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
|
|
8590
|
-
const snapshotPath =
|
|
8877
|
+
const snapshotPath = join23(projectRoot, ".fabric", "AGENTS.md");
|
|
8591
8878
|
await ensureParentDirectory(snapshotPath);
|
|
8592
8879
|
await atomicWriteText4(snapshotPath, resolveBootstrapCanonical2());
|
|
8593
8880
|
fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
|
|
@@ -8660,7 +8947,7 @@ async function runDoctorFix(target) {
|
|
|
8660
8947
|
if (before.infos.some((issue) => issue.code === "stale_serve_lock")) {
|
|
8661
8948
|
const lockInspection = inspectStaleServeLock(projectRoot, Date.now());
|
|
8662
8949
|
if (lockInspection.present && !lockInspection.pidAlive) {
|
|
8663
|
-
const lockFilePath =
|
|
8950
|
+
const lockFilePath = join23(projectRoot, ".fabric", ".serve.lock");
|
|
8664
8951
|
try {
|
|
8665
8952
|
await unlink4(lockFilePath);
|
|
8666
8953
|
} catch (err) {
|
|
@@ -8777,7 +9064,7 @@ function createApplyLintMessage(succeeded, failed, manualErrorCount) {
|
|
|
8777
9064
|
}
|
|
8778
9065
|
async function applySessionHintsStaleCleanup(projectRoot, candidate) {
|
|
8779
9066
|
const detail = `deleted (${candidate.age_days}d old)`;
|
|
8780
|
-
const absPath =
|
|
9067
|
+
const absPath = join23(projectRoot, candidate.path);
|
|
8781
9068
|
try {
|
|
8782
9069
|
const { unlink: unlink5 } = await import("fs/promises");
|
|
8783
9070
|
await unlink5(absPath);
|
|
@@ -8802,9 +9089,9 @@ function truncateErrorMessage(error) {
|
|
|
8802
9089
|
return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
|
|
8803
9090
|
}
|
|
8804
9091
|
async function inspectForensic(projectRoot) {
|
|
8805
|
-
const path =
|
|
9092
|
+
const path = join23(projectRoot, ".fabric", "forensic.json");
|
|
8806
9093
|
try {
|
|
8807
|
-
const parsed = forensicReportSchema.parse(JSON.parse(await
|
|
9094
|
+
const parsed = forensicReportSchema.parse(JSON.parse(await readFile17(path, "utf8")));
|
|
8808
9095
|
return { present: true, valid: true, report: parsed };
|
|
8809
9096
|
} catch (error) {
|
|
8810
9097
|
if (isMissingFileError(error)) {
|
|
@@ -8834,7 +9121,7 @@ async function inspectEventLedger(projectRoot) {
|
|
|
8834
9121
|
try {
|
|
8835
9122
|
await access4(path, constants2.W_OK);
|
|
8836
9123
|
const { warnings } = await readEventLedger(projectRoot);
|
|
8837
|
-
const raw = await
|
|
9124
|
+
const raw = await readFile17(path, "utf8");
|
|
8838
9125
|
const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
|
|
8839
9126
|
const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
|
|
8840
9127
|
const schemaVersionSamples = [];
|
|
@@ -9379,7 +9666,7 @@ async function inspectPreexistingRootFiles(projectRoot) {
|
|
|
9379
9666
|
const candidates = ["CLAUDE.md", "AGENTS.md"];
|
|
9380
9667
|
const detected = [];
|
|
9381
9668
|
for (const name of candidates) {
|
|
9382
|
-
if (await pathExists(
|
|
9669
|
+
if (await pathExists(join23(projectRoot, name))) {
|
|
9383
9670
|
detected.push(name);
|
|
9384
9671
|
}
|
|
9385
9672
|
}
|
|
@@ -9464,7 +9751,7 @@ async function buildLastActiveIndex(projectRoot) {
|
|
|
9464
9751
|
return map;
|
|
9465
9752
|
}
|
|
9466
9753
|
async function inspectSessionHintsStale(projectRoot, now) {
|
|
9467
|
-
const cacheDir =
|
|
9754
|
+
const cacheDir = join23(projectRoot, ".fabric", ".cache");
|
|
9468
9755
|
let entries;
|
|
9469
9756
|
try {
|
|
9470
9757
|
entries = await readdirAsync(cacheDir, { withFileTypes: true });
|
|
@@ -9476,7 +9763,7 @@ async function inspectSessionHintsStale(projectRoot, now) {
|
|
|
9476
9763
|
if (!entry.isFile()) continue;
|
|
9477
9764
|
if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
|
|
9478
9765
|
if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
|
|
9479
|
-
const absPath =
|
|
9766
|
+
const absPath = join23(cacheDir, entry.name);
|
|
9480
9767
|
let mtimeMs = 0;
|
|
9481
9768
|
try {
|
|
9482
9769
|
mtimeMs = (await statAsync(absPath)).mtimeMs;
|
|
@@ -9508,9 +9795,9 @@ function inspectStaleServeLock(projectRoot, now) {
|
|
|
9508
9795
|
};
|
|
9509
9796
|
}
|
|
9510
9797
|
async function readUnderseedThresholdFromConfig(projectRoot) {
|
|
9511
|
-
const configPath =
|
|
9798
|
+
const configPath = join23(projectRoot, ".fabric", "fabric-config.json");
|
|
9512
9799
|
try {
|
|
9513
|
-
const raw = await
|
|
9800
|
+
const raw = await readFile17(configPath, "utf8");
|
|
9514
9801
|
const parsed = JSON.parse(raw);
|
|
9515
9802
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
9516
9803
|
const v = parsed.underseed_node_threshold;
|
|
@@ -9626,10 +9913,10 @@ async function inspectOnboardCoverage(projectRoot) {
|
|
|
9626
9913
|
return { filled, missing, opted_out: optedOut };
|
|
9627
9914
|
}
|
|
9628
9915
|
async function readOnboardOptedOut(projectRoot) {
|
|
9629
|
-
const path =
|
|
9916
|
+
const path = join23(projectRoot, ".fabric", "fabric-config.json");
|
|
9630
9917
|
let raw;
|
|
9631
9918
|
try {
|
|
9632
|
-
raw = await
|
|
9919
|
+
raw = await readFile17(path, "utf8");
|
|
9633
9920
|
} catch {
|
|
9634
9921
|
return [];
|
|
9635
9922
|
}
|
|
@@ -9731,22 +10018,22 @@ async function* iterateCanonicalFilenames(projectRoot) {
|
|
|
9731
10018
|
}
|
|
9732
10019
|
}
|
|
9733
10020
|
async function rewriteThreeEndManagedBlocks(projectRoot) {
|
|
9734
|
-
const snapshotPath =
|
|
10021
|
+
const snapshotPath = join23(projectRoot, ".fabric", "AGENTS.md");
|
|
9735
10022
|
if (!await pathExists(snapshotPath)) {
|
|
9736
10023
|
return;
|
|
9737
10024
|
}
|
|
9738
10025
|
let snapshot;
|
|
9739
10026
|
try {
|
|
9740
|
-
snapshot = await
|
|
10027
|
+
snapshot = await readFile17(snapshotPath, "utf8");
|
|
9741
10028
|
} catch {
|
|
9742
10029
|
return;
|
|
9743
10030
|
}
|
|
9744
|
-
const projectRulesPath =
|
|
10031
|
+
const projectRulesPath = join23(projectRoot, ".fabric", "project-rules.md");
|
|
9745
10032
|
const hasProjectRules = await pathExists(projectRulesPath);
|
|
9746
10033
|
let expectedBody = snapshot;
|
|
9747
10034
|
if (hasProjectRules) {
|
|
9748
10035
|
try {
|
|
9749
|
-
const projectRules = await
|
|
10036
|
+
const projectRules = await readFile17(projectRulesPath, "utf8");
|
|
9750
10037
|
expectedBody = `${snapshot}
|
|
9751
10038
|
---
|
|
9752
10039
|
${projectRules}`;
|
|
@@ -9757,7 +10044,7 @@ ${projectRules}`;
|
|
|
9757
10044
|
${expectedBody}
|
|
9758
10045
|
${BOOTSTRAP_MARKER_END2}`;
|
|
9759
10046
|
const blockTargets = [
|
|
9760
|
-
|
|
10047
|
+
join23(projectRoot, "AGENTS.md")
|
|
9761
10048
|
];
|
|
9762
10049
|
for (const abs of blockTargets) {
|
|
9763
10050
|
if (!await pathExists(abs)) {
|
|
@@ -9765,7 +10052,7 @@ ${BOOTSTRAP_MARKER_END2}`;
|
|
|
9765
10052
|
}
|
|
9766
10053
|
let existing;
|
|
9767
10054
|
try {
|
|
9768
|
-
existing = await
|
|
10055
|
+
existing = await readFile17(abs, "utf8");
|
|
9769
10056
|
} catch {
|
|
9770
10057
|
continue;
|
|
9771
10058
|
}
|
|
@@ -9790,11 +10077,11 @@ ${managedBlock}
|
|
|
9790
10077
|
}
|
|
9791
10078
|
await atomicWriteText4(abs, next);
|
|
9792
10079
|
}
|
|
9793
|
-
const claudeMdPath =
|
|
10080
|
+
const claudeMdPath = join23(projectRoot, "CLAUDE.md");
|
|
9794
10081
|
if (await pathExists(claudeMdPath)) {
|
|
9795
10082
|
let claudeContent;
|
|
9796
10083
|
try {
|
|
9797
|
-
claudeContent = await
|
|
10084
|
+
claudeContent = await readFile17(claudeMdPath, "utf8");
|
|
9798
10085
|
} catch {
|
|
9799
10086
|
return;
|
|
9800
10087
|
}
|
|
@@ -9821,7 +10108,7 @@ ${managedBlock}
|
|
|
9821
10108
|
async function ensureEventLedger(projectRoot) {
|
|
9822
10109
|
const path = getEventLedgerPath(projectRoot);
|
|
9823
10110
|
await ensureParentDirectory(path);
|
|
9824
|
-
await
|
|
10111
|
+
await writeFile4(path, "", { encoding: "utf8", flag: "a" });
|
|
9825
10112
|
}
|
|
9826
10113
|
function createFixMessage(fixed, report) {
|
|
9827
10114
|
const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
|
|
@@ -9860,7 +10147,7 @@ async function collectEntryPoints(root) {
|
|
|
9860
10147
|
continue;
|
|
9861
10148
|
}
|
|
9862
10149
|
for (const entry of await readdirAsync(current, { withFileTypes: true })) {
|
|
9863
|
-
const absolutePath =
|
|
10150
|
+
const absolutePath = join23(current, entry.name);
|
|
9864
10151
|
const relativePath = normalizePath2(absolutePath.slice(root.length + 1));
|
|
9865
10152
|
if (relativePath.length === 0) {
|
|
9866
10153
|
continue;
|
|
@@ -9936,7 +10223,7 @@ async function enrichDescriptions(projectRoot, opts = {}) {
|
|
|
9936
10223
|
scanned += 1;
|
|
9937
10224
|
let source;
|
|
9938
10225
|
try {
|
|
9939
|
-
source = await
|
|
10226
|
+
source = await readFile17(absPath, "utf8");
|
|
9940
10227
|
} catch {
|
|
9941
10228
|
continue;
|
|
9942
10229
|
}
|
|
@@ -10074,14 +10361,14 @@ async function runDoctorConflictLint(projectRoot, opts = {}) {
|
|
|
10074
10361
|
}
|
|
10075
10362
|
|
|
10076
10363
|
// src/services/why-not-surfaced.ts
|
|
10077
|
-
import { readFile as
|
|
10078
|
-
import { basename as basename3, join as
|
|
10364
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
10365
|
+
import { basename as basename3, join as join24 } from "path";
|
|
10079
10366
|
import {
|
|
10080
10367
|
buildStoreResolveInput as buildStoreResolveInput7,
|
|
10081
10368
|
createStoreResolver as createStoreResolver7,
|
|
10082
10369
|
loadProjectConfig as loadProjectConfig4,
|
|
10083
10370
|
readKnowledgeAcrossStores as readKnowledgeAcrossStores4,
|
|
10084
|
-
resolveGlobalRoot as
|
|
10371
|
+
resolveGlobalRoot as resolveGlobalRoot8,
|
|
10085
10372
|
scopeRoot as scopeRoot3,
|
|
10086
10373
|
storeRelativePathForMount as storeRelativePathForMount6
|
|
10087
10374
|
} from "@fenglimg/fabric-shared";
|
|
@@ -10111,11 +10398,11 @@ async function explainWhyNotSurfaced(projectRoot, query) {
|
|
|
10111
10398
|
if (input === null) {
|
|
10112
10399
|
return base;
|
|
10113
10400
|
}
|
|
10114
|
-
const globalRoot =
|
|
10401
|
+
const globalRoot = resolveGlobalRoot8();
|
|
10115
10402
|
const allStores = input.mountedStores.map((s) => ({
|
|
10116
10403
|
store_uuid: s.store_uuid,
|
|
10117
10404
|
alias: s.alias,
|
|
10118
|
-
dir:
|
|
10405
|
+
dir: join24(globalRoot, storeRelativePathForMount6(s))
|
|
10119
10406
|
}));
|
|
10120
10407
|
const refs = await readKnowledgeAcrossStores4(allStores);
|
|
10121
10408
|
const candidate = refs.find((ref) => fileMatchesId(ref.file, localId));
|
|
@@ -10124,7 +10411,7 @@ async function explainWhyNotSurfaced(projectRoot, query) {
|
|
|
10124
10411
|
}
|
|
10125
10412
|
let source;
|
|
10126
10413
|
try {
|
|
10127
|
-
source = await
|
|
10414
|
+
source = await readFile18(candidate.file, "utf8");
|
|
10128
10415
|
} catch {
|
|
10129
10416
|
return base;
|
|
10130
10417
|
}
|
|
@@ -10212,7 +10499,7 @@ import { IOFabricError as IOFabricError2, RuleError } from "@fenglimg/fabric-sha
|
|
|
10212
10499
|
|
|
10213
10500
|
// src/services/read-ledger.ts
|
|
10214
10501
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
10215
|
-
import { access as access5, copyFile, readFile as
|
|
10502
|
+
import { access as access5, copyFile, readFile as readFile19, rm } from "fs/promises";
|
|
10216
10503
|
import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
|
|
10217
10504
|
async function resolveLedgerPaths(projectRoot) {
|
|
10218
10505
|
const primaryPath = getLedgerPath(projectRoot);
|
|
@@ -10240,7 +10527,7 @@ async function readLegacyLedger(projectRoot) {
|
|
|
10240
10527
|
const { readPath } = await resolveLedgerPaths(projectRoot);
|
|
10241
10528
|
let raw;
|
|
10242
10529
|
try {
|
|
10243
|
-
raw = await
|
|
10530
|
+
raw = await readFile19(readPath, "utf8");
|
|
10244
10531
|
} catch (error) {
|
|
10245
10532
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
10246
10533
|
return [];
|
|
@@ -10495,8 +10782,8 @@ function formatError(error) {
|
|
|
10495
10782
|
}
|
|
10496
10783
|
function formatPreexistingRootMessage(projectRoot) {
|
|
10497
10784
|
const preexisting = [];
|
|
10498
|
-
if (existsSync9(
|
|
10499
|
-
if (existsSync9(
|
|
10785
|
+
if (existsSync9(join25(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
|
|
10786
|
+
if (existsSync9(join25(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
|
|
10500
10787
|
if (preexisting.length === 0) return null;
|
|
10501
10788
|
return `[startup] info: detected ${preexisting.join(", ")} at project root. Note: Fabric serves knowledge from mounted stores via MCP \u2014 root markdown files are not auto-loaded into the AI context.`;
|
|
10502
10789
|
}
|
|
@@ -10523,7 +10810,7 @@ function createFabricServer(tracker) {
|
|
|
10523
10810
|
const server = new McpServer(
|
|
10524
10811
|
{
|
|
10525
10812
|
name: "fabric-knowledge-server",
|
|
10526
|
-
version: "2.3.0-rc.
|
|
10813
|
+
version: "2.3.0-rc.3"
|
|
10527
10814
|
},
|
|
10528
10815
|
{
|
|
10529
10816
|
instructions: FABRIC_SERVER_INSTRUCTIONS
|
|
@@ -10543,10 +10830,10 @@ function createFabricServer(tracker) {
|
|
|
10543
10830
|
},
|
|
10544
10831
|
async (_uri) => {
|
|
10545
10832
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
10546
|
-
const path =
|
|
10833
|
+
const path = join25(projectRoot, ".fabric", "bootstrap", "README.md");
|
|
10547
10834
|
let text = "";
|
|
10548
10835
|
if (existsSync9(path)) {
|
|
10549
|
-
text = await
|
|
10836
|
+
text = await readFile20(path, "utf8");
|
|
10550
10837
|
}
|
|
10551
10838
|
return {
|
|
10552
10839
|
contents: [
|
|
@@ -10644,6 +10931,7 @@ export {
|
|
|
10644
10931
|
LEGACY_LEDGER_PATH,
|
|
10645
10932
|
METRICS_LEDGER_PATH,
|
|
10646
10933
|
METRIC_COUNTER_NAMES,
|
|
10934
|
+
OPTIONAL_EMBED_PACKAGE,
|
|
10647
10935
|
RETIRED_TOKENS,
|
|
10648
10936
|
appendEventLedgerEvent,
|
|
10649
10937
|
buildAlwaysActiveBodies,
|
|
@@ -10654,6 +10942,7 @@ export {
|
|
|
10654
10942
|
createFabricServer,
|
|
10655
10943
|
createInFlightTracker,
|
|
10656
10944
|
createShutdownHandler,
|
|
10945
|
+
defaultEmbedCacheDir,
|
|
10657
10946
|
detectUnboundProject,
|
|
10658
10947
|
drainCounters,
|
|
10659
10948
|
enrichDescriptions,
|
|
@@ -10670,9 +10959,12 @@ export {
|
|
|
10670
10959
|
inspectRetiredReferences,
|
|
10671
10960
|
lintConflicts,
|
|
10672
10961
|
loadConflictEntries,
|
|
10962
|
+
loadEmbedder,
|
|
10673
10963
|
pairSimilarity,
|
|
10674
10964
|
planContext,
|
|
10965
|
+
readEmbedConfig,
|
|
10675
10966
|
readEventLedger,
|
|
10967
|
+
readFusion,
|
|
10676
10968
|
readLedger,
|
|
10677
10969
|
readMetrics,
|
|
10678
10970
|
readSelectionToken,
|