@fenglimg/fabric-server 2.3.0-rc.1 → 2.3.0-rc.10
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 +242 -6
- package/dist/index.js +1666 -368
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { existsSync as
|
|
3
|
-
import { readFile as
|
|
4
|
-
import { join as
|
|
2
|
+
import { existsSync as existsSync10 } from "fs";
|
|
3
|
+
import { readFile as readFile22 } from "fs/promises";
|
|
4
|
+
import { join as join27, 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,73 @@ function readOrphanDemoteThresholdDays(projectRoot) {
|
|
|
817
817
|
return {};
|
|
818
818
|
}
|
|
819
819
|
}
|
|
820
|
+
function readCredibilityHalfLives(projectRoot) {
|
|
821
|
+
const defaults = {
|
|
822
|
+
decisions: 180,
|
|
823
|
+
guidelines: 150,
|
|
824
|
+
models: 150,
|
|
825
|
+
pitfalls: 120,
|
|
826
|
+
processes: 120
|
|
827
|
+
};
|
|
828
|
+
try {
|
|
829
|
+
const cfg = readFabricConfig(projectRoot);
|
|
830
|
+
const validate = (v) => {
|
|
831
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 1 || v > 3650 || !Number.isInteger(v)) {
|
|
832
|
+
return void 0;
|
|
833
|
+
}
|
|
834
|
+
return v;
|
|
835
|
+
};
|
|
836
|
+
const out = { ...defaults };
|
|
837
|
+
const dec = validate(cfg.credibility_half_life_decisions_days);
|
|
838
|
+
if (dec !== void 0) out.decisions = dec;
|
|
839
|
+
const gui = validate(cfg.credibility_half_life_guidelines_days);
|
|
840
|
+
if (gui !== void 0) out.guidelines = gui;
|
|
841
|
+
const mod = validate(cfg.credibility_half_life_models_days);
|
|
842
|
+
if (mod !== void 0) out.models = mod;
|
|
843
|
+
const pit = validate(cfg.credibility_half_life_pitfalls_days);
|
|
844
|
+
if (pit !== void 0) out.pitfalls = pit;
|
|
845
|
+
const pro = validate(cfg.credibility_half_life_processes_days);
|
|
846
|
+
if (pro !== void 0) out.processes = pro;
|
|
847
|
+
return out;
|
|
848
|
+
} catch {
|
|
849
|
+
return { ...defaults };
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
function readCredibilityFloors(projectRoot) {
|
|
853
|
+
const defaults = {
|
|
854
|
+
draft: 0.4,
|
|
855
|
+
verified: 0.55,
|
|
856
|
+
proven: 0.7
|
|
857
|
+
};
|
|
858
|
+
try {
|
|
859
|
+
const cfg = readFabricConfig(projectRoot);
|
|
860
|
+
const validate = (v) => {
|
|
861
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 1) {
|
|
862
|
+
return void 0;
|
|
863
|
+
}
|
|
864
|
+
return v;
|
|
865
|
+
};
|
|
866
|
+
const out = { ...defaults };
|
|
867
|
+
const draft = validate(cfg.credibility_floor_draft);
|
|
868
|
+
if (draft !== void 0) out.draft = draft;
|
|
869
|
+
const verified = validate(cfg.credibility_floor_verified);
|
|
870
|
+
if (verified !== void 0) out.verified = verified;
|
|
871
|
+
const proven = validate(cfg.credibility_floor_proven);
|
|
872
|
+
if (proven !== void 0) out.proven = proven;
|
|
873
|
+
return out;
|
|
874
|
+
} catch {
|
|
875
|
+
return { ...defaults };
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function readFusion(projectRoot) {
|
|
879
|
+
try {
|
|
880
|
+
const raw = readFabricConfig(projectRoot).fusion;
|
|
881
|
+
if (raw === "rrf" || raw === "additive") return raw;
|
|
882
|
+
return "auto";
|
|
883
|
+
} catch {
|
|
884
|
+
return "auto";
|
|
885
|
+
}
|
|
886
|
+
}
|
|
820
887
|
function readConflictLintThreshold(projectRoot) {
|
|
821
888
|
try {
|
|
822
889
|
const cfgPath = join4(projectRoot, ".fabric", "fabric-config.json");
|
|
@@ -884,7 +951,7 @@ async function awaitFirstReconcileGate(timeoutMs = 5e3) {
|
|
|
884
951
|
|
|
885
952
|
// src/services/extract-knowledge.ts
|
|
886
953
|
import { existsSync as existsSync3 } from "fs";
|
|
887
|
-
import { readFile as
|
|
954
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
888
955
|
import { join as join7 } from "path";
|
|
889
956
|
import {
|
|
890
957
|
PROPOSED_REASON_DESCRIPTIONS_BY_LOCALE
|
|
@@ -892,10 +959,14 @@ import {
|
|
|
892
959
|
import { hasSecrets, isPersonalScope, redactPii, resolveGlobalLocale } from "@fenglimg/fabric-shared";
|
|
893
960
|
|
|
894
961
|
// src/services/cross-store-write.ts
|
|
962
|
+
import { createHash as createHash2 } from "crypto";
|
|
963
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
895
964
|
import { join as join5 } from "path";
|
|
896
965
|
import {
|
|
966
|
+
STORE_KNOWLEDGE_TYPE_DIRS,
|
|
897
967
|
STORE_LAYOUT,
|
|
898
968
|
STORE_PENDING_DIR,
|
|
969
|
+
STORE_PROJECT_ID_PATTERN,
|
|
899
970
|
buildStoreResolveInput,
|
|
900
971
|
createStoreResolver,
|
|
901
972
|
isPersonalLeakIntoSharedStore,
|
|
@@ -907,6 +978,7 @@ import {
|
|
|
907
978
|
PersonalScopeLeakError,
|
|
908
979
|
StoreWriteTargetUnresolvedError
|
|
909
980
|
} from "@fenglimg/fabric-shared/errors";
|
|
981
|
+
import { withFileLock as withFileLock2 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
910
982
|
function writeTargetUnresolved(scope, layer) {
|
|
911
983
|
const actionHint = layer === "personal" ? "run `fabric install --global` to mint your personal store, then retry" : `mount + bind a shared store, then set an explicit route: \`fabric store switch-write <alias> --scope ${scope}\``;
|
|
912
984
|
return new StoreWriteTargetUnresolvedError(
|
|
@@ -943,8 +1015,25 @@ function resolveWriteTargetStoreDir(layer, projectRoot, semanticScope) {
|
|
|
943
1015
|
function resolveStorePendingBase(layer, projectRoot, semanticScope) {
|
|
944
1016
|
return join5(resolveWriteTargetStoreDir(layer, projectRoot, semanticScope), STORE_LAYOUT.knowledgeDir, STORE_PENDING_DIR);
|
|
945
1017
|
}
|
|
946
|
-
|
|
947
|
-
|
|
1018
|
+
var STORE_PROJECTS_DIR = "projects";
|
|
1019
|
+
function isValidWriteProjectSegment(project) {
|
|
1020
|
+
if (project.length === 0) {
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
if (project === STORE_PROJECTS_DIR) {
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
if (STORE_KNOWLEDGE_TYPE_DIRS.includes(project)) {
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
return STORE_PROJECT_ID_PATTERN.test(project);
|
|
1030
|
+
}
|
|
1031
|
+
function resolveStoreCanonicalBase(layer, projectRoot, project) {
|
|
1032
|
+
const base = join5(resolveWriteTargetStoreDir(layer, projectRoot), STORE_LAYOUT.knowledgeDir);
|
|
1033
|
+
if (layer === "team" && project !== void 0 && isValidWriteProjectSegment(project)) {
|
|
1034
|
+
return join5(base, STORE_PROJECTS_DIR, project);
|
|
1035
|
+
}
|
|
1036
|
+
return base;
|
|
948
1037
|
}
|
|
949
1038
|
function resolveWriteScopeMeta(layer, projectRoot, semanticScope) {
|
|
950
1039
|
const input = buildStoreResolveInput(projectRoot);
|
|
@@ -969,7 +1058,7 @@ function resolveWriteScopeMeta(layer, projectRoot, semanticScope) {
|
|
|
969
1058
|
}
|
|
970
1059
|
|
|
971
1060
|
// src/services/cross-store-recall.ts
|
|
972
|
-
import { readFile as
|
|
1061
|
+
import { readFile as readFile4, stat } from "fs/promises";
|
|
973
1062
|
import { join as join6 } from "path";
|
|
974
1063
|
import {
|
|
975
1064
|
buildStoreResolveInput as buildStoreResolveInput2,
|
|
@@ -986,7 +1075,6 @@ import {
|
|
|
986
1075
|
deriveAgentsMetaStableId,
|
|
987
1076
|
isKnowledgeStableId,
|
|
988
1077
|
KnowledgeTypeSchema,
|
|
989
|
-
LayerSchema,
|
|
990
1078
|
MaturitySchema,
|
|
991
1079
|
parseKnowledgeId,
|
|
992
1080
|
StableIdSchema
|
|
@@ -1069,8 +1157,7 @@ function extractRuleDescription(source) {
|
|
|
1069
1157
|
id: knowledge?.id,
|
|
1070
1158
|
knowledge_type: knowledge?.knowledge_type,
|
|
1071
1159
|
maturity: knowledge?.maturity,
|
|
1072
|
-
|
|
1073
|
-
layer_reason: knowledge?.layer_reason,
|
|
1160
|
+
// W4/Track1 (D1): no `knowledge_layer` — layer derives from the id prefix.
|
|
1074
1161
|
created_at: knowledge?.created_at,
|
|
1075
1162
|
tags: knowledge?.tags,
|
|
1076
1163
|
// v2.0-rc.5 (C1): default-safe values when there is no frontmatter at all;
|
|
@@ -1080,7 +1167,9 @@ function extractRuleDescription(source) {
|
|
|
1080
1167
|
relevance_scope: knowledge?.relevance_scope ?? "broad",
|
|
1081
1168
|
relevance_paths: knowledge?.relevance_paths ?? [],
|
|
1082
1169
|
// v2.2 H2-related (W1-T7): graph edges, undefined when absent.
|
|
1083
|
-
related: knowledge?.related
|
|
1170
|
+
related: knowledge?.related,
|
|
1171
|
+
// v2.2 glossary aliases FIELD (C-002): synonym terms, undefined when absent.
|
|
1172
|
+
aliases: knowledge?.aliases
|
|
1084
1173
|
};
|
|
1085
1174
|
}
|
|
1086
1175
|
function extractDescriptionFromFrontmatter(frontmatter) {
|
|
@@ -1095,18 +1184,18 @@ function extractDescriptionFromFrontmatter(frontmatter) {
|
|
|
1095
1184
|
tech_stack: extractInlineArray(frontmatter, "tech_stack"),
|
|
1096
1185
|
impact: extractInlineArray(frontmatter, "impact"),
|
|
1097
1186
|
must_read_if: extractScalar(frontmatter, "must_read_if") ?? summary,
|
|
1098
|
-
entities: extractInlineArray(frontmatter, "entities"),
|
|
1099
1187
|
id: knowledge.id,
|
|
1100
1188
|
knowledge_type: knowledge.knowledge_type,
|
|
1101
1189
|
maturity: knowledge.maturity,
|
|
1102
|
-
|
|
1103
|
-
layer_reason: knowledge.layer_reason,
|
|
1190
|
+
// W4/Track1 (D1): no `knowledge_layer` — layer derives from the id prefix.
|
|
1104
1191
|
created_at: knowledge.created_at,
|
|
1105
1192
|
tags: knowledge.tags,
|
|
1106
1193
|
relevance_scope: knowledge.relevance_scope,
|
|
1107
1194
|
relevance_paths: knowledge.relevance_paths,
|
|
1108
1195
|
// v2.2 H2-related (W1-T7): graph edges parsed from frontmatter.
|
|
1109
|
-
related: knowledge.related
|
|
1196
|
+
related: knowledge.related,
|
|
1197
|
+
// v2.2 glossary aliases FIELD (C-002): synonym terms for the BM25 body.
|
|
1198
|
+
aliases: knowledge.aliases
|
|
1110
1199
|
};
|
|
1111
1200
|
}
|
|
1112
1201
|
function isForbiddenCrossLayerEdge(sourceLayer, targetId) {
|
|
@@ -1131,8 +1220,6 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
|
|
|
1131
1220
|
const rawId = extractScalar(frontmatter, "id");
|
|
1132
1221
|
const rawType = extractScalar(frontmatter, "type");
|
|
1133
1222
|
const rawMaturity = extractScalar(frontmatter, "maturity");
|
|
1134
|
-
const rawLayer = extractScalar(frontmatter, "layer");
|
|
1135
|
-
const rawLayerReason = extractScalar(frontmatter, "layer_reason");
|
|
1136
1223
|
const rawCreatedAt = extractScalar(frontmatter, "created_at");
|
|
1137
1224
|
let id;
|
|
1138
1225
|
if (rawId !== void 0) {
|
|
@@ -1169,16 +1256,6 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
|
|
|
1169
1256
|
maturity = parsed.data;
|
|
1170
1257
|
} else {
|
|
1171
1258
|
process.stderr.write(`[fabric] frontmatter: unknown maturity ${JSON.stringify(rawMaturity)}; skipping
|
|
1172
|
-
`);
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
let knowledge_layer;
|
|
1176
|
-
if (rawLayer !== void 0) {
|
|
1177
|
-
const parsed = LayerSchema.safeParse(rawLayer);
|
|
1178
|
-
if (parsed.success) {
|
|
1179
|
-
knowledge_layer = parsed.data;
|
|
1180
|
-
} else {
|
|
1181
|
-
process.stderr.write(`[fabric] frontmatter: unknown layer ${JSON.stringify(rawLayer)}; skipping
|
|
1182
1259
|
`);
|
|
1183
1260
|
}
|
|
1184
1261
|
}
|
|
@@ -1191,23 +1268,12 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
|
|
|
1191
1268
|
`);
|
|
1192
1269
|
}
|
|
1193
1270
|
}
|
|
1194
|
-
if (id !== void 0 && knowledge_layer !== void 0) {
|
|
1195
|
-
const decoded = parseKnowledgeId(id);
|
|
1196
|
-
if (decoded !== null && decoded.layer !== knowledge_layer) {
|
|
1197
|
-
process.stderr.write(
|
|
1198
|
-
`[fabric] frontmatter: id ${id} encodes layer ${decoded.layer} but layer field says ${knowledge_layer}; dropping both
|
|
1199
|
-
`
|
|
1200
|
-
);
|
|
1201
|
-
id = void 0;
|
|
1202
|
-
knowledge_layer = void 0;
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
1271
|
const tags = extractInlineArray(frontmatter, "tags");
|
|
1206
1272
|
const rawRelevanceScope = extractScalar(frontmatter, "relevance_scope");
|
|
1207
1273
|
const relevance_scope = rawRelevanceScope === "narrow" || rawRelevanceScope === "broad" ? rawRelevanceScope : "broad";
|
|
1208
1274
|
const relevance_paths = extractInlineArray(frontmatter, "relevance_paths");
|
|
1209
1275
|
const rawRelated = extractInlineArray(frontmatter, "related");
|
|
1210
|
-
const sourceLayer =
|
|
1276
|
+
const sourceLayer = id !== void 0 ? parseKnowledgeId(id)?.layer ?? "team" : "team";
|
|
1211
1277
|
const related = rawRelated.filter((targetId) => {
|
|
1212
1278
|
if (isForbiddenCrossLayerEdge(sourceLayer, targetId)) {
|
|
1213
1279
|
process.stderr.write(
|
|
@@ -1218,17 +1284,17 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
|
|
|
1218
1284
|
}
|
|
1219
1285
|
return true;
|
|
1220
1286
|
});
|
|
1287
|
+
const aliases = extractInlineArray(frontmatter, "aliases");
|
|
1221
1288
|
return {
|
|
1222
1289
|
id,
|
|
1223
1290
|
knowledge_type,
|
|
1224
1291
|
maturity,
|
|
1225
|
-
knowledge_layer,
|
|
1226
|
-
layer_reason: rawLayerReason,
|
|
1227
1292
|
created_at,
|
|
1228
1293
|
tags: tags.length > 0 ? tags : void 0,
|
|
1229
1294
|
relevance_scope,
|
|
1230
1295
|
relevance_paths,
|
|
1231
|
-
related: related.length > 0 ? related : void 0
|
|
1296
|
+
related: related.length > 0 ? related : void 0,
|
|
1297
|
+
aliases: aliases.length > 0 ? aliases : void 0
|
|
1232
1298
|
};
|
|
1233
1299
|
}
|
|
1234
1300
|
function extractScalar(frontmatter, key) {
|
|
@@ -1274,7 +1340,13 @@ async function readSetFingerprint(refs) {
|
|
|
1274
1340
|
return parts.sort().join("\n");
|
|
1275
1341
|
}
|
|
1276
1342
|
var SEMANTIC_SCOPE_LINE = /^semantic_scope:\s*"?([^"\n]+?)"?\s*$/mu;
|
|
1277
|
-
function readSemanticScope(source, layer) {
|
|
1343
|
+
function readSemanticScope(source, layer, project) {
|
|
1344
|
+
if (layer === "personal") {
|
|
1345
|
+
return "personal";
|
|
1346
|
+
}
|
|
1347
|
+
if (typeof project === "string" && project.length > 0) {
|
|
1348
|
+
return `project:${project}`;
|
|
1349
|
+
}
|
|
1278
1350
|
const match = SEMANTIC_SCOPE_LINE.exec(source);
|
|
1279
1351
|
return match?.[1] ?? layer;
|
|
1280
1352
|
}
|
|
@@ -1341,7 +1413,7 @@ async function walkReadSetStoresUncached(snapshot) {
|
|
|
1341
1413
|
const entries = await Promise.all(snapshot.refs.map(async (ref) => {
|
|
1342
1414
|
let source;
|
|
1343
1415
|
try {
|
|
1344
|
-
source = await
|
|
1416
|
+
source = await readFile4(ref.file, "utf8");
|
|
1345
1417
|
} catch {
|
|
1346
1418
|
return null;
|
|
1347
1419
|
}
|
|
@@ -1353,7 +1425,7 @@ async function walkReadSetStoresUncached(snapshot) {
|
|
|
1353
1425
|
type: ref.type,
|
|
1354
1426
|
alias: ref.alias,
|
|
1355
1427
|
layer,
|
|
1356
|
-
semanticScope: readSemanticScope(source, layer),
|
|
1428
|
+
semanticScope: readSemanticScope(source, layer, ref.project),
|
|
1357
1429
|
source
|
|
1358
1430
|
};
|
|
1359
1431
|
}));
|
|
@@ -1371,7 +1443,9 @@ async function buildCrossStoreRawItems(projectRoot) {
|
|
|
1371
1443
|
stable_id: entry.qualifiedId,
|
|
1372
1444
|
description: {
|
|
1373
1445
|
...baseDescription,
|
|
1374
|
-
|
|
1446
|
+
// W4/Track1 (D1): no `knowledge_layer` backfill — a candidate's layer is
|
|
1447
|
+
// derived from its stable_id prefix (layerFromStableId in plan-context),
|
|
1448
|
+
// the single source of truth (KT-DEC-0004).
|
|
1375
1449
|
semantic_scope: entry.semanticScope
|
|
1376
1450
|
}
|
|
1377
1451
|
});
|
|
@@ -1534,11 +1608,44 @@ function buildBm25Model(docs) {
|
|
|
1534
1608
|
for (const field of BM25_FIELDS) {
|
|
1535
1609
|
avgFieldLength[field] = totalDocs > 0 ? totalFieldLength[field] / totalDocs : 0;
|
|
1536
1610
|
}
|
|
1611
|
+
const serialized = {
|
|
1612
|
+
version: 1,
|
|
1613
|
+
totalDocs,
|
|
1614
|
+
documentFrequency: [...documentFrequency],
|
|
1615
|
+
avgFieldLength,
|
|
1616
|
+
perDoc: [...perDoc].map(([id, stats]) => ({
|
|
1617
|
+
id,
|
|
1618
|
+
fieldTermFreq: emptyFieldRecord(() => []),
|
|
1619
|
+
fieldLength: { ...stats.fieldLength }
|
|
1620
|
+
}))
|
|
1621
|
+
};
|
|
1622
|
+
for (const entry of serialized.perDoc) {
|
|
1623
|
+
const stats = perDoc.get(entry.id);
|
|
1624
|
+
if (stats === void 0) continue;
|
|
1625
|
+
for (const field of BM25_FIELDS) {
|
|
1626
|
+
entry.fieldTermFreq[field] = [...stats.fieldTermFreq[field]];
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
return modelFromStats(serialized);
|
|
1630
|
+
}
|
|
1631
|
+
function modelFromStats(serialized) {
|
|
1632
|
+
const totalDocs = serialized.totalDocs;
|
|
1633
|
+
const documentFrequency = new Map(serialized.documentFrequency);
|
|
1634
|
+
const avgFieldLength = serialized.avgFieldLength;
|
|
1635
|
+
const perDoc = /* @__PURE__ */ new Map();
|
|
1636
|
+
for (const entry of serialized.perDoc) {
|
|
1637
|
+
const fieldTermFreq = emptyFieldRecord(() => /* @__PURE__ */ new Map());
|
|
1638
|
+
for (const field of BM25_FIELDS) {
|
|
1639
|
+
fieldTermFreq[field] = new Map(entry.fieldTermFreq[field]);
|
|
1640
|
+
}
|
|
1641
|
+
perDoc.set(entry.id, { fieldTermFreq, fieldLength: entry.fieldLength });
|
|
1642
|
+
}
|
|
1537
1643
|
const idf = (term) => {
|
|
1538
1644
|
const n = documentFrequency.get(term) ?? 0;
|
|
1539
1645
|
return Math.log(1 + (totalDocs - n + 0.5) / (n + 0.5));
|
|
1540
1646
|
};
|
|
1541
1647
|
return {
|
|
1648
|
+
__serialized: serialized,
|
|
1542
1649
|
scoreDoc(id, queryTerms) {
|
|
1543
1650
|
const data = perDoc.get(id);
|
|
1544
1651
|
if (data === void 0 || queryTerms.length === 0) {
|
|
@@ -1574,9 +1681,186 @@ function buildBm25Model(docs) {
|
|
|
1574
1681
|
}
|
|
1575
1682
|
};
|
|
1576
1683
|
}
|
|
1684
|
+
function serializeBm25Model(model) {
|
|
1685
|
+
return model.__serialized;
|
|
1686
|
+
}
|
|
1687
|
+
function rehydrateBm25Model(serialized) {
|
|
1688
|
+
return modelFromStats(serialized);
|
|
1689
|
+
}
|
|
1690
|
+
var SYNONYM_PAIRS = {
|
|
1691
|
+
// Code / engineering actions
|
|
1692
|
+
refactor: ["restructure", "rewrite", "redesign", "reorganize", "clean", "rework"],
|
|
1693
|
+
optimize: ["improve", "speed-up", "tune", "accelerate", "perf", "performance"],
|
|
1694
|
+
debug: ["fix", "troubleshoot", "diagnose", "investigate", "resolve", "correct"],
|
|
1695
|
+
migrate: ["port", "move", "transfer", "upgrade", "transition", "convert"],
|
|
1696
|
+
"set up": ["init", "initialize", "configure", "bootstrap", "install", "onboard"],
|
|
1697
|
+
implement: ["add", "build", "create", "develop", "write", "introduce", "introduce"],
|
|
1698
|
+
// Architecture / design
|
|
1699
|
+
architecture: ["design", "structure", "layout", "organization", "pattern", "system"],
|
|
1700
|
+
"data model": ["schema", "entity", "type", "structure", "contract", "shape"],
|
|
1701
|
+
// Quality / correctness
|
|
1702
|
+
test: ["verify", "validate", "assert", "check", "spec", "coverage"],
|
|
1703
|
+
lint: ["check", "validate", "audit", "inspect", "analyze"],
|
|
1704
|
+
// Communication / impact
|
|
1705
|
+
documentation: ["docs", "readme", "guide", "spec", "explanation", "reference"],
|
|
1706
|
+
decision: ["adr", "rationale", "why", "motivation", "reason", "trade-off"],
|
|
1707
|
+
// Change management
|
|
1708
|
+
release: ["deploy", "ship", "publish", "cut", "version", "tag"],
|
|
1709
|
+
rollback: ["revert", "undo", "back-out", "backout", "restore"],
|
|
1710
|
+
// Containers / infra
|
|
1711
|
+
container: ["docker", "image", "oci", "cri-o"],
|
|
1712
|
+
deploy: ["release", "rollout", "ship", "publish", "promote"],
|
|
1713
|
+
// Project / process
|
|
1714
|
+
on_boarding: ["getting-started", "quickstart", "newcomer", "new-hire", "first-time"],
|
|
1715
|
+
best_practice: ["convention", "guideline", "standard", "rule", "recommendation"],
|
|
1716
|
+
// Tech stacks
|
|
1717
|
+
api: ["endpoint", "route", "service", "interface", "rpc", "rest"],
|
|
1718
|
+
typescript: ["ts", "types", "type-safe"],
|
|
1719
|
+
react: ["jsx", "tsx", "component", "ui"],
|
|
1720
|
+
node: ["nodejs", "runtime", "backend", "server"],
|
|
1721
|
+
database: ["db", "sql", "nosql", "storage", "persistence"]
|
|
1722
|
+
};
|
|
1723
|
+
var STEMMING_PATTERNS = [
|
|
1724
|
+
{ suffix: "e", alternatives: ["es", "ed", "ing", "ation"] },
|
|
1725
|
+
{ suffix: "y", alternatives: ["ies", "ied", "ying"] }
|
|
1726
|
+
// Catch-all for non-verb / already-stemmed query terms: no-op by default.
|
|
1727
|
+
];
|
|
1728
|
+
var DEFAULT_IDF_WEIGHT = 1;
|
|
1729
|
+
var COMMON_TERMS = /* @__PURE__ */ new Set([
|
|
1730
|
+
"a",
|
|
1731
|
+
"an",
|
|
1732
|
+
"the",
|
|
1733
|
+
"and",
|
|
1734
|
+
"or",
|
|
1735
|
+
"but",
|
|
1736
|
+
"in",
|
|
1737
|
+
"on",
|
|
1738
|
+
"at",
|
|
1739
|
+
"to",
|
|
1740
|
+
"for",
|
|
1741
|
+
"of",
|
|
1742
|
+
"with",
|
|
1743
|
+
"by",
|
|
1744
|
+
"from",
|
|
1745
|
+
"as",
|
|
1746
|
+
"is",
|
|
1747
|
+
"it",
|
|
1748
|
+
"at",
|
|
1749
|
+
"be",
|
|
1750
|
+
"do",
|
|
1751
|
+
"has",
|
|
1752
|
+
"have",
|
|
1753
|
+
"was",
|
|
1754
|
+
"are",
|
|
1755
|
+
"been",
|
|
1756
|
+
"this",
|
|
1757
|
+
"that",
|
|
1758
|
+
"these",
|
|
1759
|
+
"those",
|
|
1760
|
+
"will",
|
|
1761
|
+
"can",
|
|
1762
|
+
"may",
|
|
1763
|
+
"would",
|
|
1764
|
+
"could",
|
|
1765
|
+
"should",
|
|
1766
|
+
"does",
|
|
1767
|
+
"not",
|
|
1768
|
+
"no",
|
|
1769
|
+
"if",
|
|
1770
|
+
"so",
|
|
1771
|
+
"up",
|
|
1772
|
+
"out",
|
|
1773
|
+
"all",
|
|
1774
|
+
"each",
|
|
1775
|
+
"every",
|
|
1776
|
+
"both",
|
|
1777
|
+
"some",
|
|
1778
|
+
"any",
|
|
1779
|
+
"such",
|
|
1780
|
+
"only",
|
|
1781
|
+
"own",
|
|
1782
|
+
"same",
|
|
1783
|
+
"too",
|
|
1784
|
+
"very",
|
|
1785
|
+
"just",
|
|
1786
|
+
"about",
|
|
1787
|
+
"over",
|
|
1788
|
+
"than",
|
|
1789
|
+
"then",
|
|
1790
|
+
"also"
|
|
1791
|
+
]);
|
|
1792
|
+
function idfWeight(term) {
|
|
1793
|
+
return COMMON_TERMS.has(term) ? 0.3 : DEFAULT_IDF_WEIGHT;
|
|
1794
|
+
}
|
|
1795
|
+
function expandQueryTerms(text) {
|
|
1796
|
+
const baseTerms = tokenize(text);
|
|
1797
|
+
const weighted = /* @__PURE__ */ new Map();
|
|
1798
|
+
for (const term of baseTerms) {
|
|
1799
|
+
const lower = term.toLowerCase();
|
|
1800
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
1801
|
+
const syns = SYNONYM_PAIRS[lower];
|
|
1802
|
+
if (syns !== void 0) {
|
|
1803
|
+
for (const s of syns) aliases.add(s);
|
|
1804
|
+
}
|
|
1805
|
+
for (const [key, values] of Object.entries(SYNONYM_PAIRS)) {
|
|
1806
|
+
if (values.includes(lower)) {
|
|
1807
|
+
aliases.add(key);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
for (const { suffix, alternatives } of STEMMING_PATTERNS) {
|
|
1811
|
+
if (lower.endsWith(suffix) && lower.length > suffix.length) {
|
|
1812
|
+
const stem = lower.slice(0, -suffix.length);
|
|
1813
|
+
for (const alt of alternatives) {
|
|
1814
|
+
aliases.add(stem + alt);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
const originalWeight = idfWeight(lower);
|
|
1819
|
+
weighted.set(term, originalWeight);
|
|
1820
|
+
for (const alias of aliases) {
|
|
1821
|
+
if (!weighted.has(alias)) {
|
|
1822
|
+
weighted.set(alias, originalWeight * 0.5);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
return weighted;
|
|
1827
|
+
}
|
|
1577
1828
|
function buildQueryTerms(text) {
|
|
1578
1829
|
return tokenize(text);
|
|
1579
1830
|
}
|
|
1831
|
+
function rankDocuments(model, ids, queryTerms) {
|
|
1832
|
+
if (queryTerms.length === 0) {
|
|
1833
|
+
return /* @__PURE__ */ new Map();
|
|
1834
|
+
}
|
|
1835
|
+
const scored = [];
|
|
1836
|
+
ids.forEach((id, order) => {
|
|
1837
|
+
const score = model.scoreDoc(id, queryTerms);
|
|
1838
|
+
if (score > 0) {
|
|
1839
|
+
scored.push({ id, score, order });
|
|
1840
|
+
}
|
|
1841
|
+
});
|
|
1842
|
+
scored.sort((a, b) => a.score !== b.score ? b.score - a.score : a.order - b.order);
|
|
1843
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
1844
|
+
scored.forEach((entry, index) => {
|
|
1845
|
+
ranks.set(entry.id, index + 1);
|
|
1846
|
+
});
|
|
1847
|
+
return ranks;
|
|
1848
|
+
}
|
|
1849
|
+
function rankByScore(ids, scores) {
|
|
1850
|
+
const scored = [];
|
|
1851
|
+
ids.forEach((id, order) => {
|
|
1852
|
+
const score = scores.get(id) ?? 0;
|
|
1853
|
+
if (score > 0) {
|
|
1854
|
+
scored.push({ id, score, order });
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
scored.sort((a, b) => a.score !== b.score ? b.score - a.score : a.order - b.order);
|
|
1858
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
1859
|
+
scored.forEach((entry, index) => {
|
|
1860
|
+
ranks.set(entry.id, index + 1);
|
|
1861
|
+
});
|
|
1862
|
+
return ranks;
|
|
1863
|
+
}
|
|
1580
1864
|
|
|
1581
1865
|
// src/services/conflict-lint.ts
|
|
1582
1866
|
function buildSimilarityModel(docs) {
|
|
@@ -1940,7 +2224,7 @@ async function extractKnowledge(projectRoot, input) {
|
|
|
1940
2224
|
const writeScopeMeta = resolveWriteScopeMeta(layer, projectRoot, semanticScope);
|
|
1941
2225
|
await ensureParentDirectory(absolutePath);
|
|
1942
2226
|
if (existsSync3(absolutePath)) {
|
|
1943
|
-
const existing = await
|
|
2227
|
+
const existing = await readFile5(absolutePath, "utf8");
|
|
1944
2228
|
const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
|
|
1945
2229
|
if (existingKey === effectiveIdempotencyKey) {
|
|
1946
2230
|
const fresh2 = renderFreshEntry({
|
|
@@ -2073,7 +2357,7 @@ async function resolveDisambiguatedSlugPath(args) {
|
|
|
2073
2357
|
idempotencyKey: candidateKey
|
|
2074
2358
|
};
|
|
2075
2359
|
}
|
|
2076
|
-
const existing = await
|
|
2360
|
+
const existing = await readFile5(candidatePath, "utf8");
|
|
2077
2361
|
const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
|
|
2078
2362
|
if (existingKey === candidateKey) {
|
|
2079
2363
|
return {
|
|
@@ -2670,13 +2954,47 @@ var LEDGER_DUAL_WRITE_METRIC_NAMES = {
|
|
|
2670
2954
|
edit_intent_checked: METRIC_COUNTER_NAMES.edit_intent_checked
|
|
2671
2955
|
};
|
|
2672
2956
|
|
|
2957
|
+
// src/services/plan-context.ts
|
|
2958
|
+
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
|
|
2959
|
+
import { join as join10 } from "path";
|
|
2960
|
+
|
|
2673
2961
|
// src/services/vector-retrieval.ts
|
|
2962
|
+
import { mkdirSync } from "fs";
|
|
2963
|
+
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
|
|
2964
|
+
import { createRequire } from "module";
|
|
2965
|
+
import { join as join9 } from "path";
|
|
2966
|
+
import { resolveGlobalRoot as resolveGlobalRoot3 } from "@fenglimg/fabric-shared";
|
|
2674
2967
|
var embedderLoad;
|
|
2675
2968
|
var OPTIONAL_EMBED_PACKAGE = "fastembed";
|
|
2969
|
+
function isEmbedderResolvable() {
|
|
2970
|
+
try {
|
|
2971
|
+
createRequire(import.meta.url).resolve(OPTIONAL_EMBED_PACKAGE);
|
|
2972
|
+
return true;
|
|
2973
|
+
} catch {
|
|
2974
|
+
return false;
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
var embedderModuleLoader = (name) => import(name);
|
|
2978
|
+
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";
|
|
2979
|
+
var defaultMissingEmbedderHint = () => {
|
|
2980
|
+
process.stderr.write(MISSING_EMBEDDER_HINT);
|
|
2981
|
+
};
|
|
2982
|
+
var missingEmbedderHinted = false;
|
|
2983
|
+
var emitMissingEmbedderHint = defaultMissingEmbedderHint;
|
|
2984
|
+
function hintMissingEmbedderOnce() {
|
|
2985
|
+
if (missingEmbedderHinted) {
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
missingEmbedderHinted = true;
|
|
2989
|
+
emitMissingEmbedderHint();
|
|
2990
|
+
}
|
|
2991
|
+
function defaultEmbedCacheDir() {
|
|
2992
|
+
return join9(resolveGlobalRoot3(), "cache", "embed");
|
|
2993
|
+
}
|
|
2676
2994
|
function buildEmbedInitOptions(modelName) {
|
|
2677
2995
|
return {
|
|
2678
2996
|
maxLength: 512,
|
|
2679
|
-
cacheDir: process.env.FABRIC_EMBED_CACHE_DIR,
|
|
2997
|
+
cacheDir: process.env.FABRIC_EMBED_CACHE_DIR ?? defaultEmbedCacheDir(),
|
|
2680
2998
|
...typeof modelName === "string" && modelName.length > 0 ? { model: modelName } : {}
|
|
2681
2999
|
};
|
|
2682
3000
|
}
|
|
@@ -2685,11 +3003,14 @@ async function loadEmbedder(modelName) {
|
|
|
2685
3003
|
embedderLoad = (async () => {
|
|
2686
3004
|
try {
|
|
2687
3005
|
const moduleName = OPTIONAL_EMBED_PACKAGE;
|
|
2688
|
-
const mod = await
|
|
3006
|
+
const mod = await embedderModuleLoader(moduleName);
|
|
2689
3007
|
if (mod?.FlagEmbedding?.init === void 0) {
|
|
3008
|
+
hintMissingEmbedderOnce();
|
|
2690
3009
|
return null;
|
|
2691
3010
|
}
|
|
2692
|
-
const
|
|
3011
|
+
const initOpts = buildEmbedInitOptions(modelName);
|
|
3012
|
+
mkdirSync(initOpts.cacheDir, { recursive: true });
|
|
3013
|
+
const model = await mod.FlagEmbedding.init(initOpts);
|
|
2693
3014
|
return {
|
|
2694
3015
|
async embed(texts) {
|
|
2695
3016
|
const out = [];
|
|
@@ -2702,6 +3023,7 @@ async function loadEmbedder(modelName) {
|
|
|
2702
3023
|
}
|
|
2703
3024
|
};
|
|
2704
3025
|
} catch {
|
|
3026
|
+
hintMissingEmbedderOnce();
|
|
2705
3027
|
return null;
|
|
2706
3028
|
}
|
|
2707
3029
|
})();
|
|
@@ -2732,7 +3054,7 @@ function cosineSimilarity(a, b) {
|
|
|
2732
3054
|
if (!Number.isFinite(sim)) {
|
|
2733
3055
|
return 0;
|
|
2734
3056
|
}
|
|
2735
|
-
return Math.max(
|
|
3057
|
+
return Math.max(0, Math.min(1, sim));
|
|
2736
3058
|
}
|
|
2737
3059
|
var docVectorCache = /* @__PURE__ */ new Map();
|
|
2738
3060
|
var DOC_VECTOR_CACHE_MAX = 1e4;
|
|
@@ -2749,11 +3071,63 @@ function cacheDocVector(text, vector) {
|
|
|
2749
3071
|
docVectorCache.delete(lru);
|
|
2750
3072
|
}
|
|
2751
3073
|
}
|
|
2752
|
-
|
|
3074
|
+
var VECTOR_CACHE_VERSION = 1;
|
|
3075
|
+
var VECTOR_CACHE_DIR = ".fabric/cache/vectors";
|
|
3076
|
+
function vectorCachePath(projectRoot, revision) {
|
|
3077
|
+
const safe = revision.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
3078
|
+
return join9(projectRoot, VECTOR_CACHE_DIR, `${safe}.json`);
|
|
3079
|
+
}
|
|
3080
|
+
async function loadVectorCacheFromDisk(ctx) {
|
|
3081
|
+
try {
|
|
3082
|
+
const raw = await readFile6(vectorCachePath(ctx.projectRoot, ctx.corpusRevision), "utf8");
|
|
3083
|
+
const parsed = JSON.parse(raw);
|
|
3084
|
+
if (parsed.version !== VECTOR_CACHE_VERSION) return null;
|
|
3085
|
+
if (parsed.embedding_model !== ctx.embeddingModel) return null;
|
|
3086
|
+
if (parsed.corpus_revision !== ctx.corpusRevision) return null;
|
|
3087
|
+
if (!Array.isArray(parsed.vectors) || parsed.vectors.length === 0) return null;
|
|
3088
|
+
const dimension = parsed.dimension;
|
|
3089
|
+
if (!Number.isInteger(dimension) || dimension <= 0) return null;
|
|
3090
|
+
const out = /* @__PURE__ */ new Map();
|
|
3091
|
+
for (const entry of parsed.vectors) {
|
|
3092
|
+
if (!Array.isArray(entry) || entry.length !== 2) return null;
|
|
3093
|
+
const [text, vector] = entry;
|
|
3094
|
+
if (typeof text !== "string" || !Array.isArray(vector)) return null;
|
|
3095
|
+
if (vector.length !== dimension) return null;
|
|
3096
|
+
out.set(text, vector);
|
|
3097
|
+
}
|
|
3098
|
+
return out;
|
|
3099
|
+
} catch {
|
|
3100
|
+
return null;
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
async function saveVectorCacheToDisk(ctx, vectors, dimension) {
|
|
3104
|
+
try {
|
|
3105
|
+
const payload = {
|
|
3106
|
+
version: VECTOR_CACHE_VERSION,
|
|
3107
|
+
embedding_model: ctx.embeddingModel,
|
|
3108
|
+
dimension,
|
|
3109
|
+
corpus_revision: ctx.corpusRevision,
|
|
3110
|
+
vectors
|
|
3111
|
+
};
|
|
3112
|
+
const path = vectorCachePath(ctx.projectRoot, ctx.corpusRevision);
|
|
3113
|
+
await mkdir4(join9(ctx.projectRoot, VECTOR_CACHE_DIR), { recursive: true });
|
|
3114
|
+
await writeFile2(path, JSON.stringify(payload), "utf8");
|
|
3115
|
+
} catch {
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
async function buildVectorScores(embedder, queryText, items, cache2) {
|
|
2753
3119
|
if (embedder === null || queryText.trim().length === 0 || items.length === 0) {
|
|
2754
3120
|
return null;
|
|
2755
3121
|
}
|
|
2756
3122
|
try {
|
|
3123
|
+
if (cache2 !== void 0) {
|
|
3124
|
+
const fromDisk = await loadVectorCacheFromDisk(cache2);
|
|
3125
|
+
if (fromDisk !== null) {
|
|
3126
|
+
for (const [text, vector] of fromDisk) {
|
|
3127
|
+
cacheDocVector(text, vector);
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
2757
3131
|
const missTexts = [];
|
|
2758
3132
|
for (const item of items) {
|
|
2759
3133
|
if (!docVectorCache.has(item.text)) {
|
|
@@ -2769,6 +3143,24 @@ async function buildVectorScores(embedder, queryText, items) {
|
|
|
2769
3143
|
for (let m = 0; m < missTexts.length; m += 1) {
|
|
2770
3144
|
cacheDocVector(missTexts[m], embedded[m + 1]);
|
|
2771
3145
|
}
|
|
3146
|
+
const staleTexts = [];
|
|
3147
|
+
for (const item of items) {
|
|
3148
|
+
const cached = docVectorCache.get(item.text);
|
|
3149
|
+
if (cached !== void 0 && cached.length !== queryVec.length) {
|
|
3150
|
+
docVectorCache.delete(item.text);
|
|
3151
|
+
staleTexts.push(item.text);
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
if (staleTexts.length > 0) {
|
|
3155
|
+
const reEmbedded = await embedder.embed(staleTexts);
|
|
3156
|
+
if (reEmbedded.length !== staleTexts.length) {
|
|
3157
|
+
return null;
|
|
3158
|
+
}
|
|
3159
|
+
for (let s = 0; s < staleTexts.length; s += 1) {
|
|
3160
|
+
cacheDocVector(staleTexts[s], reEmbedded[s]);
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
const embeddedNewDoc = missTexts.length > 0 || staleTexts.length > 0;
|
|
2772
3164
|
const scores = /* @__PURE__ */ new Map();
|
|
2773
3165
|
for (const item of items) {
|
|
2774
3166
|
const docVec = docVectorCache.get(item.text);
|
|
@@ -2777,6 +3169,20 @@ async function buildVectorScores(embedder, queryText, items) {
|
|
|
2777
3169
|
}
|
|
2778
3170
|
scores.set(item.stable_id, cosineSimilarity(queryVec, docVec));
|
|
2779
3171
|
}
|
|
3172
|
+
if (cache2 !== void 0 && embeddedNewDoc) {
|
|
3173
|
+
const snapshot = [];
|
|
3174
|
+
for (const item of items) {
|
|
3175
|
+
const vec = docVectorCache.get(item.text);
|
|
3176
|
+
if (vec !== void 0) {
|
|
3177
|
+
snapshot.push([item.text, vec]);
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
const dimension = snapshot.length > 0 ? snapshot[0][1].length : 0;
|
|
3181
|
+
const uniform = dimension > 0 && snapshot.every(([, v]) => v.length === dimension);
|
|
3182
|
+
if (uniform) {
|
|
3183
|
+
await saveVectorCacheToDisk(cache2, snapshot, dimension);
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
2780
3186
|
return scores;
|
|
2781
3187
|
} catch {
|
|
2782
3188
|
return null;
|
|
@@ -2784,6 +3190,11 @@ async function buildVectorScores(embedder, queryText, items) {
|
|
|
2784
3190
|
}
|
|
2785
3191
|
|
|
2786
3192
|
// src/services/plan-context.ts
|
|
3193
|
+
function layerFromStableId(qualifiedId) {
|
|
3194
|
+
const colon = qualifiedId.lastIndexOf(":");
|
|
3195
|
+
const localId = colon === -1 ? qualifiedId : qualifiedId.slice(colon + 1);
|
|
3196
|
+
return localId.startsWith("KP-") ? "personal" : "team";
|
|
3197
|
+
}
|
|
2787
3198
|
var SELECTION_TOKEN_TTL_DEFAULT_MS = 30 * 60 * 1e3;
|
|
2788
3199
|
var selectionTokenCache = /* @__PURE__ */ new Map();
|
|
2789
3200
|
var SELECTION_TOKEN_CACHE_MAX = 1e3;
|
|
@@ -2837,56 +3248,28 @@ async function planContext(projectRoot, input) {
|
|
|
2837
3248
|
...input.known_tech ?? [],
|
|
2838
3249
|
...Object.values(input.detected_entities ?? {}).flat()
|
|
2839
3250
|
].join(" ");
|
|
2840
|
-
const scoringContext = {
|
|
2841
|
-
nowMs: Date.now(),
|
|
2842
|
-
targetPaths: input.target_paths ?? dedupePaths(input.paths),
|
|
2843
|
-
queryTerms: buildQueryTerms(queryText)
|
|
2844
|
-
};
|
|
2845
3251
|
const storeRawItems = await buildCrossStoreRawItems(projectRoot).catch(() => []);
|
|
2846
3252
|
const { rawItems: allRawItems, suppressedStableIds } = partitionEmptyShells(storeRawItems);
|
|
2847
3253
|
const effectiveLayerFilter = input.layer_filter ?? readDefaultLayerFilter(projectRoot);
|
|
2848
|
-
const rawItems = effectiveLayerFilter === "both" ? allRawItems : allRawItems.filter((item) => item.
|
|
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;
|
|
3254
|
+
const rawItems = effectiveLayerFilter === "both" ? allRawItems : allRawItems.filter((item) => layerFromStableId(item.stable_id) === effectiveLayerFilter);
|
|
3255
|
+
const scoringContext = await buildScoringContext(projectRoot, revision, rawItems, {
|
|
3256
|
+
queryText,
|
|
3257
|
+
targetPaths: input.target_paths ?? dedupePaths(input.paths)
|
|
2880
3258
|
});
|
|
3259
|
+
const rankedScored = rankDescriptionItems(rawItems, scoringContext, "triage");
|
|
2881
3260
|
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;
|
|
3261
|
+
const survivingScored = rankDescriptionItems(rawItems, scoringContext, "recall", {
|
|
3262
|
+
topK: readPlanContextTopK(projectRoot),
|
|
3263
|
+
relevanceRatio: readRecallRelevanceRatio(projectRoot)
|
|
3264
|
+
});
|
|
2889
3265
|
const topKCandidates = survivingScored.map((entry) => entry.item);
|
|
3266
|
+
const candidateScores = /* @__PURE__ */ new Map();
|
|
3267
|
+
for (const entry of survivingScored) {
|
|
3268
|
+
candidateScores.set(entry.item.stable_id, {
|
|
3269
|
+
score: entry.score,
|
|
3270
|
+
score_breakdown: scoreBreakdownForItem(entry.item, scoringContext)
|
|
3271
|
+
});
|
|
3272
|
+
}
|
|
2890
3273
|
const topKIds = new Set(topKCandidates.map((item) => item.stable_id));
|
|
2891
3274
|
const retrievalDropped = rankedCandidates.filter((item) => !topKIds.has(item.stable_id)).map((item) => ({ id: item.stable_id, reason: "retrieval_budget" }));
|
|
2892
3275
|
const omittedCandidateCount = retrievalDropped.length;
|
|
@@ -2992,6 +3375,8 @@ async function planContext(projectRoot, input) {
|
|
|
2992
3375
|
// ONLY when at least one neighbour was actually appended. Empty (graph-empty
|
|
2993
3376
|
// no-op) → field omitted, steady-state wire shape unchanged.
|
|
2994
3377
|
...Object.keys(relatedAppended).length > 0 ? { related_appended: relatedAppended } : {},
|
|
3378
|
+
// P1 recall-observability: runtime-only score channel (Map → {} on the wire).
|
|
3379
|
+
...candidateScores.size > 0 ? { candidate_scores: candidateScores } : {},
|
|
2995
3380
|
...payloadTrimDropped > 0 ? { payload_trimmed: true } : {},
|
|
2996
3381
|
...payloadOverBudget ? { payload_over_budget: true } : {}
|
|
2997
3382
|
};
|
|
@@ -3080,10 +3465,38 @@ function partitionEmptyShells(items) {
|
|
|
3080
3465
|
}
|
|
3081
3466
|
var bm25ModelCache = null;
|
|
3082
3467
|
var bm25BuildCount = 0;
|
|
3083
|
-
|
|
3468
|
+
var BM25_CACHE_DIR = ".fabric/cache/bm25";
|
|
3469
|
+
function bm25CachePath(projectRoot, revision) {
|
|
3470
|
+
const safe = revision.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
3471
|
+
return join10(projectRoot, BM25_CACHE_DIR, `${safe}.json`);
|
|
3472
|
+
}
|
|
3473
|
+
async function loadBm25ModelFromDisk(projectRoot, revision) {
|
|
3474
|
+
try {
|
|
3475
|
+
const raw = await readFile7(bm25CachePath(projectRoot, revision), "utf8");
|
|
3476
|
+
const parsed = JSON.parse(raw);
|
|
3477
|
+
if (parsed.version !== 1) return null;
|
|
3478
|
+
return rehydrateBm25Model(parsed);
|
|
3479
|
+
} catch {
|
|
3480
|
+
return null;
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
async function saveBm25ModelToDisk(projectRoot, revision, model) {
|
|
3484
|
+
try {
|
|
3485
|
+
const path = bm25CachePath(projectRoot, revision);
|
|
3486
|
+
await mkdir5(join10(projectRoot, BM25_CACHE_DIR), { recursive: true });
|
|
3487
|
+
await writeFile3(path, JSON.stringify(serializeBm25Model(model)), "utf8");
|
|
3488
|
+
} catch {
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
async function getOrBuildBm25Model(projectRoot, revision, rawItems, docTexts) {
|
|
3084
3492
|
if (bm25ModelCache !== null && bm25ModelCache.revision === revision) {
|
|
3085
3493
|
return bm25ModelCache.model;
|
|
3086
3494
|
}
|
|
3495
|
+
const fromDisk = await loadBm25ModelFromDisk(projectRoot, revision);
|
|
3496
|
+
if (fromDisk !== null) {
|
|
3497
|
+
bm25ModelCache = { revision, model: fromDisk };
|
|
3498
|
+
return fromDisk;
|
|
3499
|
+
}
|
|
3087
3500
|
bm25BuildCount += 1;
|
|
3088
3501
|
const model = buildBm25Model(
|
|
3089
3502
|
rawItems.map((item) => ({
|
|
@@ -3092,6 +3505,7 @@ function getOrBuildBm25Model(revision, rawItems, docTexts) {
|
|
|
3092
3505
|
}))
|
|
3093
3506
|
);
|
|
3094
3507
|
bm25ModelCache = { revision, model };
|
|
3508
|
+
await saveBm25ModelToDisk(projectRoot, revision, model);
|
|
3095
3509
|
return model;
|
|
3096
3510
|
}
|
|
3097
3511
|
function compareStableIds(a, b) {
|
|
@@ -3111,7 +3525,7 @@ function buildScopeRankMap(items, projectRoot) {
|
|
|
3111
3525
|
const colon = it.stable_id.indexOf(":");
|
|
3112
3526
|
const alias = colon === -1 ? "" : it.stable_id.slice(0, colon);
|
|
3113
3527
|
const localId = colon === -1 ? it.stable_id : it.stable_id.slice(colon + 1);
|
|
3114
|
-
const semanticScope = it.description.semantic_scope ?? it.
|
|
3528
|
+
const semanticScope = it.description.semantic_scope ?? layerFromStableId(it.stable_id);
|
|
3115
3529
|
return {
|
|
3116
3530
|
global_ref: it.stable_id,
|
|
3117
3531
|
store_uuid: aliasToUuid.get(alias) ?? alias,
|
|
@@ -3137,6 +3551,103 @@ function compareScopeThenId(left, right, scopeRank) {
|
|
|
3137
3551
|
}
|
|
3138
3552
|
return compareStableIds(left.stable_id, right.stable_id);
|
|
3139
3553
|
}
|
|
3554
|
+
async function buildScoringContext(projectRoot, revision, rawItems, opts) {
|
|
3555
|
+
const scoringContext = {
|
|
3556
|
+
nowMs: Date.now(),
|
|
3557
|
+
targetPaths: opts.targetPaths,
|
|
3558
|
+
queryTerms: buildQueryTerms(opts.queryText),
|
|
3559
|
+
// PLN-004 F1: resolve the credibility half-lives + floors ONCE here (never per
|
|
3560
|
+
// candidate) so credibilityFactor is a pure lookup on the ranking hot path.
|
|
3561
|
+
credibilityHalfLives: readCredibilityHalfLives(projectRoot),
|
|
3562
|
+
credibilityFloors: readCredibilityFloors(projectRoot)
|
|
3563
|
+
};
|
|
3564
|
+
const docTexts = /* @__PURE__ */ new Map();
|
|
3565
|
+
for (const item of rawItems) {
|
|
3566
|
+
docTexts.set(item.stable_id, documentTextForItem(item.description));
|
|
3567
|
+
}
|
|
3568
|
+
scoringContext.docTexts = docTexts;
|
|
3569
|
+
if (scoringContext.queryTerms.length > 0 && rawItems.length > 0) {
|
|
3570
|
+
scoringContext.bm25 = await getOrBuildBm25Model(projectRoot, revision, rawItems, docTexts);
|
|
3571
|
+
}
|
|
3572
|
+
scoringContext.scopeRank = buildScopeRankMap(rawItems, projectRoot);
|
|
3573
|
+
const embedConfig = readEmbedConfig(projectRoot);
|
|
3574
|
+
if (embedConfig.enabled && opts.queryText.trim().length > 0 && rawItems.length > 0) {
|
|
3575
|
+
const embedder = await loadEmbedder(embedConfig.model);
|
|
3576
|
+
const vectorScores = await buildVectorScores(
|
|
3577
|
+
embedder,
|
|
3578
|
+
opts.queryText,
|
|
3579
|
+
rawItems.map((item) => ({
|
|
3580
|
+
stable_id: item.stable_id,
|
|
3581
|
+
text: docTexts.get(item.stable_id) ?? documentTextForItem(item.description)
|
|
3582
|
+
})),
|
|
3583
|
+
{ projectRoot, corpusRevision: revision, embeddingModel: embedConfig.model }
|
|
3584
|
+
);
|
|
3585
|
+
if (vectorScores !== null) {
|
|
3586
|
+
scoringContext.vectorScores = vectorScores;
|
|
3587
|
+
scoringContext.vectorWeight = embedConfig.weight;
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
const configuredFusion = readFusion(projectRoot);
|
|
3591
|
+
const vectorActive = scoringContext.vectorScores !== void 0 && scoringContext.vectorScores.size > 0;
|
|
3592
|
+
scoringContext.fusion = configuredFusion === "auto" ? vectorActive ? "rrf" : "additive" : configuredFusion;
|
|
3593
|
+
let queryTermWeights;
|
|
3594
|
+
if (scoringContext.queryTerms.length > 0) {
|
|
3595
|
+
queryTermWeights = expandQueryTerms(opts.queryText);
|
|
3596
|
+
}
|
|
3597
|
+
if (scoringContext.fusion === "rrf" && scoringContext.queryTerms.length > 0 && rawItems.length > 0) {
|
|
3598
|
+
const rankIds = rawItems.map((item) => item.stable_id).sort((a, b) => compareStableIds(a, b));
|
|
3599
|
+
if (scoringContext.bm25 !== void 0) {
|
|
3600
|
+
scoringContext.bm25Ranks = rankDocuments(scoringContext.bm25, rankIds, scoringContext.queryTerms);
|
|
3601
|
+
}
|
|
3602
|
+
if (scoringContext.vectorScores !== void 0) {
|
|
3603
|
+
scoringContext.vectorRanks = rankByScore(rankIds, scoringContext.vectorScores);
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
return scoringContext;
|
|
3607
|
+
}
|
|
3608
|
+
var PROXIMITY_WINDOW = 6;
|
|
3609
|
+
var PROXIMITY_BOOST_CAP = 0.15;
|
|
3610
|
+
function proximityBoost(item, context, contentScore2) {
|
|
3611
|
+
if (contentScore2 <= 0) return 0;
|
|
3612
|
+
const text = context.docTexts?.get(item.stable_id);
|
|
3613
|
+
if (text === void 0 || text.length === 0) return 0;
|
|
3614
|
+
const tokens = text.toLowerCase().split(/[^a-z0-9_$#]+/u).filter(Boolean);
|
|
3615
|
+
const queryTerms = context.queryTerms;
|
|
3616
|
+
if (queryTerms.length < 2) return 0;
|
|
3617
|
+
const positions = /* @__PURE__ */ new Map();
|
|
3618
|
+
for (const qt of queryTerms) {
|
|
3619
|
+
const qtLower = qt.toLowerCase();
|
|
3620
|
+
const pos = [];
|
|
3621
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
3622
|
+
if (tokens[i] === qtLower) {
|
|
3623
|
+
pos.push(i);
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
if (pos.length > 0) {
|
|
3627
|
+
positions.set(qtLower, pos);
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
if (positions.size < 2) return 0;
|
|
3631
|
+
const termList = [...positions.entries()];
|
|
3632
|
+
let minDist = Infinity;
|
|
3633
|
+
for (let i = 0; i < termList.length; i++) {
|
|
3634
|
+
const [, posI] = termList[i];
|
|
3635
|
+
for (let j = i + 1; j < termList.length; j++) {
|
|
3636
|
+
const [, posJ] = termList[j];
|
|
3637
|
+
for (const pi of posI) {
|
|
3638
|
+
for (const pj of posJ) {
|
|
3639
|
+
const dist = Math.abs(pi - pj);
|
|
3640
|
+
if (dist < minDist) minDist = dist;
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
if (!Number.isFinite(minDist)) return 0;
|
|
3646
|
+
if (minDist >= PROXIMITY_WINDOW) return 0;
|
|
3647
|
+
const ratio = 1 - minDist / PROXIMITY_WINDOW;
|
|
3648
|
+
const boost = contentScore2 * PROXIMITY_BOOST_CAP * ratio;
|
|
3649
|
+
return boost;
|
|
3650
|
+
}
|
|
3140
3651
|
function sortDescriptionItems(rawItems, scoringContext) {
|
|
3141
3652
|
if (scoringContext === void 0) {
|
|
3142
3653
|
return [...rawItems].sort((left, right) => compareStableIds(left.stable_id, right.stable_id)).map((item) => ({ item, score: 0 }));
|
|
@@ -3151,6 +3662,25 @@ function sortDescriptionItems(rawItems, scoringContext) {
|
|
|
3151
3662
|
);
|
|
3152
3663
|
return scored;
|
|
3153
3664
|
}
|
|
3665
|
+
function rankDescriptionItems(items, scoringContext, mode, options = {}) {
|
|
3666
|
+
const sorted = sortDescriptionItems(items, scoringContext);
|
|
3667
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3668
|
+
const rankedScored = sorted.filter(({ item }) => {
|
|
3669
|
+
if (seen.has(item.stable_id)) return false;
|
|
3670
|
+
seen.add(item.stable_id);
|
|
3671
|
+
return true;
|
|
3672
|
+
});
|
|
3673
|
+
if (mode === "triage") {
|
|
3674
|
+
return rankedScored;
|
|
3675
|
+
}
|
|
3676
|
+
const topK = options.topK ?? rankedScored.length;
|
|
3677
|
+
const cappedScored = rankedScored.slice(0, topK);
|
|
3678
|
+
const relevanceRatio = options.relevanceRatio ?? 0;
|
|
3679
|
+
const hasQuery = scoringContext.queryTerms.length > 0;
|
|
3680
|
+
const maxScore = rankedScored.length > 0 ? rankedScored[0].score : 0;
|
|
3681
|
+
const relevanceFloor = maxScore * relevanceRatio;
|
|
3682
|
+
return hasQuery && maxScore > 0 && relevanceRatio > 0 ? cappedScored.filter((entry) => entry.score >= relevanceFloor) : cappedScored;
|
|
3683
|
+
}
|
|
3154
3684
|
function documentTextForItem(description) {
|
|
3155
3685
|
return [
|
|
3156
3686
|
description.summary,
|
|
@@ -3158,17 +3688,21 @@ function documentTextForItem(description) {
|
|
|
3158
3688
|
...description.intent_clues,
|
|
3159
3689
|
...description.tech_stack,
|
|
3160
3690
|
...description.impact,
|
|
3161
|
-
...description.
|
|
3162
|
-
|
|
3691
|
+
...description.tags ?? [],
|
|
3692
|
+
// v2.2 glossary aliases (C-002): long-tail synonym terms feed the flat
|
|
3693
|
+
// vector-embedding document verbatim, same as the BM25F summary slot below.
|
|
3694
|
+
...description.aliases ?? []
|
|
3163
3695
|
].join(" ");
|
|
3164
3696
|
}
|
|
3165
3697
|
function documentFieldsForItem(description) {
|
|
3166
3698
|
return {
|
|
3167
3699
|
title: tokenize2(description.summary),
|
|
3168
|
-
tags: tokenize2(
|
|
3169
|
-
|
|
3700
|
+
tags: tokenize2([...description.tags ?? [], ...description.tech_stack].join(" ")),
|
|
3701
|
+
summary: tokenize2(
|
|
3702
|
+
[description.must_read_if, ...description.intent_clues, ...description.aliases ?? []].join(
|
|
3703
|
+
" "
|
|
3704
|
+
)
|
|
3170
3705
|
),
|
|
3171
|
-
summary: tokenize2([description.must_read_if, ...description.intent_clues].join(" ")),
|
|
3172
3706
|
body: tokenize2(description.impact.join(" "))
|
|
3173
3707
|
};
|
|
3174
3708
|
}
|
|
@@ -3199,6 +3733,9 @@ var BM25_WEIGHT = 50;
|
|
|
3199
3733
|
var SALIENCE_PROVEN = 15;
|
|
3200
3734
|
var SALIENCE_VERIFIED = 8;
|
|
3201
3735
|
var SALIENCE_DRAFT = 0;
|
|
3736
|
+
var RRF_K = 10;
|
|
3737
|
+
var RRF_NORMALIZATION = 2e3;
|
|
3738
|
+
var RRF_STRUCTURAL_SCALE = 0.2;
|
|
3202
3739
|
function salienceScore(item) {
|
|
3203
3740
|
switch (item.description?.maturity) {
|
|
3204
3741
|
case "proven":
|
|
@@ -3209,35 +3746,109 @@ function salienceScore(item) {
|
|
|
3209
3746
|
return SALIENCE_DRAFT;
|
|
3210
3747
|
}
|
|
3211
3748
|
}
|
|
3212
|
-
function
|
|
3213
|
-
|
|
3214
|
-
if (context.
|
|
3215
|
-
|
|
3749
|
+
function contentScore(item, context) {
|
|
3750
|
+
const hasQuery = context.queryTerms.length > 0;
|
|
3751
|
+
if (context.fusion === "rrf" && hasQuery) {
|
|
3752
|
+
let rrf = 0;
|
|
3753
|
+
const bm25Rank = context.bm25Ranks?.get(item.stable_id);
|
|
3754
|
+
if (bm25Rank !== void 0) rrf += 1 / (RRF_K + bm25Rank);
|
|
3755
|
+
const vectorRank = context.vectorRanks?.get(item.stable_id);
|
|
3756
|
+
if (vectorRank !== void 0) rrf += 1 / (RRF_K + vectorRank);
|
|
3757
|
+
return RRF_NORMALIZATION * rrf;
|
|
3758
|
+
}
|
|
3759
|
+
let content = 0;
|
|
3760
|
+
if (context.bm25 !== void 0 && hasQuery) {
|
|
3761
|
+
content += BM25_WEIGHT * context.bm25.scoreDoc(item.stable_id, context.queryTerms);
|
|
3216
3762
|
}
|
|
3217
3763
|
if (context.vectorScores !== void 0) {
|
|
3218
|
-
|
|
3764
|
+
content += (context.vectorWeight ?? 0) * (context.vectorScores.get(item.stable_id) ?? 0);
|
|
3219
3765
|
}
|
|
3220
|
-
|
|
3766
|
+
return content;
|
|
3767
|
+
}
|
|
3768
|
+
function recencyBoost(item, context) {
|
|
3221
3769
|
const createdAtRaw = item.description?.created_at;
|
|
3222
3770
|
if (typeof createdAtRaw === "string" && createdAtRaw.length > 0) {
|
|
3223
3771
|
const createdMs = Date.parse(createdAtRaw);
|
|
3224
3772
|
if (Number.isFinite(createdMs) && context.nowMs - createdMs < RECENCY_WINDOW_MS) {
|
|
3225
|
-
|
|
3773
|
+
return RECENCY_BOOST;
|
|
3226
3774
|
}
|
|
3227
3775
|
}
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3776
|
+
return 0;
|
|
3777
|
+
}
|
|
3778
|
+
function localityBoost(item, context) {
|
|
3779
|
+
if (context.targetPaths.length === 0) return 0;
|
|
3780
|
+
const relevancePaths = item.description?.relevance_paths ?? [];
|
|
3781
|
+
let best = 0;
|
|
3782
|
+
outer: for (const rp of relevancePaths) {
|
|
3783
|
+
for (const tp of context.targetPaths) {
|
|
3784
|
+
const tier = localityTier(rp, tp);
|
|
3785
|
+
if (tier > best) best = tier;
|
|
3786
|
+
if (best === LOCALITY_SAME_FILE) break outer;
|
|
3237
3787
|
}
|
|
3238
|
-
score += best;
|
|
3239
3788
|
}
|
|
3240
|
-
return
|
|
3789
|
+
return best;
|
|
3790
|
+
}
|
|
3791
|
+
function structuralScaleFor(context) {
|
|
3792
|
+
return context.fusion === "rrf" && context.queryTerms.length > 0 ? RRF_STRUCTURAL_SCALE : 1;
|
|
3793
|
+
}
|
|
3794
|
+
function credibilityFactor(item, context) {
|
|
3795
|
+
const halfLives = context.credibilityHalfLives;
|
|
3796
|
+
const floors = context.credibilityFloors;
|
|
3797
|
+
if (halfLives === void 0 || floors === void 0) return 1;
|
|
3798
|
+
const createdAtRaw = item.description?.created_at;
|
|
3799
|
+
if (typeof createdAtRaw !== "string" || createdAtRaw.length === 0) return 1;
|
|
3800
|
+
const createdMs = Date.parse(createdAtRaw);
|
|
3801
|
+
if (!Number.isFinite(createdMs)) return 1;
|
|
3802
|
+
const ageDays = (context.nowMs - createdMs) / (24 * 60 * 60 * 1e3);
|
|
3803
|
+
if (ageDays <= 0) return 1;
|
|
3804
|
+
const type = item.description?.knowledge_type;
|
|
3805
|
+
const halfLife = type !== void 0 ? halfLives[type] : halfLives.decisions;
|
|
3806
|
+
const factor = Math.pow(2, -ageDays / halfLife);
|
|
3807
|
+
const maturity = item.description?.maturity;
|
|
3808
|
+
const floor = maturity !== void 0 ? floors[maturity] : floors.draft;
|
|
3809
|
+
return Math.max(floor, Math.min(1, factor));
|
|
3810
|
+
}
|
|
3811
|
+
function scoreDescriptionItem(item, context) {
|
|
3812
|
+
const content = contentScore(item, context);
|
|
3813
|
+
const structural = salienceScore(item) + recencyBoost(item, context) + localityBoost(item, context);
|
|
3814
|
+
const proximity = proximityBoost(item, context, content);
|
|
3815
|
+
return (content + structuralScaleFor(context) * structural + proximity) * credibilityFactor(item, context);
|
|
3816
|
+
}
|
|
3817
|
+
function scoreBreakdownForItem(item, context) {
|
|
3818
|
+
const hasQuery = context.queryTerms.length > 0;
|
|
3819
|
+
const rrfMode = context.fusion === "rrf" && hasQuery;
|
|
3820
|
+
let bm25 = 0;
|
|
3821
|
+
let vector = 0;
|
|
3822
|
+
let bm25Rank;
|
|
3823
|
+
let vectorRank;
|
|
3824
|
+
if (rrfMode) {
|
|
3825
|
+
bm25Rank = context.bm25Ranks?.get(item.stable_id);
|
|
3826
|
+
if (bm25Rank !== void 0) bm25 = RRF_NORMALIZATION * (1 / (RRF_K + bm25Rank));
|
|
3827
|
+
vectorRank = context.vectorRanks?.get(item.stable_id);
|
|
3828
|
+
if (vectorRank !== void 0) vector = RRF_NORMALIZATION * (1 / (RRF_K + vectorRank));
|
|
3829
|
+
} else {
|
|
3830
|
+
bm25 = context.bm25 !== void 0 && hasQuery ? BM25_WEIGHT * context.bm25.scoreDoc(item.stable_id, context.queryTerms) : 0;
|
|
3831
|
+
vector = context.vectorScores !== void 0 ? (context.vectorWeight ?? 0) * (context.vectorScores.get(item.stable_id) ?? 0) : 0;
|
|
3832
|
+
}
|
|
3833
|
+
const scale = structuralScaleFor(context);
|
|
3834
|
+
const salience = salienceScore(item) * scale;
|
|
3835
|
+
const recency = recencyBoost(item, context) * scale;
|
|
3836
|
+
const locality = localityBoost(item, context) * scale;
|
|
3837
|
+
const proximity = proximityBoost(item, context, bm25 + vector);
|
|
3838
|
+
const credibility = credibilityFactor(item, context);
|
|
3839
|
+
const final = (bm25 + vector + salience + recency + locality + proximity) * credibility;
|
|
3840
|
+
return {
|
|
3841
|
+
final,
|
|
3842
|
+
...bm25 !== 0 ? { bm25 } : {},
|
|
3843
|
+
...bm25Rank !== void 0 ? { bm25_rank: bm25Rank } : {},
|
|
3844
|
+
...vector !== 0 ? { vector } : {},
|
|
3845
|
+
...vectorRank !== void 0 ? { vector_rank: vectorRank } : {},
|
|
3846
|
+
salience,
|
|
3847
|
+
recency,
|
|
3848
|
+
locality,
|
|
3849
|
+
proximity,
|
|
3850
|
+
credibility
|
|
3851
|
+
};
|
|
3241
3852
|
}
|
|
3242
3853
|
function localityTier(relevancePath, targetPath) {
|
|
3243
3854
|
if (relevancePath === targetPath) return LOCALITY_SAME_FILE;
|
|
@@ -3274,7 +3885,13 @@ function relatedLookupKeys(stableId) {
|
|
|
3274
3885
|
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
3886
|
async function recall(projectRoot, input) {
|
|
3276
3887
|
const planResult = await planContext(projectRoot, input);
|
|
3277
|
-
const {
|
|
3888
|
+
const {
|
|
3889
|
+
selection_token: _token,
|
|
3890
|
+
payload_trimmed: _pt,
|
|
3891
|
+
payload_over_budget: _pob,
|
|
3892
|
+
candidate_scores: candidateScores,
|
|
3893
|
+
...planRest
|
|
3894
|
+
} = planResult;
|
|
3278
3895
|
let bodyIndex;
|
|
3279
3896
|
try {
|
|
3280
3897
|
bodyIndex = await buildCrossStoreBodyIndex(projectRoot);
|
|
@@ -3323,13 +3940,15 @@ async function recall(projectRoot, input) {
|
|
|
3323
3940
|
const pathByStableId = new Map(paths.map((p) => [p.stable_id, p]));
|
|
3324
3941
|
const entries = planRest.candidates.map((c, index) => {
|
|
3325
3942
|
const readPath = pathByStableId.get(c.stable_id);
|
|
3943
|
+
const scored = candidateScores?.get(c.stable_id);
|
|
3326
3944
|
return {
|
|
3327
3945
|
stable_id: c.stable_id,
|
|
3328
3946
|
rank: index + 1,
|
|
3329
|
-
description: c.description,
|
|
3947
|
+
description: slimDescription(c.description),
|
|
3330
3948
|
...readPath ? { read_path: readPath.path } : {},
|
|
3331
3949
|
...readPath?.store ? { store: readPath.store } : {},
|
|
3332
|
-
...isAlwaysActive(c) ? { body_in_context: true } : {}
|
|
3950
|
+
...isAlwaysActive(c) ? { body_in_context: true } : {},
|
|
3951
|
+
...scored ? { score: scored.score, score_breakdown: scored.score_breakdown } : {}
|
|
3333
3952
|
};
|
|
3334
3953
|
});
|
|
3335
3954
|
const { entries: _reqProfiles, candidates: _candidates, ...planRestNoLists } = planRest;
|
|
@@ -3345,6 +3964,14 @@ function isAlwaysActive(candidate) {
|
|
|
3345
3964
|
const { relevance_scope, knowledge_type } = candidate.description;
|
|
3346
3965
|
return (relevance_scope ?? "broad") !== "narrow" && ALWAYS_ACTIVE_TYPES2.has(knowledge_type ?? "");
|
|
3347
3966
|
}
|
|
3967
|
+
function slimDescription(d) {
|
|
3968
|
+
return {
|
|
3969
|
+
summary: d.summary,
|
|
3970
|
+
must_read_if: d.must_read_if,
|
|
3971
|
+
intent_clues: d.intent_clues,
|
|
3972
|
+
...d.knowledge_type !== void 0 ? { knowledge_type: d.knowledge_type } : {}
|
|
3973
|
+
};
|
|
3974
|
+
}
|
|
3348
3975
|
function buildNextSteps(planResult, paths, candidateById, candidateLookup) {
|
|
3349
3976
|
const nextSteps = [];
|
|
3350
3977
|
const omitted = (planResult.dropped ?? []).filter((d) => d.reason === "retrieval_budget").length;
|
|
@@ -3489,22 +4116,8 @@ import {
|
|
|
3489
4116
|
import { enforcePayloadLimit as enforcePayloadLimit3 } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
3490
4117
|
|
|
3491
4118
|
// src/services/archive-scan.ts
|
|
4119
|
+
import { isHighValueArchiveCandidate } from "@fenglimg/fabric-shared";
|
|
3492
4120
|
var ANTI_LOOP_HOURS = 12;
|
|
3493
|
-
var HIGH_VALUE_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
3494
|
-
"knowledge_context_planned",
|
|
3495
|
-
"edit_paths_recorded",
|
|
3496
|
-
"edit_intent_checked"
|
|
3497
|
-
// the real high-freq edit signal (rc.37 NEW-14/B3)
|
|
3498
|
-
]);
|
|
3499
|
-
var NORMATIVE_KEYWORDS = [
|
|
3500
|
-
"\u4EE5\u540E",
|
|
3501
|
-
"always",
|
|
3502
|
-
"never",
|
|
3503
|
-
"from now on",
|
|
3504
|
-
"\u4E0B\u6B21",
|
|
3505
|
-
"\u8BB0\u4E00\u4E0B",
|
|
3506
|
-
"\u6C38\u8FDC\u4E0D\u8981"
|
|
3507
|
-
];
|
|
3508
4121
|
var PROPOSED_KEYS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
3509
4122
|
async function collectArchiveScan(projectRoot, input = {}) {
|
|
3510
4123
|
const nowMs = typeof input.now_ms === "number" ? input.now_ms : Date.now();
|
|
@@ -3572,7 +4185,7 @@ async function collectArchiveScan(projectRoot, input = {}) {
|
|
|
3572
4185
|
continue;
|
|
3573
4186
|
}
|
|
3574
4187
|
if (typeof attempt.covered_through_ts === "number") {
|
|
3575
|
-
if (
|
|
4188
|
+
if (isHighValueArchiveCandidate(events, sid, attempt.covered_through_ts)) {
|
|
3576
4189
|
kept.push(sid);
|
|
3577
4190
|
} else {
|
|
3578
4191
|
dropped.push({ session_id: sid, reason: "no_new_signal" });
|
|
@@ -3598,28 +4211,6 @@ async function collectArchiveScan(projectRoot, input = {}) {
|
|
|
3598
4211
|
already_proposed_keys: [...proposedKeys]
|
|
3599
4212
|
};
|
|
3600
4213
|
}
|
|
3601
|
-
function hasHighValueSignal(events, sessionId, watermarkTs) {
|
|
3602
|
-
let latestTurn = null;
|
|
3603
|
-
for (const e of events) {
|
|
3604
|
-
if (e.session_id !== sessionId) continue;
|
|
3605
|
-
if (typeof e.ts !== "number" || e.ts <= watermarkTs) continue;
|
|
3606
|
-
if (typeof e.event_type === "string" && HIGH_VALUE_EVENT_TYPES.has(e.event_type)) {
|
|
3607
|
-
return true;
|
|
3608
|
-
}
|
|
3609
|
-
if (e.event_type === "assistant_turn_observed") {
|
|
3610
|
-
if (!latestTurn || typeof latestTurn.ts === "number" && e.ts > latestTurn.ts) {
|
|
3611
|
-
latestTurn = e;
|
|
3612
|
-
}
|
|
3613
|
-
}
|
|
3614
|
-
}
|
|
3615
|
-
if (latestTurn) {
|
|
3616
|
-
const haystack = JSON.stringify(latestTurn).toLowerCase();
|
|
3617
|
-
for (const kw of NORMATIVE_KEYWORDS) {
|
|
3618
|
-
if (haystack.includes(kw.toLowerCase())) return true;
|
|
3619
|
-
}
|
|
3620
|
-
}
|
|
3621
|
-
return false;
|
|
3622
|
-
}
|
|
3623
4214
|
|
|
3624
4215
|
// src/tools/archive-scan.ts
|
|
3625
4216
|
function registerArchiveScan(server, tracker) {
|
|
@@ -3679,9 +4270,9 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
|
|
|
3679
4270
|
// src/services/review.ts
|
|
3680
4271
|
import { execFileSync } from "child_process";
|
|
3681
4272
|
import { existsSync as existsSync5 } from "fs";
|
|
3682
|
-
import { readFile as
|
|
4273
|
+
import { readFile as readFile9, readdir as readdir2, stat as stat2, unlink as unlink2 } from "fs/promises";
|
|
3683
4274
|
import { homedir } from "os";
|
|
3684
|
-
import { basename, isAbsolute, join as
|
|
4275
|
+
import { basename, isAbsolute, join as join12, relative, resolve as resolve2, sep as sep2 } from "path";
|
|
3685
4276
|
|
|
3686
4277
|
// src/services/promotion-gate.ts
|
|
3687
4278
|
function toLocalId(id) {
|
|
@@ -3717,12 +4308,12 @@ async function hasUnresolvedDismissal(projectRoot, id) {
|
|
|
3717
4308
|
}
|
|
3718
4309
|
|
|
3719
4310
|
// src/services/review.ts
|
|
3720
|
-
import { allocateStoreKnowledgeId, isPersonalScope as isPersonalScope2 } from "@fenglimg/fabric-shared";
|
|
4311
|
+
import { allocateStoreKnowledgeId, isPersonalScope as isPersonalScope2, loadProjectConfig as loadProjectConfig3 } from "@fenglimg/fabric-shared";
|
|
3721
4312
|
|
|
3722
4313
|
// src/services/pending-dedupe.ts
|
|
3723
4314
|
import { existsSync as existsSync4 } from "fs";
|
|
3724
|
-
import { readdir, readFile as
|
|
3725
|
-
import { join as
|
|
4315
|
+
import { readdir, readFile as readFile8, unlink } from "fs/promises";
|
|
4316
|
+
import { join as join11 } from "path";
|
|
3726
4317
|
var PENDING_TYPES = ["decisions", "pitfalls", "guidelines", "models", "processes"];
|
|
3727
4318
|
var DISAMBIGUATION_SUFFIX = /^(.+)-([2-9])\.md$/u;
|
|
3728
4319
|
var SOURCE_SESSIONS_LINE = /^source_sessions:\s*\[(.*)\]\s*$/mu;
|
|
@@ -3795,7 +4386,7 @@ async function mergePendingTwins(projectRoot) {
|
|
|
3795
4386
|
continue;
|
|
3796
4387
|
}
|
|
3797
4388
|
for (const type of PENDING_TYPES) {
|
|
3798
|
-
const dir =
|
|
4389
|
+
const dir = join11(pendingBase2, type);
|
|
3799
4390
|
if (!existsSync4(dir)) continue;
|
|
3800
4391
|
let names;
|
|
3801
4392
|
try {
|
|
@@ -3816,10 +4407,10 @@ async function mergePendingTwins(projectRoot) {
|
|
|
3816
4407
|
if (groupNames.length < 2) continue;
|
|
3817
4408
|
const parsed = [];
|
|
3818
4409
|
for (const name of groupNames) {
|
|
3819
|
-
const abs =
|
|
4410
|
+
const abs = join11(dir, name);
|
|
3820
4411
|
let content;
|
|
3821
4412
|
try {
|
|
3822
|
-
content = await
|
|
4413
|
+
content = await readFile8(abs, "utf8");
|
|
3823
4414
|
} catch {
|
|
3824
4415
|
continue;
|
|
3825
4416
|
}
|
|
@@ -3919,7 +4510,7 @@ async function reviewPending(projectRoot, input) {
|
|
|
3919
4510
|
case "search":
|
|
3920
4511
|
return {
|
|
3921
4512
|
action: "search",
|
|
3922
|
-
items: await
|
|
4513
|
+
items: await triageSearch(projectRoot, input.query, input.filters)
|
|
3923
4514
|
};
|
|
3924
4515
|
default: {
|
|
3925
4516
|
const exhaustive = input;
|
|
@@ -3999,7 +4590,7 @@ async function listPending(projectRoot, filters) {
|
|
|
3999
4590
|
}
|
|
4000
4591
|
for (const source of sources) {
|
|
4001
4592
|
for (const type of typesToScan) {
|
|
4002
|
-
const dir =
|
|
4593
|
+
const dir = join12(source.root, type);
|
|
4003
4594
|
if (!existsSync5(dir)) {
|
|
4004
4595
|
continue;
|
|
4005
4596
|
}
|
|
@@ -4011,10 +4602,10 @@ async function listPending(projectRoot, filters) {
|
|
|
4011
4602
|
}
|
|
4012
4603
|
for (const name of entries) {
|
|
4013
4604
|
if (!name.endsWith(".md")) continue;
|
|
4014
|
-
const absolutePath =
|
|
4605
|
+
const absolutePath = join12(dir, name);
|
|
4015
4606
|
let content;
|
|
4016
4607
|
try {
|
|
4017
|
-
content = await
|
|
4608
|
+
content = await readFile9(absolutePath, "utf8");
|
|
4018
4609
|
} catch {
|
|
4019
4610
|
continue;
|
|
4020
4611
|
}
|
|
@@ -4124,7 +4715,7 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
4124
4715
|
let targetAbs;
|
|
4125
4716
|
let writtenTarget = false;
|
|
4126
4717
|
try {
|
|
4127
|
-
const content = await
|
|
4718
|
+
const content = await readFile9(sourceAbs, "utf8");
|
|
4128
4719
|
const fm = parseFrontmatter(content);
|
|
4129
4720
|
const pluralType = fm.type;
|
|
4130
4721
|
if (pluralType === void 0 || !PLURAL_TYPES.includes(pluralType)) {
|
|
@@ -4138,7 +4729,12 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
4138
4729
|
);
|
|
4139
4730
|
allocatedId = stableId;
|
|
4140
4731
|
const newFilename = `${stableId}--${slug}.md`;
|
|
4141
|
-
|
|
4732
|
+
const promoteProject = layer === "team" ? loadProjectConfig3(projectRoot)?.active_project : void 0;
|
|
4733
|
+
targetAbs = join12(
|
|
4734
|
+
resolveStoreCanonicalBase(layer, projectRoot, promoteProject),
|
|
4735
|
+
pluralType,
|
|
4736
|
+
newFilename
|
|
4737
|
+
);
|
|
4142
4738
|
await ensureParentDirectory(targetAbs);
|
|
4143
4739
|
const rewritten = rewriteFrontmatterMerge(
|
|
4144
4740
|
rewriteFrontmatterForPromote(content, stableId),
|
|
@@ -4196,7 +4792,7 @@ async function rejectAll(projectRoot, pendingPaths, reason) {
|
|
|
4196
4792
|
try {
|
|
4197
4793
|
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
4198
4794
|
if (existsSync5(sandboxed.abs)) {
|
|
4199
|
-
const content = await
|
|
4795
|
+
const content = await readFile9(sandboxed.abs, "utf8");
|
|
4200
4796
|
const merged = rewriteFrontmatterMerge(content, { status: "rejected" });
|
|
4201
4797
|
const rejectedAbs = sandboxed.abs.includes(`${sep2}pending${sep2}`) ? sandboxed.abs.replace(`${sep2}pending${sep2}`, `${sep2}rejected${sep2}`) : null;
|
|
4202
4798
|
if (rejectedAbs !== null) {
|
|
@@ -4223,7 +4819,7 @@ async function modifyEntry(projectRoot, pendingPath, changes) {
|
|
|
4223
4819
|
if (target === null) {
|
|
4224
4820
|
throw new Error(`modify target not found: ${pendingPath}`);
|
|
4225
4821
|
}
|
|
4226
|
-
const content = await
|
|
4822
|
+
const content = await readFile9(target.absPath, "utf8");
|
|
4227
4823
|
const fm = parseFrontmatter(content);
|
|
4228
4824
|
const currentLayer = fm.layer ?? "team";
|
|
4229
4825
|
if (fm.maturity === "verified" && changes.maturity === "proven" && fm.id !== void 0) {
|
|
@@ -4325,8 +4921,9 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
|
4325
4921
|
pluralType,
|
|
4326
4922
|
resolveWriteTargetStoreDir(toLayer, projectRoot)
|
|
4327
4923
|
);
|
|
4328
|
-
const
|
|
4329
|
-
|
|
4924
|
+
const flipProject = toLayer === "team" ? loadProjectConfig3(projectRoot)?.active_project : void 0;
|
|
4925
|
+
const toAbs = join12(
|
|
4926
|
+
resolveStoreCanonicalBase(toLayer, projectRoot, flipProject),
|
|
4330
4927
|
pluralType,
|
|
4331
4928
|
`${newStableId}--${slug}.md`
|
|
4332
4929
|
);
|
|
@@ -4348,20 +4945,22 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
|
4348
4945
|
{ ...effectivePatch, last_review_confirmed_at: (/* @__PURE__ */ new Date()).toISOString() },
|
|
4349
4946
|
{ id: newStableId }
|
|
4350
4947
|
);
|
|
4351
|
-
|
|
4948
|
+
let moved = false;
|
|
4352
4949
|
if (target.isInProjectTree) {
|
|
4353
4950
|
const relSource = relative(projectRoot, target.absPath);
|
|
4951
|
+
const relDest = relative(projectRoot, toAbs);
|
|
4354
4952
|
try {
|
|
4355
|
-
execFileSync("git", ["
|
|
4953
|
+
execFileSync("git", ["mv", "-f", relSource, relDest], {
|
|
4356
4954
|
cwd: projectRoot,
|
|
4357
4955
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4358
4956
|
});
|
|
4957
|
+
moved = true;
|
|
4359
4958
|
} catch {
|
|
4360
|
-
|
|
4361
|
-
await unlink2(target.absPath);
|
|
4362
|
-
}
|
|
4959
|
+
moved = false;
|
|
4363
4960
|
}
|
|
4364
|
-
}
|
|
4961
|
+
}
|
|
4962
|
+
await atomicWriteText(toAbs, rewritten);
|
|
4963
|
+
if (!moved && existsSync5(target.absPath) && target.absPath !== toAbs) {
|
|
4365
4964
|
await unlink2(target.absPath);
|
|
4366
4965
|
}
|
|
4367
4966
|
const flipReason = `layer_flip:${priorStableId ?? "<unassigned>"}->${newStableId}`;
|
|
@@ -4425,7 +5024,7 @@ function getSearchDirectoryCache(cacheKey) {
|
|
|
4425
5024
|
return created;
|
|
4426
5025
|
}
|
|
4427
5026
|
async function listIndexedSearchEntries(source, type) {
|
|
4428
|
-
const dir =
|
|
5027
|
+
const dir = join12(source.root, type);
|
|
4429
5028
|
let entries;
|
|
4430
5029
|
try {
|
|
4431
5030
|
entries = await readdir2(dir);
|
|
@@ -4438,7 +5037,7 @@ async function listIndexedSearchEntries(source, type) {
|
|
|
4438
5037
|
const indexed = [];
|
|
4439
5038
|
for (const name of entries) {
|
|
4440
5039
|
if (!name.endsWith(".md")) continue;
|
|
4441
|
-
const absolutePath =
|
|
5040
|
+
const absolutePath = join12(dir, name);
|
|
4442
5041
|
let fingerprint;
|
|
4443
5042
|
try {
|
|
4444
5043
|
const st = await stat2(absolutePath);
|
|
@@ -4455,7 +5054,7 @@ async function listIndexedSearchEntries(source, type) {
|
|
|
4455
5054
|
}
|
|
4456
5055
|
let content;
|
|
4457
5056
|
try {
|
|
4458
|
-
content = await
|
|
5057
|
+
content = await readFile9(absolutePath, "utf8");
|
|
4459
5058
|
searchEntryIndexContentReads += 1;
|
|
4460
5059
|
} catch {
|
|
4461
5060
|
directoryCache.files.delete(absolutePath);
|
|
@@ -4482,9 +5081,42 @@ async function listIndexedSearchEntries(source, type) {
|
|
|
4482
5081
|
}
|
|
4483
5082
|
return indexed;
|
|
4484
5083
|
}
|
|
4485
|
-
|
|
5084
|
+
function matchesTriageQuery(indexed, lowerQuery, includeBody) {
|
|
5085
|
+
const haystacks = [
|
|
5086
|
+
indexed.fm.title ?? "",
|
|
5087
|
+
indexed.fm.summary ?? "",
|
|
5088
|
+
...indexed.fm.tags ?? [],
|
|
5089
|
+
indexed.name,
|
|
5090
|
+
includeBody ? indexed.body : ""
|
|
5091
|
+
].map((s) => s.toLowerCase());
|
|
5092
|
+
return haystacks.some((h) => h.includes(lowerQuery));
|
|
5093
|
+
}
|
|
5094
|
+
function pendingEntryToRankerItem(indexed) {
|
|
5095
|
+
const { fm, name } = indexed;
|
|
5096
|
+
const slug = name.replace(/\.md$/u, "");
|
|
5097
|
+
const summary = fm.summary ?? fm.title ?? slug;
|
|
5098
|
+
const description = {
|
|
5099
|
+
summary,
|
|
5100
|
+
intent_clues: [],
|
|
5101
|
+
tech_stack: [],
|
|
5102
|
+
impact: [],
|
|
5103
|
+
must_read_if: fm.title ?? summary,
|
|
5104
|
+
...fm.id !== void 0 ? { id: fm.id } : {},
|
|
5105
|
+
...fm.type !== void 0 ? { knowledge_type: fm.type } : {},
|
|
5106
|
+
maturity: fm.maturity ?? "draft",
|
|
5107
|
+
// W4/Track1 (D1): no `knowledge_layer` field — layer is derived from the
|
|
5108
|
+
// stable_id prefix (KT-DEC-0004), never carried on the description.
|
|
5109
|
+
...fm.semantic_scope !== void 0 ? { semantic_scope: fm.semantic_scope } : {},
|
|
5110
|
+
...fm.created_at !== void 0 ? { created_at: fm.created_at } : {},
|
|
5111
|
+
tags: fm.tags ?? [],
|
|
5112
|
+
relevance_scope: fm.relevance_scope ?? "broad",
|
|
5113
|
+
relevance_paths: fm.relevance_paths ?? []
|
|
5114
|
+
};
|
|
5115
|
+
return { stable_id: indexed.absolutePath, description };
|
|
5116
|
+
}
|
|
5117
|
+
async function triageSearch(projectRoot, query, filters) {
|
|
4486
5118
|
const lowerQuery = query.toLowerCase();
|
|
4487
|
-
const
|
|
5119
|
+
const includeBody = filters?.include_body === true;
|
|
4488
5120
|
const sources = [];
|
|
4489
5121
|
for (const layer of ["team", "personal"]) {
|
|
4490
5122
|
const isPersonal = layer === "personal";
|
|
@@ -4507,10 +5139,12 @@ async function searchEntries(projectRoot, query, filters) {
|
|
|
4507
5139
|
}
|
|
4508
5140
|
}
|
|
4509
5141
|
const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
|
|
5142
|
+
const matchedByKey = /* @__PURE__ */ new Map();
|
|
5143
|
+
const rankerItems = [];
|
|
4510
5144
|
for (const source of sources) {
|
|
4511
5145
|
for (const type of typesToScan) {
|
|
4512
5146
|
for (const indexed of await listIndexedSearchEntries(source, type)) {
|
|
4513
|
-
const {
|
|
5147
|
+
const { fm, layer, maturity } = indexed;
|
|
4514
5148
|
if (filters?.layer !== void 0 && filters.layer !== "both" && filters.layer !== layer) {
|
|
4515
5149
|
continue;
|
|
4516
5150
|
}
|
|
@@ -4531,42 +5165,56 @@ async function searchEntries(projectRoot, query, filters) {
|
|
|
4531
5165
|
if (!isVisibleByLifecycle(fm, filters)) {
|
|
4532
5166
|
continue;
|
|
4533
5167
|
}
|
|
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
|
-
});
|
|
5168
|
+
if (!matchesTriageQuery(indexed, lowerQuery, includeBody)) continue;
|
|
5169
|
+
const item = pendingEntryToRankerItem(indexed);
|
|
5170
|
+
matchedByKey.set(item.stable_id, { indexed, source, type });
|
|
5171
|
+
rankerItems.push(item);
|
|
4567
5172
|
}
|
|
4568
5173
|
}
|
|
4569
5174
|
}
|
|
5175
|
+
if (rankerItems.length === 0) {
|
|
5176
|
+
return [];
|
|
5177
|
+
}
|
|
5178
|
+
let revision;
|
|
5179
|
+
try {
|
|
5180
|
+
revision = await computeReadSetRevision(projectRoot);
|
|
5181
|
+
} catch {
|
|
5182
|
+
revision = "triage-search";
|
|
5183
|
+
}
|
|
5184
|
+
const scoringContext = await buildScoringContext(projectRoot, revision, rankerItems, {
|
|
5185
|
+
queryText: query,
|
|
5186
|
+
targetPaths: []
|
|
5187
|
+
});
|
|
5188
|
+
const ranked = rankDescriptionItems(rankerItems, scoringContext, "triage");
|
|
5189
|
+
const items = [];
|
|
5190
|
+
for (const { item } of ranked) {
|
|
5191
|
+
const match = matchedByKey.get(item.stable_id);
|
|
5192
|
+
if (match === void 0) continue;
|
|
5193
|
+
const { indexed, source, type } = match;
|
|
5194
|
+
const { absolutePath, fm, layer, maturity } = indexed;
|
|
5195
|
+
const reportedPath = source.isStore ? absolutePath : source.isPersonal ? `~/${relative(resolvePersonalRoot(), absolutePath)}` : relative(projectRoot, absolutePath);
|
|
5196
|
+
items.push({
|
|
5197
|
+
area: source.isPending ? "pending" : "canonical",
|
|
5198
|
+
path: reportedPath,
|
|
5199
|
+
...source.isPersonal ? { path_absolute: absolutePath } : {},
|
|
5200
|
+
type,
|
|
5201
|
+
layer,
|
|
5202
|
+
maturity,
|
|
5203
|
+
// Only pending entries carry an origin tag (canonical hits live
|
|
5204
|
+
// outside the dual-pending-root convention).
|
|
5205
|
+
...source.isPending ? { origin: source.isPersonal ? "personal" : "team" } : {},
|
|
5206
|
+
...fm.tags !== void 0 && fm.tags.length > 0 ? { tags: fm.tags } : {},
|
|
5207
|
+
...fm.title !== void 0 ? { title: fm.title } : {},
|
|
5208
|
+
...fm.summary !== void 0 ? { summary: fm.summary } : {},
|
|
5209
|
+
...fm.status !== void 0 ? { status: fm.status } : {},
|
|
5210
|
+
...fm.deferred_until !== void 0 ? { deferred_until: fm.deferred_until } : {},
|
|
5211
|
+
// v2.0.0-rc.27 TASK-006 (audit §2.23): body emission when opted in.
|
|
5212
|
+
...includeBody ? { body: indexed.body } : {},
|
|
5213
|
+
// Canonical hits always have an id; pending hits typically don't yet —
|
|
5214
|
+
// surface the frontmatter id when present so consumers can dedupe.
|
|
5215
|
+
...fm.id !== void 0 ? { stable_id: fm.id } : {}
|
|
5216
|
+
});
|
|
5217
|
+
}
|
|
4570
5218
|
return items;
|
|
4571
5219
|
}
|
|
4572
5220
|
async function deferAll(projectRoot, pendingPaths, until, reason) {
|
|
@@ -4576,7 +5224,7 @@ async function deferAll(projectRoot, pendingPaths, until, reason) {
|
|
|
4576
5224
|
try {
|
|
4577
5225
|
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
4578
5226
|
if (existsSync5(sandboxed.abs)) {
|
|
4579
|
-
const content = await
|
|
5227
|
+
const content = await readFile9(sandboxed.abs, "utf8");
|
|
4580
5228
|
stableId = parseFrontmatter(content).id;
|
|
4581
5229
|
const patch = {
|
|
4582
5230
|
status: "deferred",
|
|
@@ -4737,6 +5385,9 @@ ${content}`;
|
|
|
4737
5385
|
if (patch.relevance_paths !== void 0) updates.relevance_paths = `relevance_paths: ${flowArray(patch.relevance_paths)}`;
|
|
4738
5386
|
if (patch.semantic_scope !== void 0) updates.semantic_scope = `semantic_scope: ${patch.semantic_scope}`;
|
|
4739
5387
|
if (patch.related !== void 0) updates.related = `related: ${flowArray(patch.related)}`;
|
|
5388
|
+
if (patch.must_read_if !== void 0) updates.must_read_if = `must_read_if: ${quoteIfNeeded(patch.must_read_if)}`;
|
|
5389
|
+
if (patch.intent_clues !== void 0) updates.intent_clues = `intent_clues: ${flowArray(patch.intent_clues)}`;
|
|
5390
|
+
if (patch.impact !== void 0) updates.impact = `impact: ${flowArray(patch.impact)}`;
|
|
4740
5391
|
if (patch.status !== void 0) updates.status = `status: ${patch.status}`;
|
|
4741
5392
|
if (patch.deferred_until !== void 0) updates.deferred_until = `deferred_until: ${quoteIfNeeded(patch.deferred_until)}`;
|
|
4742
5393
|
if (patch.last_review_confirmed_at !== void 0) updates.last_review_confirmed_at = `last_review_confirmed_at: ${quoteIfNeeded(patch.last_review_confirmed_at)}`;
|
|
@@ -4774,6 +5425,9 @@ function appendPatchLines(lines, patch) {
|
|
|
4774
5425
|
if (patch.relevance_scope !== void 0) lines.push(`relevance_scope: ${patch.relevance_scope}`);
|
|
4775
5426
|
if (patch.relevance_paths !== void 0) lines.push(`relevance_paths: ${flowArray(patch.relevance_paths)}`);
|
|
4776
5427
|
if (patch.related !== void 0) lines.push(`related: ${flowArray(patch.related)}`);
|
|
5428
|
+
if (patch.must_read_if !== void 0) lines.push(`must_read_if: ${quoteIfNeeded(patch.must_read_if)}`);
|
|
5429
|
+
if (patch.intent_clues !== void 0) lines.push(`intent_clues: ${flowArray(patch.intent_clues)}`);
|
|
5430
|
+
if (patch.impact !== void 0) lines.push(`impact: ${flowArray(patch.impact)}`);
|
|
4777
5431
|
if (patch.status !== void 0) lines.push(`status: ${patch.status}`);
|
|
4778
5432
|
if (patch.deferred_until !== void 0) lines.push(`deferred_until: ${quoteIfNeeded(patch.deferred_until)}`);
|
|
4779
5433
|
if (patch.last_review_confirmed_at !== void 0) lines.push(`last_review_confirmed_at: ${quoteIfNeeded(patch.last_review_confirmed_at)}`);
|
|
@@ -4917,9 +5571,9 @@ function registerPending(server, tracker) {
|
|
|
4917
5571
|
}
|
|
4918
5572
|
|
|
4919
5573
|
// src/services/doctor.ts
|
|
4920
|
-
import { access as access4, readFile as
|
|
5574
|
+
import { access as access4, readFile as readFile19, readdir as readdirAsync, stat as statAsync, unlink as unlink4, writeFile as writeFile5 } from "fs/promises";
|
|
4921
5575
|
import { constants as constants2 } from "fs";
|
|
4922
|
-
import { isAbsolute as isAbsolute2, join as
|
|
5576
|
+
import { isAbsolute as isAbsolute2, join as join24, posix as posix5, resolve as resolve3 } from "path";
|
|
4923
5577
|
import {
|
|
4924
5578
|
createTranslator,
|
|
4925
5579
|
forensicReportSchema,
|
|
@@ -5027,6 +5681,7 @@ function createGlobalCliVersionCheck(t, inspection) {
|
|
|
5027
5681
|
}
|
|
5028
5682
|
|
|
5029
5683
|
// src/services/doctor-health.ts
|
|
5684
|
+
import { isHighValueArchiveCandidate as isHighValueArchiveCandidate2 } from "@fenglimg/fabric-shared";
|
|
5030
5685
|
var DOCTOR_HEALTH_PENALTY_MANUAL_ERROR = 15;
|
|
5031
5686
|
var DOCTOR_HEALTH_PENALTY_FIXABLE_ERROR = 8;
|
|
5032
5687
|
var DOCTOR_HEALTH_PENALTY_WARNING = 3;
|
|
@@ -5046,6 +5701,64 @@ function computeDoctorHealth(manualErrorCount, fixableErrorCount, warningCount)
|
|
|
5046
5701
|
}
|
|
5047
5702
|
};
|
|
5048
5703
|
}
|
|
5704
|
+
async function checkBacklogAge(projectRoot, nowMs = Date.now()) {
|
|
5705
|
+
const empty = {
|
|
5706
|
+
count: 0,
|
|
5707
|
+
oldest_days: null,
|
|
5708
|
+
median_age_days: 0,
|
|
5709
|
+
ages_days: []
|
|
5710
|
+
};
|
|
5711
|
+
let events = [];
|
|
5712
|
+
try {
|
|
5713
|
+
const result = await readEventLedger(projectRoot);
|
|
5714
|
+
events = result.events;
|
|
5715
|
+
} catch {
|
|
5716
|
+
return empty;
|
|
5717
|
+
}
|
|
5718
|
+
if (!Array.isArray(events) || events.length === 0) return empty;
|
|
5719
|
+
const sids = /* @__PURE__ */ new Set();
|
|
5720
|
+
const lastAttemptCoveredThrough = /* @__PURE__ */ new Map();
|
|
5721
|
+
const firstEventTs = /* @__PURE__ */ new Map();
|
|
5722
|
+
for (const e of events) {
|
|
5723
|
+
if (typeof e.session_id !== "string" || e.session_id.length === 0) continue;
|
|
5724
|
+
if (typeof e.ts !== "number") continue;
|
|
5725
|
+
sids.add(e.session_id);
|
|
5726
|
+
const priorFirst = firstEventTs.get(e.session_id);
|
|
5727
|
+
if (priorFirst === void 0 || e.ts < priorFirst) {
|
|
5728
|
+
firstEventTs.set(e.session_id, e.ts);
|
|
5729
|
+
}
|
|
5730
|
+
if (e.event_type === "session_archive_attempted" && typeof e.covered_through_ts === "number") {
|
|
5731
|
+
const prior = lastAttemptCoveredThrough.get(e.session_id);
|
|
5732
|
+
if (prior === void 0 || e.ts > prior) {
|
|
5733
|
+
lastAttemptCoveredThrough.set(e.session_id, e.covered_through_ts);
|
|
5734
|
+
}
|
|
5735
|
+
}
|
|
5736
|
+
}
|
|
5737
|
+
const agesMs = [];
|
|
5738
|
+
for (const sid of sids) {
|
|
5739
|
+
const watermark = lastAttemptCoveredThrough.get(sid) ?? null;
|
|
5740
|
+
if (!isHighValueArchiveCandidate2(events, sid, watermark)) continue;
|
|
5741
|
+
const first = firstEventTs.get(sid);
|
|
5742
|
+
if (typeof first !== "number") continue;
|
|
5743
|
+
const ageMs = Math.max(0, nowMs - first);
|
|
5744
|
+
agesMs.push(ageMs);
|
|
5745
|
+
}
|
|
5746
|
+
if (agesMs.length === 0) return empty;
|
|
5747
|
+
const agesDays = agesMs.map((ms) => Math.floor(ms / 864e5));
|
|
5748
|
+
agesDays.sort((a, b) => a - b);
|
|
5749
|
+
const oldestDays = agesDays[agesDays.length - 1];
|
|
5750
|
+
const medianAgeDays = agesDays.length % 2 === 1 ? agesDays[Math.floor(agesDays.length / 2)] : Math.floor((agesDays[agesDays.length / 2 - 1] + agesDays[agesDays.length / 2]) / 2);
|
|
5751
|
+
return {
|
|
5752
|
+
count: agesDays.length,
|
|
5753
|
+
oldest_days: oldestDays,
|
|
5754
|
+
median_age_days: medianAgeDays,
|
|
5755
|
+
ages_days: agesDays
|
|
5756
|
+
};
|
|
5757
|
+
}
|
|
5758
|
+
function renderBacklogAgeLine(metric) {
|
|
5759
|
+
if (metric.count === 0) return " backlog: 0 high-value";
|
|
5760
|
+
return ` backlog: ${metric.count} high-value, oldest ${metric.oldest_days}d`;
|
|
5761
|
+
}
|
|
5049
5762
|
|
|
5050
5763
|
// src/services/doctor-summary-opaque.ts
|
|
5051
5764
|
var KNOWLEDGE_SUMMARY_OPAQUE_THRESHOLD = 0.3;
|
|
@@ -5133,13 +5846,13 @@ function createKnowledgeSummaryOpaqueCheck(t, inspection) {
|
|
|
5133
5846
|
}
|
|
5134
5847
|
|
|
5135
5848
|
// src/services/doctor-stable-id-collision.ts
|
|
5136
|
-
import { readFile as
|
|
5137
|
-
import { basename as basename2, join as
|
|
5849
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
5850
|
+
import { basename as basename2, join as join13 } from "path";
|
|
5138
5851
|
import {
|
|
5139
5852
|
buildStoreResolveInput as buildStoreResolveInput4,
|
|
5140
5853
|
createStoreResolver as createStoreResolver4,
|
|
5141
5854
|
readKnowledgeAcrossStores as readKnowledgeAcrossStores2,
|
|
5142
|
-
resolveGlobalRoot as
|
|
5855
|
+
resolveGlobalRoot as resolveGlobalRoot4,
|
|
5143
5856
|
storeRelativePathForMount as storeRelativePathForMount3
|
|
5144
5857
|
} from "@fenglimg/fabric-shared";
|
|
5145
5858
|
var ID_LINE = /^id:\s*"?([^"\n]+?)"?\s*$/mu;
|
|
@@ -5155,13 +5868,13 @@ function resolveIntegrityStores(projectRoot) {
|
|
|
5155
5868
|
const personalUuids = new Set(
|
|
5156
5869
|
input.mountedStores.filter((s) => s.personal).map((s) => s.store_uuid)
|
|
5157
5870
|
);
|
|
5158
|
-
const globalRoot =
|
|
5871
|
+
const globalRoot = resolveGlobalRoot4();
|
|
5159
5872
|
const dirs = readSet.stores.map((entry) => {
|
|
5160
5873
|
const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
|
|
5161
5874
|
return {
|
|
5162
5875
|
store_uuid: entry.store_uuid,
|
|
5163
5876
|
alias: entry.alias,
|
|
5164
|
-
dir:
|
|
5877
|
+
dir: join13(globalRoot, storeRelativePathForMount3(mounted ?? { store_uuid: entry.store_uuid }))
|
|
5165
5878
|
};
|
|
5166
5879
|
});
|
|
5167
5880
|
return { dirs, personalUuids };
|
|
@@ -5180,7 +5893,7 @@ async function inspectStoreStableIdIntegrity(projectRoot) {
|
|
|
5180
5893
|
for (const ref of await readKnowledgeAcrossStores2(resolved.dirs)) {
|
|
5181
5894
|
let source;
|
|
5182
5895
|
try {
|
|
5183
|
-
source = await
|
|
5896
|
+
source = await readFile10(ref.file, "utf8");
|
|
5184
5897
|
} catch {
|
|
5185
5898
|
continue;
|
|
5186
5899
|
}
|
|
@@ -5265,7 +5978,7 @@ function createLayerMismatchCheck(t, inspection) {
|
|
|
5265
5978
|
// src/services/doctor-relevance-paths.ts
|
|
5266
5979
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
5267
5980
|
import { existsSync as existsSync6, readdirSync, statSync as statSync3 } from "fs";
|
|
5268
|
-
import { join as
|
|
5981
|
+
import { join as join14, sep as sep3 } from "path";
|
|
5269
5982
|
import { minimatch } from "minimatch";
|
|
5270
5983
|
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
5271
5984
|
var RELEVANCE_PATHS_DRIFT_WINDOW_DAYS = 90;
|
|
@@ -5308,7 +6021,7 @@ function collectWorkspacePaths(projectRoot) {
|
|
|
5308
6021
|
continue;
|
|
5309
6022
|
}
|
|
5310
6023
|
for (const entry of entries) {
|
|
5311
|
-
const abs =
|
|
6024
|
+
const abs = join14(current, entry.name);
|
|
5312
6025
|
const rel = toPosix(abs.slice(projectRoot.length + 1));
|
|
5313
6026
|
if (rel.length === 0) continue;
|
|
5314
6027
|
if (entry.isDirectory()) {
|
|
@@ -5501,16 +6214,16 @@ function createNarrowNoPathsCheck(t, inspection) {
|
|
|
5501
6214
|
}
|
|
5502
6215
|
|
|
5503
6216
|
// src/services/doctor-broad-index.ts
|
|
5504
|
-
import { readFile as
|
|
5505
|
-
import { join as
|
|
6217
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
6218
|
+
import { join as join15 } from "path";
|
|
5506
6219
|
var DEFAULT_BROAD_INDEX_BACKSTOP = 50;
|
|
5507
6220
|
var BROAD_INDEX_BACKSTOP_MIN = 20;
|
|
5508
6221
|
var BROAD_INDEX_BACKSTOP_MAX = 500;
|
|
5509
6222
|
var BROAD_INDEX_DRIFT_RATIO = 0.8;
|
|
5510
6223
|
async function readBroadIndexBackstop(projectRoot) {
|
|
5511
|
-
const configPath =
|
|
6224
|
+
const configPath = join15(projectRoot, ".fabric", "fabric-config.json");
|
|
5512
6225
|
try {
|
|
5513
|
-
const raw = await
|
|
6226
|
+
const raw = await readFile11(configPath, "utf8");
|
|
5514
6227
|
const parsed = JSON.parse(raw);
|
|
5515
6228
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
5516
6229
|
const v = parsed.broad_index_backstop;
|
|
@@ -5827,15 +6540,15 @@ function createBroadReviewRecheckCheck(t, inspection) {
|
|
|
5827
6540
|
}
|
|
5828
6541
|
|
|
5829
6542
|
// src/services/doctor-scope-lint.ts
|
|
5830
|
-
import { readFile as
|
|
5831
|
-
import { join as
|
|
6543
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
6544
|
+
import { join as join16 } from "path";
|
|
5832
6545
|
import {
|
|
5833
6546
|
buildStoreResolveInput as buildStoreResolveInput5,
|
|
5834
6547
|
createStoreResolver as createStoreResolver5,
|
|
5835
6548
|
isPersonalScope as isPersonalScope3,
|
|
5836
6549
|
readKnowledgeAcrossStores as readKnowledgeAcrossStores3,
|
|
5837
6550
|
readStoreProjects,
|
|
5838
|
-
resolveGlobalRoot as
|
|
6551
|
+
resolveGlobalRoot as resolveGlobalRoot5,
|
|
5839
6552
|
scopeRoot as scopeRoot2,
|
|
5840
6553
|
storeRelativePathForMount as storeRelativePathForMount4
|
|
5841
6554
|
} from "@fenglimg/fabric-shared";
|
|
@@ -5860,10 +6573,10 @@ async function resolveLintStores(projectRoot) {
|
|
|
5860
6573
|
const personalUuids = new Set(
|
|
5861
6574
|
input.mountedStores.filter((s) => s.personal).map((s) => s.store_uuid)
|
|
5862
6575
|
);
|
|
5863
|
-
const globalRoot =
|
|
6576
|
+
const globalRoot = resolveGlobalRoot5();
|
|
5864
6577
|
return Promise.all(readSet.stores.map(async (entry) => {
|
|
5865
6578
|
const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
|
|
5866
|
-
const dir =
|
|
6579
|
+
const dir = join16(
|
|
5867
6580
|
globalRoot,
|
|
5868
6581
|
storeRelativePathForMount4(mounted ?? { store_uuid: entry.store_uuid })
|
|
5869
6582
|
);
|
|
@@ -5895,7 +6608,7 @@ async function lintStoreScopes(projectRoot) {
|
|
|
5895
6608
|
}
|
|
5896
6609
|
let source;
|
|
5897
6610
|
try {
|
|
5898
|
-
source = await
|
|
6611
|
+
source = await readFile12(ref.file, "utf8");
|
|
5899
6612
|
} catch {
|
|
5900
6613
|
continue;
|
|
5901
6614
|
}
|
|
@@ -5978,9 +6691,9 @@ function createScopeLintCheck(t, violations) {
|
|
|
5978
6691
|
}
|
|
5979
6692
|
|
|
5980
6693
|
// src/services/doctor-unbound-project.ts
|
|
5981
|
-
import { loadProjectConfig as
|
|
6694
|
+
import { loadProjectConfig as loadProjectConfig4 } from "@fenglimg/fabric-shared";
|
|
5982
6695
|
function detectUnboundProject(projectRoot) {
|
|
5983
|
-
const config =
|
|
6696
|
+
const config = loadProjectConfig4(projectRoot);
|
|
5984
6697
|
const alias = config?.active_write_store;
|
|
5985
6698
|
if (typeof alias !== "string" || alias.length === 0) {
|
|
5986
6699
|
return null;
|
|
@@ -6016,9 +6729,56 @@ function createUnboundProjectCheck(t, violation) {
|
|
|
6016
6729
|
};
|
|
6017
6730
|
}
|
|
6018
6731
|
|
|
6732
|
+
// src/services/doctor-write-route-lint.ts
|
|
6733
|
+
import { loadProjectConfig as loadProjectConfig5 } from "@fenglimg/fabric-shared";
|
|
6734
|
+
function detectWriteRouteUnbound(projectRoot) {
|
|
6735
|
+
const config = loadProjectConfig5(projectRoot);
|
|
6736
|
+
const routes = config?.write_routes ?? [];
|
|
6737
|
+
if (!Array.isArray(routes) || routes.length === 0) {
|
|
6738
|
+
return [];
|
|
6739
|
+
}
|
|
6740
|
+
const boundIds = new Set(
|
|
6741
|
+
(config?.required_stores ?? []).map((r) => typeof r?.id === "string" ? r.id : "").filter((id) => id.length > 0)
|
|
6742
|
+
);
|
|
6743
|
+
const violations = [];
|
|
6744
|
+
for (const route of routes) {
|
|
6745
|
+
const scope = typeof route?.scope === "string" ? route.scope : "";
|
|
6746
|
+
const store = typeof route?.store === "string" ? route.store : "";
|
|
6747
|
+
if (scope.length === 0 || store.length === 0) {
|
|
6748
|
+
continue;
|
|
6749
|
+
}
|
|
6750
|
+
if (!boundIds.has(store)) {
|
|
6751
|
+
violations.push({ scope, store });
|
|
6752
|
+
}
|
|
6753
|
+
}
|
|
6754
|
+
return violations;
|
|
6755
|
+
}
|
|
6756
|
+
function createWriteRouteUnboundCheck(t, violations) {
|
|
6757
|
+
if (violations.length === 0) {
|
|
6758
|
+
return {
|
|
6759
|
+
name: t("doctor.check.write_route_target_unbound.name"),
|
|
6760
|
+
status: "ok",
|
|
6761
|
+
message: t("doctor.check.write_route_target_unbound.ok")
|
|
6762
|
+
};
|
|
6763
|
+
}
|
|
6764
|
+
const summary = violations.map((v) => `${v.scope} \u2192 ${v.store}`).join(", ");
|
|
6765
|
+
return {
|
|
6766
|
+
name: t("doctor.check.write_route_target_unbound.name"),
|
|
6767
|
+
status: "warn",
|
|
6768
|
+
kind: "warning",
|
|
6769
|
+
code: "write_route_target_unbound",
|
|
6770
|
+
fixable: false,
|
|
6771
|
+
message: t("doctor.check.write_route_target_unbound.message", {
|
|
6772
|
+
count: String(violations.length),
|
|
6773
|
+
routes: summary
|
|
6774
|
+
}),
|
|
6775
|
+
actionHint: t("doctor.check.write_route_target_unbound.remediation")
|
|
6776
|
+
};
|
|
6777
|
+
}
|
|
6778
|
+
|
|
6019
6779
|
// src/services/doctor-store-counters.ts
|
|
6020
6780
|
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
6021
|
-
import { join as
|
|
6781
|
+
import { join as join17 } from "path";
|
|
6022
6782
|
import {
|
|
6023
6783
|
buildStoreResolveInput as buildStoreResolveInput6,
|
|
6024
6784
|
createStoreResolver as createStoreResolver6,
|
|
@@ -6027,8 +6787,8 @@ import {
|
|
|
6027
6787
|
parseKnowledgeId as parseKnowledgeId2,
|
|
6028
6788
|
readStoreCounters,
|
|
6029
6789
|
reconcileStoreCounters,
|
|
6030
|
-
resolveGlobalRoot as
|
|
6031
|
-
STORE_KNOWLEDGE_TYPE_DIRS,
|
|
6790
|
+
resolveGlobalRoot as resolveGlobalRoot6,
|
|
6791
|
+
STORE_KNOWLEDGE_TYPE_DIRS as STORE_KNOWLEDGE_TYPE_DIRS2,
|
|
6032
6792
|
STORE_LAYOUT as STORE_LAYOUT2,
|
|
6033
6793
|
storeRelativePathForMount as storeRelativePathForMount5
|
|
6034
6794
|
} from "@fenglimg/fabric-shared";
|
|
@@ -6041,11 +6801,11 @@ function resolveCounterStores(projectRoot) {
|
|
|
6041
6801
|
if (readSet.stores.length === 0) {
|
|
6042
6802
|
return [];
|
|
6043
6803
|
}
|
|
6044
|
-
const globalRoot =
|
|
6804
|
+
const globalRoot = resolveGlobalRoot6();
|
|
6045
6805
|
return readSet.stores.map((entry) => ({
|
|
6046
6806
|
uuid: entry.store_uuid,
|
|
6047
6807
|
alias: entry.alias,
|
|
6048
|
-
dir:
|
|
6808
|
+
dir: join17(
|
|
6049
6809
|
globalRoot,
|
|
6050
6810
|
storeRelativePathForMount5(
|
|
6051
6811
|
input.mountedStores.find((s) => s.store_uuid === entry.store_uuid) ?? {
|
|
@@ -6072,8 +6832,8 @@ function readEntryId(file) {
|
|
|
6072
6832
|
}
|
|
6073
6833
|
function computeStoreDiskMax(storeDir) {
|
|
6074
6834
|
const max = defaultAgentsMetaCounters();
|
|
6075
|
-
for (const type of
|
|
6076
|
-
const dir =
|
|
6835
|
+
for (const type of STORE_KNOWLEDGE_TYPE_DIRS2) {
|
|
6836
|
+
const dir = join17(storeDir, STORE_LAYOUT2.knowledgeDir, type);
|
|
6077
6837
|
if (!existsSync7(dir)) {
|
|
6078
6838
|
continue;
|
|
6079
6839
|
}
|
|
@@ -6087,7 +6847,7 @@ function computeStoreDiskMax(storeDir) {
|
|
|
6087
6847
|
if (!name.endsWith(".md")) {
|
|
6088
6848
|
continue;
|
|
6089
6849
|
}
|
|
6090
|
-
const parsed = parseKnowledgeId2(readEntryId(
|
|
6850
|
+
const parsed = parseKnowledgeId2(readEntryId(join17(dir, name)) ?? "");
|
|
6091
6851
|
if (parsed === null) {
|
|
6092
6852
|
continue;
|
|
6093
6853
|
}
|
|
@@ -6170,14 +6930,14 @@ function createStoreCounterCheck(t, drifts) {
|
|
|
6170
6930
|
|
|
6171
6931
|
// src/services/doctor-store-orphan.ts
|
|
6172
6932
|
import { readdirSync as readdirSync3 } from "fs";
|
|
6173
|
-
import { join as
|
|
6933
|
+
import { join as join18 } from "path";
|
|
6174
6934
|
import {
|
|
6175
6935
|
STORES_ROOT_DIR,
|
|
6176
6936
|
addMountedStore,
|
|
6177
6937
|
disambiguateAlias,
|
|
6178
6938
|
loadGlobalConfig,
|
|
6179
6939
|
readStoreIdentity,
|
|
6180
|
-
resolveGlobalRoot as
|
|
6940
|
+
resolveGlobalRoot as resolveGlobalRoot7,
|
|
6181
6941
|
saveGlobalConfig
|
|
6182
6942
|
} from "@fenglimg/fabric-shared";
|
|
6183
6943
|
var STORE_BY_ALIAS_DIR = "by-alias";
|
|
@@ -6188,21 +6948,21 @@ function listDir(dir) {
|
|
|
6188
6948
|
return [];
|
|
6189
6949
|
}
|
|
6190
6950
|
}
|
|
6191
|
-
function inspectStoreOrphans(globalRoot =
|
|
6951
|
+
function inspectStoreOrphans(globalRoot = resolveGlobalRoot7()) {
|
|
6192
6952
|
const config = loadGlobalConfig(globalRoot);
|
|
6193
6953
|
if (config === null) {
|
|
6194
6954
|
return [];
|
|
6195
6955
|
}
|
|
6196
6956
|
const registered = new Set(config.stores.map((s) => s.store_uuid));
|
|
6197
|
-
const storesRoot =
|
|
6957
|
+
const storesRoot = join18(globalRoot, STORES_ROOT_DIR);
|
|
6198
6958
|
const orphans = [];
|
|
6199
6959
|
for (const group of listDir(storesRoot)) {
|
|
6200
6960
|
if (group === STORE_BY_ALIAS_DIR) {
|
|
6201
6961
|
continue;
|
|
6202
6962
|
}
|
|
6203
|
-
const groupDir =
|
|
6963
|
+
const groupDir = join18(storesRoot, group);
|
|
6204
6964
|
for (const mount of listDir(groupDir)) {
|
|
6205
|
-
const dir =
|
|
6965
|
+
const dir = join18(groupDir, mount);
|
|
6206
6966
|
const identity = readStoreIdentity(dir);
|
|
6207
6967
|
if (identity === null || registered.has(identity.store_uuid)) {
|
|
6208
6968
|
continue;
|
|
@@ -6212,7 +6972,7 @@ function inspectStoreOrphans(globalRoot = resolveGlobalRoot6()) {
|
|
|
6212
6972
|
}
|
|
6213
6973
|
return orphans;
|
|
6214
6974
|
}
|
|
6215
|
-
function fixStoreOrphans(globalRoot =
|
|
6975
|
+
function fixStoreOrphans(globalRoot = resolveGlobalRoot7()) {
|
|
6216
6976
|
let config = loadGlobalConfig(globalRoot);
|
|
6217
6977
|
if (config === null) {
|
|
6218
6978
|
return [];
|
|
@@ -6267,6 +7027,195 @@ function createStoreOrphanCheck(t, orphans) {
|
|
|
6267
7027
|
};
|
|
6268
7028
|
}
|
|
6269
7029
|
|
|
7030
|
+
// src/services/doctor-project-registry-drift.ts
|
|
7031
|
+
import { readdir as readdir3, rmdir } from "fs/promises";
|
|
7032
|
+
import { join as join19 } from "path";
|
|
7033
|
+
import {
|
|
7034
|
+
addStoreProject,
|
|
7035
|
+
buildStoreResolveInput as buildStoreResolveInput7,
|
|
7036
|
+
createStoreResolver as createStoreResolver7,
|
|
7037
|
+
readStoreProjects as readStoreProjects2,
|
|
7038
|
+
resolveGlobalRoot as resolveGlobalRoot8,
|
|
7039
|
+
STORE_KNOWLEDGE_TYPE_DIRS as STORE_KNOWLEDGE_TYPE_DIRS3,
|
|
7040
|
+
STORE_LAYOUT as STORE_LAYOUT3,
|
|
7041
|
+
STORE_PROJECT_ID_PATTERN as STORE_PROJECT_ID_PATTERN2,
|
|
7042
|
+
storeRelativePathForMount as storeRelativePathForMount6
|
|
7043
|
+
} from "@fenglimg/fabric-shared";
|
|
7044
|
+
var PROJECTS_DIR = "projects";
|
|
7045
|
+
var EMPTY_INSPECTION = { findings: [] };
|
|
7046
|
+
function resolveDriftStores(projectRoot) {
|
|
7047
|
+
const input = buildStoreResolveInput7(projectRoot);
|
|
7048
|
+
if (input === null) {
|
|
7049
|
+
return [];
|
|
7050
|
+
}
|
|
7051
|
+
const readSet = createStoreResolver7().resolveReadSet(input);
|
|
7052
|
+
if (readSet.stores.length === 0) {
|
|
7053
|
+
return [];
|
|
7054
|
+
}
|
|
7055
|
+
const globalRoot = resolveGlobalRoot8();
|
|
7056
|
+
return readSet.stores.map((entry) => {
|
|
7057
|
+
const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
|
|
7058
|
+
return {
|
|
7059
|
+
uuid: entry.store_uuid,
|
|
7060
|
+
alias: entry.alias,
|
|
7061
|
+
dir: join19(globalRoot, storeRelativePathForMount6(mounted ?? { store_uuid: entry.store_uuid }))
|
|
7062
|
+
};
|
|
7063
|
+
});
|
|
7064
|
+
}
|
|
7065
|
+
async function listDir2(dir) {
|
|
7066
|
+
try {
|
|
7067
|
+
return await readdir3(dir);
|
|
7068
|
+
} catch {
|
|
7069
|
+
return [];
|
|
7070
|
+
}
|
|
7071
|
+
}
|
|
7072
|
+
async function listProjectFolders(storeDir) {
|
|
7073
|
+
const projectsRoot = join19(storeDir, STORE_LAYOUT3.knowledgeDir, PROJECTS_DIR);
|
|
7074
|
+
const reserved = /* @__PURE__ */ new Set([PROJECTS_DIR, ...STORE_KNOWLEDGE_TYPE_DIRS3]);
|
|
7075
|
+
return (await listDir2(projectsRoot)).filter((id) => STORE_PROJECT_ID_PATTERN2.test(id) && !reserved.has(id)).sort();
|
|
7076
|
+
}
|
|
7077
|
+
async function folderHasEntries(storeDir, projectId) {
|
|
7078
|
+
const base = join19(storeDir, STORE_LAYOUT3.knowledgeDir, PROJECTS_DIR, projectId);
|
|
7079
|
+
for (const type of STORE_KNOWLEDGE_TYPE_DIRS3) {
|
|
7080
|
+
const names = await listDir2(join19(base, type));
|
|
7081
|
+
if (names.some((name) => name.endsWith(".md"))) {
|
|
7082
|
+
return true;
|
|
7083
|
+
}
|
|
7084
|
+
}
|
|
7085
|
+
return false;
|
|
7086
|
+
}
|
|
7087
|
+
async function inspectProjectRegistryDrift(projectRoot) {
|
|
7088
|
+
const stores = resolveDriftStores(projectRoot);
|
|
7089
|
+
if (stores.length === 0) {
|
|
7090
|
+
return EMPTY_INSPECTION;
|
|
7091
|
+
}
|
|
7092
|
+
const findings = [];
|
|
7093
|
+
for (const store of stores) {
|
|
7094
|
+
const registered = new Set((await readStoreProjects2(store.dir)).map((p) => p.id));
|
|
7095
|
+
const folders = await listProjectFolders(store.dir);
|
|
7096
|
+
const folderSet = new Set(folders);
|
|
7097
|
+
const base = { store_alias: store.alias, store_uuid: store.uuid, store_dir: store.dir };
|
|
7098
|
+
for (const projectId of folders) {
|
|
7099
|
+
if (registered.has(projectId)) {
|
|
7100
|
+
if (!await folderHasEntries(store.dir, projectId)) {
|
|
7101
|
+
findings.push({ ...base, project_id: projectId, kind: "empty_folder" });
|
|
7102
|
+
}
|
|
7103
|
+
continue;
|
|
7104
|
+
}
|
|
7105
|
+
const kind = await folderHasEntries(store.dir, projectId) ? "unregistered_write" : "orphan_folder";
|
|
7106
|
+
findings.push({ ...base, project_id: projectId, kind });
|
|
7107
|
+
}
|
|
7108
|
+
void folderSet;
|
|
7109
|
+
}
|
|
7110
|
+
findings.sort(
|
|
7111
|
+
(a, b) => a.store_alias.localeCompare(b.store_alias) || a.project_id.localeCompare(b.project_id)
|
|
7112
|
+
);
|
|
7113
|
+
return { findings };
|
|
7114
|
+
}
|
|
7115
|
+
async function fixProjectRegistryDrift(projectRoot) {
|
|
7116
|
+
const registered = [];
|
|
7117
|
+
const pruned = [];
|
|
7118
|
+
const { findings } = await inspectProjectRegistryDrift(projectRoot);
|
|
7119
|
+
for (const finding of findings) {
|
|
7120
|
+
if (finding.kind === "empty_folder") {
|
|
7121
|
+
if (await folderHasEntries(finding.store_dir, finding.project_id)) {
|
|
7122
|
+
continue;
|
|
7123
|
+
}
|
|
7124
|
+
try {
|
|
7125
|
+
const base = join19(
|
|
7126
|
+
finding.store_dir,
|
|
7127
|
+
STORE_LAYOUT3.knowledgeDir,
|
|
7128
|
+
PROJECTS_DIR,
|
|
7129
|
+
finding.project_id
|
|
7130
|
+
);
|
|
7131
|
+
for (const type of STORE_KNOWLEDGE_TYPE_DIRS3) {
|
|
7132
|
+
await rmdir(join19(base, type)).catch(() => void 0);
|
|
7133
|
+
}
|
|
7134
|
+
await rmdir(base);
|
|
7135
|
+
pruned.push(finding);
|
|
7136
|
+
} catch {
|
|
7137
|
+
}
|
|
7138
|
+
continue;
|
|
7139
|
+
}
|
|
7140
|
+
try {
|
|
7141
|
+
await addStoreProject(finding.store_dir, {
|
|
7142
|
+
id: finding.project_id,
|
|
7143
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
7144
|
+
});
|
|
7145
|
+
registered.push(finding);
|
|
7146
|
+
} catch {
|
|
7147
|
+
}
|
|
7148
|
+
}
|
|
7149
|
+
return { registered, pruned };
|
|
7150
|
+
}
|
|
7151
|
+
function createProjectRegistryDriftCheck(t, inspection) {
|
|
7152
|
+
const { findings } = inspection;
|
|
7153
|
+
if (findings.length === 0) {
|
|
7154
|
+
return {
|
|
7155
|
+
name: t("doctor.check.project_registry_drift.name"),
|
|
7156
|
+
status: "ok",
|
|
7157
|
+
message: t("doctor.check.project_registry_drift.ok")
|
|
7158
|
+
};
|
|
7159
|
+
}
|
|
7160
|
+
const unregistered = findings.filter((f) => f.kind === "unregistered_write").length;
|
|
7161
|
+
const orphans = findings.filter((f) => f.kind === "orphan_folder").length;
|
|
7162
|
+
const empties = findings.filter((f) => f.kind === "empty_folder").length;
|
|
7163
|
+
const breakdown = [
|
|
7164
|
+
unregistered > 0 ? `${unregistered} unregistered-write` : null,
|
|
7165
|
+
orphans > 0 ? `${orphans} orphan-folder` : null,
|
|
7166
|
+
empties > 0 ? `${empties} empty-folder` : null
|
|
7167
|
+
].filter((part) => part !== null).join(", ");
|
|
7168
|
+
if (unregistered > 0) {
|
|
7169
|
+
const first2 = findings.find((f) => f.kind === "unregistered_write");
|
|
7170
|
+
return {
|
|
7171
|
+
name: t("doctor.check.project_registry_drift.name"),
|
|
7172
|
+
status: "error",
|
|
7173
|
+
kind: "manual_error",
|
|
7174
|
+
code: "project_registry_drift",
|
|
7175
|
+
fixable: true,
|
|
7176
|
+
message: t("doctor.check.project_registry_drift.message.unregistered", {
|
|
7177
|
+
total: String(findings.length),
|
|
7178
|
+
breakdown,
|
|
7179
|
+
projectId: first2.project_id,
|
|
7180
|
+
storeAlias: first2.store_alias
|
|
7181
|
+
}),
|
|
7182
|
+
actionHint: t("doctor.check.project_registry_drift.remediation")
|
|
7183
|
+
};
|
|
7184
|
+
}
|
|
7185
|
+
if (orphans > 0) {
|
|
7186
|
+
const first2 = findings.find((f) => f.kind === "orphan_folder");
|
|
7187
|
+
return {
|
|
7188
|
+
name: t("doctor.check.project_registry_drift.name"),
|
|
7189
|
+
status: "warn",
|
|
7190
|
+
kind: "warning",
|
|
7191
|
+
code: "project_registry_drift",
|
|
7192
|
+
fixable: true,
|
|
7193
|
+
message: t("doctor.check.project_registry_drift.message.orphan", {
|
|
7194
|
+
total: String(findings.length),
|
|
7195
|
+
breakdown,
|
|
7196
|
+
projectId: first2.project_id,
|
|
7197
|
+
storeAlias: first2.store_alias
|
|
7198
|
+
}),
|
|
7199
|
+
actionHint: t("doctor.check.project_registry_drift.remediation")
|
|
7200
|
+
};
|
|
7201
|
+
}
|
|
7202
|
+
const first = findings.find((f) => f.kind === "empty_folder");
|
|
7203
|
+
return {
|
|
7204
|
+
name: t("doctor.check.project_registry_drift.name"),
|
|
7205
|
+
status: "warn",
|
|
7206
|
+
kind: "info",
|
|
7207
|
+
code: "project_registry_drift",
|
|
7208
|
+
fixable: true,
|
|
7209
|
+
message: t("doctor.check.project_registry_drift.message.empty", {
|
|
7210
|
+
total: String(findings.length),
|
|
7211
|
+
breakdown,
|
|
7212
|
+
projectId: first.project_id,
|
|
7213
|
+
storeAlias: first.store_alias
|
|
7214
|
+
}),
|
|
7215
|
+
actionHint: t("doctor.check.project_registry_drift.remediation")
|
|
7216
|
+
};
|
|
7217
|
+
}
|
|
7218
|
+
|
|
6270
7219
|
// src/services/legacy-serve-lock-probe.ts
|
|
6271
7220
|
import {
|
|
6272
7221
|
isAlive,
|
|
@@ -6339,12 +7288,37 @@ async function inspectEventsJsonlGates(projectRoot, options = {}) {
|
|
|
6339
7288
|
}
|
|
6340
7289
|
|
|
6341
7290
|
// src/services/doctor-skill-lints.ts
|
|
6342
|
-
import { readdir as
|
|
6343
|
-
import { join as
|
|
7291
|
+
import { readdir as readdir4, readFile as readFile13 } from "fs/promises";
|
|
7292
|
+
import { join as join20, posix as posix2 } from "path";
|
|
6344
7293
|
var FABRIC_SKILL_SLUGS = ["fabric-archive", "fabric-review"];
|
|
7294
|
+
var FABRIC_CONTRACT_SKILL_SLUGS = ["fabric-archive", "fabric-review", "fabric-store", "fabric-sync"];
|
|
6345
7295
|
var SKILL_MD_FRONTMATTER_ROOTS = [".claude/skills", ".codex/skills"];
|
|
6346
7296
|
var SKILL_FRONTMATTER_KEY_PATTERN = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]+(.+?)[ \t]*$/u;
|
|
6347
7297
|
var SKILL_QUOTED_VALUE_LEADS = /* @__PURE__ */ new Set(['"', "'", "[", "{", ">", "|"]);
|
|
7298
|
+
var SKILL_CONTRACT_TOKENS = {
|
|
7299
|
+
"fabric-archive": [
|
|
7300
|
+
"## Hard Rules",
|
|
7301
|
+
"### DISPLAY Rules",
|
|
7302
|
+
"### WRITE Rules",
|
|
7303
|
+
"mcp__fabric__fab_propose",
|
|
7304
|
+
"only legal write path",
|
|
7305
|
+
"reached-but-inert"
|
|
7306
|
+
],
|
|
7307
|
+
"fabric-review": [
|
|
7308
|
+
"## Hard Rules",
|
|
7309
|
+
"### DISPLAY Rules",
|
|
7310
|
+
"### WRITE Rules",
|
|
7311
|
+
"mcp__fabric__fab_review",
|
|
7312
|
+
"only legal mutation path",
|
|
7313
|
+
"reached-but-inert",
|
|
7314
|
+
"changes next action",
|
|
7315
|
+
"must_read_if",
|
|
7316
|
+
"intent_clues",
|
|
7317
|
+
"impact"
|
|
7318
|
+
],
|
|
7319
|
+
"fabric-store": ["thin shim", "CLI", "\u672C skill \u53EA", "NEVER"],
|
|
7320
|
+
"fabric-sync": ["thin shim", "CLI", "\u672C skill \u53EA", "NEVER"]
|
|
7321
|
+
};
|
|
6348
7322
|
function okCheck(name, message) {
|
|
6349
7323
|
return { name, status: "ok", message };
|
|
6350
7324
|
}
|
|
@@ -6362,7 +7336,7 @@ function issueCheck(name, status, kind, code, message, actionHint, audience) {
|
|
|
6362
7336
|
}
|
|
6363
7337
|
async function listMarkdownFiles(dir) {
|
|
6364
7338
|
try {
|
|
6365
|
-
return (await
|
|
7339
|
+
return (await readdir4(dir)).filter((name) => name.endsWith(".md"));
|
|
6366
7340
|
} catch {
|
|
6367
7341
|
return null;
|
|
6368
7342
|
}
|
|
@@ -6370,8 +7344,8 @@ async function listMarkdownFiles(dir) {
|
|
|
6370
7344
|
async function inspectSkillRefMirror(projectRoot) {
|
|
6371
7345
|
const driftedPaths = [];
|
|
6372
7346
|
for (const slug of FABRIC_SKILL_SLUGS) {
|
|
6373
|
-
const claudeRef =
|
|
6374
|
-
const codexRef =
|
|
7347
|
+
const claudeRef = join20(projectRoot, ".claude", "skills", slug, "ref");
|
|
7348
|
+
const codexRef = join20(projectRoot, ".codex", "skills", slug, "ref");
|
|
6375
7349
|
const [claudeFiles, codexFiles] = await Promise.all([listMarkdownFiles(claudeRef), listMarkdownFiles(codexRef)]);
|
|
6376
7350
|
if (claudeFiles === null || codexFiles === null) continue;
|
|
6377
7351
|
const claudeSet = new Set(claudeFiles);
|
|
@@ -6388,8 +7362,8 @@ async function inspectSkillRefMirror(projectRoot) {
|
|
|
6388
7362
|
let codexBody;
|
|
6389
7363
|
try {
|
|
6390
7364
|
[claudeBody, codexBody] = await Promise.all([
|
|
6391
|
-
|
|
6392
|
-
|
|
7365
|
+
readFile13(join20(claudeRef, fname), "utf8"),
|
|
7366
|
+
readFile13(join20(codexRef, fname), "utf8")
|
|
6393
7367
|
]);
|
|
6394
7368
|
} catch {
|
|
6395
7369
|
continue;
|
|
@@ -6408,10 +7382,10 @@ async function inspectSkillTokenBudget(projectRoot) {
|
|
|
6408
7382
|
const overSize = [];
|
|
6409
7383
|
let highestSeverity = "ok";
|
|
6410
7384
|
for (const slug of FABRIC_SKILL_SLUGS) {
|
|
6411
|
-
const skillMdPath =
|
|
7385
|
+
const skillMdPath = join20(projectRoot, ".claude", "skills", slug, "SKILL.md");
|
|
6412
7386
|
let body;
|
|
6413
7387
|
try {
|
|
6414
|
-
body = await
|
|
7388
|
+
body = await readFile13(skillMdPath, "utf8");
|
|
6415
7389
|
} catch {
|
|
6416
7390
|
continue;
|
|
6417
7391
|
}
|
|
@@ -6431,11 +7405,12 @@ async function inspectSkillDescription(projectRoot) {
|
|
|
6431
7405
|
const issues = [];
|
|
6432
7406
|
const CJK_PATTERN = /[\u3400-\u4dbf\u4e00-\u9fff]/u;
|
|
6433
7407
|
const ASCII_PATTERN = /[a-zA-Z]{2,}/u;
|
|
7408
|
+
const ANTI_TRIGGER_PATTERN = /\bNOT\b|ONLY when|do not|不要|不是|非/u;
|
|
6434
7409
|
for (const slug of FABRIC_SKILL_SLUGS) {
|
|
6435
|
-
const skillMdPath =
|
|
7410
|
+
const skillMdPath = join20(projectRoot, ".claude", "skills", slug, "SKILL.md");
|
|
6436
7411
|
let body;
|
|
6437
7412
|
try {
|
|
6438
|
-
body = await
|
|
7413
|
+
body = await readFile13(skillMdPath, "utf8");
|
|
6439
7414
|
} catch {
|
|
6440
7415
|
continue;
|
|
6441
7416
|
}
|
|
@@ -6460,25 +7435,75 @@ async function inspectSkillDescription(projectRoot) {
|
|
|
6460
7435
|
if (!ASCII_PATTERN.test(description)) {
|
|
6461
7436
|
issues.push({ slug, problem: "no_ascii", detail: "no English trigger phrase" });
|
|
6462
7437
|
}
|
|
7438
|
+
if (!ANTI_TRIGGER_PATTERN.test(description)) {
|
|
7439
|
+
issues.push({
|
|
7440
|
+
slug,
|
|
7441
|
+
problem: "missing_anti_trigger",
|
|
7442
|
+
detail: "no explicit non-trigger phrase such as NOT/\u4E0D\u662F/\u4E0D\u8981"
|
|
7443
|
+
});
|
|
7444
|
+
}
|
|
7445
|
+
}
|
|
7446
|
+
return { status: issues.length === 0 ? "ok" : "warn", issues };
|
|
7447
|
+
}
|
|
7448
|
+
async function inspectSkillContract(projectRoot) {
|
|
7449
|
+
const issues = [];
|
|
7450
|
+
for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
|
|
7451
|
+
const client = rootRel.startsWith(".claude") ? ".claude" : ".codex";
|
|
7452
|
+
for (const slug of FABRIC_CONTRACT_SKILL_SLUGS) {
|
|
7453
|
+
const skillDir = join20(projectRoot, rootRel, slug);
|
|
7454
|
+
const skillMdPath = join20(skillDir, "SKILL.md");
|
|
7455
|
+
let body;
|
|
7456
|
+
try {
|
|
7457
|
+
body = await readFile13(skillMdPath, "utf8");
|
|
7458
|
+
} catch {
|
|
7459
|
+
continue;
|
|
7460
|
+
}
|
|
7461
|
+
for (const token of SKILL_CONTRACT_TOKENS[slug]) {
|
|
7462
|
+
if (body.includes(token)) continue;
|
|
7463
|
+
issues.push({
|
|
7464
|
+
slug,
|
|
7465
|
+
client,
|
|
7466
|
+
problem: slug === "fabric-store" || slug === "fabric-sync" ? "missing_thin_shim_token" : "missing_contract_token",
|
|
7467
|
+
detail: token
|
|
7468
|
+
});
|
|
7469
|
+
}
|
|
7470
|
+
const refDir = join20(skillDir, "ref");
|
|
7471
|
+
let refFiles;
|
|
7472
|
+
try {
|
|
7473
|
+
refFiles = (await readdir4(refDir)).filter((name) => name.endsWith(".md"));
|
|
7474
|
+
} catch {
|
|
7475
|
+
refFiles = null;
|
|
7476
|
+
}
|
|
7477
|
+
if (refFiles === null) continue;
|
|
7478
|
+
for (const fname of refFiles) {
|
|
7479
|
+
if (body.includes(fname) || body.includes(`ref/${fname}`)) continue;
|
|
7480
|
+
issues.push({
|
|
7481
|
+
slug,
|
|
7482
|
+
client,
|
|
7483
|
+
problem: "missing_ref_entry",
|
|
7484
|
+
detail: fname
|
|
7485
|
+
});
|
|
7486
|
+
}
|
|
7487
|
+
}
|
|
6463
7488
|
}
|
|
6464
7489
|
return { status: issues.length === 0 ? "ok" : "warn", issues };
|
|
6465
7490
|
}
|
|
6466
7491
|
async function inspectSkillMdYamlInvalid(projectRoot) {
|
|
6467
7492
|
const candidates = [];
|
|
6468
7493
|
for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
|
|
6469
|
-
const rootAbs =
|
|
7494
|
+
const rootAbs = join20(projectRoot, rootRel);
|
|
6470
7495
|
let dirEntries;
|
|
6471
7496
|
try {
|
|
6472
|
-
dirEntries = await
|
|
7497
|
+
dirEntries = await readdir4(rootAbs, { withFileTypes: true });
|
|
6473
7498
|
} catch {
|
|
6474
7499
|
continue;
|
|
6475
7500
|
}
|
|
6476
7501
|
for (const dirEntry of dirEntries) {
|
|
6477
7502
|
if (!dirEntry.isDirectory()) continue;
|
|
6478
|
-
const skillFile =
|
|
7503
|
+
const skillFile = join20(rootAbs, dirEntry.name, "SKILL.md");
|
|
6479
7504
|
let raw;
|
|
6480
7505
|
try {
|
|
6481
|
-
raw = await
|
|
7506
|
+
raw = await readFile13(skillFile, "utf8");
|
|
6482
7507
|
} catch {
|
|
6483
7508
|
continue;
|
|
6484
7509
|
}
|
|
@@ -6580,6 +7605,25 @@ function createSkillDescriptionCheck(t, inspection) {
|
|
|
6580
7605
|
"maintainer"
|
|
6581
7606
|
);
|
|
6582
7607
|
}
|
|
7608
|
+
function createSkillContractCheck(t, inspection) {
|
|
7609
|
+
if (inspection.status === "ok") {
|
|
7610
|
+
return okCheck(t("doctor.check.skill_contract.name"), t("doctor.check.skill_contract.ok"));
|
|
7611
|
+
}
|
|
7612
|
+
const list = inspection.issues.map((issue) => `${issue.client}/${issue.slug}: ${issue.problem} (${issue.detail})`).join("; ");
|
|
7613
|
+
const count = inspection.issues.length;
|
|
7614
|
+
return issueCheck(
|
|
7615
|
+
t("doctor.check.skill_contract.name"),
|
|
7616
|
+
"warn",
|
|
7617
|
+
"warning",
|
|
7618
|
+
"skill_contract_integrity",
|
|
7619
|
+
t(`doctor.check.skill_contract.message.${count === 1 ? "singular" : "plural"}`, {
|
|
7620
|
+
count: String(count),
|
|
7621
|
+
list
|
|
7622
|
+
}),
|
|
7623
|
+
t("doctor.check.skill_contract.remediation"),
|
|
7624
|
+
"maintainer"
|
|
7625
|
+
);
|
|
7626
|
+
}
|
|
6583
7627
|
function createSkillMdYamlInvalidCheck(t, inspection) {
|
|
6584
7628
|
if (inspection.candidates.length === 0) {
|
|
6585
7629
|
return okCheck(t("doctor.check.skill_md_yaml_invalid.name"), t("doctor.check.skill_md_yaml_invalid.ok"));
|
|
@@ -6601,8 +7645,8 @@ function createSkillMdYamlInvalidCheck(t, inspection) {
|
|
|
6601
7645
|
}
|
|
6602
7646
|
|
|
6603
7647
|
// src/services/doctor-retired-references-lint.ts
|
|
6604
|
-
import { readdir as
|
|
6605
|
-
import { join as
|
|
7648
|
+
import { readdir as readdir5, readFile as readFile14, stat as stat3 } from "fs/promises";
|
|
7649
|
+
import { join as join21, posix as posix3, relative as relative2 } from "path";
|
|
6606
7650
|
var RETIRED_TOKENS = [
|
|
6607
7651
|
{ token: "fab_plan_context", replacement: "fab_recall", reason: "retrieval collapsed to one lean fab_recall (KT-DEC-0026)" },
|
|
6608
7652
|
{ token: "fab_get_knowledge_sections", replacement: "fab_recall", reason: "two-step fetch retired (KT-DEC-0026)" },
|
|
@@ -6625,7 +7669,7 @@ var RETIRED_TOKENS = [
|
|
|
6625
7669
|
];
|
|
6626
7670
|
var HOOK_DIRS = [".claude/hooks", ".codex/hooks"];
|
|
6627
7671
|
var SKILL_DIRS = [".claude/skills", ".codex/skills"];
|
|
6628
|
-
var BOOTSTRAP_FILES = ["AGENTS.md", "CLAUDE.md",
|
|
7672
|
+
var BOOTSTRAP_FILES = ["AGENTS.md", "CLAUDE.md", join21(".fabric", "AGENTS.md")];
|
|
6629
7673
|
function isCommentLine(line) {
|
|
6630
7674
|
const t = line.trim();
|
|
6631
7675
|
return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*");
|
|
@@ -6633,13 +7677,13 @@ function isCommentLine(line) {
|
|
|
6633
7677
|
async function walkFiles(dir, exts) {
|
|
6634
7678
|
let entries;
|
|
6635
7679
|
try {
|
|
6636
|
-
entries = await
|
|
7680
|
+
entries = await readdir5(dir, { withFileTypes: true });
|
|
6637
7681
|
} catch {
|
|
6638
7682
|
return [];
|
|
6639
7683
|
}
|
|
6640
7684
|
const out = [];
|
|
6641
7685
|
for (const e of entries) {
|
|
6642
|
-
const full =
|
|
7686
|
+
const full = join21(dir, e.name);
|
|
6643
7687
|
if (e.isDirectory()) {
|
|
6644
7688
|
out.push(...await walkFiles(full, exts));
|
|
6645
7689
|
} else if (exts.some((ext) => e.name.endsWith(ext))) {
|
|
@@ -6665,29 +7709,29 @@ async function inspectRetiredReferences(projectRoot) {
|
|
|
6665
7709
|
let scannedFiles = 0;
|
|
6666
7710
|
const toRel = (p) => posix3.normalize(relative2(projectRoot, p).split("\\").join("/"));
|
|
6667
7711
|
for (const rel of BOOTSTRAP_FILES) {
|
|
6668
|
-
const abs =
|
|
7712
|
+
const abs = join21(projectRoot, rel);
|
|
6669
7713
|
try {
|
|
6670
7714
|
if ((await stat3(abs)).isFile()) {
|
|
6671
7715
|
scannedFiles += 1;
|
|
6672
|
-
scanText(toRel(abs), await
|
|
7716
|
+
scanText(toRel(abs), await readFile14(abs, "utf8"), false, hits);
|
|
6673
7717
|
}
|
|
6674
7718
|
} catch {
|
|
6675
7719
|
}
|
|
6676
7720
|
}
|
|
6677
7721
|
for (const dir of SKILL_DIRS) {
|
|
6678
|
-
for (const file of await walkFiles(
|
|
7722
|
+
for (const file of await walkFiles(join21(projectRoot, dir), [".md"])) {
|
|
6679
7723
|
scannedFiles += 1;
|
|
6680
7724
|
try {
|
|
6681
|
-
scanText(toRel(file), await
|
|
7725
|
+
scanText(toRel(file), await readFile14(file, "utf8"), false, hits);
|
|
6682
7726
|
} catch {
|
|
6683
7727
|
}
|
|
6684
7728
|
}
|
|
6685
7729
|
}
|
|
6686
7730
|
for (const dir of HOOK_DIRS) {
|
|
6687
|
-
for (const file of await walkFiles(
|
|
7731
|
+
for (const file of await walkFiles(join21(projectRoot, dir), [".cjs"])) {
|
|
6688
7732
|
scannedFiles += 1;
|
|
6689
7733
|
try {
|
|
6690
|
-
scanText(toRel(file), await
|
|
7734
|
+
scanText(toRel(file), await readFile14(file, "utf8"), true, hits);
|
|
6691
7735
|
} catch {
|
|
6692
7736
|
}
|
|
6693
7737
|
}
|
|
@@ -6722,8 +7766,8 @@ function createRetiredReferenceCheck(t, inspection) {
|
|
|
6722
7766
|
|
|
6723
7767
|
// src/services/doctor-hooks-lints.ts
|
|
6724
7768
|
import { constants } from "fs";
|
|
6725
|
-
import { access, readdir as
|
|
6726
|
-
import { join as
|
|
7769
|
+
import { access, readdir as readdir6, readFile as readFile15, stat as stat4 } from "fs/promises";
|
|
7770
|
+
import { join as join22, posix as posix4 } from "path";
|
|
6727
7771
|
import { Script } from "vm";
|
|
6728
7772
|
var HOOKS_RUNTIME_CLIENT_DIRS = [
|
|
6729
7773
|
{ client: "claude", dir: ".claude/hooks" },
|
|
@@ -6772,7 +7816,7 @@ function isHookWiredForEvent(hooks, event, hookFile) {
|
|
|
6772
7816
|
}
|
|
6773
7817
|
async function readDirectoryFileNames(dir) {
|
|
6774
7818
|
try {
|
|
6775
|
-
return await
|
|
7819
|
+
return await readdir6(dir);
|
|
6776
7820
|
} catch {
|
|
6777
7821
|
return null;
|
|
6778
7822
|
}
|
|
@@ -6785,21 +7829,27 @@ async function isFile(absPath) {
|
|
|
6785
7829
|
}
|
|
6786
7830
|
}
|
|
6787
7831
|
async function inspectHooksWired(projectRoot) {
|
|
6788
|
-
const claudeEntries = await readDirectoryFileNames(
|
|
7832
|
+
const claudeEntries = await readDirectoryFileNames(join22(projectRoot, ".claude"));
|
|
6789
7833
|
if (claudeEntries === null) {
|
|
6790
7834
|
return { status: "skipped", missingHooks: [] };
|
|
6791
7835
|
}
|
|
6792
|
-
const settingsPath =
|
|
7836
|
+
const settingsPath = join22(projectRoot, ".claude", "settings.json");
|
|
6793
7837
|
let parsed;
|
|
6794
7838
|
try {
|
|
6795
|
-
parsed = JSON.parse(await
|
|
7839
|
+
parsed = JSON.parse(await readFile15(settingsPath, "utf8"));
|
|
6796
7840
|
} catch {
|
|
6797
7841
|
return { status: "missing-settings", missingHooks: [] };
|
|
6798
7842
|
}
|
|
6799
7843
|
const required = [
|
|
6800
7844
|
{ event: "Stop", hookFile: "fabric-hint.cjs" },
|
|
6801
7845
|
{ event: "SessionStart", hookFile: "knowledge-hint-broad.cjs" },
|
|
6802
|
-
|
|
7846
|
+
// ux-w2-6 (rc.9): PreToolUse is the SINGLE merged orchestrator
|
|
7847
|
+
// knowledge-pretooluse.cjs which internally requires knowledge-hint-narrow.cjs
|
|
7848
|
+
// (and cite-policy-evict.cjs) so a single edit produces ONE additionalContext
|
|
7849
|
+
// envelope instead of two. The stale reference to narrow.cjs here was the
|
|
7850
|
+
// rc.30 BUG-M3 false-negative source — narrow.cjs still exists on disk as a
|
|
7851
|
+
// lib but is NOT registered as a PreToolUse hook.
|
|
7852
|
+
{ event: "PreToolUse", hookFile: "knowledge-pretooluse.cjs" }
|
|
6803
7853
|
];
|
|
6804
7854
|
const missing = [];
|
|
6805
7855
|
const hooksSection = isRecord(parsed) ? parsed.hooks : void 0;
|
|
@@ -6815,8 +7865,8 @@ async function inspectHooksWired(projectRoot) {
|
|
|
6815
7865
|
}
|
|
6816
7866
|
async function inspectHookCacheWritability(projectRoot) {
|
|
6817
7867
|
const relPath = posix4.join(".fabric", ".cache");
|
|
6818
|
-
const fabricDir =
|
|
6819
|
-
const cacheDir =
|
|
7868
|
+
const fabricDir = join22(projectRoot, ".fabric");
|
|
7869
|
+
const cacheDir = join22(projectRoot, ".fabric", ".cache");
|
|
6820
7870
|
try {
|
|
6821
7871
|
try {
|
|
6822
7872
|
const cacheStats = await stat4(cacheDir);
|
|
@@ -6865,12 +7915,12 @@ async function inspectHookCacheWritability(projectRoot) {
|
|
|
6865
7915
|
async function inspectHooksContentDrift(projectRoot) {
|
|
6866
7916
|
const hookFilesByBasename = /* @__PURE__ */ new Map();
|
|
6867
7917
|
for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
|
|
6868
|
-
const absDir =
|
|
7918
|
+
const absDir = join22(projectRoot, dir);
|
|
6869
7919
|
const entries = await readDirectoryFileNames(absDir);
|
|
6870
7920
|
if (entries === null) continue;
|
|
6871
7921
|
for (const name of entries) {
|
|
6872
7922
|
if (!name.endsWith(".cjs")) continue;
|
|
6873
|
-
const abs =
|
|
7923
|
+
const abs = join22(absDir, name);
|
|
6874
7924
|
if (!await isFile(abs)) continue;
|
|
6875
7925
|
const arr = hookFilesByBasename.get(name) ?? [];
|
|
6876
7926
|
arr.push({ client, abs });
|
|
@@ -6885,7 +7935,7 @@ async function inspectHooksContentDrift(projectRoot) {
|
|
|
6885
7935
|
const hashes = [];
|
|
6886
7936
|
for (const { client, abs } of copies) {
|
|
6887
7937
|
try {
|
|
6888
|
-
const body = await
|
|
7938
|
+
const body = await readFile15(abs, "utf8");
|
|
6889
7939
|
hashes.push({ client, sha: sha256(body) });
|
|
6890
7940
|
} catch {
|
|
6891
7941
|
}
|
|
@@ -6907,18 +7957,18 @@ async function inspectHooksRuntime(projectRoot) {
|
|
|
6907
7957
|
const issues = [];
|
|
6908
7958
|
let scanned = 0;
|
|
6909
7959
|
for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
|
|
6910
|
-
const absDir =
|
|
7960
|
+
const absDir = join22(projectRoot, dir);
|
|
6911
7961
|
const entries = await readDirectoryFileNames(absDir);
|
|
6912
7962
|
if (entries === null) continue;
|
|
6913
7963
|
for (const name of entries) {
|
|
6914
7964
|
if (!name.endsWith(".cjs")) continue;
|
|
6915
|
-
const abs =
|
|
7965
|
+
const abs = join22(absDir, name);
|
|
6916
7966
|
const displayPath = `${dir}/${name}`;
|
|
6917
7967
|
if (!await isFile(abs)) continue;
|
|
6918
7968
|
scanned += 1;
|
|
6919
7969
|
let body;
|
|
6920
7970
|
try {
|
|
6921
|
-
body = await
|
|
7971
|
+
body = await readFile15(abs, "utf8");
|
|
6922
7972
|
} catch (err) {
|
|
6923
7973
|
issues.push({
|
|
6924
7974
|
path: displayPath,
|
|
@@ -7055,8 +8105,8 @@ function createHookCacheWritabilityCheck(t, inspection) {
|
|
|
7055
8105
|
}
|
|
7056
8106
|
|
|
7057
8107
|
// src/services/doctor-bootstrap-lints.ts
|
|
7058
|
-
import { access as access2, readFile as
|
|
7059
|
-
import { join as
|
|
8108
|
+
import { access as access2, readFile as readFile16 } from "fs/promises";
|
|
8109
|
+
import { join as join23 } from "path";
|
|
7060
8110
|
import {
|
|
7061
8111
|
BOOTSTRAP_MARKER_BEGIN,
|
|
7062
8112
|
BOOTSTRAP_MARKER_END,
|
|
@@ -7088,17 +8138,17 @@ async function fileExists(path) {
|
|
|
7088
8138
|
}
|
|
7089
8139
|
async function inspectBootstrapAnchor(projectRoot) {
|
|
7090
8140
|
const [hasAgentsMd, hasClaudeMd] = await Promise.all([
|
|
7091
|
-
fileExists(
|
|
7092
|
-
fileExists(
|
|
8141
|
+
fileExists(join23(projectRoot, "AGENTS.md")),
|
|
8142
|
+
fileExists(join23(projectRoot, "CLAUDE.md"))
|
|
7093
8143
|
]);
|
|
7094
8144
|
return { hasAgentsMd, hasClaudeMd };
|
|
7095
8145
|
}
|
|
7096
8146
|
async function inspectL1BootstrapSnapshotDrift(target) {
|
|
7097
|
-
const abs =
|
|
8147
|
+
const abs = join23(target, ".fabric", "AGENTS.md");
|
|
7098
8148
|
const canonical = resolveBootstrapCanonical();
|
|
7099
8149
|
let onDisk;
|
|
7100
8150
|
try {
|
|
7101
|
-
onDisk = await
|
|
8151
|
+
onDisk = await readFile16(abs, "utf8");
|
|
7102
8152
|
} catch {
|
|
7103
8153
|
return { status: "missing", canonical, onDisk: null };
|
|
7104
8154
|
}
|
|
@@ -7124,17 +8174,17 @@ function createL1BootstrapSnapshotDriftCheck(t, inspection) {
|
|
|
7124
8174
|
);
|
|
7125
8175
|
}
|
|
7126
8176
|
async function inspectL2ManagedBlockDrift(target) {
|
|
7127
|
-
const snapshotPath =
|
|
8177
|
+
const snapshotPath = join23(target, ".fabric", "AGENTS.md");
|
|
7128
8178
|
let snapshot;
|
|
7129
8179
|
try {
|
|
7130
|
-
snapshot = await
|
|
8180
|
+
snapshot = await readFile16(snapshotPath, "utf8");
|
|
7131
8181
|
} catch {
|
|
7132
8182
|
return { status: "ok", drifted: [] };
|
|
7133
8183
|
}
|
|
7134
|
-
const projectRulesPath =
|
|
8184
|
+
const projectRulesPath = join23(target, ".fabric", "project-rules.md");
|
|
7135
8185
|
let expectedBody = snapshot;
|
|
7136
8186
|
try {
|
|
7137
|
-
const projectRules = await
|
|
8187
|
+
const projectRules = await readFile16(projectRulesPath, "utf8");
|
|
7138
8188
|
expectedBody = `${snapshot}
|
|
7139
8189
|
---
|
|
7140
8190
|
${projectRules}`;
|
|
@@ -7143,12 +8193,12 @@ ${projectRules}`;
|
|
|
7143
8193
|
const drifted = [];
|
|
7144
8194
|
let anyManagedBlockFound = false;
|
|
7145
8195
|
const blockTargets = [
|
|
7146
|
-
|
|
8196
|
+
join23(target, "AGENTS.md")
|
|
7147
8197
|
];
|
|
7148
8198
|
for (const abs of blockTargets) {
|
|
7149
8199
|
let content;
|
|
7150
8200
|
try {
|
|
7151
|
-
content = await
|
|
8201
|
+
content = await readFile16(abs, "utf8");
|
|
7152
8202
|
} catch {
|
|
7153
8203
|
continue;
|
|
7154
8204
|
}
|
|
@@ -7171,9 +8221,9 @@ ${projectRules}`;
|
|
|
7171
8221
|
drifted.push({ path: abs, expected: expectedBody, actual: body });
|
|
7172
8222
|
}
|
|
7173
8223
|
}
|
|
7174
|
-
const claudeMdPath =
|
|
8224
|
+
const claudeMdPath = join23(target, "CLAUDE.md");
|
|
7175
8225
|
try {
|
|
7176
|
-
const claudeContent = await
|
|
8226
|
+
const claudeContent = await readFile16(claudeMdPath, "utf8");
|
|
7177
8227
|
anyManagedBlockFound = true;
|
|
7178
8228
|
const lines = claudeContent.split(/\r?\n/u);
|
|
7179
8229
|
const hasAtImport = lines.some((line) => line.trim() === "@.fabric/AGENTS.md");
|
|
@@ -7241,7 +8291,7 @@ import { appendFile as appendFile3 } from "fs/promises";
|
|
|
7241
8291
|
import { minimatch as minimatch2 } from "minimatch";
|
|
7242
8292
|
|
|
7243
8293
|
// src/services/cite-rollup.ts
|
|
7244
|
-
import { readFile as
|
|
8294
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
7245
8295
|
import { createLedgerWriteQueue as createLedgerWriteQueue3 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
7246
8296
|
var citeRollupQueue = createLedgerWriteQueue3();
|
|
7247
8297
|
async function appendCiteRollupRow(projectRoot, row) {
|
|
@@ -7253,7 +8303,7 @@ async function readCiteRollup(projectRoot) {
|
|
|
7253
8303
|
const path = getCiteRollupPath(projectRoot);
|
|
7254
8304
|
let raw;
|
|
7255
8305
|
try {
|
|
7256
|
-
raw = await
|
|
8306
|
+
raw = await readFile17(path, "utf8");
|
|
7257
8307
|
} catch (error) {
|
|
7258
8308
|
if (isNodeError(error) && error.code === "ENOENT") return [];
|
|
7259
8309
|
throw error;
|
|
@@ -7844,6 +8894,18 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
7844
8894
|
}
|
|
7845
8895
|
}
|
|
7846
8896
|
}
|
|
8897
|
+
const editSessionIds = /* @__PURE__ */ new Set();
|
|
8898
|
+
for (const edit of editEvents) {
|
|
8899
|
+
if (typeof edit.session_id === "string" && edit.session_id.length > 0) {
|
|
8900
|
+
editSessionIds.add(edit.session_id);
|
|
8901
|
+
}
|
|
8902
|
+
}
|
|
8903
|
+
let recallsInWindow = 0;
|
|
8904
|
+
for (const list of plannedBySession.values()) recallsInWindow += list.length;
|
|
8905
|
+
let recallSessionsCorrelated = 0;
|
|
8906
|
+
for (const sid of plannedBySession.keys()) {
|
|
8907
|
+
if (editSessionIds.has(sid)) recallSessionsCorrelated += 1;
|
|
8908
|
+
}
|
|
7847
8909
|
const noneTotal = Object.values(noneHistogram).reduce((a, b) => a + b, 0);
|
|
7848
8910
|
const compliantCites = qualifyingCites + noneTotal;
|
|
7849
8911
|
const noncompliantCites = expectedButMissed;
|
|
@@ -7911,6 +8973,11 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
7911
8973
|
uncorrelatable_edits: uncorrelatableEdits,
|
|
7912
8974
|
recall_backed_edits: recallBackedEdits,
|
|
7913
8975
|
recall_coverage_rate: recallCoverageRate,
|
|
8976
|
+
recall_diagnostics: {
|
|
8977
|
+
recalls_in_window: recallsInWindow,
|
|
8978
|
+
recall_sessions: plannedBySession.size,
|
|
8979
|
+
recall_sessions_correlated: recallSessionsCorrelated
|
|
8980
|
+
},
|
|
7914
8981
|
exposed_and_mutated: {
|
|
7915
8982
|
count: exposedAndMutated.count,
|
|
7916
8983
|
...exposedAndMutated.ids.length > 0 ? { ids: exposedAndMutated.ids } : {}
|
|
@@ -8316,6 +9383,7 @@ async function runDoctorReport(target) {
|
|
|
8316
9383
|
skillRefMirror,
|
|
8317
9384
|
skillTokenBudget,
|
|
8318
9385
|
skillDescription,
|
|
9386
|
+
skillContract,
|
|
8319
9387
|
retiredReferences
|
|
8320
9388
|
] = await Promise.all([
|
|
8321
9389
|
inspectForensic(projectRoot),
|
|
@@ -8337,6 +9405,7 @@ async function runDoctorReport(target) {
|
|
|
8337
9405
|
inspectSkillRefMirror(projectRoot),
|
|
8338
9406
|
inspectSkillTokenBudget(projectRoot),
|
|
8339
9407
|
inspectSkillDescription(projectRoot),
|
|
9408
|
+
inspectSkillContract(projectRoot),
|
|
8340
9409
|
// ux-w2-2: registry-driven stale-pointer scan over the agent-consumed
|
|
8341
9410
|
// surface (bootstrap + SKILL.md + installed hooks).
|
|
8342
9411
|
inspectRetiredReferences(projectRoot)
|
|
@@ -8354,6 +9423,7 @@ async function runDoctorReport(target) {
|
|
|
8354
9423
|
const driftUnconsumed = await inspectDriftUnconsumed(projectRoot);
|
|
8355
9424
|
const storeCounterDrift = inspectStoreCounters(projectRoot);
|
|
8356
9425
|
const storeOrphans = inspectStoreOrphans();
|
|
9426
|
+
const projectRegistryDrift = await inspectProjectRegistryDrift(projectRoot);
|
|
8357
9427
|
const stableIdIntegrity = await inspectStoreStableIdIntegrity(projectRoot);
|
|
8358
9428
|
const relevancePaths = await inspectStoreRelevancePaths(projectRoot);
|
|
8359
9429
|
const broadIndexDrift = await inspectBroadIndexDrift(projectRoot);
|
|
@@ -8383,7 +9453,7 @@ async function runDoctorReport(target) {
|
|
|
8383
9453
|
const globalCliVersion = process.env.VITEST === "true" ? { status: "ok", version: "test-skipped" } : inspectGlobalCliVersion();
|
|
8384
9454
|
const targetFiles = Object.fromEntries(
|
|
8385
9455
|
await Promise.all(
|
|
8386
|
-
TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(
|
|
9456
|
+
TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(join24(projectRoot, path))])
|
|
8387
9457
|
)
|
|
8388
9458
|
);
|
|
8389
9459
|
const checks = [
|
|
@@ -8422,6 +9492,7 @@ async function runDoctorReport(target) {
|
|
|
8422
9492
|
createSkillRefMirrorCheck(t, skillRefMirror),
|
|
8423
9493
|
createSkillTokenBudgetCheck(t, skillTokenBudget),
|
|
8424
9494
|
createSkillDescriptionCheck(t, skillDescription),
|
|
9495
|
+
createSkillContractCheck(t, skillContract),
|
|
8425
9496
|
// ux-w2-2: retired-reference (stale-pointer) lint — registry-driven.
|
|
8426
9497
|
createRetiredReferenceCheck(t, retiredReferences),
|
|
8427
9498
|
createCiteGoodhartCheck(t, citeGoodhart),
|
|
@@ -8437,6 +9508,12 @@ async function runDoctorReport(target) {
|
|
|
8437
9508
|
// (warning). `--fix` adopts it (rescue-before-delete — re-register, never
|
|
8438
9509
|
// an on-disk delete).
|
|
8439
9510
|
createStoreOrphanCheck(t, storeOrphans),
|
|
9511
|
+
// W2 (F-003): project-registry drift over projects.json ↔ projects/ folder
|
|
9512
|
+
// tree. unregistered-write (data unrouted) → manual_error; orphan-folder →
|
|
9513
|
+
// warning; empty-folder → info; ghost-registration emits nothing (DA-05).
|
|
9514
|
+
// `--fix` rescue-registers orphans/unregistered (addStoreProject — never a
|
|
9515
|
+
// non-empty delete) and prunes only genuinely-empty registered folders.
|
|
9516
|
+
createProjectRegistryDriftCheck(t, projectRegistryDrift),
|
|
8440
9517
|
// rc.5 TASK-010: read-side underseeded-corpus check (#22). Info kind —
|
|
8441
9518
|
// does not bump report status. Recommends running the fabric-import skill
|
|
8442
9519
|
// to backfill knowledge when the corpus is below the threshold floor.
|
|
@@ -8512,6 +9589,14 @@ async function runDoctorReport(target) {
|
|
|
8512
9589
|
// fresh-install hole is sealed in store.stage.ts; this covers existing repos
|
|
8513
9590
|
// (CLI `--fix` backfills via ensureStoreProjectBinding).
|
|
8514
9591
|
createUnboundProjectCheck(t, detectUnboundProject(projectRoot)),
|
|
9592
|
+
// write_route_target_unbound — every write_routes[i].store must resolve
|
|
9593
|
+
// against required_stores. Catches "route survived migration to single
|
|
9594
|
+
// team slot" (werewolf-minigame case): route says `team → cocos` but
|
|
9595
|
+
// required_stores only holds `team`, so fab_propose(audience:"team")
|
|
9596
|
+
// would report "no write-target store resolved" at runtime. Static
|
|
9597
|
+
// config-level check, no resolver, aligned with KT-DEC-0048 read-tolerant
|
|
9598
|
+
// doctrine.
|
|
9599
|
+
createWriteRouteUnboundCheck(t, detectWriteRouteUnbound(projectRoot)),
|
|
8515
9600
|
// rc.31 BUG-G2/G5: promote-ledger invariant. Sits adjacent to onboard
|
|
8516
9601
|
// coverage — both are observability advisories built off events.jsonl.
|
|
8517
9602
|
...promoteLedgerInvariant === null ? [] : [createPromoteLedgerInvariantCheck(t, promoteLedgerInvariant)],
|
|
@@ -8587,7 +9672,7 @@ async function runDoctorFix(target) {
|
|
|
8587
9672
|
const fixed = [];
|
|
8588
9673
|
const ledgerWarnings = [];
|
|
8589
9674
|
if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
|
|
8590
|
-
const snapshotPath =
|
|
9675
|
+
const snapshotPath = join24(projectRoot, ".fabric", "AGENTS.md");
|
|
8591
9676
|
await ensureParentDirectory(snapshotPath);
|
|
8592
9677
|
await atomicWriteText4(snapshotPath, resolveBootstrapCanonical2());
|
|
8593
9678
|
fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
|
|
@@ -8611,6 +9696,18 @@ async function runDoctorFix(target) {
|
|
|
8611
9696
|
fixed.push(findIssue(before.warnings, "store_orphan"));
|
|
8612
9697
|
}
|
|
8613
9698
|
}
|
|
9699
|
+
const registryDriftFound = before.manual_errors.some((issue) => issue.code === "project_registry_drift") || before.warnings.some((issue) => issue.code === "project_registry_drift") || before.infos.some((issue) => issue.code === "project_registry_drift");
|
|
9700
|
+
if (registryDriftFound) {
|
|
9701
|
+
const result = await fixProjectRegistryDrift(projectRoot);
|
|
9702
|
+
if (result.registered.length > 0 || result.pruned.length > 0) {
|
|
9703
|
+
fixed.push(
|
|
9704
|
+
findIssue(
|
|
9705
|
+
[...before.manual_errors, ...before.warnings, ...before.infos],
|
|
9706
|
+
"project_registry_drift"
|
|
9707
|
+
)
|
|
9708
|
+
);
|
|
9709
|
+
}
|
|
9710
|
+
}
|
|
8614
9711
|
if (before.fixable_errors.some((issue) => issue.code === "event_ledger_partial_write")) {
|
|
8615
9712
|
const ledgerPath = getEventLedgerPath(projectRoot);
|
|
8616
9713
|
const truncResult = await truncateLedgerToLastNewline(ledgerPath);
|
|
@@ -8660,7 +9757,7 @@ async function runDoctorFix(target) {
|
|
|
8660
9757
|
if (before.infos.some((issue) => issue.code === "stale_serve_lock")) {
|
|
8661
9758
|
const lockInspection = inspectStaleServeLock(projectRoot, Date.now());
|
|
8662
9759
|
if (lockInspection.present && !lockInspection.pidAlive) {
|
|
8663
|
-
const lockFilePath =
|
|
9760
|
+
const lockFilePath = join24(projectRoot, ".fabric", ".serve.lock");
|
|
8664
9761
|
try {
|
|
8665
9762
|
await unlink4(lockFilePath);
|
|
8666
9763
|
} catch (err) {
|
|
@@ -8777,7 +9874,7 @@ function createApplyLintMessage(succeeded, failed, manualErrorCount) {
|
|
|
8777
9874
|
}
|
|
8778
9875
|
async function applySessionHintsStaleCleanup(projectRoot, candidate) {
|
|
8779
9876
|
const detail = `deleted (${candidate.age_days}d old)`;
|
|
8780
|
-
const absPath =
|
|
9877
|
+
const absPath = join24(projectRoot, candidate.path);
|
|
8781
9878
|
try {
|
|
8782
9879
|
const { unlink: unlink5 } = await import("fs/promises");
|
|
8783
9880
|
await unlink5(absPath);
|
|
@@ -8802,9 +9899,9 @@ function truncateErrorMessage(error) {
|
|
|
8802
9899
|
return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
|
|
8803
9900
|
}
|
|
8804
9901
|
async function inspectForensic(projectRoot) {
|
|
8805
|
-
const path =
|
|
9902
|
+
const path = join24(projectRoot, ".fabric", "forensic.json");
|
|
8806
9903
|
try {
|
|
8807
|
-
const parsed = forensicReportSchema.parse(JSON.parse(await
|
|
9904
|
+
const parsed = forensicReportSchema.parse(JSON.parse(await readFile19(path, "utf8")));
|
|
8808
9905
|
return { present: true, valid: true, report: parsed };
|
|
8809
9906
|
} catch (error) {
|
|
8810
9907
|
if (isMissingFileError(error)) {
|
|
@@ -8834,7 +9931,7 @@ async function inspectEventLedger(projectRoot) {
|
|
|
8834
9931
|
try {
|
|
8835
9932
|
await access4(path, constants2.W_OK);
|
|
8836
9933
|
const { warnings } = await readEventLedger(projectRoot);
|
|
8837
|
-
const raw = await
|
|
9934
|
+
const raw = await readFile19(path, "utf8");
|
|
8838
9935
|
const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
|
|
8839
9936
|
const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
|
|
8840
9937
|
const schemaVersionSamples = [];
|
|
@@ -9379,7 +10476,7 @@ async function inspectPreexistingRootFiles(projectRoot) {
|
|
|
9379
10476
|
const candidates = ["CLAUDE.md", "AGENTS.md"];
|
|
9380
10477
|
const detected = [];
|
|
9381
10478
|
for (const name of candidates) {
|
|
9382
|
-
if (await pathExists(
|
|
10479
|
+
if (await pathExists(join24(projectRoot, name))) {
|
|
9383
10480
|
detected.push(name);
|
|
9384
10481
|
}
|
|
9385
10482
|
}
|
|
@@ -9464,7 +10561,7 @@ async function buildLastActiveIndex(projectRoot) {
|
|
|
9464
10561
|
return map;
|
|
9465
10562
|
}
|
|
9466
10563
|
async function inspectSessionHintsStale(projectRoot, now) {
|
|
9467
|
-
const cacheDir =
|
|
10564
|
+
const cacheDir = join24(projectRoot, ".fabric", ".cache");
|
|
9468
10565
|
let entries;
|
|
9469
10566
|
try {
|
|
9470
10567
|
entries = await readdirAsync(cacheDir, { withFileTypes: true });
|
|
@@ -9476,7 +10573,7 @@ async function inspectSessionHintsStale(projectRoot, now) {
|
|
|
9476
10573
|
if (!entry.isFile()) continue;
|
|
9477
10574
|
if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
|
|
9478
10575
|
if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
|
|
9479
|
-
const absPath =
|
|
10576
|
+
const absPath = join24(cacheDir, entry.name);
|
|
9480
10577
|
let mtimeMs = 0;
|
|
9481
10578
|
try {
|
|
9482
10579
|
mtimeMs = (await statAsync(absPath)).mtimeMs;
|
|
@@ -9508,9 +10605,9 @@ function inspectStaleServeLock(projectRoot, now) {
|
|
|
9508
10605
|
};
|
|
9509
10606
|
}
|
|
9510
10607
|
async function readUnderseedThresholdFromConfig(projectRoot) {
|
|
9511
|
-
const configPath =
|
|
10608
|
+
const configPath = join24(projectRoot, ".fabric", "fabric-config.json");
|
|
9512
10609
|
try {
|
|
9513
|
-
const raw = await
|
|
10610
|
+
const raw = await readFile19(configPath, "utf8");
|
|
9514
10611
|
const parsed = JSON.parse(raw);
|
|
9515
10612
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
9516
10613
|
const v = parsed.underseed_node_threshold;
|
|
@@ -9626,10 +10723,10 @@ async function inspectOnboardCoverage(projectRoot) {
|
|
|
9626
10723
|
return { filled, missing, opted_out: optedOut };
|
|
9627
10724
|
}
|
|
9628
10725
|
async function readOnboardOptedOut(projectRoot) {
|
|
9629
|
-
const path =
|
|
10726
|
+
const path = join24(projectRoot, ".fabric", "fabric-config.json");
|
|
9630
10727
|
let raw;
|
|
9631
10728
|
try {
|
|
9632
|
-
raw = await
|
|
10729
|
+
raw = await readFile19(path, "utf8");
|
|
9633
10730
|
} catch {
|
|
9634
10731
|
return [];
|
|
9635
10732
|
}
|
|
@@ -9731,22 +10828,22 @@ async function* iterateCanonicalFilenames(projectRoot) {
|
|
|
9731
10828
|
}
|
|
9732
10829
|
}
|
|
9733
10830
|
async function rewriteThreeEndManagedBlocks(projectRoot) {
|
|
9734
|
-
const snapshotPath =
|
|
10831
|
+
const snapshotPath = join24(projectRoot, ".fabric", "AGENTS.md");
|
|
9735
10832
|
if (!await pathExists(snapshotPath)) {
|
|
9736
10833
|
return;
|
|
9737
10834
|
}
|
|
9738
10835
|
let snapshot;
|
|
9739
10836
|
try {
|
|
9740
|
-
snapshot = await
|
|
10837
|
+
snapshot = await readFile19(snapshotPath, "utf8");
|
|
9741
10838
|
} catch {
|
|
9742
10839
|
return;
|
|
9743
10840
|
}
|
|
9744
|
-
const projectRulesPath =
|
|
10841
|
+
const projectRulesPath = join24(projectRoot, ".fabric", "project-rules.md");
|
|
9745
10842
|
const hasProjectRules = await pathExists(projectRulesPath);
|
|
9746
10843
|
let expectedBody = snapshot;
|
|
9747
10844
|
if (hasProjectRules) {
|
|
9748
10845
|
try {
|
|
9749
|
-
const projectRules = await
|
|
10846
|
+
const projectRules = await readFile19(projectRulesPath, "utf8");
|
|
9750
10847
|
expectedBody = `${snapshot}
|
|
9751
10848
|
---
|
|
9752
10849
|
${projectRules}`;
|
|
@@ -9757,7 +10854,7 @@ ${projectRules}`;
|
|
|
9757
10854
|
${expectedBody}
|
|
9758
10855
|
${BOOTSTRAP_MARKER_END2}`;
|
|
9759
10856
|
const blockTargets = [
|
|
9760
|
-
|
|
10857
|
+
join24(projectRoot, "AGENTS.md")
|
|
9761
10858
|
];
|
|
9762
10859
|
for (const abs of blockTargets) {
|
|
9763
10860
|
if (!await pathExists(abs)) {
|
|
@@ -9765,7 +10862,7 @@ ${BOOTSTRAP_MARKER_END2}`;
|
|
|
9765
10862
|
}
|
|
9766
10863
|
let existing;
|
|
9767
10864
|
try {
|
|
9768
|
-
existing = await
|
|
10865
|
+
existing = await readFile19(abs, "utf8");
|
|
9769
10866
|
} catch {
|
|
9770
10867
|
continue;
|
|
9771
10868
|
}
|
|
@@ -9790,11 +10887,11 @@ ${managedBlock}
|
|
|
9790
10887
|
}
|
|
9791
10888
|
await atomicWriteText4(abs, next);
|
|
9792
10889
|
}
|
|
9793
|
-
const claudeMdPath =
|
|
10890
|
+
const claudeMdPath = join24(projectRoot, "CLAUDE.md");
|
|
9794
10891
|
if (await pathExists(claudeMdPath)) {
|
|
9795
10892
|
let claudeContent;
|
|
9796
10893
|
try {
|
|
9797
|
-
claudeContent = await
|
|
10894
|
+
claudeContent = await readFile19(claudeMdPath, "utf8");
|
|
9798
10895
|
} catch {
|
|
9799
10896
|
return;
|
|
9800
10897
|
}
|
|
@@ -9821,7 +10918,7 @@ ${managedBlock}
|
|
|
9821
10918
|
async function ensureEventLedger(projectRoot) {
|
|
9822
10919
|
const path = getEventLedgerPath(projectRoot);
|
|
9823
10920
|
await ensureParentDirectory(path);
|
|
9824
|
-
await
|
|
10921
|
+
await writeFile5(path, "", { encoding: "utf8", flag: "a" });
|
|
9825
10922
|
}
|
|
9826
10923
|
function createFixMessage(fixed, report) {
|
|
9827
10924
|
const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
|
|
@@ -9860,7 +10957,7 @@ async function collectEntryPoints(root) {
|
|
|
9860
10957
|
continue;
|
|
9861
10958
|
}
|
|
9862
10959
|
for (const entry of await readdirAsync(current, { withFileTypes: true })) {
|
|
9863
|
-
const absolutePath =
|
|
10960
|
+
const absolutePath = join24(current, entry.name);
|
|
9864
10961
|
const relativePath = normalizePath2(absolutePath.slice(root.length + 1));
|
|
9865
10962
|
if (relativePath.length === 0) {
|
|
9866
10963
|
continue;
|
|
@@ -9936,7 +11033,7 @@ async function enrichDescriptions(projectRoot, opts = {}) {
|
|
|
9936
11033
|
scanned += 1;
|
|
9937
11034
|
let source;
|
|
9938
11035
|
try {
|
|
9939
|
-
source = await
|
|
11036
|
+
source = await readFile19(absPath, "utf8");
|
|
9940
11037
|
} catch {
|
|
9941
11038
|
continue;
|
|
9942
11039
|
}
|
|
@@ -10074,16 +11171,16 @@ async function runDoctorConflictLint(projectRoot, opts = {}) {
|
|
|
10074
11171
|
}
|
|
10075
11172
|
|
|
10076
11173
|
// src/services/why-not-surfaced.ts
|
|
10077
|
-
import { readFile as
|
|
10078
|
-
import { basename as basename3, join as
|
|
11174
|
+
import { readFile as readFile20 } from "fs/promises";
|
|
11175
|
+
import { basename as basename3, join as join25 } from "path";
|
|
10079
11176
|
import {
|
|
10080
|
-
buildStoreResolveInput as
|
|
10081
|
-
createStoreResolver as
|
|
10082
|
-
loadProjectConfig as
|
|
11177
|
+
buildStoreResolveInput as buildStoreResolveInput8,
|
|
11178
|
+
createStoreResolver as createStoreResolver8,
|
|
11179
|
+
loadProjectConfig as loadProjectConfig6,
|
|
10083
11180
|
readKnowledgeAcrossStores as readKnowledgeAcrossStores4,
|
|
10084
|
-
resolveGlobalRoot as
|
|
11181
|
+
resolveGlobalRoot as resolveGlobalRoot9,
|
|
10085
11182
|
scopeRoot as scopeRoot3,
|
|
10086
|
-
storeRelativePathForMount as
|
|
11183
|
+
storeRelativePathForMount as storeRelativePathForMount7
|
|
10087
11184
|
} from "@fenglimg/fabric-shared";
|
|
10088
11185
|
var SEMANTIC_SCOPE_LINE3 = /^semantic_scope:\s*"?([^"\n]+?)"?\s*$/mu;
|
|
10089
11186
|
var RELEVANCE_SCOPE_LINE = /^relevance_scope:\s*"?(broad|narrow)"?\s*$/mu;
|
|
@@ -10107,15 +11204,15 @@ async function explainWhyNotSurfaced(projectRoot, query) {
|
|
|
10107
11204
|
activeProject: null,
|
|
10108
11205
|
relevanceScope: null
|
|
10109
11206
|
};
|
|
10110
|
-
const input =
|
|
11207
|
+
const input = buildStoreResolveInput8(projectRoot);
|
|
10111
11208
|
if (input === null) {
|
|
10112
11209
|
return base;
|
|
10113
11210
|
}
|
|
10114
|
-
const globalRoot =
|
|
11211
|
+
const globalRoot = resolveGlobalRoot9();
|
|
10115
11212
|
const allStores = input.mountedStores.map((s) => ({
|
|
10116
11213
|
store_uuid: s.store_uuid,
|
|
10117
11214
|
alias: s.alias,
|
|
10118
|
-
dir:
|
|
11215
|
+
dir: join25(globalRoot, storeRelativePathForMount7(s))
|
|
10119
11216
|
}));
|
|
10120
11217
|
const refs = await readKnowledgeAcrossStores4(allStores);
|
|
10121
11218
|
const candidate = refs.find((ref) => fileMatchesId(ref.file, localId));
|
|
@@ -10124,7 +11221,7 @@ async function explainWhyNotSurfaced(projectRoot, query) {
|
|
|
10124
11221
|
}
|
|
10125
11222
|
let source;
|
|
10126
11223
|
try {
|
|
10127
|
-
source = await
|
|
11224
|
+
source = await readFile20(candidate.file, "utf8");
|
|
10128
11225
|
} catch {
|
|
10129
11226
|
return base;
|
|
10130
11227
|
}
|
|
@@ -10133,9 +11230,9 @@ async function explainWhyNotSurfaced(projectRoot, query) {
|
|
|
10133
11230
|
}
|
|
10134
11231
|
const semanticScope = SEMANTIC_SCOPE_LINE3.exec(source)?.[1] ?? null;
|
|
10135
11232
|
const relevanceScope = RELEVANCE_SCOPE_LINE.exec(source)?.[1] ?? "broad";
|
|
10136
|
-
const activeProject =
|
|
11233
|
+
const activeProject = loadProjectConfig6(projectRoot)?.active_project ?? null;
|
|
10137
11234
|
const boundUuids = new Set(
|
|
10138
|
-
|
|
11235
|
+
createStoreResolver8().resolveReadSet(input).stores.map((s) => s.store_uuid)
|
|
10139
11236
|
);
|
|
10140
11237
|
const storeBound = boundUuids.has(candidate.store_uuid);
|
|
10141
11238
|
const found = {
|
|
@@ -10177,6 +11274,191 @@ function buildColdEvalBatch(candidates) {
|
|
|
10177
11274
|
return { rubric: COLD_EVAL_RUBRIC, candidates: judgeable };
|
|
10178
11275
|
}
|
|
10179
11276
|
|
|
11277
|
+
// src/services/doctor-related-graph.ts
|
|
11278
|
+
function buildRelatedGraph(nodes) {
|
|
11279
|
+
const brokenLinks = [];
|
|
11280
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
11281
|
+
const allIds = /* @__PURE__ */ new Set();
|
|
11282
|
+
const edgeSources = [];
|
|
11283
|
+
for (const node of nodes) {
|
|
11284
|
+
const bareId = extractBareStableId(node.qualifiedId);
|
|
11285
|
+
allIds.add(node.qualifiedId);
|
|
11286
|
+
if (bareId !== null) {
|
|
11287
|
+
allIds.add(bareId);
|
|
11288
|
+
}
|
|
11289
|
+
const related = node.related;
|
|
11290
|
+
if (related !== void 0 && related.length > 0) {
|
|
11291
|
+
edgeSources.push({ source: node.qualifiedId, related });
|
|
11292
|
+
for (const rel of related) {
|
|
11293
|
+
inDegree.set(rel, (inDegree.get(rel) ?? 0) + 1);
|
|
11294
|
+
}
|
|
11295
|
+
}
|
|
11296
|
+
}
|
|
11297
|
+
for (const { source, related } of edgeSources) {
|
|
11298
|
+
for (const rel of related) {
|
|
11299
|
+
if (!allIds.has(rel)) {
|
|
11300
|
+
brokenLinks.push({ source, target: rel });
|
|
11301
|
+
}
|
|
11302
|
+
}
|
|
11303
|
+
}
|
|
11304
|
+
const hubEntries = [...inDegree.entries()].map(([stableId, degree]) => ({ stableId, inDegree: degree })).sort((a, b) => b.inDegree - a.inDegree || a.stableId.localeCompare(b.stableId));
|
|
11305
|
+
return {
|
|
11306
|
+
brokenLinks,
|
|
11307
|
+
hubEntries,
|
|
11308
|
+
totalEntries: nodes.length
|
|
11309
|
+
};
|
|
11310
|
+
}
|
|
11311
|
+
function extractBareStableId(qualifiedId) {
|
|
11312
|
+
const colonIdx = qualifiedId.indexOf(":");
|
|
11313
|
+
if (colonIdx > 0 && colonIdx < qualifiedId.length - 1) {
|
|
11314
|
+
return qualifiedId.slice(colonIdx + 1);
|
|
11315
|
+
}
|
|
11316
|
+
return null;
|
|
11317
|
+
}
|
|
11318
|
+
async function inspectRelatedGraph(projectRoot) {
|
|
11319
|
+
const entries = await collectStoreCanonicalEntries(projectRoot);
|
|
11320
|
+
return buildRelatedGraph(
|
|
11321
|
+
entries.map((entry) => ({
|
|
11322
|
+
qualifiedId: entry.qualifiedId,
|
|
11323
|
+
related: entry.description.related
|
|
11324
|
+
}))
|
|
11325
|
+
);
|
|
11326
|
+
}
|
|
11327
|
+
|
|
11328
|
+
// src/services/store-precheck.ts
|
|
11329
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
|
|
11330
|
+
import { join as join26 } from "path";
|
|
11331
|
+
import {
|
|
11332
|
+
buildStoreResolveInput as buildStoreResolveInput9,
|
|
11333
|
+
createStoreResolver as createStoreResolver9,
|
|
11334
|
+
resolveGlobalRoot as resolveGlobalRoot10,
|
|
11335
|
+
storeRelativePathForMount as storeRelativePathForMount8
|
|
11336
|
+
} from "@fenglimg/fabric-shared";
|
|
11337
|
+
var CACHE_TTL_MS = 6e4;
|
|
11338
|
+
var cache = /* @__PURE__ */ new Map();
|
|
11339
|
+
function clearPrecheckCache() {
|
|
11340
|
+
cache.clear();
|
|
11341
|
+
}
|
|
11342
|
+
function evaluateStoreDir(storeDir, identity) {
|
|
11343
|
+
if (!existsSync9(storeDir)) {
|
|
11344
|
+
return {
|
|
11345
|
+
uuid: identity.uuid,
|
|
11346
|
+
alias: identity.alias,
|
|
11347
|
+
reachable: false,
|
|
11348
|
+
reason: `directory not found at ${storeDir}`
|
|
11349
|
+
};
|
|
11350
|
+
}
|
|
11351
|
+
const storeJsonPath = join26(storeDir, "store.json");
|
|
11352
|
+
const gitDirPath = join26(storeDir, ".git");
|
|
11353
|
+
const hasStoreJson = existsSync9(storeJsonPath) && isValidStoreJson(storeJsonPath);
|
|
11354
|
+
const hasGitDir = existsSync9(gitDirPath);
|
|
11355
|
+
if (!hasStoreJson && !hasGitDir) {
|
|
11356
|
+
return {
|
|
11357
|
+
uuid: identity.uuid,
|
|
11358
|
+
alias: identity.alias,
|
|
11359
|
+
reachable: false,
|
|
11360
|
+
reason: `no store.json or .git found in ${storeDir}`
|
|
11361
|
+
};
|
|
11362
|
+
}
|
|
11363
|
+
return { uuid: identity.uuid, alias: identity.alias, reachable: true };
|
|
11364
|
+
}
|
|
11365
|
+
function isValidStoreJson(path) {
|
|
11366
|
+
try {
|
|
11367
|
+
JSON.parse(readFileSync4(path, "utf8"));
|
|
11368
|
+
return true;
|
|
11369
|
+
} catch {
|
|
11370
|
+
return false;
|
|
11371
|
+
}
|
|
11372
|
+
}
|
|
11373
|
+
async function precheckStoreReachability(projectRoot, globalRoot = resolveGlobalRoot10(), now = Date.now()) {
|
|
11374
|
+
const cached = cache.get(projectRoot);
|
|
11375
|
+
if (cached !== void 0 && cached.expiresAt > now) {
|
|
11376
|
+
return cached.result;
|
|
11377
|
+
}
|
|
11378
|
+
const input = buildStoreResolveInput9(projectRoot, globalRoot);
|
|
11379
|
+
if (input === null) {
|
|
11380
|
+
const result2 = { stores: [], allReachable: true };
|
|
11381
|
+
cache.set(projectRoot, { result: result2, expiresAt: now + CACHE_TTL_MS });
|
|
11382
|
+
return result2;
|
|
11383
|
+
}
|
|
11384
|
+
const readSet = createStoreResolver9().resolveReadSet(input);
|
|
11385
|
+
const stores = readSet.stores.map((entry) => {
|
|
11386
|
+
const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
|
|
11387
|
+
const storeDir = join26(
|
|
11388
|
+
globalRoot,
|
|
11389
|
+
storeRelativePathForMount8(mounted ?? { store_uuid: entry.store_uuid })
|
|
11390
|
+
);
|
|
11391
|
+
return evaluateStoreDir(storeDir, { uuid: entry.store_uuid, alias: entry.alias });
|
|
11392
|
+
});
|
|
11393
|
+
const result = {
|
|
11394
|
+
stores,
|
|
11395
|
+
allReachable: stores.every((s) => s.reachable)
|
|
11396
|
+
};
|
|
11397
|
+
cache.set(projectRoot, { result, expiresAt: now + CACHE_TTL_MS });
|
|
11398
|
+
return result;
|
|
11399
|
+
}
|
|
11400
|
+
|
|
11401
|
+
// src/services/doctor-consumption-lint.ts
|
|
11402
|
+
var DEFAULT_WINDOW_DAYS = 30;
|
|
11403
|
+
var DEFAULT_TOP_N = 10;
|
|
11404
|
+
var DEFAULT_MIN_TOTAL_ENTRIES = 20;
|
|
11405
|
+
var DEFAULT_MIN_CONSUMED_WINDOWS = 30;
|
|
11406
|
+
var DEFAULT_MIN_CONSUMED_EVENTS = 50;
|
|
11407
|
+
var CONSUMED_PREFIX = "knowledge_consumed:";
|
|
11408
|
+
var MS_PER_DAY5 = 24 * 60 * 60 * 1e3;
|
|
11409
|
+
function resolveConfig(config) {
|
|
11410
|
+
return {
|
|
11411
|
+
windowDays: config?.windowDays ?? DEFAULT_WINDOW_DAYS,
|
|
11412
|
+
topN: config?.topN ?? DEFAULT_TOP_N,
|
|
11413
|
+
minTotalEntries: config?.minTotalEntries ?? DEFAULT_MIN_TOTAL_ENTRIES,
|
|
11414
|
+
minConsumedWindows: config?.minConsumedWindows ?? DEFAULT_MIN_CONSUMED_WINDOWS,
|
|
11415
|
+
minConsumedEvents: config?.minConsumedEvents ?? DEFAULT_MIN_CONSUMED_EVENTS
|
|
11416
|
+
};
|
|
11417
|
+
}
|
|
11418
|
+
function aggregateConsumption(rows, allQualifiedIds, now, config) {
|
|
11419
|
+
const cfg = resolveConfig(config);
|
|
11420
|
+
const cutoffMs = now - cfg.windowDays * MS_PER_DAY5;
|
|
11421
|
+
const consumed = /* @__PURE__ */ new Map();
|
|
11422
|
+
let consumedWindows = 0;
|
|
11423
|
+
let totalConsumedEvents = 0;
|
|
11424
|
+
for (const row of rows) {
|
|
11425
|
+
const rowTs = Date.parse(row.timestamp);
|
|
11426
|
+
if (!Number.isFinite(rowTs) || rowTs < cutoffMs) continue;
|
|
11427
|
+
if (row.counters === null || typeof row.counters !== "object") continue;
|
|
11428
|
+
let windowHadConsumption = false;
|
|
11429
|
+
for (const [counterName, rawCount] of Object.entries(row.counters)) {
|
|
11430
|
+
if (!counterName.startsWith(CONSUMED_PREFIX)) continue;
|
|
11431
|
+
const count = typeof rawCount === "number" ? rawCount : 0;
|
|
11432
|
+
if (count <= 0) continue;
|
|
11433
|
+
const id = counterName.slice(CONSUMED_PREFIX.length);
|
|
11434
|
+
if (id.length === 0) continue;
|
|
11435
|
+
consumed.set(id, (consumed.get(id) ?? 0) + count);
|
|
11436
|
+
totalConsumedEvents += count;
|
|
11437
|
+
windowHadConsumption = true;
|
|
11438
|
+
}
|
|
11439
|
+
if (windowHadConsumption) consumedWindows += 1;
|
|
11440
|
+
}
|
|
11441
|
+
const topConsumed = [...consumed.entries()].map(([stableId, count]) => ({ stableId, count })).sort((a, b) => b.count - a.count || a.stableId.localeCompare(b.stableId)).slice(0, cfg.topN);
|
|
11442
|
+
const dataMature = consumedWindows >= cfg.minConsumedWindows && totalConsumedEvents >= cfg.minConsumedEvents && allQualifiedIds.length >= cfg.minTotalEntries;
|
|
11443
|
+
const zeroConsumed = dataMature ? allQualifiedIds.filter((id) => !consumed.has(id)).sort((a, b) => a.localeCompare(b)) : [];
|
|
11444
|
+
return {
|
|
11445
|
+
topConsumed,
|
|
11446
|
+
zeroConsumed,
|
|
11447
|
+
totalEntries: allQualifiedIds.length,
|
|
11448
|
+
consumedEntries: consumed.size,
|
|
11449
|
+
consumedWindows,
|
|
11450
|
+
totalConsumedEvents,
|
|
11451
|
+
dataMature,
|
|
11452
|
+
windowDays: cfg.windowDays
|
|
11453
|
+
};
|
|
11454
|
+
}
|
|
11455
|
+
async function inspectConsumption(projectRoot, config, now = Date.now()) {
|
|
11456
|
+
const rows = await readMetrics(projectRoot);
|
|
11457
|
+
const entries = await collectStoreCanonicalEntries(projectRoot);
|
|
11458
|
+
const allQualifiedIds = entries.map((entry) => entry.qualifiedId);
|
|
11459
|
+
return aggregateConsumption(rows, allQualifiedIds, now, config);
|
|
11460
|
+
}
|
|
11461
|
+
|
|
10180
11462
|
// src/services/rotation-tick.ts
|
|
10181
11463
|
var DEFAULT_TICK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
10182
11464
|
var tickTimers = /* @__PURE__ */ new Map();
|
|
@@ -10212,7 +11494,7 @@ import { IOFabricError as IOFabricError2, RuleError } from "@fenglimg/fabric-sha
|
|
|
10212
11494
|
|
|
10213
11495
|
// src/services/read-ledger.ts
|
|
10214
11496
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
10215
|
-
import { access as access5, copyFile, readFile as
|
|
11497
|
+
import { access as access5, copyFile, readFile as readFile21, rm } from "fs/promises";
|
|
10216
11498
|
import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
|
|
10217
11499
|
async function resolveLedgerPaths(projectRoot) {
|
|
10218
11500
|
const primaryPath = getLedgerPath(projectRoot);
|
|
@@ -10240,7 +11522,7 @@ async function readLegacyLedger(projectRoot) {
|
|
|
10240
11522
|
const { readPath } = await resolveLedgerPaths(projectRoot);
|
|
10241
11523
|
let raw;
|
|
10242
11524
|
try {
|
|
10243
|
-
raw = await
|
|
11525
|
+
raw = await readFile21(readPath, "utf8");
|
|
10244
11526
|
} catch (error) {
|
|
10245
11527
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
10246
11528
|
return [];
|
|
@@ -10495,8 +11777,8 @@ function formatError(error) {
|
|
|
10495
11777
|
}
|
|
10496
11778
|
function formatPreexistingRootMessage(projectRoot) {
|
|
10497
11779
|
const preexisting = [];
|
|
10498
|
-
if (
|
|
10499
|
-
if (
|
|
11780
|
+
if (existsSync10(join27(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
|
|
11781
|
+
if (existsSync10(join27(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
|
|
10500
11782
|
if (preexisting.length === 0) return null;
|
|
10501
11783
|
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
11784
|
}
|
|
@@ -10523,7 +11805,7 @@ function createFabricServer(tracker) {
|
|
|
10523
11805
|
const server = new McpServer(
|
|
10524
11806
|
{
|
|
10525
11807
|
name: "fabric-knowledge-server",
|
|
10526
|
-
version: "2.3.0-rc.
|
|
11808
|
+
version: "2.3.0-rc.10"
|
|
10527
11809
|
},
|
|
10528
11810
|
{
|
|
10529
11811
|
instructions: FABRIC_SERVER_INSTRUCTIONS
|
|
@@ -10543,10 +11825,10 @@ function createFabricServer(tracker) {
|
|
|
10543
11825
|
},
|
|
10544
11826
|
async (_uri) => {
|
|
10545
11827
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
10546
|
-
const path =
|
|
11828
|
+
const path = join27(projectRoot, ".fabric", "bootstrap", "README.md");
|
|
10547
11829
|
let text = "";
|
|
10548
|
-
if (
|
|
10549
|
-
text = await
|
|
11830
|
+
if (existsSync10(path)) {
|
|
11831
|
+
text = await readFile22(path, "utf8");
|
|
10550
11832
|
}
|
|
10551
11833
|
return {
|
|
10552
11834
|
contents: [
|
|
@@ -10644,19 +11926,27 @@ export {
|
|
|
10644
11926
|
LEGACY_LEDGER_PATH,
|
|
10645
11927
|
METRICS_LEDGER_PATH,
|
|
10646
11928
|
METRIC_COUNTER_NAMES,
|
|
11929
|
+
OPTIONAL_EMBED_PACKAGE,
|
|
10647
11930
|
RETIRED_TOKENS,
|
|
11931
|
+
aggregateConsumption,
|
|
10648
11932
|
appendEventLedgerEvent,
|
|
10649
11933
|
buildAlwaysActiveBodies,
|
|
10650
11934
|
buildColdEvalBatch,
|
|
10651
11935
|
buildKnowledgeCensus,
|
|
11936
|
+
buildRelatedGraph,
|
|
10652
11937
|
bumpCounter,
|
|
11938
|
+
checkBacklogAge,
|
|
11939
|
+
clearPrecheckCache,
|
|
11940
|
+
collectStoreCanonicalEntries,
|
|
10653
11941
|
contextCache,
|
|
10654
11942
|
createFabricServer,
|
|
10655
11943
|
createInFlightTracker,
|
|
10656
11944
|
createShutdownHandler,
|
|
11945
|
+
defaultEmbedCacheDir,
|
|
10657
11946
|
detectUnboundProject,
|
|
10658
11947
|
drainCounters,
|
|
10659
11948
|
enrichDescriptions,
|
|
11949
|
+
evaluateStoreDir,
|
|
10660
11950
|
explainWhyNotSurfaced,
|
|
10661
11951
|
extractKnowledge,
|
|
10662
11952
|
findConflictCandidates,
|
|
@@ -10667,17 +11957,25 @@ export {
|
|
|
10667
11957
|
getLedgerPath,
|
|
10668
11958
|
getLegacyLedgerPath,
|
|
10669
11959
|
getMetricsLedgerPath,
|
|
11960
|
+
inspectConsumption,
|
|
11961
|
+
inspectRelatedGraph,
|
|
10670
11962
|
inspectRetiredReferences,
|
|
11963
|
+
isEmbedderResolvable,
|
|
10671
11964
|
lintConflicts,
|
|
10672
11965
|
loadConflictEntries,
|
|
11966
|
+
loadEmbedder,
|
|
10673
11967
|
pairSimilarity,
|
|
10674
11968
|
planContext,
|
|
11969
|
+
precheckStoreReachability,
|
|
11970
|
+
readEmbedConfig,
|
|
10675
11971
|
readEventLedger,
|
|
11972
|
+
readFusion,
|
|
10676
11973
|
readLedger,
|
|
10677
11974
|
readMetrics,
|
|
10678
11975
|
readSelectionToken,
|
|
10679
11976
|
recall,
|
|
10680
11977
|
rehydrateAgentsMetaAt,
|
|
11978
|
+
renderBacklogAgeLine,
|
|
10681
11979
|
resolveLedgerPaths,
|
|
10682
11980
|
reviewKnowledge,
|
|
10683
11981
|
reviewPending,
|