@fenglimg/fabric-server 2.3.0-rc.3 → 2.3.0-rc.5
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 +175 -2
- package/dist/index.js +903 -191
- package/package.json +2 -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";
|
|
@@ -893,7 +893,7 @@ async function awaitFirstReconcileGate(timeoutMs = 5e3) {
|
|
|
893
893
|
|
|
894
894
|
// src/services/extract-knowledge.ts
|
|
895
895
|
import { existsSync as existsSync3 } from "fs";
|
|
896
|
-
import { readFile as
|
|
896
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
897
897
|
import { join as join7 } from "path";
|
|
898
898
|
import {
|
|
899
899
|
PROPOSED_REASON_DESCRIPTIONS_BY_LOCALE
|
|
@@ -901,10 +901,14 @@ import {
|
|
|
901
901
|
import { hasSecrets, isPersonalScope, redactPii, resolveGlobalLocale } from "@fenglimg/fabric-shared";
|
|
902
902
|
|
|
903
903
|
// src/services/cross-store-write.ts
|
|
904
|
+
import { createHash as createHash2 } from "crypto";
|
|
905
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
904
906
|
import { join as join5 } from "path";
|
|
905
907
|
import {
|
|
908
|
+
STORE_KNOWLEDGE_TYPE_DIRS,
|
|
906
909
|
STORE_LAYOUT,
|
|
907
910
|
STORE_PENDING_DIR,
|
|
911
|
+
STORE_PROJECT_ID_PATTERN,
|
|
908
912
|
buildStoreResolveInput,
|
|
909
913
|
createStoreResolver,
|
|
910
914
|
isPersonalLeakIntoSharedStore,
|
|
@@ -916,6 +920,7 @@ import {
|
|
|
916
920
|
PersonalScopeLeakError,
|
|
917
921
|
StoreWriteTargetUnresolvedError
|
|
918
922
|
} from "@fenglimg/fabric-shared/errors";
|
|
923
|
+
import { withFileLock as withFileLock2 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
919
924
|
function writeTargetUnresolved(scope, layer) {
|
|
920
925
|
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}\``;
|
|
921
926
|
return new StoreWriteTargetUnresolvedError(
|
|
@@ -952,8 +957,25 @@ function resolveWriteTargetStoreDir(layer, projectRoot, semanticScope) {
|
|
|
952
957
|
function resolveStorePendingBase(layer, projectRoot, semanticScope) {
|
|
953
958
|
return join5(resolveWriteTargetStoreDir(layer, projectRoot, semanticScope), STORE_LAYOUT.knowledgeDir, STORE_PENDING_DIR);
|
|
954
959
|
}
|
|
955
|
-
|
|
956
|
-
|
|
960
|
+
var STORE_PROJECTS_DIR = "projects";
|
|
961
|
+
function isValidWriteProjectSegment(project) {
|
|
962
|
+
if (project.length === 0) {
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
if (project === STORE_PROJECTS_DIR) {
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
if (STORE_KNOWLEDGE_TYPE_DIRS.includes(project)) {
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
return STORE_PROJECT_ID_PATTERN.test(project);
|
|
972
|
+
}
|
|
973
|
+
function resolveStoreCanonicalBase(layer, projectRoot, project) {
|
|
974
|
+
const base = join5(resolveWriteTargetStoreDir(layer, projectRoot), STORE_LAYOUT.knowledgeDir);
|
|
975
|
+
if (layer === "team" && project !== void 0 && isValidWriteProjectSegment(project)) {
|
|
976
|
+
return join5(base, STORE_PROJECTS_DIR, project);
|
|
977
|
+
}
|
|
978
|
+
return base;
|
|
957
979
|
}
|
|
958
980
|
function resolveWriteScopeMeta(layer, projectRoot, semanticScope) {
|
|
959
981
|
const input = buildStoreResolveInput(projectRoot);
|
|
@@ -978,7 +1000,7 @@ function resolveWriteScopeMeta(layer, projectRoot, semanticScope) {
|
|
|
978
1000
|
}
|
|
979
1001
|
|
|
980
1002
|
// src/services/cross-store-recall.ts
|
|
981
|
-
import { readFile as
|
|
1003
|
+
import { readFile as readFile4, stat } from "fs/promises";
|
|
982
1004
|
import { join as join6 } from "path";
|
|
983
1005
|
import {
|
|
984
1006
|
buildStoreResolveInput as buildStoreResolveInput2,
|
|
@@ -995,7 +1017,6 @@ import {
|
|
|
995
1017
|
deriveAgentsMetaStableId,
|
|
996
1018
|
isKnowledgeStableId,
|
|
997
1019
|
KnowledgeTypeSchema,
|
|
998
|
-
LayerSchema,
|
|
999
1020
|
MaturitySchema,
|
|
1000
1021
|
parseKnowledgeId,
|
|
1001
1022
|
StableIdSchema
|
|
@@ -1078,8 +1099,7 @@ function extractRuleDescription(source) {
|
|
|
1078
1099
|
id: knowledge?.id,
|
|
1079
1100
|
knowledge_type: knowledge?.knowledge_type,
|
|
1080
1101
|
maturity: knowledge?.maturity,
|
|
1081
|
-
|
|
1082
|
-
layer_reason: knowledge?.layer_reason,
|
|
1102
|
+
// W4/Track1 (D1): no `knowledge_layer` — layer derives from the id prefix.
|
|
1083
1103
|
created_at: knowledge?.created_at,
|
|
1084
1104
|
tags: knowledge?.tags,
|
|
1085
1105
|
// v2.0-rc.5 (C1): default-safe values when there is no frontmatter at all;
|
|
@@ -1104,12 +1124,10 @@ function extractDescriptionFromFrontmatter(frontmatter) {
|
|
|
1104
1124
|
tech_stack: extractInlineArray(frontmatter, "tech_stack"),
|
|
1105
1125
|
impact: extractInlineArray(frontmatter, "impact"),
|
|
1106
1126
|
must_read_if: extractScalar(frontmatter, "must_read_if") ?? summary,
|
|
1107
|
-
entities: extractInlineArray(frontmatter, "entities"),
|
|
1108
1127
|
id: knowledge.id,
|
|
1109
1128
|
knowledge_type: knowledge.knowledge_type,
|
|
1110
1129
|
maturity: knowledge.maturity,
|
|
1111
|
-
|
|
1112
|
-
layer_reason: knowledge.layer_reason,
|
|
1130
|
+
// W4/Track1 (D1): no `knowledge_layer` — layer derives from the id prefix.
|
|
1113
1131
|
created_at: knowledge.created_at,
|
|
1114
1132
|
tags: knowledge.tags,
|
|
1115
1133
|
relevance_scope: knowledge.relevance_scope,
|
|
@@ -1140,8 +1158,6 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
|
|
|
1140
1158
|
const rawId = extractScalar(frontmatter, "id");
|
|
1141
1159
|
const rawType = extractScalar(frontmatter, "type");
|
|
1142
1160
|
const rawMaturity = extractScalar(frontmatter, "maturity");
|
|
1143
|
-
const rawLayer = extractScalar(frontmatter, "layer");
|
|
1144
|
-
const rawLayerReason = extractScalar(frontmatter, "layer_reason");
|
|
1145
1161
|
const rawCreatedAt = extractScalar(frontmatter, "created_at");
|
|
1146
1162
|
let id;
|
|
1147
1163
|
if (rawId !== void 0) {
|
|
@@ -1178,16 +1194,6 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
|
|
|
1178
1194
|
maturity = parsed.data;
|
|
1179
1195
|
} else {
|
|
1180
1196
|
process.stderr.write(`[fabric] frontmatter: unknown maturity ${JSON.stringify(rawMaturity)}; skipping
|
|
1181
|
-
`);
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
let knowledge_layer;
|
|
1185
|
-
if (rawLayer !== void 0) {
|
|
1186
|
-
const parsed = LayerSchema.safeParse(rawLayer);
|
|
1187
|
-
if (parsed.success) {
|
|
1188
|
-
knowledge_layer = parsed.data;
|
|
1189
|
-
} else {
|
|
1190
|
-
process.stderr.write(`[fabric] frontmatter: unknown layer ${JSON.stringify(rawLayer)}; skipping
|
|
1191
1197
|
`);
|
|
1192
1198
|
}
|
|
1193
1199
|
}
|
|
@@ -1200,23 +1206,12 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
|
|
|
1200
1206
|
`);
|
|
1201
1207
|
}
|
|
1202
1208
|
}
|
|
1203
|
-
if (id !== void 0 && knowledge_layer !== void 0) {
|
|
1204
|
-
const decoded = parseKnowledgeId(id);
|
|
1205
|
-
if (decoded !== null && decoded.layer !== knowledge_layer) {
|
|
1206
|
-
process.stderr.write(
|
|
1207
|
-
`[fabric] frontmatter: id ${id} encodes layer ${decoded.layer} but layer field says ${knowledge_layer}; dropping both
|
|
1208
|
-
`
|
|
1209
|
-
);
|
|
1210
|
-
id = void 0;
|
|
1211
|
-
knowledge_layer = void 0;
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
1209
|
const tags = extractInlineArray(frontmatter, "tags");
|
|
1215
1210
|
const rawRelevanceScope = extractScalar(frontmatter, "relevance_scope");
|
|
1216
1211
|
const relevance_scope = rawRelevanceScope === "narrow" || rawRelevanceScope === "broad" ? rawRelevanceScope : "broad";
|
|
1217
1212
|
const relevance_paths = extractInlineArray(frontmatter, "relevance_paths");
|
|
1218
1213
|
const rawRelated = extractInlineArray(frontmatter, "related");
|
|
1219
|
-
const sourceLayer =
|
|
1214
|
+
const sourceLayer = id !== void 0 ? parseKnowledgeId(id)?.layer ?? "team" : "team";
|
|
1220
1215
|
const related = rawRelated.filter((targetId) => {
|
|
1221
1216
|
if (isForbiddenCrossLayerEdge(sourceLayer, targetId)) {
|
|
1222
1217
|
process.stderr.write(
|
|
@@ -1231,8 +1226,6 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
|
|
|
1231
1226
|
id,
|
|
1232
1227
|
knowledge_type,
|
|
1233
1228
|
maturity,
|
|
1234
|
-
knowledge_layer,
|
|
1235
|
-
layer_reason: rawLayerReason,
|
|
1236
1229
|
created_at,
|
|
1237
1230
|
tags: tags.length > 0 ? tags : void 0,
|
|
1238
1231
|
relevance_scope,
|
|
@@ -1283,7 +1276,13 @@ async function readSetFingerprint(refs) {
|
|
|
1283
1276
|
return parts.sort().join("\n");
|
|
1284
1277
|
}
|
|
1285
1278
|
var SEMANTIC_SCOPE_LINE = /^semantic_scope:\s*"?([^"\n]+?)"?\s*$/mu;
|
|
1286
|
-
function readSemanticScope(source, layer) {
|
|
1279
|
+
function readSemanticScope(source, layer, project) {
|
|
1280
|
+
if (layer === "personal") {
|
|
1281
|
+
return "personal";
|
|
1282
|
+
}
|
|
1283
|
+
if (typeof project === "string" && project.length > 0) {
|
|
1284
|
+
return `project:${project}`;
|
|
1285
|
+
}
|
|
1287
1286
|
const match = SEMANTIC_SCOPE_LINE.exec(source);
|
|
1288
1287
|
return match?.[1] ?? layer;
|
|
1289
1288
|
}
|
|
@@ -1350,7 +1349,7 @@ async function walkReadSetStoresUncached(snapshot) {
|
|
|
1350
1349
|
const entries = await Promise.all(snapshot.refs.map(async (ref) => {
|
|
1351
1350
|
let source;
|
|
1352
1351
|
try {
|
|
1353
|
-
source = await
|
|
1352
|
+
source = await readFile4(ref.file, "utf8");
|
|
1354
1353
|
} catch {
|
|
1355
1354
|
return null;
|
|
1356
1355
|
}
|
|
@@ -1362,7 +1361,7 @@ async function walkReadSetStoresUncached(snapshot) {
|
|
|
1362
1361
|
type: ref.type,
|
|
1363
1362
|
alias: ref.alias,
|
|
1364
1363
|
layer,
|
|
1365
|
-
semanticScope: readSemanticScope(source, layer),
|
|
1364
|
+
semanticScope: readSemanticScope(source, layer, ref.project),
|
|
1366
1365
|
source
|
|
1367
1366
|
};
|
|
1368
1367
|
}));
|
|
@@ -1380,7 +1379,9 @@ async function buildCrossStoreRawItems(projectRoot) {
|
|
|
1380
1379
|
stable_id: entry.qualifiedId,
|
|
1381
1380
|
description: {
|
|
1382
1381
|
...baseDescription,
|
|
1383
|
-
|
|
1382
|
+
// W4/Track1 (D1): no `knowledge_layer` backfill — a candidate's layer is
|
|
1383
|
+
// derived from its stable_id prefix (layerFromStableId in plan-context),
|
|
1384
|
+
// the single source of truth (KT-DEC-0004).
|
|
1384
1385
|
semantic_scope: entry.semanticScope
|
|
1385
1386
|
}
|
|
1386
1387
|
});
|
|
@@ -1622,6 +1623,144 @@ function serializeBm25Model(model) {
|
|
|
1622
1623
|
function rehydrateBm25Model(serialized) {
|
|
1623
1624
|
return modelFromStats(serialized);
|
|
1624
1625
|
}
|
|
1626
|
+
var SYNONYM_PAIRS = {
|
|
1627
|
+
// Code / engineering actions
|
|
1628
|
+
refactor: ["restructure", "rewrite", "redesign", "reorganize", "clean", "rework"],
|
|
1629
|
+
optimize: ["improve", "speed-up", "tune", "accelerate", "perf", "performance"],
|
|
1630
|
+
debug: ["fix", "troubleshoot", "diagnose", "investigate", "resolve", "correct"],
|
|
1631
|
+
migrate: ["port", "move", "transfer", "upgrade", "transition", "convert"],
|
|
1632
|
+
"set up": ["init", "initialize", "configure", "bootstrap", "install", "onboard"],
|
|
1633
|
+
implement: ["add", "build", "create", "develop", "write", "introduce", "introduce"],
|
|
1634
|
+
// Architecture / design
|
|
1635
|
+
architecture: ["design", "structure", "layout", "organization", "pattern", "system"],
|
|
1636
|
+
"data model": ["schema", "entity", "type", "structure", "contract", "shape"],
|
|
1637
|
+
// Quality / correctness
|
|
1638
|
+
test: ["verify", "validate", "assert", "check", "spec", "coverage"],
|
|
1639
|
+
lint: ["check", "validate", "audit", "inspect", "analyze"],
|
|
1640
|
+
// Communication / impact
|
|
1641
|
+
documentation: ["docs", "readme", "guide", "spec", "explanation", "reference"],
|
|
1642
|
+
decision: ["adr", "rationale", "why", "motivation", "reason", "trade-off"],
|
|
1643
|
+
// Change management
|
|
1644
|
+
release: ["deploy", "ship", "publish", "cut", "version", "tag"],
|
|
1645
|
+
rollback: ["revert", "undo", "back-out", "backout", "restore"],
|
|
1646
|
+
// Containers / infra
|
|
1647
|
+
container: ["docker", "image", "oci", "cri-o"],
|
|
1648
|
+
deploy: ["release", "rollout", "ship", "publish", "promote"],
|
|
1649
|
+
// Project / process
|
|
1650
|
+
on_boarding: ["getting-started", "quickstart", "newcomer", "new-hire", "first-time"],
|
|
1651
|
+
best_practice: ["convention", "guideline", "standard", "rule", "recommendation"],
|
|
1652
|
+
// Tech stacks
|
|
1653
|
+
api: ["endpoint", "route", "service", "interface", "rpc", "rest"],
|
|
1654
|
+
typescript: ["ts", "types", "type-safe"],
|
|
1655
|
+
react: ["jsx", "tsx", "component", "ui"],
|
|
1656
|
+
node: ["nodejs", "runtime", "backend", "server"],
|
|
1657
|
+
database: ["db", "sql", "nosql", "storage", "persistence"]
|
|
1658
|
+
};
|
|
1659
|
+
var STEMMING_PATTERNS = [
|
|
1660
|
+
{ suffix: "e", alternatives: ["es", "ed", "ing", "ation"] },
|
|
1661
|
+
{ suffix: "y", alternatives: ["ies", "ied", "ying"] }
|
|
1662
|
+
// Catch-all for non-verb / already-stemmed query terms: no-op by default.
|
|
1663
|
+
];
|
|
1664
|
+
var DEFAULT_IDF_WEIGHT = 1;
|
|
1665
|
+
var COMMON_TERMS = /* @__PURE__ */ new Set([
|
|
1666
|
+
"a",
|
|
1667
|
+
"an",
|
|
1668
|
+
"the",
|
|
1669
|
+
"and",
|
|
1670
|
+
"or",
|
|
1671
|
+
"but",
|
|
1672
|
+
"in",
|
|
1673
|
+
"on",
|
|
1674
|
+
"at",
|
|
1675
|
+
"to",
|
|
1676
|
+
"for",
|
|
1677
|
+
"of",
|
|
1678
|
+
"with",
|
|
1679
|
+
"by",
|
|
1680
|
+
"from",
|
|
1681
|
+
"as",
|
|
1682
|
+
"is",
|
|
1683
|
+
"it",
|
|
1684
|
+
"at",
|
|
1685
|
+
"be",
|
|
1686
|
+
"do",
|
|
1687
|
+
"has",
|
|
1688
|
+
"have",
|
|
1689
|
+
"was",
|
|
1690
|
+
"are",
|
|
1691
|
+
"been",
|
|
1692
|
+
"this",
|
|
1693
|
+
"that",
|
|
1694
|
+
"these",
|
|
1695
|
+
"those",
|
|
1696
|
+
"will",
|
|
1697
|
+
"can",
|
|
1698
|
+
"may",
|
|
1699
|
+
"would",
|
|
1700
|
+
"could",
|
|
1701
|
+
"should",
|
|
1702
|
+
"does",
|
|
1703
|
+
"not",
|
|
1704
|
+
"no",
|
|
1705
|
+
"if",
|
|
1706
|
+
"so",
|
|
1707
|
+
"up",
|
|
1708
|
+
"out",
|
|
1709
|
+
"all",
|
|
1710
|
+
"each",
|
|
1711
|
+
"every",
|
|
1712
|
+
"both",
|
|
1713
|
+
"some",
|
|
1714
|
+
"any",
|
|
1715
|
+
"such",
|
|
1716
|
+
"only",
|
|
1717
|
+
"own",
|
|
1718
|
+
"same",
|
|
1719
|
+
"too",
|
|
1720
|
+
"very",
|
|
1721
|
+
"just",
|
|
1722
|
+
"about",
|
|
1723
|
+
"over",
|
|
1724
|
+
"than",
|
|
1725
|
+
"then",
|
|
1726
|
+
"also"
|
|
1727
|
+
]);
|
|
1728
|
+
function idfWeight(term) {
|
|
1729
|
+
return COMMON_TERMS.has(term) ? 0.3 : DEFAULT_IDF_WEIGHT;
|
|
1730
|
+
}
|
|
1731
|
+
function expandQueryTerms(text) {
|
|
1732
|
+
const baseTerms = tokenize(text);
|
|
1733
|
+
const weighted = /* @__PURE__ */ new Map();
|
|
1734
|
+
for (const term of baseTerms) {
|
|
1735
|
+
const lower = term.toLowerCase();
|
|
1736
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
1737
|
+
const syns = SYNONYM_PAIRS[lower];
|
|
1738
|
+
if (syns !== void 0) {
|
|
1739
|
+
for (const s of syns) aliases.add(s);
|
|
1740
|
+
}
|
|
1741
|
+
for (const [key, values] of Object.entries(SYNONYM_PAIRS)) {
|
|
1742
|
+
if (values.includes(lower)) {
|
|
1743
|
+
aliases.add(key);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
for (const { suffix, alternatives } of STEMMING_PATTERNS) {
|
|
1747
|
+
if (lower.endsWith(suffix) && lower.length > suffix.length) {
|
|
1748
|
+
const stem = lower.slice(0, -suffix.length);
|
|
1749
|
+
for (const alt of alternatives) {
|
|
1750
|
+
aliases.add(stem + alt);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
const originalWeight = idfWeight(lower);
|
|
1755
|
+
weighted.set(term, originalWeight);
|
|
1756
|
+
for (const alias of aliases) {
|
|
1757
|
+
if (!weighted.has(alias)) {
|
|
1758
|
+
weighted.set(alias, originalWeight * 0.5);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
return weighted;
|
|
1763
|
+
}
|
|
1625
1764
|
function buildQueryTerms(text) {
|
|
1626
1765
|
return tokenize(text);
|
|
1627
1766
|
}
|
|
@@ -2021,7 +2160,7 @@ async function extractKnowledge(projectRoot, input) {
|
|
|
2021
2160
|
const writeScopeMeta = resolveWriteScopeMeta(layer, projectRoot, semanticScope);
|
|
2022
2161
|
await ensureParentDirectory(absolutePath);
|
|
2023
2162
|
if (existsSync3(absolutePath)) {
|
|
2024
|
-
const existing = await
|
|
2163
|
+
const existing = await readFile5(absolutePath, "utf8");
|
|
2025
2164
|
const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
|
|
2026
2165
|
if (existingKey === effectiveIdempotencyKey) {
|
|
2027
2166
|
const fresh2 = renderFreshEntry({
|
|
@@ -2154,7 +2293,7 @@ async function resolveDisambiguatedSlugPath(args) {
|
|
|
2154
2293
|
idempotencyKey: candidateKey
|
|
2155
2294
|
};
|
|
2156
2295
|
}
|
|
2157
|
-
const existing = await
|
|
2296
|
+
const existing = await readFile5(candidatePath, "utf8");
|
|
2158
2297
|
const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
|
|
2159
2298
|
if (existingKey === candidateKey) {
|
|
2160
2299
|
return {
|
|
@@ -2752,15 +2891,25 @@ var LEDGER_DUAL_WRITE_METRIC_NAMES = {
|
|
|
2752
2891
|
};
|
|
2753
2892
|
|
|
2754
2893
|
// src/services/plan-context.ts
|
|
2755
|
-
import { mkdir as
|
|
2894
|
+
import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
|
|
2756
2895
|
import { join as join10 } from "path";
|
|
2757
2896
|
|
|
2758
2897
|
// src/services/vector-retrieval.ts
|
|
2759
2898
|
import { mkdirSync } from "fs";
|
|
2899
|
+
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
|
|
2900
|
+
import { createRequire } from "module";
|
|
2760
2901
|
import { join as join9 } from "path";
|
|
2761
2902
|
import { resolveGlobalRoot as resolveGlobalRoot3 } from "@fenglimg/fabric-shared";
|
|
2762
2903
|
var embedderLoad;
|
|
2763
2904
|
var OPTIONAL_EMBED_PACKAGE = "fastembed";
|
|
2905
|
+
function isEmbedderResolvable() {
|
|
2906
|
+
try {
|
|
2907
|
+
createRequire(import.meta.url).resolve(OPTIONAL_EMBED_PACKAGE);
|
|
2908
|
+
return true;
|
|
2909
|
+
} catch {
|
|
2910
|
+
return false;
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2764
2913
|
var embedderModuleLoader = (name) => import(name);
|
|
2765
2914
|
var MISSING_EMBEDDER_HINT = "[fabric] vector semantic recall is enabled but the optional 'fastembed' package is unavailable \u2014 falling back to text-only ranking. Install it where the server resolves modules (e.g. `npm i -g fastembed`) to enable embeddings, or set embed_enabled:false to silence this.\n";
|
|
2766
2915
|
var defaultMissingEmbedderHint = () => {
|
|
@@ -2858,11 +3007,63 @@ function cacheDocVector(text, vector) {
|
|
|
2858
3007
|
docVectorCache.delete(lru);
|
|
2859
3008
|
}
|
|
2860
3009
|
}
|
|
2861
|
-
|
|
3010
|
+
var VECTOR_CACHE_VERSION = 1;
|
|
3011
|
+
var VECTOR_CACHE_DIR = ".fabric/cache/vectors";
|
|
3012
|
+
function vectorCachePath(projectRoot, revision) {
|
|
3013
|
+
const safe = revision.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
3014
|
+
return join9(projectRoot, VECTOR_CACHE_DIR, `${safe}.json`);
|
|
3015
|
+
}
|
|
3016
|
+
async function loadVectorCacheFromDisk(ctx) {
|
|
3017
|
+
try {
|
|
3018
|
+
const raw = await readFile6(vectorCachePath(ctx.projectRoot, ctx.corpusRevision), "utf8");
|
|
3019
|
+
const parsed = JSON.parse(raw);
|
|
3020
|
+
if (parsed.version !== VECTOR_CACHE_VERSION) return null;
|
|
3021
|
+
if (parsed.embedding_model !== ctx.embeddingModel) return null;
|
|
3022
|
+
if (parsed.corpus_revision !== ctx.corpusRevision) return null;
|
|
3023
|
+
if (!Array.isArray(parsed.vectors) || parsed.vectors.length === 0) return null;
|
|
3024
|
+
const dimension = parsed.dimension;
|
|
3025
|
+
if (!Number.isInteger(dimension) || dimension <= 0) return null;
|
|
3026
|
+
const out = /* @__PURE__ */ new Map();
|
|
3027
|
+
for (const entry of parsed.vectors) {
|
|
3028
|
+
if (!Array.isArray(entry) || entry.length !== 2) return null;
|
|
3029
|
+
const [text, vector] = entry;
|
|
3030
|
+
if (typeof text !== "string" || !Array.isArray(vector)) return null;
|
|
3031
|
+
if (vector.length !== dimension) return null;
|
|
3032
|
+
out.set(text, vector);
|
|
3033
|
+
}
|
|
3034
|
+
return out;
|
|
3035
|
+
} catch {
|
|
3036
|
+
return null;
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
async function saveVectorCacheToDisk(ctx, vectors, dimension) {
|
|
3040
|
+
try {
|
|
3041
|
+
const payload = {
|
|
3042
|
+
version: VECTOR_CACHE_VERSION,
|
|
3043
|
+
embedding_model: ctx.embeddingModel,
|
|
3044
|
+
dimension,
|
|
3045
|
+
corpus_revision: ctx.corpusRevision,
|
|
3046
|
+
vectors
|
|
3047
|
+
};
|
|
3048
|
+
const path = vectorCachePath(ctx.projectRoot, ctx.corpusRevision);
|
|
3049
|
+
await mkdir4(join9(ctx.projectRoot, VECTOR_CACHE_DIR), { recursive: true });
|
|
3050
|
+
await writeFile2(path, JSON.stringify(payload), "utf8");
|
|
3051
|
+
} catch {
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
async function buildVectorScores(embedder, queryText, items, cache2) {
|
|
2862
3055
|
if (embedder === null || queryText.trim().length === 0 || items.length === 0) {
|
|
2863
3056
|
return null;
|
|
2864
3057
|
}
|
|
2865
3058
|
try {
|
|
3059
|
+
if (cache2 !== void 0) {
|
|
3060
|
+
const fromDisk = await loadVectorCacheFromDisk(cache2);
|
|
3061
|
+
if (fromDisk !== null) {
|
|
3062
|
+
for (const [text, vector] of fromDisk) {
|
|
3063
|
+
cacheDocVector(text, vector);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
2866
3067
|
const missTexts = [];
|
|
2867
3068
|
for (const item of items) {
|
|
2868
3069
|
if (!docVectorCache.has(item.text)) {
|
|
@@ -2878,6 +3079,24 @@ async function buildVectorScores(embedder, queryText, items) {
|
|
|
2878
3079
|
for (let m = 0; m < missTexts.length; m += 1) {
|
|
2879
3080
|
cacheDocVector(missTexts[m], embedded[m + 1]);
|
|
2880
3081
|
}
|
|
3082
|
+
const staleTexts = [];
|
|
3083
|
+
for (const item of items) {
|
|
3084
|
+
const cached = docVectorCache.get(item.text);
|
|
3085
|
+
if (cached !== void 0 && cached.length !== queryVec.length) {
|
|
3086
|
+
docVectorCache.delete(item.text);
|
|
3087
|
+
staleTexts.push(item.text);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
if (staleTexts.length > 0) {
|
|
3091
|
+
const reEmbedded = await embedder.embed(staleTexts);
|
|
3092
|
+
if (reEmbedded.length !== staleTexts.length) {
|
|
3093
|
+
return null;
|
|
3094
|
+
}
|
|
3095
|
+
for (let s = 0; s < staleTexts.length; s += 1) {
|
|
3096
|
+
cacheDocVector(staleTexts[s], reEmbedded[s]);
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
const embeddedNewDoc = missTexts.length > 0 || staleTexts.length > 0;
|
|
2881
3100
|
const scores = /* @__PURE__ */ new Map();
|
|
2882
3101
|
for (const item of items) {
|
|
2883
3102
|
const docVec = docVectorCache.get(item.text);
|
|
@@ -2886,6 +3105,20 @@ async function buildVectorScores(embedder, queryText, items) {
|
|
|
2886
3105
|
}
|
|
2887
3106
|
scores.set(item.stable_id, cosineSimilarity(queryVec, docVec));
|
|
2888
3107
|
}
|
|
3108
|
+
if (cache2 !== void 0 && embeddedNewDoc) {
|
|
3109
|
+
const snapshot = [];
|
|
3110
|
+
for (const item of items) {
|
|
3111
|
+
const vec = docVectorCache.get(item.text);
|
|
3112
|
+
if (vec !== void 0) {
|
|
3113
|
+
snapshot.push([item.text, vec]);
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
const dimension = snapshot.length > 0 ? snapshot[0][1].length : 0;
|
|
3117
|
+
const uniform = dimension > 0 && snapshot.every(([, v]) => v.length === dimension);
|
|
3118
|
+
if (uniform) {
|
|
3119
|
+
await saveVectorCacheToDisk(cache2, snapshot, dimension);
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
2889
3122
|
return scores;
|
|
2890
3123
|
} catch {
|
|
2891
3124
|
return null;
|
|
@@ -2893,6 +3126,11 @@ async function buildVectorScores(embedder, queryText, items) {
|
|
|
2893
3126
|
}
|
|
2894
3127
|
|
|
2895
3128
|
// src/services/plan-context.ts
|
|
3129
|
+
function layerFromStableId(qualifiedId) {
|
|
3130
|
+
const colon = qualifiedId.lastIndexOf(":");
|
|
3131
|
+
const localId = colon === -1 ? qualifiedId : qualifiedId.slice(colon + 1);
|
|
3132
|
+
return localId.startsWith("KP-") ? "personal" : "team";
|
|
3133
|
+
}
|
|
2896
3134
|
var SELECTION_TOKEN_TTL_DEFAULT_MS = 30 * 60 * 1e3;
|
|
2897
3135
|
var selectionTokenCache = /* @__PURE__ */ new Map();
|
|
2898
3136
|
var SELECTION_TOKEN_CACHE_MAX = 1e3;
|
|
@@ -2949,7 +3187,7 @@ async function planContext(projectRoot, input) {
|
|
|
2949
3187
|
const storeRawItems = await buildCrossStoreRawItems(projectRoot).catch(() => []);
|
|
2950
3188
|
const { rawItems: allRawItems, suppressedStableIds } = partitionEmptyShells(storeRawItems);
|
|
2951
3189
|
const effectiveLayerFilter = input.layer_filter ?? readDefaultLayerFilter(projectRoot);
|
|
2952
|
-
const rawItems = effectiveLayerFilter === "both" ? allRawItems : allRawItems.filter((item) => item.
|
|
3190
|
+
const rawItems = effectiveLayerFilter === "both" ? allRawItems : allRawItems.filter((item) => layerFromStableId(item.stable_id) === effectiveLayerFilter);
|
|
2953
3191
|
const scoringContext = await buildScoringContext(projectRoot, revision, rawItems, {
|
|
2954
3192
|
queryText,
|
|
2955
3193
|
targetPaths: input.target_paths ?? dedupePaths(input.paths)
|
|
@@ -3170,7 +3408,7 @@ function bm25CachePath(projectRoot, revision) {
|
|
|
3170
3408
|
}
|
|
3171
3409
|
async function loadBm25ModelFromDisk(projectRoot, revision) {
|
|
3172
3410
|
try {
|
|
3173
|
-
const raw = await
|
|
3411
|
+
const raw = await readFile7(bm25CachePath(projectRoot, revision), "utf8");
|
|
3174
3412
|
const parsed = JSON.parse(raw);
|
|
3175
3413
|
if (parsed.version !== 1) return null;
|
|
3176
3414
|
return rehydrateBm25Model(parsed);
|
|
@@ -3181,8 +3419,8 @@ async function loadBm25ModelFromDisk(projectRoot, revision) {
|
|
|
3181
3419
|
async function saveBm25ModelToDisk(projectRoot, revision, model) {
|
|
3182
3420
|
try {
|
|
3183
3421
|
const path = bm25CachePath(projectRoot, revision);
|
|
3184
|
-
await
|
|
3185
|
-
await
|
|
3422
|
+
await mkdir5(join10(projectRoot, BM25_CACHE_DIR), { recursive: true });
|
|
3423
|
+
await writeFile3(path, JSON.stringify(serializeBm25Model(model)), "utf8");
|
|
3186
3424
|
} catch {
|
|
3187
3425
|
}
|
|
3188
3426
|
}
|
|
@@ -3223,7 +3461,7 @@ function buildScopeRankMap(items, projectRoot) {
|
|
|
3223
3461
|
const colon = it.stable_id.indexOf(":");
|
|
3224
3462
|
const alias = colon === -1 ? "" : it.stable_id.slice(0, colon);
|
|
3225
3463
|
const localId = colon === -1 ? it.stable_id : it.stable_id.slice(colon + 1);
|
|
3226
|
-
const semanticScope = it.description.semantic_scope ?? it.
|
|
3464
|
+
const semanticScope = it.description.semantic_scope ?? layerFromStableId(it.stable_id);
|
|
3227
3465
|
return {
|
|
3228
3466
|
global_ref: it.stable_id,
|
|
3229
3467
|
store_uuid: aliasToUuid.get(alias) ?? alias,
|
|
@@ -3273,7 +3511,8 @@ async function buildScoringContext(projectRoot, revision, rawItems, opts) {
|
|
|
3273
3511
|
rawItems.map((item) => ({
|
|
3274
3512
|
stable_id: item.stable_id,
|
|
3275
3513
|
text: docTexts.get(item.stable_id) ?? documentTextForItem(item.description)
|
|
3276
|
-
}))
|
|
3514
|
+
})),
|
|
3515
|
+
{ projectRoot, corpusRevision: revision, embeddingModel: embedConfig.model }
|
|
3277
3516
|
);
|
|
3278
3517
|
if (vectorScores !== null) {
|
|
3279
3518
|
scoringContext.vectorScores = vectorScores;
|
|
@@ -3283,6 +3522,10 @@ async function buildScoringContext(projectRoot, revision, rawItems, opts) {
|
|
|
3283
3522
|
const configuredFusion = readFusion(projectRoot);
|
|
3284
3523
|
const vectorActive = scoringContext.vectorScores !== void 0 && scoringContext.vectorScores.size > 0;
|
|
3285
3524
|
scoringContext.fusion = configuredFusion === "auto" ? vectorActive ? "rrf" : "additive" : configuredFusion;
|
|
3525
|
+
let queryTermWeights;
|
|
3526
|
+
if (scoringContext.queryTerms.length > 0) {
|
|
3527
|
+
queryTermWeights = expandQueryTerms(opts.queryText);
|
|
3528
|
+
}
|
|
3286
3529
|
if (scoringContext.fusion === "rrf" && scoringContext.queryTerms.length > 0 && rawItems.length > 0) {
|
|
3287
3530
|
const rankIds = rawItems.map((item) => item.stable_id).sort((a, b) => compareStableIds(a, b));
|
|
3288
3531
|
if (scoringContext.bm25 !== void 0) {
|
|
@@ -3294,6 +3537,49 @@ async function buildScoringContext(projectRoot, revision, rawItems, opts) {
|
|
|
3294
3537
|
}
|
|
3295
3538
|
return scoringContext;
|
|
3296
3539
|
}
|
|
3540
|
+
var PROXIMITY_WINDOW = 6;
|
|
3541
|
+
var PROXIMITY_BOOST_CAP = 0.15;
|
|
3542
|
+
function proximityBoost(item, context, contentScore2) {
|
|
3543
|
+
if (contentScore2 <= 0) return 0;
|
|
3544
|
+
const text = context.docTexts?.get(item.stable_id);
|
|
3545
|
+
if (text === void 0 || text.length === 0) return 0;
|
|
3546
|
+
const tokens = text.toLowerCase().split(/[^a-z0-9_$#]+/u).filter(Boolean);
|
|
3547
|
+
const queryTerms = context.queryTerms;
|
|
3548
|
+
if (queryTerms.length < 2) return 0;
|
|
3549
|
+
const positions = /* @__PURE__ */ new Map();
|
|
3550
|
+
for (const qt of queryTerms) {
|
|
3551
|
+
const qtLower = qt.toLowerCase();
|
|
3552
|
+
const pos = [];
|
|
3553
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
3554
|
+
if (tokens[i] === qtLower) {
|
|
3555
|
+
pos.push(i);
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
if (pos.length > 0) {
|
|
3559
|
+
positions.set(qtLower, pos);
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
if (positions.size < 2) return 0;
|
|
3563
|
+
const termList = [...positions.entries()];
|
|
3564
|
+
let minDist = Infinity;
|
|
3565
|
+
for (let i = 0; i < termList.length; i++) {
|
|
3566
|
+
const [, posI] = termList[i];
|
|
3567
|
+
for (let j = i + 1; j < termList.length; j++) {
|
|
3568
|
+
const [, posJ] = termList[j];
|
|
3569
|
+
for (const pi of posI) {
|
|
3570
|
+
for (const pj of posJ) {
|
|
3571
|
+
const dist = Math.abs(pi - pj);
|
|
3572
|
+
if (dist < minDist) minDist = dist;
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
if (!Number.isFinite(minDist)) return 0;
|
|
3578
|
+
if (minDist >= PROXIMITY_WINDOW) return 0;
|
|
3579
|
+
const ratio = 1 - minDist / PROXIMITY_WINDOW;
|
|
3580
|
+
const boost = contentScore2 * PROXIMITY_BOOST_CAP * ratio;
|
|
3581
|
+
return boost;
|
|
3582
|
+
}
|
|
3297
3583
|
function sortDescriptionItems(rawItems, scoringContext) {
|
|
3298
3584
|
if (scoringContext === void 0) {
|
|
3299
3585
|
return [...rawItems].sort((left, right) => compareStableIds(left.stable_id, right.stable_id)).map((item) => ({ item, score: 0 }));
|
|
@@ -3334,16 +3620,13 @@ function documentTextForItem(description) {
|
|
|
3334
3620
|
...description.intent_clues,
|
|
3335
3621
|
...description.tech_stack,
|
|
3336
3622
|
...description.impact,
|
|
3337
|
-
...description.entities ?? [],
|
|
3338
3623
|
...description.tags ?? []
|
|
3339
3624
|
].join(" ");
|
|
3340
3625
|
}
|
|
3341
3626
|
function documentFieldsForItem(description) {
|
|
3342
3627
|
return {
|
|
3343
3628
|
title: tokenize2(description.summary),
|
|
3344
|
-
tags: tokenize2(
|
|
3345
|
-
[...description.tags ?? [], ...description.tech_stack, ...description.entities ?? []].join(" ")
|
|
3346
|
-
),
|
|
3629
|
+
tags: tokenize2([...description.tags ?? [], ...description.tech_stack].join(" ")),
|
|
3347
3630
|
summary: tokenize2([description.must_read_if, ...description.intent_clues].join(" ")),
|
|
3348
3631
|
body: tokenize2(description.impact.join(" "))
|
|
3349
3632
|
};
|
|
@@ -3436,7 +3719,8 @@ function structuralScaleFor(context) {
|
|
|
3436
3719
|
function scoreDescriptionItem(item, context) {
|
|
3437
3720
|
const content = contentScore(item, context);
|
|
3438
3721
|
const structural = salienceScore(item) + recencyBoost(item, context) + localityBoost(item, context);
|
|
3439
|
-
|
|
3722
|
+
const proximity = proximityBoost(item, context, content);
|
|
3723
|
+
return content + structuralScaleFor(context) * structural + proximity;
|
|
3440
3724
|
}
|
|
3441
3725
|
function scoreBreakdownForItem(item, context) {
|
|
3442
3726
|
const hasQuery = context.queryTerms.length > 0;
|
|
@@ -3918,7 +4202,7 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
|
|
|
3918
4202
|
// src/services/review.ts
|
|
3919
4203
|
import { execFileSync } from "child_process";
|
|
3920
4204
|
import { existsSync as existsSync5 } from "fs";
|
|
3921
|
-
import { readFile as
|
|
4205
|
+
import { readFile as readFile9, readdir as readdir2, stat as stat2, unlink as unlink2 } from "fs/promises";
|
|
3922
4206
|
import { homedir } from "os";
|
|
3923
4207
|
import { basename, isAbsolute, join as join12, relative, resolve as resolve2, sep as sep2 } from "path";
|
|
3924
4208
|
|
|
@@ -3956,11 +4240,11 @@ async function hasUnresolvedDismissal(projectRoot, id) {
|
|
|
3956
4240
|
}
|
|
3957
4241
|
|
|
3958
4242
|
// src/services/review.ts
|
|
3959
|
-
import { allocateStoreKnowledgeId, isPersonalScope as isPersonalScope2 } from "@fenglimg/fabric-shared";
|
|
4243
|
+
import { allocateStoreKnowledgeId, isPersonalScope as isPersonalScope2, loadProjectConfig as loadProjectConfig3 } from "@fenglimg/fabric-shared";
|
|
3960
4244
|
|
|
3961
4245
|
// src/services/pending-dedupe.ts
|
|
3962
4246
|
import { existsSync as existsSync4 } from "fs";
|
|
3963
|
-
import { readdir, readFile as
|
|
4247
|
+
import { readdir, readFile as readFile8, unlink } from "fs/promises";
|
|
3964
4248
|
import { join as join11 } from "path";
|
|
3965
4249
|
var PENDING_TYPES = ["decisions", "pitfalls", "guidelines", "models", "processes"];
|
|
3966
4250
|
var DISAMBIGUATION_SUFFIX = /^(.+)-([2-9])\.md$/u;
|
|
@@ -4058,7 +4342,7 @@ async function mergePendingTwins(projectRoot) {
|
|
|
4058
4342
|
const abs = join11(dir, name);
|
|
4059
4343
|
let content;
|
|
4060
4344
|
try {
|
|
4061
|
-
content = await
|
|
4345
|
+
content = await readFile8(abs, "utf8");
|
|
4062
4346
|
} catch {
|
|
4063
4347
|
continue;
|
|
4064
4348
|
}
|
|
@@ -4253,7 +4537,7 @@ async function listPending(projectRoot, filters) {
|
|
|
4253
4537
|
const absolutePath = join12(dir, name);
|
|
4254
4538
|
let content;
|
|
4255
4539
|
try {
|
|
4256
|
-
content = await
|
|
4540
|
+
content = await readFile9(absolutePath, "utf8");
|
|
4257
4541
|
} catch {
|
|
4258
4542
|
continue;
|
|
4259
4543
|
}
|
|
@@ -4363,7 +4647,7 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
4363
4647
|
let targetAbs;
|
|
4364
4648
|
let writtenTarget = false;
|
|
4365
4649
|
try {
|
|
4366
|
-
const content = await
|
|
4650
|
+
const content = await readFile9(sourceAbs, "utf8");
|
|
4367
4651
|
const fm = parseFrontmatter(content);
|
|
4368
4652
|
const pluralType = fm.type;
|
|
4369
4653
|
if (pluralType === void 0 || !PLURAL_TYPES.includes(pluralType)) {
|
|
@@ -4377,7 +4661,12 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
4377
4661
|
);
|
|
4378
4662
|
allocatedId = stableId;
|
|
4379
4663
|
const newFilename = `${stableId}--${slug}.md`;
|
|
4380
|
-
|
|
4664
|
+
const promoteProject = layer === "team" ? loadProjectConfig3(projectRoot)?.active_project : void 0;
|
|
4665
|
+
targetAbs = join12(
|
|
4666
|
+
resolveStoreCanonicalBase(layer, projectRoot, promoteProject),
|
|
4667
|
+
pluralType,
|
|
4668
|
+
newFilename
|
|
4669
|
+
);
|
|
4381
4670
|
await ensureParentDirectory(targetAbs);
|
|
4382
4671
|
const rewritten = rewriteFrontmatterMerge(
|
|
4383
4672
|
rewriteFrontmatterForPromote(content, stableId),
|
|
@@ -4435,7 +4724,7 @@ async function rejectAll(projectRoot, pendingPaths, reason) {
|
|
|
4435
4724
|
try {
|
|
4436
4725
|
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
4437
4726
|
if (existsSync5(sandboxed.abs)) {
|
|
4438
|
-
const content = await
|
|
4727
|
+
const content = await readFile9(sandboxed.abs, "utf8");
|
|
4439
4728
|
const merged = rewriteFrontmatterMerge(content, { status: "rejected" });
|
|
4440
4729
|
const rejectedAbs = sandboxed.abs.includes(`${sep2}pending${sep2}`) ? sandboxed.abs.replace(`${sep2}pending${sep2}`, `${sep2}rejected${sep2}`) : null;
|
|
4441
4730
|
if (rejectedAbs !== null) {
|
|
@@ -4462,7 +4751,7 @@ async function modifyEntry(projectRoot, pendingPath, changes) {
|
|
|
4462
4751
|
if (target === null) {
|
|
4463
4752
|
throw new Error(`modify target not found: ${pendingPath}`);
|
|
4464
4753
|
}
|
|
4465
|
-
const content = await
|
|
4754
|
+
const content = await readFile9(target.absPath, "utf8");
|
|
4466
4755
|
const fm = parseFrontmatter(content);
|
|
4467
4756
|
const currentLayer = fm.layer ?? "team";
|
|
4468
4757
|
if (fm.maturity === "verified" && changes.maturity === "proven" && fm.id !== void 0) {
|
|
@@ -4564,8 +4853,9 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
|
4564
4853
|
pluralType,
|
|
4565
4854
|
resolveWriteTargetStoreDir(toLayer, projectRoot)
|
|
4566
4855
|
);
|
|
4856
|
+
const flipProject = toLayer === "team" ? loadProjectConfig3(projectRoot)?.active_project : void 0;
|
|
4567
4857
|
const toAbs = join12(
|
|
4568
|
-
resolveStoreCanonicalBase(toLayer, projectRoot),
|
|
4858
|
+
resolveStoreCanonicalBase(toLayer, projectRoot, flipProject),
|
|
4569
4859
|
pluralType,
|
|
4570
4860
|
`${newStableId}--${slug}.md`
|
|
4571
4861
|
);
|
|
@@ -4587,20 +4877,22 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
|
4587
4877
|
{ ...effectivePatch, last_review_confirmed_at: (/* @__PURE__ */ new Date()).toISOString() },
|
|
4588
4878
|
{ id: newStableId }
|
|
4589
4879
|
);
|
|
4590
|
-
|
|
4880
|
+
let moved = false;
|
|
4591
4881
|
if (target.isInProjectTree) {
|
|
4592
4882
|
const relSource = relative(projectRoot, target.absPath);
|
|
4883
|
+
const relDest = relative(projectRoot, toAbs);
|
|
4593
4884
|
try {
|
|
4594
|
-
execFileSync("git", ["
|
|
4885
|
+
execFileSync("git", ["mv", "-f", relSource, relDest], {
|
|
4595
4886
|
cwd: projectRoot,
|
|
4596
4887
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4597
4888
|
});
|
|
4889
|
+
moved = true;
|
|
4598
4890
|
} catch {
|
|
4599
|
-
|
|
4600
|
-
await unlink2(target.absPath);
|
|
4601
|
-
}
|
|
4891
|
+
moved = false;
|
|
4602
4892
|
}
|
|
4603
|
-
}
|
|
4893
|
+
}
|
|
4894
|
+
await atomicWriteText(toAbs, rewritten);
|
|
4895
|
+
if (!moved && existsSync5(target.absPath) && target.absPath !== toAbs) {
|
|
4604
4896
|
await unlink2(target.absPath);
|
|
4605
4897
|
}
|
|
4606
4898
|
const flipReason = `layer_flip:${priorStableId ?? "<unassigned>"}->${newStableId}`;
|
|
@@ -4694,7 +4986,7 @@ async function listIndexedSearchEntries(source, type) {
|
|
|
4694
4986
|
}
|
|
4695
4987
|
let content;
|
|
4696
4988
|
try {
|
|
4697
|
-
content = await
|
|
4989
|
+
content = await readFile9(absolutePath, "utf8");
|
|
4698
4990
|
searchEntryIndexContentReads += 1;
|
|
4699
4991
|
} catch {
|
|
4700
4992
|
directoryCache.files.delete(absolutePath);
|
|
@@ -4744,7 +5036,8 @@ function pendingEntryToRankerItem(indexed) {
|
|
|
4744
5036
|
...fm.id !== void 0 ? { id: fm.id } : {},
|
|
4745
5037
|
...fm.type !== void 0 ? { knowledge_type: fm.type } : {},
|
|
4746
5038
|
maturity: fm.maturity ?? "draft",
|
|
4747
|
-
|
|
5039
|
+
// W4/Track1 (D1): no `knowledge_layer` field — layer is derived from the
|
|
5040
|
+
// stable_id prefix (KT-DEC-0004), never carried on the description.
|
|
4748
5041
|
...fm.semantic_scope !== void 0 ? { semantic_scope: fm.semantic_scope } : {},
|
|
4749
5042
|
...fm.created_at !== void 0 ? { created_at: fm.created_at } : {},
|
|
4750
5043
|
tags: fm.tags ?? [],
|
|
@@ -4863,7 +5156,7 @@ async function deferAll(projectRoot, pendingPaths, until, reason) {
|
|
|
4863
5156
|
try {
|
|
4864
5157
|
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
4865
5158
|
if (existsSync5(sandboxed.abs)) {
|
|
4866
|
-
const content = await
|
|
5159
|
+
const content = await readFile9(sandboxed.abs, "utf8");
|
|
4867
5160
|
stableId = parseFrontmatter(content).id;
|
|
4868
5161
|
const patch = {
|
|
4869
5162
|
status: "deferred",
|
|
@@ -5204,9 +5497,9 @@ function registerPending(server, tracker) {
|
|
|
5204
5497
|
}
|
|
5205
5498
|
|
|
5206
5499
|
// src/services/doctor.ts
|
|
5207
|
-
import { access as access4, readFile as
|
|
5500
|
+
import { access as access4, readFile as readFile19, readdir as readdirAsync, stat as statAsync, unlink as unlink4, writeFile as writeFile5 } from "fs/promises";
|
|
5208
5501
|
import { constants as constants2 } from "fs";
|
|
5209
|
-
import { isAbsolute as isAbsolute2, join as
|
|
5502
|
+
import { isAbsolute as isAbsolute2, join as join24, posix as posix5, resolve as resolve3 } from "path";
|
|
5210
5503
|
import {
|
|
5211
5504
|
createTranslator,
|
|
5212
5505
|
forensicReportSchema,
|
|
@@ -5420,7 +5713,7 @@ function createKnowledgeSummaryOpaqueCheck(t, inspection) {
|
|
|
5420
5713
|
}
|
|
5421
5714
|
|
|
5422
5715
|
// src/services/doctor-stable-id-collision.ts
|
|
5423
|
-
import { readFile as
|
|
5716
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
5424
5717
|
import { basename as basename2, join as join13 } from "path";
|
|
5425
5718
|
import {
|
|
5426
5719
|
buildStoreResolveInput as buildStoreResolveInput4,
|
|
@@ -5467,7 +5760,7 @@ async function inspectStoreStableIdIntegrity(projectRoot) {
|
|
|
5467
5760
|
for (const ref of await readKnowledgeAcrossStores2(resolved.dirs)) {
|
|
5468
5761
|
let source;
|
|
5469
5762
|
try {
|
|
5470
|
-
source = await
|
|
5763
|
+
source = await readFile10(ref.file, "utf8");
|
|
5471
5764
|
} catch {
|
|
5472
5765
|
continue;
|
|
5473
5766
|
}
|
|
@@ -5788,7 +6081,7 @@ function createNarrowNoPathsCheck(t, inspection) {
|
|
|
5788
6081
|
}
|
|
5789
6082
|
|
|
5790
6083
|
// src/services/doctor-broad-index.ts
|
|
5791
|
-
import { readFile as
|
|
6084
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
5792
6085
|
import { join as join15 } from "path";
|
|
5793
6086
|
var DEFAULT_BROAD_INDEX_BACKSTOP = 50;
|
|
5794
6087
|
var BROAD_INDEX_BACKSTOP_MIN = 20;
|
|
@@ -5797,7 +6090,7 @@ var BROAD_INDEX_DRIFT_RATIO = 0.8;
|
|
|
5797
6090
|
async function readBroadIndexBackstop(projectRoot) {
|
|
5798
6091
|
const configPath = join15(projectRoot, ".fabric", "fabric-config.json");
|
|
5799
6092
|
try {
|
|
5800
|
-
const raw = await
|
|
6093
|
+
const raw = await readFile11(configPath, "utf8");
|
|
5801
6094
|
const parsed = JSON.parse(raw);
|
|
5802
6095
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
5803
6096
|
const v = parsed.broad_index_backstop;
|
|
@@ -6114,7 +6407,7 @@ function createBroadReviewRecheckCheck(t, inspection) {
|
|
|
6114
6407
|
}
|
|
6115
6408
|
|
|
6116
6409
|
// src/services/doctor-scope-lint.ts
|
|
6117
|
-
import { readFile as
|
|
6410
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
6118
6411
|
import { join as join16 } from "path";
|
|
6119
6412
|
import {
|
|
6120
6413
|
buildStoreResolveInput as buildStoreResolveInput5,
|
|
@@ -6182,7 +6475,7 @@ async function lintStoreScopes(projectRoot) {
|
|
|
6182
6475
|
}
|
|
6183
6476
|
let source;
|
|
6184
6477
|
try {
|
|
6185
|
-
source = await
|
|
6478
|
+
source = await readFile12(ref.file, "utf8");
|
|
6186
6479
|
} catch {
|
|
6187
6480
|
continue;
|
|
6188
6481
|
}
|
|
@@ -6265,9 +6558,9 @@ function createScopeLintCheck(t, violations) {
|
|
|
6265
6558
|
}
|
|
6266
6559
|
|
|
6267
6560
|
// src/services/doctor-unbound-project.ts
|
|
6268
|
-
import { loadProjectConfig as
|
|
6561
|
+
import { loadProjectConfig as loadProjectConfig4 } from "@fenglimg/fabric-shared";
|
|
6269
6562
|
function detectUnboundProject(projectRoot) {
|
|
6270
|
-
const config =
|
|
6563
|
+
const config = loadProjectConfig4(projectRoot);
|
|
6271
6564
|
const alias = config?.active_write_store;
|
|
6272
6565
|
if (typeof alias !== "string" || alias.length === 0) {
|
|
6273
6566
|
return null;
|
|
@@ -6315,7 +6608,7 @@ import {
|
|
|
6315
6608
|
readStoreCounters,
|
|
6316
6609
|
reconcileStoreCounters,
|
|
6317
6610
|
resolveGlobalRoot as resolveGlobalRoot6,
|
|
6318
|
-
STORE_KNOWLEDGE_TYPE_DIRS,
|
|
6611
|
+
STORE_KNOWLEDGE_TYPE_DIRS as STORE_KNOWLEDGE_TYPE_DIRS2,
|
|
6319
6612
|
STORE_LAYOUT as STORE_LAYOUT2,
|
|
6320
6613
|
storeRelativePathForMount as storeRelativePathForMount5
|
|
6321
6614
|
} from "@fenglimg/fabric-shared";
|
|
@@ -6359,7 +6652,7 @@ function readEntryId(file) {
|
|
|
6359
6652
|
}
|
|
6360
6653
|
function computeStoreDiskMax(storeDir) {
|
|
6361
6654
|
const max = defaultAgentsMetaCounters();
|
|
6362
|
-
for (const type of
|
|
6655
|
+
for (const type of STORE_KNOWLEDGE_TYPE_DIRS2) {
|
|
6363
6656
|
const dir = join17(storeDir, STORE_LAYOUT2.knowledgeDir, type);
|
|
6364
6657
|
if (!existsSync7(dir)) {
|
|
6365
6658
|
continue;
|
|
@@ -6554,6 +6847,195 @@ function createStoreOrphanCheck(t, orphans) {
|
|
|
6554
6847
|
};
|
|
6555
6848
|
}
|
|
6556
6849
|
|
|
6850
|
+
// src/services/doctor-project-registry-drift.ts
|
|
6851
|
+
import { readdir as readdir3, rmdir } from "fs/promises";
|
|
6852
|
+
import { join as join19 } from "path";
|
|
6853
|
+
import {
|
|
6854
|
+
addStoreProject,
|
|
6855
|
+
buildStoreResolveInput as buildStoreResolveInput7,
|
|
6856
|
+
createStoreResolver as createStoreResolver7,
|
|
6857
|
+
readStoreProjects as readStoreProjects2,
|
|
6858
|
+
resolveGlobalRoot as resolveGlobalRoot8,
|
|
6859
|
+
STORE_KNOWLEDGE_TYPE_DIRS as STORE_KNOWLEDGE_TYPE_DIRS3,
|
|
6860
|
+
STORE_LAYOUT as STORE_LAYOUT3,
|
|
6861
|
+
STORE_PROJECT_ID_PATTERN as STORE_PROJECT_ID_PATTERN2,
|
|
6862
|
+
storeRelativePathForMount as storeRelativePathForMount6
|
|
6863
|
+
} from "@fenglimg/fabric-shared";
|
|
6864
|
+
var PROJECTS_DIR = "projects";
|
|
6865
|
+
var EMPTY_INSPECTION = { findings: [] };
|
|
6866
|
+
function resolveDriftStores(projectRoot) {
|
|
6867
|
+
const input = buildStoreResolveInput7(projectRoot);
|
|
6868
|
+
if (input === null) {
|
|
6869
|
+
return [];
|
|
6870
|
+
}
|
|
6871
|
+
const readSet = createStoreResolver7().resolveReadSet(input);
|
|
6872
|
+
if (readSet.stores.length === 0) {
|
|
6873
|
+
return [];
|
|
6874
|
+
}
|
|
6875
|
+
const globalRoot = resolveGlobalRoot8();
|
|
6876
|
+
return readSet.stores.map((entry) => {
|
|
6877
|
+
const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
|
|
6878
|
+
return {
|
|
6879
|
+
uuid: entry.store_uuid,
|
|
6880
|
+
alias: entry.alias,
|
|
6881
|
+
dir: join19(globalRoot, storeRelativePathForMount6(mounted ?? { store_uuid: entry.store_uuid }))
|
|
6882
|
+
};
|
|
6883
|
+
});
|
|
6884
|
+
}
|
|
6885
|
+
async function listDir2(dir) {
|
|
6886
|
+
try {
|
|
6887
|
+
return await readdir3(dir);
|
|
6888
|
+
} catch {
|
|
6889
|
+
return [];
|
|
6890
|
+
}
|
|
6891
|
+
}
|
|
6892
|
+
async function listProjectFolders(storeDir) {
|
|
6893
|
+
const projectsRoot = join19(storeDir, STORE_LAYOUT3.knowledgeDir, PROJECTS_DIR);
|
|
6894
|
+
const reserved = /* @__PURE__ */ new Set([PROJECTS_DIR, ...STORE_KNOWLEDGE_TYPE_DIRS3]);
|
|
6895
|
+
return (await listDir2(projectsRoot)).filter((id) => STORE_PROJECT_ID_PATTERN2.test(id) && !reserved.has(id)).sort();
|
|
6896
|
+
}
|
|
6897
|
+
async function folderHasEntries(storeDir, projectId) {
|
|
6898
|
+
const base = join19(storeDir, STORE_LAYOUT3.knowledgeDir, PROJECTS_DIR, projectId);
|
|
6899
|
+
for (const type of STORE_KNOWLEDGE_TYPE_DIRS3) {
|
|
6900
|
+
const names = await listDir2(join19(base, type));
|
|
6901
|
+
if (names.some((name) => name.endsWith(".md"))) {
|
|
6902
|
+
return true;
|
|
6903
|
+
}
|
|
6904
|
+
}
|
|
6905
|
+
return false;
|
|
6906
|
+
}
|
|
6907
|
+
async function inspectProjectRegistryDrift(projectRoot) {
|
|
6908
|
+
const stores = resolveDriftStores(projectRoot);
|
|
6909
|
+
if (stores.length === 0) {
|
|
6910
|
+
return EMPTY_INSPECTION;
|
|
6911
|
+
}
|
|
6912
|
+
const findings = [];
|
|
6913
|
+
for (const store of stores) {
|
|
6914
|
+
const registered = new Set((await readStoreProjects2(store.dir)).map((p) => p.id));
|
|
6915
|
+
const folders = await listProjectFolders(store.dir);
|
|
6916
|
+
const folderSet = new Set(folders);
|
|
6917
|
+
const base = { store_alias: store.alias, store_uuid: store.uuid, store_dir: store.dir };
|
|
6918
|
+
for (const projectId of folders) {
|
|
6919
|
+
if (registered.has(projectId)) {
|
|
6920
|
+
if (!await folderHasEntries(store.dir, projectId)) {
|
|
6921
|
+
findings.push({ ...base, project_id: projectId, kind: "empty_folder" });
|
|
6922
|
+
}
|
|
6923
|
+
continue;
|
|
6924
|
+
}
|
|
6925
|
+
const kind = await folderHasEntries(store.dir, projectId) ? "unregistered_write" : "orphan_folder";
|
|
6926
|
+
findings.push({ ...base, project_id: projectId, kind });
|
|
6927
|
+
}
|
|
6928
|
+
void folderSet;
|
|
6929
|
+
}
|
|
6930
|
+
findings.sort(
|
|
6931
|
+
(a, b) => a.store_alias.localeCompare(b.store_alias) || a.project_id.localeCompare(b.project_id)
|
|
6932
|
+
);
|
|
6933
|
+
return { findings };
|
|
6934
|
+
}
|
|
6935
|
+
async function fixProjectRegistryDrift(projectRoot) {
|
|
6936
|
+
const registered = [];
|
|
6937
|
+
const pruned = [];
|
|
6938
|
+
const { findings } = await inspectProjectRegistryDrift(projectRoot);
|
|
6939
|
+
for (const finding of findings) {
|
|
6940
|
+
if (finding.kind === "empty_folder") {
|
|
6941
|
+
if (await folderHasEntries(finding.store_dir, finding.project_id)) {
|
|
6942
|
+
continue;
|
|
6943
|
+
}
|
|
6944
|
+
try {
|
|
6945
|
+
const base = join19(
|
|
6946
|
+
finding.store_dir,
|
|
6947
|
+
STORE_LAYOUT3.knowledgeDir,
|
|
6948
|
+
PROJECTS_DIR,
|
|
6949
|
+
finding.project_id
|
|
6950
|
+
);
|
|
6951
|
+
for (const type of STORE_KNOWLEDGE_TYPE_DIRS3) {
|
|
6952
|
+
await rmdir(join19(base, type)).catch(() => void 0);
|
|
6953
|
+
}
|
|
6954
|
+
await rmdir(base);
|
|
6955
|
+
pruned.push(finding);
|
|
6956
|
+
} catch {
|
|
6957
|
+
}
|
|
6958
|
+
continue;
|
|
6959
|
+
}
|
|
6960
|
+
try {
|
|
6961
|
+
await addStoreProject(finding.store_dir, {
|
|
6962
|
+
id: finding.project_id,
|
|
6963
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6964
|
+
});
|
|
6965
|
+
registered.push(finding);
|
|
6966
|
+
} catch {
|
|
6967
|
+
}
|
|
6968
|
+
}
|
|
6969
|
+
return { registered, pruned };
|
|
6970
|
+
}
|
|
6971
|
+
function createProjectRegistryDriftCheck(t, inspection) {
|
|
6972
|
+
const { findings } = inspection;
|
|
6973
|
+
if (findings.length === 0) {
|
|
6974
|
+
return {
|
|
6975
|
+
name: t("doctor.check.project_registry_drift.name"),
|
|
6976
|
+
status: "ok",
|
|
6977
|
+
message: t("doctor.check.project_registry_drift.ok")
|
|
6978
|
+
};
|
|
6979
|
+
}
|
|
6980
|
+
const unregistered = findings.filter((f) => f.kind === "unregistered_write").length;
|
|
6981
|
+
const orphans = findings.filter((f) => f.kind === "orphan_folder").length;
|
|
6982
|
+
const empties = findings.filter((f) => f.kind === "empty_folder").length;
|
|
6983
|
+
const breakdown = [
|
|
6984
|
+
unregistered > 0 ? `${unregistered} unregistered-write` : null,
|
|
6985
|
+
orphans > 0 ? `${orphans} orphan-folder` : null,
|
|
6986
|
+
empties > 0 ? `${empties} empty-folder` : null
|
|
6987
|
+
].filter((part) => part !== null).join(", ");
|
|
6988
|
+
if (unregistered > 0) {
|
|
6989
|
+
const first2 = findings.find((f) => f.kind === "unregistered_write");
|
|
6990
|
+
return {
|
|
6991
|
+
name: t("doctor.check.project_registry_drift.name"),
|
|
6992
|
+
status: "error",
|
|
6993
|
+
kind: "manual_error",
|
|
6994
|
+
code: "project_registry_drift",
|
|
6995
|
+
fixable: true,
|
|
6996
|
+
message: t("doctor.check.project_registry_drift.message.unregistered", {
|
|
6997
|
+
total: String(findings.length),
|
|
6998
|
+
breakdown,
|
|
6999
|
+
projectId: first2.project_id,
|
|
7000
|
+
storeAlias: first2.store_alias
|
|
7001
|
+
}),
|
|
7002
|
+
actionHint: t("doctor.check.project_registry_drift.remediation")
|
|
7003
|
+
};
|
|
7004
|
+
}
|
|
7005
|
+
if (orphans > 0) {
|
|
7006
|
+
const first2 = findings.find((f) => f.kind === "orphan_folder");
|
|
7007
|
+
return {
|
|
7008
|
+
name: t("doctor.check.project_registry_drift.name"),
|
|
7009
|
+
status: "warn",
|
|
7010
|
+
kind: "warning",
|
|
7011
|
+
code: "project_registry_drift",
|
|
7012
|
+
fixable: true,
|
|
7013
|
+
message: t("doctor.check.project_registry_drift.message.orphan", {
|
|
7014
|
+
total: String(findings.length),
|
|
7015
|
+
breakdown,
|
|
7016
|
+
projectId: first2.project_id,
|
|
7017
|
+
storeAlias: first2.store_alias
|
|
7018
|
+
}),
|
|
7019
|
+
actionHint: t("doctor.check.project_registry_drift.remediation")
|
|
7020
|
+
};
|
|
7021
|
+
}
|
|
7022
|
+
const first = findings.find((f) => f.kind === "empty_folder");
|
|
7023
|
+
return {
|
|
7024
|
+
name: t("doctor.check.project_registry_drift.name"),
|
|
7025
|
+
status: "warn",
|
|
7026
|
+
kind: "info",
|
|
7027
|
+
code: "project_registry_drift",
|
|
7028
|
+
fixable: true,
|
|
7029
|
+
message: t("doctor.check.project_registry_drift.message.empty", {
|
|
7030
|
+
total: String(findings.length),
|
|
7031
|
+
breakdown,
|
|
7032
|
+
projectId: first.project_id,
|
|
7033
|
+
storeAlias: first.store_alias
|
|
7034
|
+
}),
|
|
7035
|
+
actionHint: t("doctor.check.project_registry_drift.remediation")
|
|
7036
|
+
};
|
|
7037
|
+
}
|
|
7038
|
+
|
|
6557
7039
|
// src/services/legacy-serve-lock-probe.ts
|
|
6558
7040
|
import {
|
|
6559
7041
|
isAlive,
|
|
@@ -6626,8 +7108,8 @@ async function inspectEventsJsonlGates(projectRoot, options = {}) {
|
|
|
6626
7108
|
}
|
|
6627
7109
|
|
|
6628
7110
|
// src/services/doctor-skill-lints.ts
|
|
6629
|
-
import { readdir as
|
|
6630
|
-
import { join as
|
|
7111
|
+
import { readdir as readdir4, readFile as readFile13 } from "fs/promises";
|
|
7112
|
+
import { join as join20, posix as posix2 } from "path";
|
|
6631
7113
|
var FABRIC_SKILL_SLUGS = ["fabric-archive", "fabric-review"];
|
|
6632
7114
|
var SKILL_MD_FRONTMATTER_ROOTS = [".claude/skills", ".codex/skills"];
|
|
6633
7115
|
var SKILL_FRONTMATTER_KEY_PATTERN = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]+(.+?)[ \t]*$/u;
|
|
@@ -6649,7 +7131,7 @@ function issueCheck(name, status, kind, code, message, actionHint, audience) {
|
|
|
6649
7131
|
}
|
|
6650
7132
|
async function listMarkdownFiles(dir) {
|
|
6651
7133
|
try {
|
|
6652
|
-
return (await
|
|
7134
|
+
return (await readdir4(dir)).filter((name) => name.endsWith(".md"));
|
|
6653
7135
|
} catch {
|
|
6654
7136
|
return null;
|
|
6655
7137
|
}
|
|
@@ -6657,8 +7139,8 @@ async function listMarkdownFiles(dir) {
|
|
|
6657
7139
|
async function inspectSkillRefMirror(projectRoot) {
|
|
6658
7140
|
const driftedPaths = [];
|
|
6659
7141
|
for (const slug of FABRIC_SKILL_SLUGS) {
|
|
6660
|
-
const claudeRef =
|
|
6661
|
-
const codexRef =
|
|
7142
|
+
const claudeRef = join20(projectRoot, ".claude", "skills", slug, "ref");
|
|
7143
|
+
const codexRef = join20(projectRoot, ".codex", "skills", slug, "ref");
|
|
6662
7144
|
const [claudeFiles, codexFiles] = await Promise.all([listMarkdownFiles(claudeRef), listMarkdownFiles(codexRef)]);
|
|
6663
7145
|
if (claudeFiles === null || codexFiles === null) continue;
|
|
6664
7146
|
const claudeSet = new Set(claudeFiles);
|
|
@@ -6675,8 +7157,8 @@ async function inspectSkillRefMirror(projectRoot) {
|
|
|
6675
7157
|
let codexBody;
|
|
6676
7158
|
try {
|
|
6677
7159
|
[claudeBody, codexBody] = await Promise.all([
|
|
6678
|
-
|
|
6679
|
-
|
|
7160
|
+
readFile13(join20(claudeRef, fname), "utf8"),
|
|
7161
|
+
readFile13(join20(codexRef, fname), "utf8")
|
|
6680
7162
|
]);
|
|
6681
7163
|
} catch {
|
|
6682
7164
|
continue;
|
|
@@ -6695,10 +7177,10 @@ async function inspectSkillTokenBudget(projectRoot) {
|
|
|
6695
7177
|
const overSize = [];
|
|
6696
7178
|
let highestSeverity = "ok";
|
|
6697
7179
|
for (const slug of FABRIC_SKILL_SLUGS) {
|
|
6698
|
-
const skillMdPath =
|
|
7180
|
+
const skillMdPath = join20(projectRoot, ".claude", "skills", slug, "SKILL.md");
|
|
6699
7181
|
let body;
|
|
6700
7182
|
try {
|
|
6701
|
-
body = await
|
|
7183
|
+
body = await readFile13(skillMdPath, "utf8");
|
|
6702
7184
|
} catch {
|
|
6703
7185
|
continue;
|
|
6704
7186
|
}
|
|
@@ -6719,10 +7201,10 @@ async function inspectSkillDescription(projectRoot) {
|
|
|
6719
7201
|
const CJK_PATTERN = /[\u3400-\u4dbf\u4e00-\u9fff]/u;
|
|
6720
7202
|
const ASCII_PATTERN = /[a-zA-Z]{2,}/u;
|
|
6721
7203
|
for (const slug of FABRIC_SKILL_SLUGS) {
|
|
6722
|
-
const skillMdPath =
|
|
7204
|
+
const skillMdPath = join20(projectRoot, ".claude", "skills", slug, "SKILL.md");
|
|
6723
7205
|
let body;
|
|
6724
7206
|
try {
|
|
6725
|
-
body = await
|
|
7207
|
+
body = await readFile13(skillMdPath, "utf8");
|
|
6726
7208
|
} catch {
|
|
6727
7209
|
continue;
|
|
6728
7210
|
}
|
|
@@ -6753,19 +7235,19 @@ async function inspectSkillDescription(projectRoot) {
|
|
|
6753
7235
|
async function inspectSkillMdYamlInvalid(projectRoot) {
|
|
6754
7236
|
const candidates = [];
|
|
6755
7237
|
for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
|
|
6756
|
-
const rootAbs =
|
|
7238
|
+
const rootAbs = join20(projectRoot, rootRel);
|
|
6757
7239
|
let dirEntries;
|
|
6758
7240
|
try {
|
|
6759
|
-
dirEntries = await
|
|
7241
|
+
dirEntries = await readdir4(rootAbs, { withFileTypes: true });
|
|
6760
7242
|
} catch {
|
|
6761
7243
|
continue;
|
|
6762
7244
|
}
|
|
6763
7245
|
for (const dirEntry of dirEntries) {
|
|
6764
7246
|
if (!dirEntry.isDirectory()) continue;
|
|
6765
|
-
const skillFile =
|
|
7247
|
+
const skillFile = join20(rootAbs, dirEntry.name, "SKILL.md");
|
|
6766
7248
|
let raw;
|
|
6767
7249
|
try {
|
|
6768
|
-
raw = await
|
|
7250
|
+
raw = await readFile13(skillFile, "utf8");
|
|
6769
7251
|
} catch {
|
|
6770
7252
|
continue;
|
|
6771
7253
|
}
|
|
@@ -6888,8 +7370,8 @@ function createSkillMdYamlInvalidCheck(t, inspection) {
|
|
|
6888
7370
|
}
|
|
6889
7371
|
|
|
6890
7372
|
// src/services/doctor-retired-references-lint.ts
|
|
6891
|
-
import { readdir as
|
|
6892
|
-
import { join as
|
|
7373
|
+
import { readdir as readdir5, readFile as readFile14, stat as stat3 } from "fs/promises";
|
|
7374
|
+
import { join as join21, posix as posix3, relative as relative2 } from "path";
|
|
6893
7375
|
var RETIRED_TOKENS = [
|
|
6894
7376
|
{ token: "fab_plan_context", replacement: "fab_recall", reason: "retrieval collapsed to one lean fab_recall (KT-DEC-0026)" },
|
|
6895
7377
|
{ token: "fab_get_knowledge_sections", replacement: "fab_recall", reason: "two-step fetch retired (KT-DEC-0026)" },
|
|
@@ -6912,7 +7394,7 @@ var RETIRED_TOKENS = [
|
|
|
6912
7394
|
];
|
|
6913
7395
|
var HOOK_DIRS = [".claude/hooks", ".codex/hooks"];
|
|
6914
7396
|
var SKILL_DIRS = [".claude/skills", ".codex/skills"];
|
|
6915
|
-
var BOOTSTRAP_FILES = ["AGENTS.md", "CLAUDE.md",
|
|
7397
|
+
var BOOTSTRAP_FILES = ["AGENTS.md", "CLAUDE.md", join21(".fabric", "AGENTS.md")];
|
|
6916
7398
|
function isCommentLine(line) {
|
|
6917
7399
|
const t = line.trim();
|
|
6918
7400
|
return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*");
|
|
@@ -6920,13 +7402,13 @@ function isCommentLine(line) {
|
|
|
6920
7402
|
async function walkFiles(dir, exts) {
|
|
6921
7403
|
let entries;
|
|
6922
7404
|
try {
|
|
6923
|
-
entries = await
|
|
7405
|
+
entries = await readdir5(dir, { withFileTypes: true });
|
|
6924
7406
|
} catch {
|
|
6925
7407
|
return [];
|
|
6926
7408
|
}
|
|
6927
7409
|
const out = [];
|
|
6928
7410
|
for (const e of entries) {
|
|
6929
|
-
const full =
|
|
7411
|
+
const full = join21(dir, e.name);
|
|
6930
7412
|
if (e.isDirectory()) {
|
|
6931
7413
|
out.push(...await walkFiles(full, exts));
|
|
6932
7414
|
} else if (exts.some((ext) => e.name.endsWith(ext))) {
|
|
@@ -6952,29 +7434,29 @@ async function inspectRetiredReferences(projectRoot) {
|
|
|
6952
7434
|
let scannedFiles = 0;
|
|
6953
7435
|
const toRel = (p) => posix3.normalize(relative2(projectRoot, p).split("\\").join("/"));
|
|
6954
7436
|
for (const rel of BOOTSTRAP_FILES) {
|
|
6955
|
-
const abs =
|
|
7437
|
+
const abs = join21(projectRoot, rel);
|
|
6956
7438
|
try {
|
|
6957
7439
|
if ((await stat3(abs)).isFile()) {
|
|
6958
7440
|
scannedFiles += 1;
|
|
6959
|
-
scanText(toRel(abs), await
|
|
7441
|
+
scanText(toRel(abs), await readFile14(abs, "utf8"), false, hits);
|
|
6960
7442
|
}
|
|
6961
7443
|
} catch {
|
|
6962
7444
|
}
|
|
6963
7445
|
}
|
|
6964
7446
|
for (const dir of SKILL_DIRS) {
|
|
6965
|
-
for (const file of await walkFiles(
|
|
7447
|
+
for (const file of await walkFiles(join21(projectRoot, dir), [".md"])) {
|
|
6966
7448
|
scannedFiles += 1;
|
|
6967
7449
|
try {
|
|
6968
|
-
scanText(toRel(file), await
|
|
7450
|
+
scanText(toRel(file), await readFile14(file, "utf8"), false, hits);
|
|
6969
7451
|
} catch {
|
|
6970
7452
|
}
|
|
6971
7453
|
}
|
|
6972
7454
|
}
|
|
6973
7455
|
for (const dir of HOOK_DIRS) {
|
|
6974
|
-
for (const file of await walkFiles(
|
|
7456
|
+
for (const file of await walkFiles(join21(projectRoot, dir), [".cjs"])) {
|
|
6975
7457
|
scannedFiles += 1;
|
|
6976
7458
|
try {
|
|
6977
|
-
scanText(toRel(file), await
|
|
7459
|
+
scanText(toRel(file), await readFile14(file, "utf8"), true, hits);
|
|
6978
7460
|
} catch {
|
|
6979
7461
|
}
|
|
6980
7462
|
}
|
|
@@ -7009,8 +7491,8 @@ function createRetiredReferenceCheck(t, inspection) {
|
|
|
7009
7491
|
|
|
7010
7492
|
// src/services/doctor-hooks-lints.ts
|
|
7011
7493
|
import { constants } from "fs";
|
|
7012
|
-
import { access, readdir as
|
|
7013
|
-
import { join as
|
|
7494
|
+
import { access, readdir as readdir6, readFile as readFile15, stat as stat4 } from "fs/promises";
|
|
7495
|
+
import { join as join22, posix as posix4 } from "path";
|
|
7014
7496
|
import { Script } from "vm";
|
|
7015
7497
|
var HOOKS_RUNTIME_CLIENT_DIRS = [
|
|
7016
7498
|
{ client: "claude", dir: ".claude/hooks" },
|
|
@@ -7059,7 +7541,7 @@ function isHookWiredForEvent(hooks, event, hookFile) {
|
|
|
7059
7541
|
}
|
|
7060
7542
|
async function readDirectoryFileNames(dir) {
|
|
7061
7543
|
try {
|
|
7062
|
-
return await
|
|
7544
|
+
return await readdir6(dir);
|
|
7063
7545
|
} catch {
|
|
7064
7546
|
return null;
|
|
7065
7547
|
}
|
|
@@ -7072,14 +7554,14 @@ async function isFile(absPath) {
|
|
|
7072
7554
|
}
|
|
7073
7555
|
}
|
|
7074
7556
|
async function inspectHooksWired(projectRoot) {
|
|
7075
|
-
const claudeEntries = await readDirectoryFileNames(
|
|
7557
|
+
const claudeEntries = await readDirectoryFileNames(join22(projectRoot, ".claude"));
|
|
7076
7558
|
if (claudeEntries === null) {
|
|
7077
7559
|
return { status: "skipped", missingHooks: [] };
|
|
7078
7560
|
}
|
|
7079
|
-
const settingsPath =
|
|
7561
|
+
const settingsPath = join22(projectRoot, ".claude", "settings.json");
|
|
7080
7562
|
let parsed;
|
|
7081
7563
|
try {
|
|
7082
|
-
parsed = JSON.parse(await
|
|
7564
|
+
parsed = JSON.parse(await readFile15(settingsPath, "utf8"));
|
|
7083
7565
|
} catch {
|
|
7084
7566
|
return { status: "missing-settings", missingHooks: [] };
|
|
7085
7567
|
}
|
|
@@ -7102,8 +7584,8 @@ async function inspectHooksWired(projectRoot) {
|
|
|
7102
7584
|
}
|
|
7103
7585
|
async function inspectHookCacheWritability(projectRoot) {
|
|
7104
7586
|
const relPath = posix4.join(".fabric", ".cache");
|
|
7105
|
-
const fabricDir =
|
|
7106
|
-
const cacheDir =
|
|
7587
|
+
const fabricDir = join22(projectRoot, ".fabric");
|
|
7588
|
+
const cacheDir = join22(projectRoot, ".fabric", ".cache");
|
|
7107
7589
|
try {
|
|
7108
7590
|
try {
|
|
7109
7591
|
const cacheStats = await stat4(cacheDir);
|
|
@@ -7152,12 +7634,12 @@ async function inspectHookCacheWritability(projectRoot) {
|
|
|
7152
7634
|
async function inspectHooksContentDrift(projectRoot) {
|
|
7153
7635
|
const hookFilesByBasename = /* @__PURE__ */ new Map();
|
|
7154
7636
|
for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
|
|
7155
|
-
const absDir =
|
|
7637
|
+
const absDir = join22(projectRoot, dir);
|
|
7156
7638
|
const entries = await readDirectoryFileNames(absDir);
|
|
7157
7639
|
if (entries === null) continue;
|
|
7158
7640
|
for (const name of entries) {
|
|
7159
7641
|
if (!name.endsWith(".cjs")) continue;
|
|
7160
|
-
const abs =
|
|
7642
|
+
const abs = join22(absDir, name);
|
|
7161
7643
|
if (!await isFile(abs)) continue;
|
|
7162
7644
|
const arr = hookFilesByBasename.get(name) ?? [];
|
|
7163
7645
|
arr.push({ client, abs });
|
|
@@ -7172,7 +7654,7 @@ async function inspectHooksContentDrift(projectRoot) {
|
|
|
7172
7654
|
const hashes = [];
|
|
7173
7655
|
for (const { client, abs } of copies) {
|
|
7174
7656
|
try {
|
|
7175
|
-
const body = await
|
|
7657
|
+
const body = await readFile15(abs, "utf8");
|
|
7176
7658
|
hashes.push({ client, sha: sha256(body) });
|
|
7177
7659
|
} catch {
|
|
7178
7660
|
}
|
|
@@ -7194,18 +7676,18 @@ async function inspectHooksRuntime(projectRoot) {
|
|
|
7194
7676
|
const issues = [];
|
|
7195
7677
|
let scanned = 0;
|
|
7196
7678
|
for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
|
|
7197
|
-
const absDir =
|
|
7679
|
+
const absDir = join22(projectRoot, dir);
|
|
7198
7680
|
const entries = await readDirectoryFileNames(absDir);
|
|
7199
7681
|
if (entries === null) continue;
|
|
7200
7682
|
for (const name of entries) {
|
|
7201
7683
|
if (!name.endsWith(".cjs")) continue;
|
|
7202
|
-
const abs =
|
|
7684
|
+
const abs = join22(absDir, name);
|
|
7203
7685
|
const displayPath = `${dir}/${name}`;
|
|
7204
7686
|
if (!await isFile(abs)) continue;
|
|
7205
7687
|
scanned += 1;
|
|
7206
7688
|
let body;
|
|
7207
7689
|
try {
|
|
7208
|
-
body = await
|
|
7690
|
+
body = await readFile15(abs, "utf8");
|
|
7209
7691
|
} catch (err) {
|
|
7210
7692
|
issues.push({
|
|
7211
7693
|
path: displayPath,
|
|
@@ -7342,8 +7824,8 @@ function createHookCacheWritabilityCheck(t, inspection) {
|
|
|
7342
7824
|
}
|
|
7343
7825
|
|
|
7344
7826
|
// src/services/doctor-bootstrap-lints.ts
|
|
7345
|
-
import { access as access2, readFile as
|
|
7346
|
-
import { join as
|
|
7827
|
+
import { access as access2, readFile as readFile16 } from "fs/promises";
|
|
7828
|
+
import { join as join23 } from "path";
|
|
7347
7829
|
import {
|
|
7348
7830
|
BOOTSTRAP_MARKER_BEGIN,
|
|
7349
7831
|
BOOTSTRAP_MARKER_END,
|
|
@@ -7375,17 +7857,17 @@ async function fileExists(path) {
|
|
|
7375
7857
|
}
|
|
7376
7858
|
async function inspectBootstrapAnchor(projectRoot) {
|
|
7377
7859
|
const [hasAgentsMd, hasClaudeMd] = await Promise.all([
|
|
7378
|
-
fileExists(
|
|
7379
|
-
fileExists(
|
|
7860
|
+
fileExists(join23(projectRoot, "AGENTS.md")),
|
|
7861
|
+
fileExists(join23(projectRoot, "CLAUDE.md"))
|
|
7380
7862
|
]);
|
|
7381
7863
|
return { hasAgentsMd, hasClaudeMd };
|
|
7382
7864
|
}
|
|
7383
7865
|
async function inspectL1BootstrapSnapshotDrift(target) {
|
|
7384
|
-
const abs =
|
|
7866
|
+
const abs = join23(target, ".fabric", "AGENTS.md");
|
|
7385
7867
|
const canonical = resolveBootstrapCanonical();
|
|
7386
7868
|
let onDisk;
|
|
7387
7869
|
try {
|
|
7388
|
-
onDisk = await
|
|
7870
|
+
onDisk = await readFile16(abs, "utf8");
|
|
7389
7871
|
} catch {
|
|
7390
7872
|
return { status: "missing", canonical, onDisk: null };
|
|
7391
7873
|
}
|
|
@@ -7411,17 +7893,17 @@ function createL1BootstrapSnapshotDriftCheck(t, inspection) {
|
|
|
7411
7893
|
);
|
|
7412
7894
|
}
|
|
7413
7895
|
async function inspectL2ManagedBlockDrift(target) {
|
|
7414
|
-
const snapshotPath =
|
|
7896
|
+
const snapshotPath = join23(target, ".fabric", "AGENTS.md");
|
|
7415
7897
|
let snapshot;
|
|
7416
7898
|
try {
|
|
7417
|
-
snapshot = await
|
|
7899
|
+
snapshot = await readFile16(snapshotPath, "utf8");
|
|
7418
7900
|
} catch {
|
|
7419
7901
|
return { status: "ok", drifted: [] };
|
|
7420
7902
|
}
|
|
7421
|
-
const projectRulesPath =
|
|
7903
|
+
const projectRulesPath = join23(target, ".fabric", "project-rules.md");
|
|
7422
7904
|
let expectedBody = snapshot;
|
|
7423
7905
|
try {
|
|
7424
|
-
const projectRules = await
|
|
7906
|
+
const projectRules = await readFile16(projectRulesPath, "utf8");
|
|
7425
7907
|
expectedBody = `${snapshot}
|
|
7426
7908
|
---
|
|
7427
7909
|
${projectRules}`;
|
|
@@ -7430,12 +7912,12 @@ ${projectRules}`;
|
|
|
7430
7912
|
const drifted = [];
|
|
7431
7913
|
let anyManagedBlockFound = false;
|
|
7432
7914
|
const blockTargets = [
|
|
7433
|
-
|
|
7915
|
+
join23(target, "AGENTS.md")
|
|
7434
7916
|
];
|
|
7435
7917
|
for (const abs of blockTargets) {
|
|
7436
7918
|
let content;
|
|
7437
7919
|
try {
|
|
7438
|
-
content = await
|
|
7920
|
+
content = await readFile16(abs, "utf8");
|
|
7439
7921
|
} catch {
|
|
7440
7922
|
continue;
|
|
7441
7923
|
}
|
|
@@ -7458,9 +7940,9 @@ ${projectRules}`;
|
|
|
7458
7940
|
drifted.push({ path: abs, expected: expectedBody, actual: body });
|
|
7459
7941
|
}
|
|
7460
7942
|
}
|
|
7461
|
-
const claudeMdPath =
|
|
7943
|
+
const claudeMdPath = join23(target, "CLAUDE.md");
|
|
7462
7944
|
try {
|
|
7463
|
-
const claudeContent = await
|
|
7945
|
+
const claudeContent = await readFile16(claudeMdPath, "utf8");
|
|
7464
7946
|
anyManagedBlockFound = true;
|
|
7465
7947
|
const lines = claudeContent.split(/\r?\n/u);
|
|
7466
7948
|
const hasAtImport = lines.some((line) => line.trim() === "@.fabric/AGENTS.md");
|
|
@@ -7528,7 +8010,7 @@ import { appendFile as appendFile3 } from "fs/promises";
|
|
|
7528
8010
|
import { minimatch as minimatch2 } from "minimatch";
|
|
7529
8011
|
|
|
7530
8012
|
// src/services/cite-rollup.ts
|
|
7531
|
-
import { readFile as
|
|
8013
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
7532
8014
|
import { createLedgerWriteQueue as createLedgerWriteQueue3 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
7533
8015
|
var citeRollupQueue = createLedgerWriteQueue3();
|
|
7534
8016
|
async function appendCiteRollupRow(projectRoot, row) {
|
|
@@ -7540,7 +8022,7 @@ async function readCiteRollup(projectRoot) {
|
|
|
7540
8022
|
const path = getCiteRollupPath(projectRoot);
|
|
7541
8023
|
let raw;
|
|
7542
8024
|
try {
|
|
7543
|
-
raw = await
|
|
8025
|
+
raw = await readFile17(path, "utf8");
|
|
7544
8026
|
} catch (error) {
|
|
7545
8027
|
if (isNodeError(error) && error.code === "ENOENT") return [];
|
|
7546
8028
|
throw error;
|
|
@@ -8131,6 +8613,18 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
8131
8613
|
}
|
|
8132
8614
|
}
|
|
8133
8615
|
}
|
|
8616
|
+
const editSessionIds = /* @__PURE__ */ new Set();
|
|
8617
|
+
for (const edit of editEvents) {
|
|
8618
|
+
if (typeof edit.session_id === "string" && edit.session_id.length > 0) {
|
|
8619
|
+
editSessionIds.add(edit.session_id);
|
|
8620
|
+
}
|
|
8621
|
+
}
|
|
8622
|
+
let recallsInWindow = 0;
|
|
8623
|
+
for (const list of plannedBySession.values()) recallsInWindow += list.length;
|
|
8624
|
+
let recallSessionsCorrelated = 0;
|
|
8625
|
+
for (const sid of plannedBySession.keys()) {
|
|
8626
|
+
if (editSessionIds.has(sid)) recallSessionsCorrelated += 1;
|
|
8627
|
+
}
|
|
8134
8628
|
const noneTotal = Object.values(noneHistogram).reduce((a, b) => a + b, 0);
|
|
8135
8629
|
const compliantCites = qualifyingCites + noneTotal;
|
|
8136
8630
|
const noncompliantCites = expectedButMissed;
|
|
@@ -8198,6 +8692,11 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
8198
8692
|
uncorrelatable_edits: uncorrelatableEdits,
|
|
8199
8693
|
recall_backed_edits: recallBackedEdits,
|
|
8200
8694
|
recall_coverage_rate: recallCoverageRate,
|
|
8695
|
+
recall_diagnostics: {
|
|
8696
|
+
recalls_in_window: recallsInWindow,
|
|
8697
|
+
recall_sessions: plannedBySession.size,
|
|
8698
|
+
recall_sessions_correlated: recallSessionsCorrelated
|
|
8699
|
+
},
|
|
8201
8700
|
exposed_and_mutated: {
|
|
8202
8701
|
count: exposedAndMutated.count,
|
|
8203
8702
|
...exposedAndMutated.ids.length > 0 ? { ids: exposedAndMutated.ids } : {}
|
|
@@ -8641,6 +9140,7 @@ async function runDoctorReport(target) {
|
|
|
8641
9140
|
const driftUnconsumed = await inspectDriftUnconsumed(projectRoot);
|
|
8642
9141
|
const storeCounterDrift = inspectStoreCounters(projectRoot);
|
|
8643
9142
|
const storeOrphans = inspectStoreOrphans();
|
|
9143
|
+
const projectRegistryDrift = await inspectProjectRegistryDrift(projectRoot);
|
|
8644
9144
|
const stableIdIntegrity = await inspectStoreStableIdIntegrity(projectRoot);
|
|
8645
9145
|
const relevancePaths = await inspectStoreRelevancePaths(projectRoot);
|
|
8646
9146
|
const broadIndexDrift = await inspectBroadIndexDrift(projectRoot);
|
|
@@ -8670,7 +9170,7 @@ async function runDoctorReport(target) {
|
|
|
8670
9170
|
const globalCliVersion = process.env.VITEST === "true" ? { status: "ok", version: "test-skipped" } : inspectGlobalCliVersion();
|
|
8671
9171
|
const targetFiles = Object.fromEntries(
|
|
8672
9172
|
await Promise.all(
|
|
8673
|
-
TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(
|
|
9173
|
+
TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(join24(projectRoot, path))])
|
|
8674
9174
|
)
|
|
8675
9175
|
);
|
|
8676
9176
|
const checks = [
|
|
@@ -8724,6 +9224,12 @@ async function runDoctorReport(target) {
|
|
|
8724
9224
|
// (warning). `--fix` adopts it (rescue-before-delete — re-register, never
|
|
8725
9225
|
// an on-disk delete).
|
|
8726
9226
|
createStoreOrphanCheck(t, storeOrphans),
|
|
9227
|
+
// W2 (F-003): project-registry drift over projects.json ↔ projects/ folder
|
|
9228
|
+
// tree. unregistered-write (data unrouted) → manual_error; orphan-folder →
|
|
9229
|
+
// warning; empty-folder → info; ghost-registration emits nothing (DA-05).
|
|
9230
|
+
// `--fix` rescue-registers orphans/unregistered (addStoreProject — never a
|
|
9231
|
+
// non-empty delete) and prunes only genuinely-empty registered folders.
|
|
9232
|
+
createProjectRegistryDriftCheck(t, projectRegistryDrift),
|
|
8727
9233
|
// rc.5 TASK-010: read-side underseeded-corpus check (#22). Info kind —
|
|
8728
9234
|
// does not bump report status. Recommends running the fabric-import skill
|
|
8729
9235
|
// to backfill knowledge when the corpus is below the threshold floor.
|
|
@@ -8874,7 +9380,7 @@ async function runDoctorFix(target) {
|
|
|
8874
9380
|
const fixed = [];
|
|
8875
9381
|
const ledgerWarnings = [];
|
|
8876
9382
|
if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
|
|
8877
|
-
const snapshotPath =
|
|
9383
|
+
const snapshotPath = join24(projectRoot, ".fabric", "AGENTS.md");
|
|
8878
9384
|
await ensureParentDirectory(snapshotPath);
|
|
8879
9385
|
await atomicWriteText4(snapshotPath, resolveBootstrapCanonical2());
|
|
8880
9386
|
fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
|
|
@@ -8898,6 +9404,18 @@ async function runDoctorFix(target) {
|
|
|
8898
9404
|
fixed.push(findIssue(before.warnings, "store_orphan"));
|
|
8899
9405
|
}
|
|
8900
9406
|
}
|
|
9407
|
+
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");
|
|
9408
|
+
if (registryDriftFound) {
|
|
9409
|
+
const result = await fixProjectRegistryDrift(projectRoot);
|
|
9410
|
+
if (result.registered.length > 0 || result.pruned.length > 0) {
|
|
9411
|
+
fixed.push(
|
|
9412
|
+
findIssue(
|
|
9413
|
+
[...before.manual_errors, ...before.warnings, ...before.infos],
|
|
9414
|
+
"project_registry_drift"
|
|
9415
|
+
)
|
|
9416
|
+
);
|
|
9417
|
+
}
|
|
9418
|
+
}
|
|
8901
9419
|
if (before.fixable_errors.some((issue) => issue.code === "event_ledger_partial_write")) {
|
|
8902
9420
|
const ledgerPath = getEventLedgerPath(projectRoot);
|
|
8903
9421
|
const truncResult = await truncateLedgerToLastNewline(ledgerPath);
|
|
@@ -8947,7 +9465,7 @@ async function runDoctorFix(target) {
|
|
|
8947
9465
|
if (before.infos.some((issue) => issue.code === "stale_serve_lock")) {
|
|
8948
9466
|
const lockInspection = inspectStaleServeLock(projectRoot, Date.now());
|
|
8949
9467
|
if (lockInspection.present && !lockInspection.pidAlive) {
|
|
8950
|
-
const lockFilePath =
|
|
9468
|
+
const lockFilePath = join24(projectRoot, ".fabric", ".serve.lock");
|
|
8951
9469
|
try {
|
|
8952
9470
|
await unlink4(lockFilePath);
|
|
8953
9471
|
} catch (err) {
|
|
@@ -9064,7 +9582,7 @@ function createApplyLintMessage(succeeded, failed, manualErrorCount) {
|
|
|
9064
9582
|
}
|
|
9065
9583
|
async function applySessionHintsStaleCleanup(projectRoot, candidate) {
|
|
9066
9584
|
const detail = `deleted (${candidate.age_days}d old)`;
|
|
9067
|
-
const absPath =
|
|
9585
|
+
const absPath = join24(projectRoot, candidate.path);
|
|
9068
9586
|
try {
|
|
9069
9587
|
const { unlink: unlink5 } = await import("fs/promises");
|
|
9070
9588
|
await unlink5(absPath);
|
|
@@ -9089,9 +9607,9 @@ function truncateErrorMessage(error) {
|
|
|
9089
9607
|
return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
|
|
9090
9608
|
}
|
|
9091
9609
|
async function inspectForensic(projectRoot) {
|
|
9092
|
-
const path =
|
|
9610
|
+
const path = join24(projectRoot, ".fabric", "forensic.json");
|
|
9093
9611
|
try {
|
|
9094
|
-
const parsed = forensicReportSchema.parse(JSON.parse(await
|
|
9612
|
+
const parsed = forensicReportSchema.parse(JSON.parse(await readFile19(path, "utf8")));
|
|
9095
9613
|
return { present: true, valid: true, report: parsed };
|
|
9096
9614
|
} catch (error) {
|
|
9097
9615
|
if (isMissingFileError(error)) {
|
|
@@ -9121,7 +9639,7 @@ async function inspectEventLedger(projectRoot) {
|
|
|
9121
9639
|
try {
|
|
9122
9640
|
await access4(path, constants2.W_OK);
|
|
9123
9641
|
const { warnings } = await readEventLedger(projectRoot);
|
|
9124
|
-
const raw = await
|
|
9642
|
+
const raw = await readFile19(path, "utf8");
|
|
9125
9643
|
const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
|
|
9126
9644
|
const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
|
|
9127
9645
|
const schemaVersionSamples = [];
|
|
@@ -9666,7 +10184,7 @@ async function inspectPreexistingRootFiles(projectRoot) {
|
|
|
9666
10184
|
const candidates = ["CLAUDE.md", "AGENTS.md"];
|
|
9667
10185
|
const detected = [];
|
|
9668
10186
|
for (const name of candidates) {
|
|
9669
|
-
if (await pathExists(
|
|
10187
|
+
if (await pathExists(join24(projectRoot, name))) {
|
|
9670
10188
|
detected.push(name);
|
|
9671
10189
|
}
|
|
9672
10190
|
}
|
|
@@ -9751,7 +10269,7 @@ async function buildLastActiveIndex(projectRoot) {
|
|
|
9751
10269
|
return map;
|
|
9752
10270
|
}
|
|
9753
10271
|
async function inspectSessionHintsStale(projectRoot, now) {
|
|
9754
|
-
const cacheDir =
|
|
10272
|
+
const cacheDir = join24(projectRoot, ".fabric", ".cache");
|
|
9755
10273
|
let entries;
|
|
9756
10274
|
try {
|
|
9757
10275
|
entries = await readdirAsync(cacheDir, { withFileTypes: true });
|
|
@@ -9763,7 +10281,7 @@ async function inspectSessionHintsStale(projectRoot, now) {
|
|
|
9763
10281
|
if (!entry.isFile()) continue;
|
|
9764
10282
|
if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
|
|
9765
10283
|
if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
|
|
9766
|
-
const absPath =
|
|
10284
|
+
const absPath = join24(cacheDir, entry.name);
|
|
9767
10285
|
let mtimeMs = 0;
|
|
9768
10286
|
try {
|
|
9769
10287
|
mtimeMs = (await statAsync(absPath)).mtimeMs;
|
|
@@ -9795,9 +10313,9 @@ function inspectStaleServeLock(projectRoot, now) {
|
|
|
9795
10313
|
};
|
|
9796
10314
|
}
|
|
9797
10315
|
async function readUnderseedThresholdFromConfig(projectRoot) {
|
|
9798
|
-
const configPath =
|
|
10316
|
+
const configPath = join24(projectRoot, ".fabric", "fabric-config.json");
|
|
9799
10317
|
try {
|
|
9800
|
-
const raw = await
|
|
10318
|
+
const raw = await readFile19(configPath, "utf8");
|
|
9801
10319
|
const parsed = JSON.parse(raw);
|
|
9802
10320
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
9803
10321
|
const v = parsed.underseed_node_threshold;
|
|
@@ -9913,10 +10431,10 @@ async function inspectOnboardCoverage(projectRoot) {
|
|
|
9913
10431
|
return { filled, missing, opted_out: optedOut };
|
|
9914
10432
|
}
|
|
9915
10433
|
async function readOnboardOptedOut(projectRoot) {
|
|
9916
|
-
const path =
|
|
10434
|
+
const path = join24(projectRoot, ".fabric", "fabric-config.json");
|
|
9917
10435
|
let raw;
|
|
9918
10436
|
try {
|
|
9919
|
-
raw = await
|
|
10437
|
+
raw = await readFile19(path, "utf8");
|
|
9920
10438
|
} catch {
|
|
9921
10439
|
return [];
|
|
9922
10440
|
}
|
|
@@ -10018,22 +10536,22 @@ async function* iterateCanonicalFilenames(projectRoot) {
|
|
|
10018
10536
|
}
|
|
10019
10537
|
}
|
|
10020
10538
|
async function rewriteThreeEndManagedBlocks(projectRoot) {
|
|
10021
|
-
const snapshotPath =
|
|
10539
|
+
const snapshotPath = join24(projectRoot, ".fabric", "AGENTS.md");
|
|
10022
10540
|
if (!await pathExists(snapshotPath)) {
|
|
10023
10541
|
return;
|
|
10024
10542
|
}
|
|
10025
10543
|
let snapshot;
|
|
10026
10544
|
try {
|
|
10027
|
-
snapshot = await
|
|
10545
|
+
snapshot = await readFile19(snapshotPath, "utf8");
|
|
10028
10546
|
} catch {
|
|
10029
10547
|
return;
|
|
10030
10548
|
}
|
|
10031
|
-
const projectRulesPath =
|
|
10549
|
+
const projectRulesPath = join24(projectRoot, ".fabric", "project-rules.md");
|
|
10032
10550
|
const hasProjectRules = await pathExists(projectRulesPath);
|
|
10033
10551
|
let expectedBody = snapshot;
|
|
10034
10552
|
if (hasProjectRules) {
|
|
10035
10553
|
try {
|
|
10036
|
-
const projectRules = await
|
|
10554
|
+
const projectRules = await readFile19(projectRulesPath, "utf8");
|
|
10037
10555
|
expectedBody = `${snapshot}
|
|
10038
10556
|
---
|
|
10039
10557
|
${projectRules}`;
|
|
@@ -10044,7 +10562,7 @@ ${projectRules}`;
|
|
|
10044
10562
|
${expectedBody}
|
|
10045
10563
|
${BOOTSTRAP_MARKER_END2}`;
|
|
10046
10564
|
const blockTargets = [
|
|
10047
|
-
|
|
10565
|
+
join24(projectRoot, "AGENTS.md")
|
|
10048
10566
|
];
|
|
10049
10567
|
for (const abs of blockTargets) {
|
|
10050
10568
|
if (!await pathExists(abs)) {
|
|
@@ -10052,7 +10570,7 @@ ${BOOTSTRAP_MARKER_END2}`;
|
|
|
10052
10570
|
}
|
|
10053
10571
|
let existing;
|
|
10054
10572
|
try {
|
|
10055
|
-
existing = await
|
|
10573
|
+
existing = await readFile19(abs, "utf8");
|
|
10056
10574
|
} catch {
|
|
10057
10575
|
continue;
|
|
10058
10576
|
}
|
|
@@ -10077,11 +10595,11 @@ ${managedBlock}
|
|
|
10077
10595
|
}
|
|
10078
10596
|
await atomicWriteText4(abs, next);
|
|
10079
10597
|
}
|
|
10080
|
-
const claudeMdPath =
|
|
10598
|
+
const claudeMdPath = join24(projectRoot, "CLAUDE.md");
|
|
10081
10599
|
if (await pathExists(claudeMdPath)) {
|
|
10082
10600
|
let claudeContent;
|
|
10083
10601
|
try {
|
|
10084
|
-
claudeContent = await
|
|
10602
|
+
claudeContent = await readFile19(claudeMdPath, "utf8");
|
|
10085
10603
|
} catch {
|
|
10086
10604
|
return;
|
|
10087
10605
|
}
|
|
@@ -10108,7 +10626,7 @@ ${managedBlock}
|
|
|
10108
10626
|
async function ensureEventLedger(projectRoot) {
|
|
10109
10627
|
const path = getEventLedgerPath(projectRoot);
|
|
10110
10628
|
await ensureParentDirectory(path);
|
|
10111
|
-
await
|
|
10629
|
+
await writeFile5(path, "", { encoding: "utf8", flag: "a" });
|
|
10112
10630
|
}
|
|
10113
10631
|
function createFixMessage(fixed, report) {
|
|
10114
10632
|
const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
|
|
@@ -10147,7 +10665,7 @@ async function collectEntryPoints(root) {
|
|
|
10147
10665
|
continue;
|
|
10148
10666
|
}
|
|
10149
10667
|
for (const entry of await readdirAsync(current, { withFileTypes: true })) {
|
|
10150
|
-
const absolutePath =
|
|
10668
|
+
const absolutePath = join24(current, entry.name);
|
|
10151
10669
|
const relativePath = normalizePath2(absolutePath.slice(root.length + 1));
|
|
10152
10670
|
if (relativePath.length === 0) {
|
|
10153
10671
|
continue;
|
|
@@ -10223,7 +10741,7 @@ async function enrichDescriptions(projectRoot, opts = {}) {
|
|
|
10223
10741
|
scanned += 1;
|
|
10224
10742
|
let source;
|
|
10225
10743
|
try {
|
|
10226
|
-
source = await
|
|
10744
|
+
source = await readFile19(absPath, "utf8");
|
|
10227
10745
|
} catch {
|
|
10228
10746
|
continue;
|
|
10229
10747
|
}
|
|
@@ -10361,16 +10879,16 @@ async function runDoctorConflictLint(projectRoot, opts = {}) {
|
|
|
10361
10879
|
}
|
|
10362
10880
|
|
|
10363
10881
|
// src/services/why-not-surfaced.ts
|
|
10364
|
-
import { readFile as
|
|
10365
|
-
import { basename as basename3, join as
|
|
10882
|
+
import { readFile as readFile20 } from "fs/promises";
|
|
10883
|
+
import { basename as basename3, join as join25 } from "path";
|
|
10366
10884
|
import {
|
|
10367
|
-
buildStoreResolveInput as
|
|
10368
|
-
createStoreResolver as
|
|
10369
|
-
loadProjectConfig as
|
|
10885
|
+
buildStoreResolveInput as buildStoreResolveInput8,
|
|
10886
|
+
createStoreResolver as createStoreResolver8,
|
|
10887
|
+
loadProjectConfig as loadProjectConfig5,
|
|
10370
10888
|
readKnowledgeAcrossStores as readKnowledgeAcrossStores4,
|
|
10371
|
-
resolveGlobalRoot as
|
|
10889
|
+
resolveGlobalRoot as resolveGlobalRoot9,
|
|
10372
10890
|
scopeRoot as scopeRoot3,
|
|
10373
|
-
storeRelativePathForMount as
|
|
10891
|
+
storeRelativePathForMount as storeRelativePathForMount7
|
|
10374
10892
|
} from "@fenglimg/fabric-shared";
|
|
10375
10893
|
var SEMANTIC_SCOPE_LINE3 = /^semantic_scope:\s*"?([^"\n]+?)"?\s*$/mu;
|
|
10376
10894
|
var RELEVANCE_SCOPE_LINE = /^relevance_scope:\s*"?(broad|narrow)"?\s*$/mu;
|
|
@@ -10394,15 +10912,15 @@ async function explainWhyNotSurfaced(projectRoot, query) {
|
|
|
10394
10912
|
activeProject: null,
|
|
10395
10913
|
relevanceScope: null
|
|
10396
10914
|
};
|
|
10397
|
-
const input =
|
|
10915
|
+
const input = buildStoreResolveInput8(projectRoot);
|
|
10398
10916
|
if (input === null) {
|
|
10399
10917
|
return base;
|
|
10400
10918
|
}
|
|
10401
|
-
const globalRoot =
|
|
10919
|
+
const globalRoot = resolveGlobalRoot9();
|
|
10402
10920
|
const allStores = input.mountedStores.map((s) => ({
|
|
10403
10921
|
store_uuid: s.store_uuid,
|
|
10404
10922
|
alias: s.alias,
|
|
10405
|
-
dir:
|
|
10923
|
+
dir: join25(globalRoot, storeRelativePathForMount7(s))
|
|
10406
10924
|
}));
|
|
10407
10925
|
const refs = await readKnowledgeAcrossStores4(allStores);
|
|
10408
10926
|
const candidate = refs.find((ref) => fileMatchesId(ref.file, localId));
|
|
@@ -10411,7 +10929,7 @@ async function explainWhyNotSurfaced(projectRoot, query) {
|
|
|
10411
10929
|
}
|
|
10412
10930
|
let source;
|
|
10413
10931
|
try {
|
|
10414
|
-
source = await
|
|
10932
|
+
source = await readFile20(candidate.file, "utf8");
|
|
10415
10933
|
} catch {
|
|
10416
10934
|
return base;
|
|
10417
10935
|
}
|
|
@@ -10420,9 +10938,9 @@ async function explainWhyNotSurfaced(projectRoot, query) {
|
|
|
10420
10938
|
}
|
|
10421
10939
|
const semanticScope = SEMANTIC_SCOPE_LINE3.exec(source)?.[1] ?? null;
|
|
10422
10940
|
const relevanceScope = RELEVANCE_SCOPE_LINE.exec(source)?.[1] ?? "broad";
|
|
10423
|
-
const activeProject =
|
|
10941
|
+
const activeProject = loadProjectConfig5(projectRoot)?.active_project ?? null;
|
|
10424
10942
|
const boundUuids = new Set(
|
|
10425
|
-
|
|
10943
|
+
createStoreResolver8().resolveReadSet(input).stores.map((s) => s.store_uuid)
|
|
10426
10944
|
);
|
|
10427
10945
|
const storeBound = boundUuids.has(candidate.store_uuid);
|
|
10428
10946
|
const found = {
|
|
@@ -10464,6 +10982,191 @@ function buildColdEvalBatch(candidates) {
|
|
|
10464
10982
|
return { rubric: COLD_EVAL_RUBRIC, candidates: judgeable };
|
|
10465
10983
|
}
|
|
10466
10984
|
|
|
10985
|
+
// src/services/doctor-related-graph.ts
|
|
10986
|
+
function buildRelatedGraph(nodes) {
|
|
10987
|
+
const brokenLinks = [];
|
|
10988
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
10989
|
+
const allIds = /* @__PURE__ */ new Set();
|
|
10990
|
+
const edgeSources = [];
|
|
10991
|
+
for (const node of nodes) {
|
|
10992
|
+
const bareId = extractBareStableId(node.qualifiedId);
|
|
10993
|
+
allIds.add(node.qualifiedId);
|
|
10994
|
+
if (bareId !== null) {
|
|
10995
|
+
allIds.add(bareId);
|
|
10996
|
+
}
|
|
10997
|
+
const related = node.related;
|
|
10998
|
+
if (related !== void 0 && related.length > 0) {
|
|
10999
|
+
edgeSources.push({ source: node.qualifiedId, related });
|
|
11000
|
+
for (const rel of related) {
|
|
11001
|
+
inDegree.set(rel, (inDegree.get(rel) ?? 0) + 1);
|
|
11002
|
+
}
|
|
11003
|
+
}
|
|
11004
|
+
}
|
|
11005
|
+
for (const { source, related } of edgeSources) {
|
|
11006
|
+
for (const rel of related) {
|
|
11007
|
+
if (!allIds.has(rel)) {
|
|
11008
|
+
brokenLinks.push({ source, target: rel });
|
|
11009
|
+
}
|
|
11010
|
+
}
|
|
11011
|
+
}
|
|
11012
|
+
const hubEntries = [...inDegree.entries()].map(([stableId, degree]) => ({ stableId, inDegree: degree })).sort((a, b) => b.inDegree - a.inDegree || a.stableId.localeCompare(b.stableId));
|
|
11013
|
+
return {
|
|
11014
|
+
brokenLinks,
|
|
11015
|
+
hubEntries,
|
|
11016
|
+
totalEntries: nodes.length
|
|
11017
|
+
};
|
|
11018
|
+
}
|
|
11019
|
+
function extractBareStableId(qualifiedId) {
|
|
11020
|
+
const colonIdx = qualifiedId.indexOf(":");
|
|
11021
|
+
if (colonIdx > 0 && colonIdx < qualifiedId.length - 1) {
|
|
11022
|
+
return qualifiedId.slice(colonIdx + 1);
|
|
11023
|
+
}
|
|
11024
|
+
return null;
|
|
11025
|
+
}
|
|
11026
|
+
async function inspectRelatedGraph(projectRoot) {
|
|
11027
|
+
const entries = await collectStoreCanonicalEntries(projectRoot);
|
|
11028
|
+
return buildRelatedGraph(
|
|
11029
|
+
entries.map((entry) => ({
|
|
11030
|
+
qualifiedId: entry.qualifiedId,
|
|
11031
|
+
related: entry.description.related
|
|
11032
|
+
}))
|
|
11033
|
+
);
|
|
11034
|
+
}
|
|
11035
|
+
|
|
11036
|
+
// src/services/store-precheck.ts
|
|
11037
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
|
|
11038
|
+
import { join as join26 } from "path";
|
|
11039
|
+
import {
|
|
11040
|
+
buildStoreResolveInput as buildStoreResolveInput9,
|
|
11041
|
+
createStoreResolver as createStoreResolver9,
|
|
11042
|
+
resolveGlobalRoot as resolveGlobalRoot10,
|
|
11043
|
+
storeRelativePathForMount as storeRelativePathForMount8
|
|
11044
|
+
} from "@fenglimg/fabric-shared";
|
|
11045
|
+
var CACHE_TTL_MS = 6e4;
|
|
11046
|
+
var cache = /* @__PURE__ */ new Map();
|
|
11047
|
+
function clearPrecheckCache() {
|
|
11048
|
+
cache.clear();
|
|
11049
|
+
}
|
|
11050
|
+
function evaluateStoreDir(storeDir, identity) {
|
|
11051
|
+
if (!existsSync9(storeDir)) {
|
|
11052
|
+
return {
|
|
11053
|
+
uuid: identity.uuid,
|
|
11054
|
+
alias: identity.alias,
|
|
11055
|
+
reachable: false,
|
|
11056
|
+
reason: `directory not found at ${storeDir}`
|
|
11057
|
+
};
|
|
11058
|
+
}
|
|
11059
|
+
const storeJsonPath = join26(storeDir, "store.json");
|
|
11060
|
+
const gitDirPath = join26(storeDir, ".git");
|
|
11061
|
+
const hasStoreJson = existsSync9(storeJsonPath) && isValidStoreJson(storeJsonPath);
|
|
11062
|
+
const hasGitDir = existsSync9(gitDirPath);
|
|
11063
|
+
if (!hasStoreJson && !hasGitDir) {
|
|
11064
|
+
return {
|
|
11065
|
+
uuid: identity.uuid,
|
|
11066
|
+
alias: identity.alias,
|
|
11067
|
+
reachable: false,
|
|
11068
|
+
reason: `no store.json or .git found in ${storeDir}`
|
|
11069
|
+
};
|
|
11070
|
+
}
|
|
11071
|
+
return { uuid: identity.uuid, alias: identity.alias, reachable: true };
|
|
11072
|
+
}
|
|
11073
|
+
function isValidStoreJson(path) {
|
|
11074
|
+
try {
|
|
11075
|
+
JSON.parse(readFileSync4(path, "utf8"));
|
|
11076
|
+
return true;
|
|
11077
|
+
} catch {
|
|
11078
|
+
return false;
|
|
11079
|
+
}
|
|
11080
|
+
}
|
|
11081
|
+
async function precheckStoreReachability(projectRoot, globalRoot = resolveGlobalRoot10(), now = Date.now()) {
|
|
11082
|
+
const cached = cache.get(projectRoot);
|
|
11083
|
+
if (cached !== void 0 && cached.expiresAt > now) {
|
|
11084
|
+
return cached.result;
|
|
11085
|
+
}
|
|
11086
|
+
const input = buildStoreResolveInput9(projectRoot, globalRoot);
|
|
11087
|
+
if (input === null) {
|
|
11088
|
+
const result2 = { stores: [], allReachable: true };
|
|
11089
|
+
cache.set(projectRoot, { result: result2, expiresAt: now + CACHE_TTL_MS });
|
|
11090
|
+
return result2;
|
|
11091
|
+
}
|
|
11092
|
+
const readSet = createStoreResolver9().resolveReadSet(input);
|
|
11093
|
+
const stores = readSet.stores.map((entry) => {
|
|
11094
|
+
const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
|
|
11095
|
+
const storeDir = join26(
|
|
11096
|
+
globalRoot,
|
|
11097
|
+
storeRelativePathForMount8(mounted ?? { store_uuid: entry.store_uuid })
|
|
11098
|
+
);
|
|
11099
|
+
return evaluateStoreDir(storeDir, { uuid: entry.store_uuid, alias: entry.alias });
|
|
11100
|
+
});
|
|
11101
|
+
const result = {
|
|
11102
|
+
stores,
|
|
11103
|
+
allReachable: stores.every((s) => s.reachable)
|
|
11104
|
+
};
|
|
11105
|
+
cache.set(projectRoot, { result, expiresAt: now + CACHE_TTL_MS });
|
|
11106
|
+
return result;
|
|
11107
|
+
}
|
|
11108
|
+
|
|
11109
|
+
// src/services/doctor-consumption-lint.ts
|
|
11110
|
+
var DEFAULT_WINDOW_DAYS = 30;
|
|
11111
|
+
var DEFAULT_TOP_N = 10;
|
|
11112
|
+
var DEFAULT_MIN_TOTAL_ENTRIES = 20;
|
|
11113
|
+
var DEFAULT_MIN_CONSUMED_WINDOWS = 30;
|
|
11114
|
+
var DEFAULT_MIN_CONSUMED_EVENTS = 50;
|
|
11115
|
+
var CONSUMED_PREFIX = "knowledge_consumed:";
|
|
11116
|
+
var MS_PER_DAY5 = 24 * 60 * 60 * 1e3;
|
|
11117
|
+
function resolveConfig(config) {
|
|
11118
|
+
return {
|
|
11119
|
+
windowDays: config?.windowDays ?? DEFAULT_WINDOW_DAYS,
|
|
11120
|
+
topN: config?.topN ?? DEFAULT_TOP_N,
|
|
11121
|
+
minTotalEntries: config?.minTotalEntries ?? DEFAULT_MIN_TOTAL_ENTRIES,
|
|
11122
|
+
minConsumedWindows: config?.minConsumedWindows ?? DEFAULT_MIN_CONSUMED_WINDOWS,
|
|
11123
|
+
minConsumedEvents: config?.minConsumedEvents ?? DEFAULT_MIN_CONSUMED_EVENTS
|
|
11124
|
+
};
|
|
11125
|
+
}
|
|
11126
|
+
function aggregateConsumption(rows, allQualifiedIds, now, config) {
|
|
11127
|
+
const cfg = resolveConfig(config);
|
|
11128
|
+
const cutoffMs = now - cfg.windowDays * MS_PER_DAY5;
|
|
11129
|
+
const consumed = /* @__PURE__ */ new Map();
|
|
11130
|
+
let consumedWindows = 0;
|
|
11131
|
+
let totalConsumedEvents = 0;
|
|
11132
|
+
for (const row of rows) {
|
|
11133
|
+
const rowTs = Date.parse(row.timestamp);
|
|
11134
|
+
if (!Number.isFinite(rowTs) || rowTs < cutoffMs) continue;
|
|
11135
|
+
if (row.counters === null || typeof row.counters !== "object") continue;
|
|
11136
|
+
let windowHadConsumption = false;
|
|
11137
|
+
for (const [counterName, rawCount] of Object.entries(row.counters)) {
|
|
11138
|
+
if (!counterName.startsWith(CONSUMED_PREFIX)) continue;
|
|
11139
|
+
const count = typeof rawCount === "number" ? rawCount : 0;
|
|
11140
|
+
if (count <= 0) continue;
|
|
11141
|
+
const id = counterName.slice(CONSUMED_PREFIX.length);
|
|
11142
|
+
if (id.length === 0) continue;
|
|
11143
|
+
consumed.set(id, (consumed.get(id) ?? 0) + count);
|
|
11144
|
+
totalConsumedEvents += count;
|
|
11145
|
+
windowHadConsumption = true;
|
|
11146
|
+
}
|
|
11147
|
+
if (windowHadConsumption) consumedWindows += 1;
|
|
11148
|
+
}
|
|
11149
|
+
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);
|
|
11150
|
+
const dataMature = consumedWindows >= cfg.minConsumedWindows && totalConsumedEvents >= cfg.minConsumedEvents && allQualifiedIds.length >= cfg.minTotalEntries;
|
|
11151
|
+
const zeroConsumed = dataMature ? allQualifiedIds.filter((id) => !consumed.has(id)).sort((a, b) => a.localeCompare(b)) : [];
|
|
11152
|
+
return {
|
|
11153
|
+
topConsumed,
|
|
11154
|
+
zeroConsumed,
|
|
11155
|
+
totalEntries: allQualifiedIds.length,
|
|
11156
|
+
consumedEntries: consumed.size,
|
|
11157
|
+
consumedWindows,
|
|
11158
|
+
totalConsumedEvents,
|
|
11159
|
+
dataMature,
|
|
11160
|
+
windowDays: cfg.windowDays
|
|
11161
|
+
};
|
|
11162
|
+
}
|
|
11163
|
+
async function inspectConsumption(projectRoot, config, now = Date.now()) {
|
|
11164
|
+
const rows = await readMetrics(projectRoot);
|
|
11165
|
+
const entries = await collectStoreCanonicalEntries(projectRoot);
|
|
11166
|
+
const allQualifiedIds = entries.map((entry) => entry.qualifiedId);
|
|
11167
|
+
return aggregateConsumption(rows, allQualifiedIds, now, config);
|
|
11168
|
+
}
|
|
11169
|
+
|
|
10467
11170
|
// src/services/rotation-tick.ts
|
|
10468
11171
|
var DEFAULT_TICK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
10469
11172
|
var tickTimers = /* @__PURE__ */ new Map();
|
|
@@ -10499,7 +11202,7 @@ import { IOFabricError as IOFabricError2, RuleError } from "@fenglimg/fabric-sha
|
|
|
10499
11202
|
|
|
10500
11203
|
// src/services/read-ledger.ts
|
|
10501
11204
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
10502
|
-
import { access as access5, copyFile, readFile as
|
|
11205
|
+
import { access as access5, copyFile, readFile as readFile21, rm } from "fs/promises";
|
|
10503
11206
|
import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
|
|
10504
11207
|
async function resolveLedgerPaths(projectRoot) {
|
|
10505
11208
|
const primaryPath = getLedgerPath(projectRoot);
|
|
@@ -10527,7 +11230,7 @@ async function readLegacyLedger(projectRoot) {
|
|
|
10527
11230
|
const { readPath } = await resolveLedgerPaths(projectRoot);
|
|
10528
11231
|
let raw;
|
|
10529
11232
|
try {
|
|
10530
|
-
raw = await
|
|
11233
|
+
raw = await readFile21(readPath, "utf8");
|
|
10531
11234
|
} catch (error) {
|
|
10532
11235
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
10533
11236
|
return [];
|
|
@@ -10782,8 +11485,8 @@ function formatError(error) {
|
|
|
10782
11485
|
}
|
|
10783
11486
|
function formatPreexistingRootMessage(projectRoot) {
|
|
10784
11487
|
const preexisting = [];
|
|
10785
|
-
if (
|
|
10786
|
-
if (
|
|
11488
|
+
if (existsSync10(join27(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
|
|
11489
|
+
if (existsSync10(join27(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
|
|
10787
11490
|
if (preexisting.length === 0) return null;
|
|
10788
11491
|
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.`;
|
|
10789
11492
|
}
|
|
@@ -10810,7 +11513,7 @@ function createFabricServer(tracker) {
|
|
|
10810
11513
|
const server = new McpServer(
|
|
10811
11514
|
{
|
|
10812
11515
|
name: "fabric-knowledge-server",
|
|
10813
|
-
version: "2.3.0-rc.
|
|
11516
|
+
version: "2.3.0-rc.5"
|
|
10814
11517
|
},
|
|
10815
11518
|
{
|
|
10816
11519
|
instructions: FABRIC_SERVER_INSTRUCTIONS
|
|
@@ -10830,10 +11533,10 @@ function createFabricServer(tracker) {
|
|
|
10830
11533
|
},
|
|
10831
11534
|
async (_uri) => {
|
|
10832
11535
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
10833
|
-
const path =
|
|
11536
|
+
const path = join27(projectRoot, ".fabric", "bootstrap", "README.md");
|
|
10834
11537
|
let text = "";
|
|
10835
|
-
if (
|
|
10836
|
-
text = await
|
|
11538
|
+
if (existsSync10(path)) {
|
|
11539
|
+
text = await readFile22(path, "utf8");
|
|
10837
11540
|
}
|
|
10838
11541
|
return {
|
|
10839
11542
|
contents: [
|
|
@@ -10933,11 +11636,15 @@ export {
|
|
|
10933
11636
|
METRIC_COUNTER_NAMES,
|
|
10934
11637
|
OPTIONAL_EMBED_PACKAGE,
|
|
10935
11638
|
RETIRED_TOKENS,
|
|
11639
|
+
aggregateConsumption,
|
|
10936
11640
|
appendEventLedgerEvent,
|
|
10937
11641
|
buildAlwaysActiveBodies,
|
|
10938
11642
|
buildColdEvalBatch,
|
|
10939
11643
|
buildKnowledgeCensus,
|
|
11644
|
+
buildRelatedGraph,
|
|
10940
11645
|
bumpCounter,
|
|
11646
|
+
clearPrecheckCache,
|
|
11647
|
+
collectStoreCanonicalEntries,
|
|
10941
11648
|
contextCache,
|
|
10942
11649
|
createFabricServer,
|
|
10943
11650
|
createInFlightTracker,
|
|
@@ -10946,6 +11653,7 @@ export {
|
|
|
10946
11653
|
detectUnboundProject,
|
|
10947
11654
|
drainCounters,
|
|
10948
11655
|
enrichDescriptions,
|
|
11656
|
+
evaluateStoreDir,
|
|
10949
11657
|
explainWhyNotSurfaced,
|
|
10950
11658
|
extractKnowledge,
|
|
10951
11659
|
findConflictCandidates,
|
|
@@ -10956,12 +11664,16 @@ export {
|
|
|
10956
11664
|
getLedgerPath,
|
|
10957
11665
|
getLegacyLedgerPath,
|
|
10958
11666
|
getMetricsLedgerPath,
|
|
11667
|
+
inspectConsumption,
|
|
11668
|
+
inspectRelatedGraph,
|
|
10959
11669
|
inspectRetiredReferences,
|
|
11670
|
+
isEmbedderResolvable,
|
|
10960
11671
|
lintConflicts,
|
|
10961
11672
|
loadConflictEntries,
|
|
10962
11673
|
loadEmbedder,
|
|
10963
11674
|
pairSimilarity,
|
|
10964
11675
|
planContext,
|
|
11676
|
+
precheckStoreReachability,
|
|
10965
11677
|
readEmbedConfig,
|
|
10966
11678
|
readEventLedger,
|
|
10967
11679
|
readFusion,
|