@fenglimg/fabric-server 2.2.0-rc.9 → 2.3.0-rc.1
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/README.md +1 -1
- package/dist/index.d.ts +87 -20
- package/dist/index.js +2232 -1480
- 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 existsSync9 } from "fs";
|
|
3
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
4
|
+
import { join as join23, 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";
|
|
@@ -675,12 +675,14 @@ function createInFlightTracker() {
|
|
|
675
675
|
|
|
676
676
|
// src/tools/extract-knowledge.ts
|
|
677
677
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
678
|
+
import { ZodError } from "zod";
|
|
678
679
|
import {
|
|
679
680
|
FabExtractKnowledgeInputShape,
|
|
680
681
|
FabExtractKnowledgeInputSchema,
|
|
681
682
|
FabExtractKnowledgeOutputSchema,
|
|
682
683
|
fabExtractKnowledgeAnnotations
|
|
683
684
|
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
685
|
+
import { SCOPE_COORDINATE_HINT } from "@fenglimg/fabric-shared";
|
|
684
686
|
import { enforcePayloadLimit } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
685
687
|
|
|
686
688
|
// src/tools/payload-warning.ts
|
|
@@ -782,6 +784,18 @@ function readRecallRelevanceRatio(projectRoot) {
|
|
|
782
784
|
return RECALL_RELEVANCE_RATIO_DEFAULT;
|
|
783
785
|
}
|
|
784
786
|
}
|
|
787
|
+
var BROAD_REVIEW_RECHECK_DAYS_DEFAULT = 180;
|
|
788
|
+
function readBroadReviewRecheckThresholdDays(projectRoot) {
|
|
789
|
+
try {
|
|
790
|
+
const raw = readFabricConfig(projectRoot).broad_review_recheck_days;
|
|
791
|
+
if (typeof raw === "number" && Number.isFinite(raw) && Number.isInteger(raw) && raw >= 1 && raw <= 3650) {
|
|
792
|
+
return raw;
|
|
793
|
+
}
|
|
794
|
+
return BROAD_REVIEW_RECHECK_DAYS_DEFAULT;
|
|
795
|
+
} catch {
|
|
796
|
+
return BROAD_REVIEW_RECHECK_DAYS_DEFAULT;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
785
799
|
function readOrphanDemoteThresholdDays(projectRoot) {
|
|
786
800
|
try {
|
|
787
801
|
const cfg = readFabricConfig(projectRoot);
|
|
@@ -870,8 +884,8 @@ async function awaitFirstReconcileGate(timeoutMs = 5e3) {
|
|
|
870
884
|
|
|
871
885
|
// src/services/extract-knowledge.ts
|
|
872
886
|
import { existsSync as existsSync3 } from "fs";
|
|
873
|
-
import { readFile as
|
|
874
|
-
import { join as
|
|
887
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
888
|
+
import { join as join7 } from "path";
|
|
875
889
|
import {
|
|
876
890
|
PROPOSED_REASON_DESCRIPTIONS_BY_LOCALE
|
|
877
891
|
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
@@ -894,7 +908,7 @@ import {
|
|
|
894
908
|
StoreWriteTargetUnresolvedError
|
|
895
909
|
} from "@fenglimg/fabric-shared/errors";
|
|
896
910
|
function writeTargetUnresolved(scope, layer) {
|
|
897
|
-
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
|
|
911
|
+
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}\``;
|
|
898
912
|
return new StoreWriteTargetUnresolvedError(
|
|
899
913
|
`no write-target store resolved for scope '${scope}' \u2014 knowledge writes are store-only (dual-root co-location removed)`,
|
|
900
914
|
{ actionHint, fixable: true, details: { layer, scope } }
|
|
@@ -954,1169 +968,1443 @@ function resolveWriteScopeMeta(layer, projectRoot, semanticScope) {
|
|
|
954
968
|
return { semantic_scope, visibility_store: target.alias };
|
|
955
969
|
}
|
|
956
970
|
|
|
957
|
-
// src/services/
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
971
|
+
// src/services/cross-store-recall.ts
|
|
972
|
+
import { readFile as readFile3, stat } from "fs/promises";
|
|
973
|
+
import { join as join6 } from "path";
|
|
974
|
+
import {
|
|
975
|
+
buildStoreResolveInput as buildStoreResolveInput2,
|
|
976
|
+
createStoreResolver as createStoreResolver2,
|
|
977
|
+
loadProjectConfig as loadProjectConfig2,
|
|
978
|
+
readKnowledgeAcrossStores,
|
|
979
|
+
resolveGlobalRoot as resolveGlobalRoot2,
|
|
980
|
+
scopeRoot,
|
|
981
|
+
storeRelativePathForMount as storeRelativePathForMount2
|
|
982
|
+
} from "@fenglimg/fabric-shared";
|
|
983
|
+
|
|
984
|
+
// src/services/knowledge-meta-builder.ts
|
|
985
|
+
import {
|
|
986
|
+
deriveAgentsMetaStableId,
|
|
987
|
+
isKnowledgeStableId,
|
|
988
|
+
KnowledgeTypeSchema,
|
|
989
|
+
LayerSchema,
|
|
990
|
+
MaturitySchema,
|
|
991
|
+
parseKnowledgeId,
|
|
992
|
+
StableIdSchema
|
|
993
|
+
} from "@fenglimg/fabric-shared";
|
|
994
|
+
function toAgentsCompatiblePath(contentRef) {
|
|
995
|
+
return contentRef.replace(/^~\/\.fabric\/knowledge\//u, ".fabric/agents/").replace(/^\.fabric\/knowledge\//u, ".fabric/agents/");
|
|
996
|
+
}
|
|
997
|
+
function deriveRuleIdentity(file, source, existing) {
|
|
998
|
+
const declaredKnowledgeId = extractDeclaredKnowledgeId(source);
|
|
999
|
+
if (declaredKnowledgeId !== void 0) {
|
|
1000
|
+
return {
|
|
1001
|
+
stableId: declaredKnowledgeId,
|
|
1002
|
+
identitySource: "declared"
|
|
1003
|
+
};
|
|
987
1004
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
for (const { name, pattern } of INJECTION_PATTERNS) {
|
|
994
|
-
const matches = sanitized.match(pattern);
|
|
995
|
-
if (matches === null || matches.length === 0) continue;
|
|
996
|
-
redactions.push({ name, matches: matches.length });
|
|
997
|
-
sanitized = sanitized.replace(pattern, INJECTION_REDACTION_MARKER);
|
|
1005
|
+
if (existing?.stable_id !== void 0 && isKnowledgeStableId(existing.stable_id)) {
|
|
1006
|
+
return {
|
|
1007
|
+
stableId: existing.stable_id,
|
|
1008
|
+
identitySource: "declared"
|
|
1009
|
+
};
|
|
998
1010
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
const { sanitized, redactions } = sanitizeInjectionPatterns(value);
|
|
1007
|
-
out[key] = sanitized;
|
|
1008
|
-
for (const r of redactions) {
|
|
1009
|
-
allRedactions.push({ field: key, ...r });
|
|
1010
|
-
}
|
|
1011
|
-
} else if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
|
|
1012
|
-
const cleaned = [];
|
|
1013
|
-
for (const entry of value) {
|
|
1014
|
-
const { sanitized, redactions } = sanitizeInjectionPatterns(entry);
|
|
1015
|
-
cleaned.push(sanitized);
|
|
1016
|
-
for (const r of redactions) {
|
|
1017
|
-
allRedactions.push({ field: key, ...r });
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
out[key] = cleaned;
|
|
1021
|
-
}
|
|
1011
|
+
const declaredStableId = extractDeclaredStableId(source);
|
|
1012
|
+
const derivedStableId = deriveAgentsMetaStableId(toAgentsCompatiblePath(file));
|
|
1013
|
+
if (declaredStableId !== void 0) {
|
|
1014
|
+
return {
|
|
1015
|
+
stableId: declaredStableId,
|
|
1016
|
+
identitySource: "declared"
|
|
1017
|
+
};
|
|
1022
1018
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
for (const [key, value] of Object.entries(fields)) {
|
|
1029
|
-
if (typeof value === "string") {
|
|
1030
|
-
const cleaned = redactPii(value);
|
|
1031
|
-
out[key] = cleaned;
|
|
1032
|
-
if (cleaned !== value) redactedFields.add(key);
|
|
1033
|
-
} else if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
|
|
1034
|
-
const cleaned = value.map((entry) => redactPii(entry));
|
|
1035
|
-
out[key] = cleaned;
|
|
1036
|
-
if (cleaned.some((entry, index) => entry !== value[index])) {
|
|
1037
|
-
redactedFields.add(key);
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1019
|
+
if (existing?.identity_source === "declared" && existing.stable_id !== void 0 && existing.stable_id !== derivedStableId) {
|
|
1020
|
+
return {
|
|
1021
|
+
stableId: existing.stable_id,
|
|
1022
|
+
identitySource: "declared"
|
|
1023
|
+
};
|
|
1040
1024
|
}
|
|
1041
|
-
return {
|
|
1025
|
+
return {
|
|
1026
|
+
stableId: derivedStableId,
|
|
1027
|
+
identitySource: "derived"
|
|
1028
|
+
};
|
|
1042
1029
|
}
|
|
1043
|
-
function
|
|
1044
|
-
|
|
1030
|
+
function extractDeclaredStableId(source) {
|
|
1031
|
+
const match = /^(?:\uFEFF)?(?:---\r?\n[\s\S]*?\r?\n---\s*(?:\r?\n|$))?<!--\s*fab:rule-id\s+([A-Za-z0-9][A-Za-z0-9/_-]*)\s*-->\s*(?:\r?\n|$)/u.exec(source);
|
|
1032
|
+
return match?.[1];
|
|
1045
1033
|
}
|
|
1046
|
-
function
|
|
1047
|
-
|
|
1034
|
+
function extractDeclaredKnowledgeId(source) {
|
|
1035
|
+
const frontmatter = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/u.exec(source);
|
|
1036
|
+
if (frontmatter === null) {
|
|
1037
|
+
return void 0;
|
|
1038
|
+
}
|
|
1039
|
+
const idMatch = /^id:\s*(.+?)\s*$/mu.exec(frontmatter[1]);
|
|
1040
|
+
if (idMatch === null) {
|
|
1041
|
+
return void 0;
|
|
1042
|
+
}
|
|
1043
|
+
const candidate = idMatch[1].replace(/^["'](.*)["']$/u, "$1").trim();
|
|
1044
|
+
return isKnowledgeStableId(candidate) ? candidate : void 0;
|
|
1048
1045
|
}
|
|
1049
|
-
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
must_read_if: input.must_read_if ?? "",
|
|
1055
|
-
intent_clues: input.intent_clues ?? []
|
|
1056
|
-
});
|
|
1057
|
-
input = {
|
|
1058
|
-
...input,
|
|
1059
|
-
slug: sanitizedInputFields.sanitized.slug,
|
|
1060
|
-
user_messages_summary: sanitizedInputFields.sanitized.user_messages_summary || void 0,
|
|
1061
|
-
session_context: sanitizedInputFields.sanitized.session_context || void 0,
|
|
1062
|
-
must_read_if: sanitizedInputFields.sanitized.must_read_if || void 0,
|
|
1063
|
-
intent_clues: sanitizedInputFields.sanitized.intent_clues.length > 0 ? sanitizedInputFields.sanitized.intent_clues : void 0
|
|
1064
|
-
};
|
|
1065
|
-
if (sanitizedInputFields.allRedactions.length > 0) {
|
|
1066
|
-
const summary2 = sanitizedInputFields.allRedactions.map((r) => `${r.field}:${r.name}x${r.matches}`).join(",");
|
|
1067
|
-
const primarySessionForLog = (input.source_sessions ?? [])[0] ?? "";
|
|
1068
|
-
await emitEventBestEffort(projectRoot, {
|
|
1069
|
-
event_type: "knowledge_archive_attempted",
|
|
1070
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1071
|
-
correlation_id: primarySessionForLog,
|
|
1072
|
-
session_id: primarySessionForLog,
|
|
1073
|
-
reason: `extract_knowledge:injection-redacted:${summary2}`
|
|
1074
|
-
});
|
|
1046
|
+
function extractRuleDescription(source) {
|
|
1047
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/u.exec(source);
|
|
1048
|
+
const description = frontmatter === null ? void 0 : extractDescriptionFromFrontmatter(frontmatter[1]);
|
|
1049
|
+
if (description !== void 0) {
|
|
1050
|
+
return description;
|
|
1075
1051
|
}
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1052
|
+
const heading = /^#\s+(.+?)\s*$/mu.exec(source);
|
|
1053
|
+
const summary = heading?.[1]?.trim();
|
|
1054
|
+
const knowledge = frontmatter !== null ? extractKnowledgeFieldsFromFrontmatter(frontmatter[1]) : void 0;
|
|
1055
|
+
const isStructurallyAKnowledgeEntry = summary !== void 0 && summary.length > 0 ? true : knowledge !== void 0 && (knowledge.id !== void 0 || knowledge.knowledge_type !== void 0 || knowledge.tags !== void 0 && knowledge.tags.length > 0);
|
|
1056
|
+
if (!isStructurallyAKnowledgeEntry) {
|
|
1057
|
+
return void 0;
|
|
1058
|
+
}
|
|
1059
|
+
const synthesizedSummary = summary !== void 0 && summary.length > 0 ? summary : knowledge?.id ?? (knowledge?.tags !== void 0 && knowledge.tags.length > 0 ? `(unnamed; tags: ${knowledge.tags.join(", ")})` : "(unnamed knowledge entry)");
|
|
1060
|
+
return {
|
|
1061
|
+
summary: synthesizedSummary,
|
|
1062
|
+
intent_clues: [],
|
|
1063
|
+
tech_stack: [],
|
|
1064
|
+
impact: [],
|
|
1065
|
+
must_read_if: synthesizedSummary,
|
|
1066
|
+
// v2.0-rc.22: when frontmatter is present, merge its knowledge fields;
|
|
1067
|
+
// when fully absent (no `---` block), all knowledge fields stay
|
|
1068
|
+
// undefined, matching the original heading-only fallback contract.
|
|
1069
|
+
id: knowledge?.id,
|
|
1070
|
+
knowledge_type: knowledge?.knowledge_type,
|
|
1071
|
+
maturity: knowledge?.maturity,
|
|
1072
|
+
knowledge_layer: knowledge?.knowledge_layer,
|
|
1073
|
+
layer_reason: knowledge?.layer_reason,
|
|
1074
|
+
created_at: knowledge?.created_at,
|
|
1075
|
+
tags: knowledge?.tags,
|
|
1076
|
+
// v2.0-rc.5 (C1): default-safe values when there is no frontmatter at all;
|
|
1077
|
+
// when frontmatter exists, honor its declared values (extractKnowledge
|
|
1078
|
+
// FieldsFromFrontmatter already applies the broad-default for missing
|
|
1079
|
+
// or malformed scopes).
|
|
1080
|
+
relevance_scope: knowledge?.relevance_scope ?? "broad",
|
|
1081
|
+
relevance_paths: knowledge?.relevance_paths ?? [],
|
|
1082
|
+
// v2.2 H2-related (W1-T7): graph edges, undefined when absent.
|
|
1083
|
+
related: knowledge?.related
|
|
1094
1084
|
};
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
correlation_id: primarySessionForLog,
|
|
1101
|
-
session_id: primarySessionForLog,
|
|
1102
|
-
reason: `extract_knowledge:pii-redacted:${piiRedactedInputFields.redactedFields.join(",")}`
|
|
1103
|
-
});
|
|
1085
|
+
}
|
|
1086
|
+
function extractDescriptionFromFrontmatter(frontmatter) {
|
|
1087
|
+
const summary = extractScalar(frontmatter, "summary") ?? extractScalar(frontmatter, "description");
|
|
1088
|
+
if (summary === void 0) {
|
|
1089
|
+
return void 0;
|
|
1104
1090
|
}
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
session_id: primarySession,
|
|
1130
|
-
reason: `extract_knowledge:${sanitizedSlug || input.slug}:${reason}`
|
|
1131
|
-
});
|
|
1132
|
-
return {
|
|
1133
|
-
pending_path: "",
|
|
1134
|
-
idempotency_key: idempotencyKey
|
|
1135
|
-
};
|
|
1136
|
-
}
|
|
1137
|
-
const secretScanTarget = [
|
|
1138
|
-
input.slug ?? "",
|
|
1139
|
-
input.user_messages_summary ?? "",
|
|
1140
|
-
input.session_context ?? "",
|
|
1141
|
-
input.must_read_if ?? "",
|
|
1142
|
-
...input.intent_clues ?? [],
|
|
1143
|
-
...input.tags ?? [],
|
|
1144
|
-
...input.evidence_paths ?? []
|
|
1145
|
-
].join("\n");
|
|
1146
|
-
if (hasSecrets(secretScanTarget)) {
|
|
1147
|
-
await emitEventBestEffort(projectRoot, {
|
|
1148
|
-
event_type: "knowledge_archive_attempted",
|
|
1149
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1150
|
-
correlation_id: primarySession,
|
|
1151
|
-
session_id: primarySession,
|
|
1152
|
-
reason: `extract_knowledge:${sanitizedSlug || input.slug}:secret_detected`
|
|
1153
|
-
});
|
|
1154
|
-
return {
|
|
1155
|
-
pending_path: "",
|
|
1156
|
-
idempotency_key: idempotencyKey
|
|
1157
|
-
};
|
|
1091
|
+
const knowledge = extractKnowledgeFieldsFromFrontmatter(frontmatter);
|
|
1092
|
+
return {
|
|
1093
|
+
summary,
|
|
1094
|
+
intent_clues: extractInlineArray(frontmatter, "intent_clues"),
|
|
1095
|
+
tech_stack: extractInlineArray(frontmatter, "tech_stack"),
|
|
1096
|
+
impact: extractInlineArray(frontmatter, "impact"),
|
|
1097
|
+
must_read_if: extractScalar(frontmatter, "must_read_if") ?? summary,
|
|
1098
|
+
entities: extractInlineArray(frontmatter, "entities"),
|
|
1099
|
+
id: knowledge.id,
|
|
1100
|
+
knowledge_type: knowledge.knowledge_type,
|
|
1101
|
+
maturity: knowledge.maturity,
|
|
1102
|
+
knowledge_layer: knowledge.knowledge_layer,
|
|
1103
|
+
layer_reason: knowledge.layer_reason,
|
|
1104
|
+
created_at: knowledge.created_at,
|
|
1105
|
+
tags: knowledge.tags,
|
|
1106
|
+
relevance_scope: knowledge.relevance_scope,
|
|
1107
|
+
relevance_paths: knowledge.relevance_paths,
|
|
1108
|
+
// v2.2 H2-related (W1-T7): graph edges parsed from frontmatter.
|
|
1109
|
+
related: knowledge.related
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
function isForbiddenCrossLayerEdge(sourceLayer, targetId) {
|
|
1113
|
+
if (sourceLayer !== "team") {
|
|
1114
|
+
return false;
|
|
1158
1115
|
}
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
throw new Error(
|
|
1163
|
-
`semantic_scope '${semanticScope}' conflicts with compatibility layer '${input.layer}'`
|
|
1164
|
-
);
|
|
1116
|
+
const decoded = parseKnowledgeId(localKnowledgeIdFromReference(targetId));
|
|
1117
|
+
if (decoded === null) {
|
|
1118
|
+
return false;
|
|
1165
1119
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
const
|
|
1170
|
-
if (
|
|
1171
|
-
|
|
1172
|
-
relevancePaths = [];
|
|
1173
|
-
await emitEventBestEffort(projectRoot, {
|
|
1174
|
-
event_type: "knowledge_scope_degraded",
|
|
1175
|
-
stable_id: `pending:${idempotencyKey}`,
|
|
1176
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1177
|
-
from_scope: "narrow",
|
|
1178
|
-
to_scope: "broad",
|
|
1179
|
-
reason: "personal-implies-broad"
|
|
1180
|
-
});
|
|
1120
|
+
return decoded.layer === "personal";
|
|
1121
|
+
}
|
|
1122
|
+
function localKnowledgeIdFromReference(ref) {
|
|
1123
|
+
const direct = parseKnowledgeId(ref);
|
|
1124
|
+
if (direct !== null) {
|
|
1125
|
+
return ref;
|
|
1181
1126
|
}
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
const
|
|
1191
|
-
const
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
type: input.type,
|
|
1201
|
-
sourceSessions,
|
|
1202
|
-
idempotencyKey: effectiveIdempotencyKey,
|
|
1203
|
-
summary,
|
|
1204
|
-
recentPaths: input.recent_paths,
|
|
1205
|
-
layer,
|
|
1206
|
-
semanticScope: writeScopeMeta.semantic_scope,
|
|
1207
|
-
visibilityStore: writeScopeMeta.visibility_store,
|
|
1208
|
-
proposedReason: input.proposed_reason,
|
|
1209
|
-
sessionContext: input.session_context,
|
|
1210
|
-
relevanceScope,
|
|
1211
|
-
relevancePaths,
|
|
1212
|
-
intentClues: input.intent_clues,
|
|
1213
|
-
techStack: input.tech_stack,
|
|
1214
|
-
impact: input.impact,
|
|
1215
|
-
mustReadIf: input.must_read_if,
|
|
1216
|
-
onboardSlot: input.onboard_slot,
|
|
1217
|
-
// v2.0.0-rc.37 NEW-37: pass-through topic tags.
|
|
1218
|
-
tags: input.tags,
|
|
1219
|
-
// v2.0.0-rc.37 NEW-7: pass-through evidence_paths to frontmatter.
|
|
1220
|
-
evidencePaths: input.evidence_paths
|
|
1221
|
-
});
|
|
1222
|
-
const augmented = mergeEvidenceNotes(existing, fresh2);
|
|
1223
|
-
await atomicWriteText(absolutePath, augmented);
|
|
1224
|
-
await emitEventBestEffort(projectRoot, {
|
|
1225
|
-
event_type: "knowledge_proposed",
|
|
1226
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1227
|
-
correlation_id: primarySession,
|
|
1228
|
-
session_id: primarySession,
|
|
1229
|
-
reason: `extract_knowledge:${effectiveSanitizedSlug}`
|
|
1230
|
-
});
|
|
1231
|
-
return {
|
|
1232
|
-
pending_path: reportedPath,
|
|
1233
|
-
idempotency_key: effectiveIdempotencyKey
|
|
1234
|
-
};
|
|
1127
|
+
const tail = ref.split(":").at(-1);
|
|
1128
|
+
return tail ?? ref;
|
|
1129
|
+
}
|
|
1130
|
+
function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
|
|
1131
|
+
const rawId = extractScalar(frontmatter, "id");
|
|
1132
|
+
const rawType = extractScalar(frontmatter, "type");
|
|
1133
|
+
const rawMaturity = extractScalar(frontmatter, "maturity");
|
|
1134
|
+
const rawLayer = extractScalar(frontmatter, "layer");
|
|
1135
|
+
const rawLayerReason = extractScalar(frontmatter, "layer_reason");
|
|
1136
|
+
const rawCreatedAt = extractScalar(frontmatter, "created_at");
|
|
1137
|
+
let id;
|
|
1138
|
+
if (rawId !== void 0) {
|
|
1139
|
+
const parsed = StableIdSchema.safeParse(rawId);
|
|
1140
|
+
if (parsed.success) {
|
|
1141
|
+
id = parsed.data;
|
|
1142
|
+
} else {
|
|
1143
|
+
process.stderr.write(`[fabric] frontmatter: invalid knowledge id format ${JSON.stringify(rawId)}; skipping
|
|
1144
|
+
`);
|
|
1235
1145
|
}
|
|
1236
|
-
throw new Error(
|
|
1237
|
-
`slug collision (unreachable after rc.37 NEW-6): pending file ${reportedPath} already exists with key ${existingKey ?? "<missing>"} != ${effectiveIdempotencyKey}`
|
|
1238
|
-
);
|
|
1239
1146
|
}
|
|
1240
|
-
const
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
layer,
|
|
1247
|
-
semanticScope: writeScopeMeta.semantic_scope,
|
|
1248
|
-
visibilityStore: writeScopeMeta.visibility_store,
|
|
1249
|
-
proposedReason: input.proposed_reason,
|
|
1250
|
-
sessionContext: input.session_context,
|
|
1251
|
-
relevanceScope,
|
|
1252
|
-
relevancePaths,
|
|
1253
|
-
intentClues: input.intent_clues,
|
|
1254
|
-
techStack: input.tech_stack,
|
|
1255
|
-
impact: input.impact,
|
|
1256
|
-
mustReadIf: input.must_read_if,
|
|
1257
|
-
onboardSlot: input.onboard_slot,
|
|
1258
|
-
tags: input.tags,
|
|
1259
|
-
// v2.0.0-rc.37 NEW-7: pass-through evidence_paths to frontmatter.
|
|
1260
|
-
evidencePaths: input.evidence_paths
|
|
1261
|
-
});
|
|
1262
|
-
await atomicWriteText(absolutePath, fresh);
|
|
1263
|
-
await emitEventBestEffort(projectRoot, {
|
|
1264
|
-
event_type: "knowledge_proposed",
|
|
1265
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1266
|
-
correlation_id: primarySession,
|
|
1267
|
-
session_id: primarySession,
|
|
1268
|
-
reason: `extract_knowledge:${effectiveSanitizedSlug}`
|
|
1269
|
-
});
|
|
1270
|
-
return {
|
|
1271
|
-
pending_path: reportedPath,
|
|
1272
|
-
idempotency_key: effectiveIdempotencyKey
|
|
1147
|
+
const SINGULAR_TO_PLURAL = {
|
|
1148
|
+
model: "models",
|
|
1149
|
+
decision: "decisions",
|
|
1150
|
+
guideline: "guidelines",
|
|
1151
|
+
pitfall: "pitfalls",
|
|
1152
|
+
process: "processes"
|
|
1273
1153
|
};
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
JSON.stringify(
|
|
1282
|
-
|
|
1283
|
-
type: args.type,
|
|
1284
|
-
slug: candidateSlug
|
|
1285
|
-
})
|
|
1286
|
-
);
|
|
1287
|
-
if (!existsSync3(candidatePath)) {
|
|
1288
|
-
return {
|
|
1289
|
-
absolutePath: candidatePath,
|
|
1290
|
-
sanitizedSlug: candidateSlug,
|
|
1291
|
-
idempotencyKey: candidateKey
|
|
1292
|
-
};
|
|
1154
|
+
let knowledge_type;
|
|
1155
|
+
if (rawType !== void 0) {
|
|
1156
|
+
const normalized = SINGULAR_TO_PLURAL[rawType] ?? rawType;
|
|
1157
|
+
const parsed = KnowledgeTypeSchema.safeParse(normalized);
|
|
1158
|
+
if (parsed.success) {
|
|
1159
|
+
knowledge_type = parsed.data;
|
|
1160
|
+
} else {
|
|
1161
|
+
process.stderr.write(`[fabric] frontmatter: unknown knowledge type ${JSON.stringify(rawType)}; skipping
|
|
1162
|
+
`);
|
|
1293
1163
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
};
|
|
1164
|
+
}
|
|
1165
|
+
let maturity;
|
|
1166
|
+
if (rawMaturity !== void 0) {
|
|
1167
|
+
const parsed = MaturitySchema.safeParse(rawMaturity);
|
|
1168
|
+
if (parsed.success) {
|
|
1169
|
+
maturity = parsed.data;
|
|
1170
|
+
} else {
|
|
1171
|
+
process.stderr.write(`[fabric] frontmatter: unknown maturity ${JSON.stringify(rawMaturity)}; skipping
|
|
1172
|
+
`);
|
|
1302
1173
|
}
|
|
1303
1174
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
return "";
|
|
1314
|
-
}
|
|
1315
|
-
return trimmed.slice(0, SLUG_MAX_LENGTH).replace(/-+$/g, "");
|
|
1316
|
-
}
|
|
1317
|
-
function renderFreshEntry(args) {
|
|
1318
|
-
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1319
|
-
const frontmatterLines = [
|
|
1320
|
-
"---",
|
|
1321
|
-
`type: ${args.type}`,
|
|
1322
|
-
"maturity: draft",
|
|
1323
|
-
`layer: ${args.layer}`,
|
|
1324
|
-
// v2.1 global-refactor (W1/A1): scope coordinate (resolution axis) + the
|
|
1325
|
-
// physical store this entry lives in. `layer` is retained for back-compat
|
|
1326
|
-
// during the co-location retirement; `semantic_scope`/`visibility_store` are
|
|
1327
|
-
// the v2.1 source of truth (scope ⊥ store).
|
|
1328
|
-
`semantic_scope: ${args.semanticScope}`,
|
|
1329
|
-
`visibility_store: ${quoteRelevancePath(args.visibilityStore)}`,
|
|
1330
|
-
`created_at: ${createdAt}`,
|
|
1331
|
-
`source_sessions: [${args.sourceSessions.map((s) => JSON.stringify(s)).join(", ")}]`,
|
|
1332
|
-
`proposed_reason: ${args.proposedReason}`,
|
|
1333
|
-
// rc.31 BUG-2.9/2.1: persist the caller-supplied summary in frontmatter so
|
|
1334
|
-
// knowledge-meta-builder.extractDescriptionFromFrontmatter picks it up
|
|
1335
|
-
// directly. Without this, the meta-builder fell back to extractRule
|
|
1336
|
-
// Description's h1-or-stable-id-or-placeholder synthesis (line ~944),
|
|
1337
|
-
// which made user-visible description.summary == stable_id for any
|
|
1338
|
-
// pending file whose body started with h2-only sections (`## Summary` is
|
|
1339
|
-
// the canonical pending shape). The frontmatter `summary:` line is the
|
|
1340
|
-
// canonical source-of-truth: `extractDescriptionFromFrontmatter` reads it
|
|
1341
|
-
// before extractRuleDescription's fallback kicks in.
|
|
1342
|
-
`summary: ${quoteRelevancePath(args.summary)}`,
|
|
1343
|
-
// v2.0.0-rc.37 NEW-37: render caller-supplied tags or fall back to empty
|
|
1344
|
-
// array. Empty array still legal but doctor's knowledge_tags_empty_ratio
|
|
1345
|
-
// lint will warn at the corpus level. Encourages 2-4 kebab-case topic
|
|
1346
|
-
// strings per entry for cross-entry retrieval signal.
|
|
1347
|
-
args.tags !== void 0 && args.tags.length > 0 ? `tags: [${args.tags.map((t) => quoteRelevancePath(t)).join(", ")}]` : "tags: []"
|
|
1348
|
-
];
|
|
1349
|
-
if (args.relevanceScope !== void 0) {
|
|
1350
|
-
frontmatterLines.push(`relevance_scope: ${args.relevanceScope}`);
|
|
1351
|
-
}
|
|
1352
|
-
if (args.relevancePaths !== void 0) {
|
|
1353
|
-
const pathsBody = args.relevancePaths.map((p) => quoteRelevancePath(p)).join(", ");
|
|
1354
|
-
frontmatterLines.push(`relevance_paths: [${pathsBody}]`);
|
|
1355
|
-
}
|
|
1356
|
-
if (args.intentClues !== void 0) {
|
|
1357
|
-
const body2 = args.intentClues.map((s) => quoteRelevancePath(s)).join(", ");
|
|
1358
|
-
frontmatterLines.push(`intent_clues: [${body2}]`);
|
|
1359
|
-
}
|
|
1360
|
-
if (args.techStack !== void 0) {
|
|
1361
|
-
const body2 = args.techStack.map((s) => quoteRelevancePath(s)).join(", ");
|
|
1362
|
-
frontmatterLines.push(`tech_stack: [${body2}]`);
|
|
1175
|
+
let knowledge_layer;
|
|
1176
|
+
if (rawLayer !== void 0) {
|
|
1177
|
+
const parsed = LayerSchema.safeParse(rawLayer);
|
|
1178
|
+
if (parsed.success) {
|
|
1179
|
+
knowledge_layer = parsed.data;
|
|
1180
|
+
} else {
|
|
1181
|
+
process.stderr.write(`[fabric] frontmatter: unknown layer ${JSON.stringify(rawLayer)}; skipping
|
|
1182
|
+
`);
|
|
1183
|
+
}
|
|
1363
1184
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1185
|
+
let created_at;
|
|
1186
|
+
if (rawCreatedAt !== void 0) {
|
|
1187
|
+
if (!Number.isNaN(Date.parse(rawCreatedAt))) {
|
|
1188
|
+
created_at = rawCreatedAt;
|
|
1189
|
+
} else {
|
|
1190
|
+
process.stderr.write(`[fabric] frontmatter: malformed created_at ${JSON.stringify(rawCreatedAt)}; skipping
|
|
1191
|
+
`);
|
|
1192
|
+
}
|
|
1367
1193
|
}
|
|
1368
|
-
if (
|
|
1369
|
-
|
|
1194
|
+
if (id !== void 0 && knowledge_layer !== void 0) {
|
|
1195
|
+
const decoded = parseKnowledgeId(id);
|
|
1196
|
+
if (decoded !== null && decoded.layer !== knowledge_layer) {
|
|
1197
|
+
process.stderr.write(
|
|
1198
|
+
`[fabric] frontmatter: id ${id} encodes layer ${decoded.layer} but layer field says ${knowledge_layer}; dropping both
|
|
1199
|
+
`
|
|
1200
|
+
);
|
|
1201
|
+
id = void 0;
|
|
1202
|
+
knowledge_layer = void 0;
|
|
1203
|
+
}
|
|
1370
1204
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1205
|
+
const tags = extractInlineArray(frontmatter, "tags");
|
|
1206
|
+
const rawRelevanceScope = extractScalar(frontmatter, "relevance_scope");
|
|
1207
|
+
const relevance_scope = rawRelevanceScope === "narrow" || rawRelevanceScope === "broad" ? rawRelevanceScope : "broad";
|
|
1208
|
+
const relevance_paths = extractInlineArray(frontmatter, "relevance_paths");
|
|
1209
|
+
const rawRelated = extractInlineArray(frontmatter, "related");
|
|
1210
|
+
const sourceLayer = knowledge_layer ?? (id !== void 0 ? parseKnowledgeId(id)?.layer ?? "team" : "team");
|
|
1211
|
+
const related = rawRelated.filter((targetId) => {
|
|
1212
|
+
if (isForbiddenCrossLayerEdge(sourceLayer, targetId)) {
|
|
1213
|
+
process.stderr.write(
|
|
1214
|
+
`[fabric] frontmatter: stripping forbidden cross-layer related edge ${id ?? "(team entry)"} \u2192 ${targetId} (KT\u2192KP topology leak; \xA74 privacy iron law)
|
|
1215
|
+
`
|
|
1216
|
+
);
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
return true;
|
|
1220
|
+
});
|
|
1221
|
+
return {
|
|
1222
|
+
id,
|
|
1223
|
+
knowledge_type,
|
|
1224
|
+
maturity,
|
|
1225
|
+
knowledge_layer,
|
|
1226
|
+
layer_reason: rawLayerReason,
|
|
1227
|
+
created_at,
|
|
1228
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
1229
|
+
relevance_scope,
|
|
1230
|
+
relevance_paths,
|
|
1231
|
+
related: related.length > 0 ? related : void 0
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
function extractScalar(frontmatter, key) {
|
|
1235
|
+
const pattern = new RegExp(`^${escapeRegExp(key)}:\\s*(.+?)\\s*$`, "mu");
|
|
1236
|
+
const match = pattern.exec(frontmatter);
|
|
1237
|
+
if (match === null) {
|
|
1238
|
+
return void 0;
|
|
1373
1239
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1240
|
+
return unquote(match[1].trim());
|
|
1241
|
+
}
|
|
1242
|
+
function extractInlineArray(frontmatter, key) {
|
|
1243
|
+
const pattern = new RegExp(`^${escapeRegExp(key)}:\\s*\\[(.*?)\\]\\s*$`, "mu");
|
|
1244
|
+
const match = pattern.exec(frontmatter);
|
|
1245
|
+
if (match === null) {
|
|
1246
|
+
return [];
|
|
1377
1247
|
}
|
|
1378
|
-
|
|
1379
|
-
`x-fabric-idempotency-key: ${args.idempotencyKey}`,
|
|
1380
|
-
"---"
|
|
1381
|
-
);
|
|
1382
|
-
const frontmatter = frontmatterLines.join("\n");
|
|
1383
|
-
const reasonExplanation = PROPOSED_REASON_DESCRIPTIONS_BY_LOCALE[resolveGlobalLocale()][args.proposedReason];
|
|
1384
|
-
const body = [
|
|
1385
|
-
"",
|
|
1386
|
-
"## Summary",
|
|
1387
|
-
"",
|
|
1388
|
-
args.summary,
|
|
1389
|
-
"",
|
|
1390
|
-
"## Why proposed",
|
|
1391
|
-
"",
|
|
1392
|
-
`${args.proposedReason} \u2014 ${reasonExplanation}`,
|
|
1393
|
-
"",
|
|
1394
|
-
"## Session context",
|
|
1395
|
-
"",
|
|
1396
|
-
args.sessionContext,
|
|
1397
|
-
"",
|
|
1398
|
-
"## Evidence",
|
|
1399
|
-
"",
|
|
1400
|
-
renderEvidenceBlock(args.summary, args.recentPaths),
|
|
1401
|
-
""
|
|
1402
|
-
].join("\n");
|
|
1403
|
-
return `${frontmatter}
|
|
1404
|
-
${body}`;
|
|
1248
|
+
return match[1].split(",").map((item) => unquote(item.trim())).filter((item) => item.length > 0);
|
|
1405
1249
|
}
|
|
1406
|
-
function
|
|
1407
|
-
|
|
1408
|
-
return `"${escaped}"`;
|
|
1250
|
+
function unquote(value) {
|
|
1251
|
+
return value.replace(/^["'](.*)["']$/u, "$1");
|
|
1409
1252
|
}
|
|
1410
|
-
function
|
|
1411
|
-
|
|
1412
|
-
return [
|
|
1413
|
-
"Recent paths:",
|
|
1414
|
-
"",
|
|
1415
|
-
pathLines,
|
|
1416
|
-
"",
|
|
1417
|
-
"Notes:",
|
|
1418
|
-
"",
|
|
1419
|
-
`- ${summary.trim()}`
|
|
1420
|
-
].join("\n");
|
|
1253
|
+
function escapeRegExp(value) {
|
|
1254
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
1421
1255
|
}
|
|
1422
|
-
function mergeEvidenceNotes(existing, fresh) {
|
|
1423
|
-
const freshSplit = splitAtEvidence(fresh);
|
|
1424
|
-
if (freshSplit === null) {
|
|
1425
|
-
return fresh.endsWith("\n") ? fresh : `${fresh}
|
|
1426
|
-
`;
|
|
1427
|
-
}
|
|
1428
|
-
const freshHead = freshSplit.head;
|
|
1429
|
-
const oldEvidence = collectEvidenceItems(existing);
|
|
1430
|
-
const freshEvidence = collectEvidenceItems(fresh);
|
|
1431
|
-
const mergedNotes = [];
|
|
1432
|
-
const seenNotes = /* @__PURE__ */ new Set();
|
|
1433
|
-
for (const note of [...oldEvidence.notes, ...freshEvidence.notes]) {
|
|
1434
|
-
const key = note.replace(/\s+/gu, " ").trim();
|
|
1435
|
-
if (key.length === 0) continue;
|
|
1436
|
-
if (seenNotes.has(key)) continue;
|
|
1437
|
-
seenNotes.add(key);
|
|
1438
|
-
mergedNotes.push(note);
|
|
1439
|
-
}
|
|
1440
|
-
const mergedPaths = [];
|
|
1441
|
-
const seenPaths = /* @__PURE__ */ new Set();
|
|
1442
|
-
for (const p of [...oldEvidence.paths, ...freshEvidence.paths]) {
|
|
1443
|
-
const key = p.trim();
|
|
1444
|
-
if (key.length === 0) continue;
|
|
1445
|
-
if (seenPaths.has(key)) continue;
|
|
1446
|
-
seenPaths.add(key);
|
|
1447
|
-
mergedPaths.push(key);
|
|
1448
|
-
}
|
|
1449
|
-
const pathLines = mergedPaths.length === 0 ? "_(no recent paths reported)_" : mergedPaths.map((p) => `- ${p}`).join("\n");
|
|
1450
|
-
const noteLines = mergedNotes.length === 0 ? "_(no notes recorded)_" : mergedNotes.map((n) => `- ${n}`).join("\n");
|
|
1451
|
-
const evidenceBody = [
|
|
1452
|
-
"Recent paths:",
|
|
1453
|
-
"",
|
|
1454
|
-
pathLines,
|
|
1455
|
-
"",
|
|
1456
|
-
"Notes:",
|
|
1457
|
-
"",
|
|
1458
|
-
noteLines
|
|
1459
|
-
].join("\n");
|
|
1460
|
-
return `${freshHead}
|
|
1461
|
-
## Evidence
|
|
1462
1256
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1257
|
+
// src/services/cross-store-recall.ts
|
|
1258
|
+
var readSetWalkCache = /* @__PURE__ */ new Map();
|
|
1259
|
+
var readSetWalkCount = 0;
|
|
1260
|
+
function readSetWalkCacheKey(projectRoot) {
|
|
1261
|
+
return `${projectRoot}\0${process.env.FABRIC_HOME ?? ""}`;
|
|
1465
1262
|
}
|
|
1466
|
-
function
|
|
1467
|
-
const
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1263
|
+
async function readSetFingerprint(refs) {
|
|
1264
|
+
const parts = await Promise.all(
|
|
1265
|
+
refs.map(async (ref) => {
|
|
1266
|
+
try {
|
|
1267
|
+
const fileStat = await stat(ref.file);
|
|
1268
|
+
return `${ref.store_uuid}|${ref.alias}|${ref.file}|${fileStat.size}|${fileStat.mtimeMs}`;
|
|
1269
|
+
} catch {
|
|
1270
|
+
return `${ref.store_uuid}|${ref.alias}|${ref.file}|missing`;
|
|
1271
|
+
}
|
|
1272
|
+
})
|
|
1273
|
+
);
|
|
1274
|
+
return parts.sort().join("\n");
|
|
1472
1275
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
const
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1276
|
+
var SEMANTIC_SCOPE_LINE = /^semantic_scope:\s*"?([^"\n]+?)"?\s*$/mu;
|
|
1277
|
+
function readSemanticScope(source, layer) {
|
|
1278
|
+
const match = SEMANTIC_SCOPE_LINE.exec(source);
|
|
1279
|
+
return match?.[1] ?? layer;
|
|
1280
|
+
}
|
|
1281
|
+
function filterByActiveProject(entries, activeProject) {
|
|
1282
|
+
if (activeProject === void 0 || activeProject.length === 0) {
|
|
1283
|
+
return entries;
|
|
1284
|
+
}
|
|
1285
|
+
const current = `project:${activeProject}`;
|
|
1286
|
+
return entries.filter(
|
|
1287
|
+
(e) => scopeRoot(e.semanticScope) !== "project" || e.semanticScope === current
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
function activeProjectOf(projectRoot) {
|
|
1291
|
+
const ap = loadProjectConfig2(projectRoot)?.active_project;
|
|
1292
|
+
return ap !== void 0 && ap.length > 0 ? ap : void 0;
|
|
1293
|
+
}
|
|
1294
|
+
async function resolveReadSetSnapshot(projectRoot) {
|
|
1295
|
+
const resolveInput = buildStoreResolveInput2(projectRoot);
|
|
1296
|
+
if (resolveInput === null) {
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
const readSet = createStoreResolver2().resolveReadSet(resolveInput);
|
|
1300
|
+
if (readSet.stores.length === 0) {
|
|
1301
|
+
return null;
|
|
1302
|
+
}
|
|
1303
|
+
const personalUuids = new Set(
|
|
1304
|
+
resolveInput.mountedStores.filter((s) => s.personal).map((s) => s.store_uuid)
|
|
1305
|
+
);
|
|
1306
|
+
const globalRoot = resolveGlobalRoot2();
|
|
1307
|
+
const dirs = readSet.stores.map((entry) => ({
|
|
1308
|
+
store_uuid: entry.store_uuid,
|
|
1309
|
+
alias: entry.alias,
|
|
1310
|
+
dir: join6(
|
|
1311
|
+
globalRoot,
|
|
1312
|
+
storeRelativePathForMount2(
|
|
1313
|
+
resolveInput.mountedStores.find((s) => s.store_uuid === entry.store_uuid) ?? {
|
|
1314
|
+
store_uuid: entry.store_uuid
|
|
1487
1315
|
}
|
|
1316
|
+
)
|
|
1317
|
+
)
|
|
1318
|
+
}));
|
|
1319
|
+
return {
|
|
1320
|
+
refs: await readKnowledgeAcrossStores(dirs),
|
|
1321
|
+
personalUuids
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
async function walkReadSetStores(projectRoot) {
|
|
1325
|
+
const snapshot = await resolveReadSetSnapshot(projectRoot);
|
|
1326
|
+
if (snapshot === null) {
|
|
1327
|
+
return [];
|
|
1328
|
+
}
|
|
1329
|
+
const key = readSetWalkCacheKey(projectRoot);
|
|
1330
|
+
const fingerprint = await readSetFingerprint(snapshot.refs);
|
|
1331
|
+
const cached = readSetWalkCache.get(key);
|
|
1332
|
+
if (cached !== void 0 && cached.fingerprint === fingerprint) {
|
|
1333
|
+
return cached.entries.slice();
|
|
1334
|
+
}
|
|
1335
|
+
const entries = await walkReadSetStoresUncached(snapshot);
|
|
1336
|
+
readSetWalkCache.set(key, { fingerprint, entries });
|
|
1337
|
+
return entries.slice();
|
|
1338
|
+
}
|
|
1339
|
+
async function walkReadSetStoresUncached(snapshot) {
|
|
1340
|
+
readSetWalkCount += 1;
|
|
1341
|
+
const entries = await Promise.all(snapshot.refs.map(async (ref) => {
|
|
1342
|
+
let source;
|
|
1343
|
+
try {
|
|
1344
|
+
source = await readFile3(ref.file, "utf8");
|
|
1345
|
+
} catch {
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
const stableId = deriveRuleIdentity(ref.file, source, void 0).stableId;
|
|
1349
|
+
const layer = snapshot.personalUuids.has(ref.store_uuid) ? "personal" : "team";
|
|
1350
|
+
return {
|
|
1351
|
+
qualifiedId: `${ref.alias}:${stableId}`,
|
|
1352
|
+
file: ref.file,
|
|
1353
|
+
type: ref.type,
|
|
1354
|
+
alias: ref.alias,
|
|
1355
|
+
layer,
|
|
1356
|
+
semanticScope: readSemanticScope(source, layer),
|
|
1357
|
+
source
|
|
1358
|
+
};
|
|
1359
|
+
}));
|
|
1360
|
+
return entries.filter((entry) => entry !== null);
|
|
1361
|
+
}
|
|
1362
|
+
async function buildCrossStoreRawItems(projectRoot) {
|
|
1363
|
+
const items = [];
|
|
1364
|
+
const activeProject = activeProjectOf(projectRoot);
|
|
1365
|
+
for (const entry of filterByActiveProject(await walkReadSetStores(projectRoot), activeProject)) {
|
|
1366
|
+
const baseDescription = extractRuleDescription(entry.source);
|
|
1367
|
+
if (baseDescription === void 0) {
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
items.push({
|
|
1371
|
+
stable_id: entry.qualifiedId,
|
|
1372
|
+
description: {
|
|
1373
|
+
...baseDescription,
|
|
1374
|
+
knowledge_layer: entry.layer,
|
|
1375
|
+
semantic_scope: entry.semanticScope
|
|
1488
1376
|
}
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
return items;
|
|
1380
|
+
}
|
|
1381
|
+
async function buildCrossStoreBodyIndex(projectRoot) {
|
|
1382
|
+
const index = /* @__PURE__ */ new Map();
|
|
1383
|
+
const activeProject = activeProjectOf(projectRoot);
|
|
1384
|
+
for (const entry of filterByActiveProject(await walkReadSetStores(projectRoot), activeProject)) {
|
|
1385
|
+
if (!index.has(entry.qualifiedId)) {
|
|
1386
|
+
index.set(entry.qualifiedId, { file: entry.file, layer: entry.layer });
|
|
1489
1387
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1388
|
+
}
|
|
1389
|
+
return index;
|
|
1390
|
+
}
|
|
1391
|
+
var ALWAYS_ACTIVE_TYPES = /* @__PURE__ */ new Set(["guidelines", "models"]);
|
|
1392
|
+
async function buildKnowledgeCensus(projectRoot) {
|
|
1393
|
+
const census = {
|
|
1394
|
+
by_type: {},
|
|
1395
|
+
by_layer: { team: 0, personal: 0, project: 0 },
|
|
1396
|
+
broad_by_type: {},
|
|
1397
|
+
narrow_total: 0,
|
|
1398
|
+
dropped_other_project: 0,
|
|
1399
|
+
total: 0
|
|
1400
|
+
};
|
|
1401
|
+
try {
|
|
1402
|
+
const activeProject = activeProjectOf(projectRoot);
|
|
1403
|
+
const all = await walkReadSetStores(projectRoot);
|
|
1404
|
+
const kept = filterByActiveProject(all, activeProject);
|
|
1405
|
+
census.dropped_other_project = all.length - kept.length;
|
|
1406
|
+
for (const entry of kept) {
|
|
1407
|
+
const desc = extractRuleDescription(entry.source);
|
|
1408
|
+
const type = desc?.knowledge_type;
|
|
1409
|
+
const isNarrow = desc?.relevance_scope === "narrow";
|
|
1410
|
+
if (typeof type === "string") {
|
|
1411
|
+
census.by_type[type] = (census.by_type[type] ?? 0) + 1;
|
|
1412
|
+
if (!isNarrow) {
|
|
1413
|
+
census.broad_by_type[type] = (census.broad_by_type[type] ?? 0) + 1;
|
|
1501
1414
|
}
|
|
1502
|
-
|
|
1415
|
+
}
|
|
1416
|
+
if (isNarrow) census.narrow_total += 1;
|
|
1417
|
+
if (scopeRoot(entry.semanticScope) === "project") {
|
|
1418
|
+
census.by_layer.project += 1;
|
|
1503
1419
|
} else {
|
|
1504
|
-
|
|
1420
|
+
census.by_layer[entry.layer] += 1;
|
|
1505
1421
|
}
|
|
1422
|
+
census.total += 1;
|
|
1506
1423
|
}
|
|
1507
|
-
|
|
1508
|
-
for (const n of bulletLines) notes.push(n);
|
|
1424
|
+
} catch {
|
|
1509
1425
|
}
|
|
1510
|
-
return
|
|
1426
|
+
return census;
|
|
1511
1427
|
}
|
|
1512
|
-
function
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1428
|
+
async function buildAlwaysActiveBodies(projectRoot) {
|
|
1429
|
+
const out = [];
|
|
1430
|
+
try {
|
|
1431
|
+
const activeProject = activeProjectOf(projectRoot);
|
|
1432
|
+
for (const entry of filterByActiveProject(
|
|
1433
|
+
await walkReadSetStores(projectRoot),
|
|
1434
|
+
activeProject
|
|
1435
|
+
)) {
|
|
1436
|
+
const desc = extractRuleDescription(entry.source);
|
|
1437
|
+
if (desc === void 0) continue;
|
|
1438
|
+
const type = desc.knowledge_type;
|
|
1439
|
+
if (typeof type !== "string" || !ALWAYS_ACTIVE_TYPES.has(type)) continue;
|
|
1440
|
+
if (desc.relevance_scope === "narrow") continue;
|
|
1441
|
+
out.push({
|
|
1442
|
+
stable_id: entry.qualifiedId,
|
|
1443
|
+
type,
|
|
1444
|
+
layer: entry.layer,
|
|
1445
|
+
summary: typeof desc.summary === "string" ? desc.summary : "",
|
|
1446
|
+
body: extractBody(entry.source)
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
} catch {
|
|
1450
|
+
return [];
|
|
1520
1451
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1452
|
+
return out;
|
|
1453
|
+
}
|
|
1454
|
+
async function computeReadSetRevision(projectRoot) {
|
|
1455
|
+
const revisionSource = (await walkReadSetStores(projectRoot)).filter((entry) => !entry.file.includes("/knowledge/pending/")).map((entry) => `${entry.qualifiedId}|${sha256(entry.source)}`).sort().join("\n");
|
|
1456
|
+
return sha256(revisionSource);
|
|
1457
|
+
}
|
|
1458
|
+
async function collectStoreCanonicalEntries(projectRoot) {
|
|
1459
|
+
const out = [];
|
|
1460
|
+
for (const entry of await walkReadSetStores(projectRoot)) {
|
|
1461
|
+
if (entry.file.includes("/knowledge/pending/")) {
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
const description = extractRuleDescription(entry.source);
|
|
1465
|
+
if (description === void 0) {
|
|
1466
|
+
continue;
|
|
1528
1467
|
}
|
|
1468
|
+
out.push({
|
|
1469
|
+
stableId: entry.qualifiedId.slice(entry.alias.length + 1),
|
|
1470
|
+
qualifiedId: entry.qualifiedId,
|
|
1471
|
+
file: entry.file,
|
|
1472
|
+
type: entry.type,
|
|
1473
|
+
layer: entry.layer,
|
|
1474
|
+
body: entry.source,
|
|
1475
|
+
description
|
|
1476
|
+
});
|
|
1529
1477
|
}
|
|
1530
|
-
return
|
|
1478
|
+
return out;
|
|
1531
1479
|
}
|
|
1532
|
-
async function
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1480
|
+
async function collectStoreKnowledgeSummaries(projectRoot) {
|
|
1481
|
+
const out = [];
|
|
1482
|
+
for (const entry of await walkReadSetStores(projectRoot)) {
|
|
1483
|
+
const description = extractRuleDescription(entry.source);
|
|
1484
|
+
if (description === void 0) {
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
out.push({
|
|
1488
|
+
stableId: entry.qualifiedId,
|
|
1489
|
+
summary: description.summary ?? "",
|
|
1490
|
+
layer: entry.layer
|
|
1491
|
+
});
|
|
1536
1492
|
}
|
|
1493
|
+
return out;
|
|
1537
1494
|
}
|
|
1538
1495
|
|
|
1539
|
-
// src/
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
);
|
|
1570
|
-
return {
|
|
1571
|
-
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
1572
|
-
structuredContent: response
|
|
1573
|
-
};
|
|
1574
|
-
} finally {
|
|
1575
|
-
tracker?.exit(requestId);
|
|
1496
|
+
// src/services/bm25.ts
|
|
1497
|
+
import { tokenize } from "@fenglimg/fabric-shared";
|
|
1498
|
+
var K1 = 1.5;
|
|
1499
|
+
var BM25_FIELDS = ["title", "summary", "tags", "body"];
|
|
1500
|
+
var FIELD_CONFIGS = {
|
|
1501
|
+
title: { boost: 3, b: 0.3 },
|
|
1502
|
+
tags: { boost: 2, b: 0 },
|
|
1503
|
+
summary: { boost: 1.5, b: 0.75 },
|
|
1504
|
+
body: { boost: 1, b: 0.75 }
|
|
1505
|
+
};
|
|
1506
|
+
function emptyFieldRecord(make) {
|
|
1507
|
+
return { title: make(), summary: make(), tags: make(), body: make() };
|
|
1508
|
+
}
|
|
1509
|
+
function buildBm25Model(docs) {
|
|
1510
|
+
const totalDocs = docs.length;
|
|
1511
|
+
const documentFrequency = /* @__PURE__ */ new Map();
|
|
1512
|
+
const perDoc = /* @__PURE__ */ new Map();
|
|
1513
|
+
const totalFieldLength = emptyFieldRecord(() => 0);
|
|
1514
|
+
for (const doc of docs) {
|
|
1515
|
+
const fieldTermFreq = emptyFieldRecord(() => /* @__PURE__ */ new Map());
|
|
1516
|
+
const fieldLength = emptyFieldRecord(() => 0);
|
|
1517
|
+
const docTerms = /* @__PURE__ */ new Set();
|
|
1518
|
+
for (const field of BM25_FIELDS) {
|
|
1519
|
+
const tokens = doc.fields[field] ?? [];
|
|
1520
|
+
fieldLength[field] = tokens.length;
|
|
1521
|
+
totalFieldLength[field] += tokens.length;
|
|
1522
|
+
const tf = fieldTermFreq[field];
|
|
1523
|
+
for (const term of tokens) {
|
|
1524
|
+
tf.set(term, (tf.get(term) ?? 0) + 1);
|
|
1525
|
+
docTerms.add(term);
|
|
1576
1526
|
}
|
|
1577
1527
|
}
|
|
1578
|
-
|
|
1528
|
+
for (const term of docTerms) {
|
|
1529
|
+
documentFrequency.set(term, (documentFrequency.get(term) ?? 0) + 1);
|
|
1530
|
+
}
|
|
1531
|
+
perDoc.set(doc.id, { fieldTermFreq, fieldLength });
|
|
1532
|
+
}
|
|
1533
|
+
const avgFieldLength = emptyFieldRecord(() => 0);
|
|
1534
|
+
for (const field of BM25_FIELDS) {
|
|
1535
|
+
avgFieldLength[field] = totalDocs > 0 ? totalFieldLength[field] / totalDocs : 0;
|
|
1536
|
+
}
|
|
1537
|
+
const idf = (term) => {
|
|
1538
|
+
const n = documentFrequency.get(term) ?? 0;
|
|
1539
|
+
return Math.log(1 + (totalDocs - n + 0.5) / (n + 0.5));
|
|
1540
|
+
};
|
|
1541
|
+
return {
|
|
1542
|
+
scoreDoc(id, queryTerms) {
|
|
1543
|
+
const data = perDoc.get(id);
|
|
1544
|
+
if (data === void 0 || queryTerms.length === 0) {
|
|
1545
|
+
return 0;
|
|
1546
|
+
}
|
|
1547
|
+
let score = 0;
|
|
1548
|
+
const scoredTerms = /* @__PURE__ */ new Set();
|
|
1549
|
+
for (const term of queryTerms) {
|
|
1550
|
+
if (scoredTerms.has(term)) {
|
|
1551
|
+
continue;
|
|
1552
|
+
}
|
|
1553
|
+
scoredTerms.add(term);
|
|
1554
|
+
let pseudoTermFreq = 0;
|
|
1555
|
+
for (const field of BM25_FIELDS) {
|
|
1556
|
+
const config = FIELD_CONFIGS[field];
|
|
1557
|
+
if (config.boost === 0) {
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1560
|
+
const freq = data.fieldTermFreq[field].get(term);
|
|
1561
|
+
if (freq === void 0 || freq === 0) {
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
const avg = avgFieldLength[field] || 1;
|
|
1565
|
+
const norm = 1 - config.b + config.b * (data.fieldLength[field] / avg);
|
|
1566
|
+
pseudoTermFreq += config.boost * (freq / (norm || 1));
|
|
1567
|
+
}
|
|
1568
|
+
if (pseudoTermFreq === 0) {
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
score += idf(term) * (pseudoTermFreq * (K1 + 1) / (pseudoTermFreq + K1));
|
|
1572
|
+
}
|
|
1573
|
+
return score;
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1579
1576
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
1583
|
-
import {
|
|
1584
|
-
recallAnnotations,
|
|
1585
|
-
recallInputSchema,
|
|
1586
|
-
recallOutputSchema
|
|
1587
|
-
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
1588
|
-
import { enforcePayloadLimit as enforcePayloadLimit2 } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
1589
|
-
|
|
1590
|
-
// src/services/plan-context.ts
|
|
1591
|
-
import {
|
|
1592
|
-
buildStoreResolveInput as buildStoreResolveInput3,
|
|
1593
|
-
createStoreResolver as createStoreResolver3,
|
|
1594
|
-
resolveCandidates,
|
|
1595
|
-
tokenize as tokenize2
|
|
1596
|
-
} from "@fenglimg/fabric-shared";
|
|
1597
|
-
import {
|
|
1598
|
-
trimToPayloadBudget
|
|
1599
|
-
} from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
1600
|
-
|
|
1601
|
-
// src/services/get-knowledge.ts
|
|
1602
|
-
function normalizeKnowledgePath(value) {
|
|
1603
|
-
return value.replaceAll("\\", "/");
|
|
1577
|
+
function buildQueryTerms(text) {
|
|
1578
|
+
return tokenize(text);
|
|
1604
1579
|
}
|
|
1605
1580
|
|
|
1606
|
-
// src/services/
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
resolveGlobalRoot as resolveGlobalRoot2,
|
|
1615
|
-
scopeRoot,
|
|
1616
|
-
storeRelativePathForMount as storeRelativePathForMount2
|
|
1617
|
-
} from "@fenglimg/fabric-shared";
|
|
1618
|
-
|
|
1619
|
-
// src/services/knowledge-meta-builder.ts
|
|
1620
|
-
import {
|
|
1621
|
-
deriveAgentsMetaStableId,
|
|
1622
|
-
isKnowledgeStableId,
|
|
1623
|
-
KnowledgeTypeSchema,
|
|
1624
|
-
LayerSchema,
|
|
1625
|
-
MaturitySchema,
|
|
1626
|
-
parseKnowledgeId,
|
|
1627
|
-
StableIdSchema
|
|
1628
|
-
} from "@fenglimg/fabric-shared";
|
|
1629
|
-
function toAgentsCompatiblePath(contentRef) {
|
|
1630
|
-
return contentRef.replace(/^~\/\.fabric\/knowledge\//u, ".fabric/agents/").replace(/^\.fabric\/knowledge\//u, ".fabric/agents/");
|
|
1581
|
+
// src/services/conflict-lint.ts
|
|
1582
|
+
function buildSimilarityModel(docs) {
|
|
1583
|
+
const tokenized = docs.map((d) => ({ id: d.id, tokens: buildQueryTerms(d.text) }));
|
|
1584
|
+
const tokensById = new Map(tokenized.map((d) => [d.id, d.tokens]));
|
|
1585
|
+
const model = buildBm25Model(
|
|
1586
|
+
tokenized.map((d) => ({ id: d.id, fields: { title: [], summary: [], tags: [], body: d.tokens } }))
|
|
1587
|
+
);
|
|
1588
|
+
return { model, tokensById };
|
|
1631
1589
|
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
const
|
|
1647
|
-
const
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1590
|
+
var DEFAULT_CONFLICT_SIMILARITY_THRESHOLD = 0.5;
|
|
1591
|
+
function groupKey(entry) {
|
|
1592
|
+
return `${entry.layer}\0${entry.knowledge_type}`;
|
|
1593
|
+
}
|
|
1594
|
+
function pairSimilarity(model, a, b) {
|
|
1595
|
+
const selfA = model.scoreDoc(a.id, a.tokens);
|
|
1596
|
+
const selfB = model.scoreDoc(b.id, b.tokens);
|
|
1597
|
+
if (selfA <= 0 || selfB <= 0) return 0;
|
|
1598
|
+
const aToB = model.scoreDoc(b.id, a.tokens) / selfB;
|
|
1599
|
+
const bToA = model.scoreDoc(a.id, b.tokens) / selfA;
|
|
1600
|
+
const sim = Math.min(aToB, bToA);
|
|
1601
|
+
return sim < 0 ? 0 : sim > 1 ? 1 : sim;
|
|
1602
|
+
}
|
|
1603
|
+
function findConflictCandidates(entries, opts = {}) {
|
|
1604
|
+
const threshold = typeof opts.threshold === "number" && opts.threshold >= 0 && opts.threshold <= 1 ? opts.threshold : DEFAULT_CONFLICT_SIMILARITY_THRESHOLD;
|
|
1605
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1606
|
+
for (const entry of entries) {
|
|
1607
|
+
if (typeof entry.stable_id !== "string" || entry.stable_id.length === 0) continue;
|
|
1608
|
+
const list = groups.get(groupKey(entry)) ?? [];
|
|
1609
|
+
list.push(entry);
|
|
1610
|
+
groups.set(groupKey(entry), list);
|
|
1653
1611
|
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1612
|
+
const pairs = [];
|
|
1613
|
+
for (const group of groups.values()) {
|
|
1614
|
+
if (group.length < 2) continue;
|
|
1615
|
+
const { model, tokensById } = buildSimilarityModel(
|
|
1616
|
+
group.map((e) => ({ id: e.stable_id, text: e.text }))
|
|
1617
|
+
);
|
|
1618
|
+
for (let i = 0; i < group.length; i += 1) {
|
|
1619
|
+
for (let j = i + 1; j < group.length; j += 1) {
|
|
1620
|
+
const ea = group[i];
|
|
1621
|
+
const eb = group[j];
|
|
1622
|
+
const sim = pairSimilarity(
|
|
1623
|
+
model,
|
|
1624
|
+
{ id: ea.stable_id, tokens: tokensById.get(ea.stable_id) ?? [] },
|
|
1625
|
+
{ id: eb.stable_id, tokens: tokensById.get(eb.stable_id) ?? [] }
|
|
1626
|
+
);
|
|
1627
|
+
if (sim < threshold) continue;
|
|
1628
|
+
const [a, b] = ea.stable_id <= eb.stable_id ? [ea.stable_id, eb.stable_id] : [eb.stable_id, ea.stable_id];
|
|
1629
|
+
pairs.push({
|
|
1630
|
+
a,
|
|
1631
|
+
b,
|
|
1632
|
+
knowledge_type: ea.knowledge_type,
|
|
1633
|
+
layer: ea.layer,
|
|
1634
|
+
similarity: sim,
|
|
1635
|
+
verdict: "unknown"
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1659
1639
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
identitySource: "derived"
|
|
1663
|
-
};
|
|
1664
|
-
}
|
|
1665
|
-
function extractDeclaredStableId(source) {
|
|
1666
|
-
const match = /^(?:\uFEFF)?(?:---\r?\n[\s\S]*?\r?\n---\s*(?:\r?\n|$))?<!--\s*fab:rule-id\s+([A-Za-z0-9][A-Za-z0-9/_-]*)\s*-->\s*(?:\r?\n|$)/u.exec(source);
|
|
1667
|
-
return match?.[1];
|
|
1640
|
+
pairs.sort((x, y) => y.similarity - x.similarity || (x.a < y.a ? -1 : x.a > y.a ? 1 : x.b < y.b ? -1 : 1));
|
|
1641
|
+
return pairs;
|
|
1668
1642
|
}
|
|
1669
|
-
function
|
|
1670
|
-
const
|
|
1671
|
-
if (
|
|
1672
|
-
return
|
|
1643
|
+
async function lintConflicts(entries, opts = {}) {
|
|
1644
|
+
const candidates = findConflictCandidates(entries, { threshold: opts.threshold });
|
|
1645
|
+
if (opts.judge === void 0 || candidates.length === 0) {
|
|
1646
|
+
return candidates;
|
|
1673
1647
|
}
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1676
|
-
|
|
1648
|
+
const byId = new Map(entries.map((e) => [e.stable_id, e]));
|
|
1649
|
+
const judged = [];
|
|
1650
|
+
for (const pair of candidates) {
|
|
1651
|
+
const ea = byId.get(pair.a);
|
|
1652
|
+
const eb = byId.get(pair.b);
|
|
1653
|
+
if (ea === void 0 || eb === void 0) {
|
|
1654
|
+
judged.push(pair);
|
|
1655
|
+
continue;
|
|
1656
|
+
}
|
|
1657
|
+
try {
|
|
1658
|
+
const verdict = await opts.judge(ea, eb);
|
|
1659
|
+
judged.push({
|
|
1660
|
+
...pair,
|
|
1661
|
+
verdict: verdict.isConflict ? "conflict" : "similar",
|
|
1662
|
+
rationale: verdict.rationale
|
|
1663
|
+
});
|
|
1664
|
+
} catch {
|
|
1665
|
+
judged.push(pair);
|
|
1666
|
+
}
|
|
1677
1667
|
}
|
|
1678
|
-
|
|
1679
|
-
return isKnowledgeStableId(candidate) ? candidate : void 0;
|
|
1668
|
+
return judged;
|
|
1680
1669
|
}
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1670
|
+
|
|
1671
|
+
// src/services/archive-dedup-gate.ts
|
|
1672
|
+
var DEDUP_NEAR_DUPLICATE_THRESHOLD = 0.85;
|
|
1673
|
+
var DEDUP_CONFLICT_THRESHOLD = 0.5;
|
|
1674
|
+
var CANDIDATE_ID = "__candidate__";
|
|
1675
|
+
function classifyArchiveCandidate(candidate, corpus, opts = {}) {
|
|
1676
|
+
const nearDuplicate = opts.nearDuplicateThreshold ?? DEDUP_NEAR_DUPLICATE_THRESHOLD;
|
|
1677
|
+
const conflict = opts.conflictThreshold ?? DEDUP_CONFLICT_THRESHOLD;
|
|
1678
|
+
const bucket = corpus.filter(
|
|
1679
|
+
(e) => e.knowledge_type === candidate.knowledge_type && e.layer === candidate.layer
|
|
1680
|
+
);
|
|
1681
|
+
if (bucket.length === 0) {
|
|
1682
|
+
return { verdict: "unique", matches: [] };
|
|
1686
1683
|
}
|
|
1687
|
-
const
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1684
|
+
const { model, tokensById } = buildSimilarityModel([
|
|
1685
|
+
{ id: CANDIDATE_ID, text: candidate.text },
|
|
1686
|
+
...bucket.map((e) => ({ id: e.stable_id, text: e.text }))
|
|
1687
|
+
]);
|
|
1688
|
+
const candidateTokens = tokensById.get(CANDIDATE_ID) ?? [];
|
|
1689
|
+
const matches = bucket.map((e) => ({
|
|
1690
|
+
stable_id: e.stable_id,
|
|
1691
|
+
similarity: pairSimilarity(
|
|
1692
|
+
model,
|
|
1693
|
+
{ id: CANDIDATE_ID, tokens: candidateTokens },
|
|
1694
|
+
{ id: e.stable_id, tokens: tokensById.get(e.stable_id) ?? [] }
|
|
1695
|
+
)
|
|
1696
|
+
})).filter((m) => m.similarity >= conflict).sort((a, b) => b.similarity - a.similarity || a.stable_id.localeCompare(b.stable_id));
|
|
1697
|
+
const top = matches[0]?.similarity ?? 0;
|
|
1698
|
+
const verdict = top >= nearDuplicate ? "near-duplicate" : top >= conflict ? "conflict" : "unique";
|
|
1699
|
+
return { verdict, matches };
|
|
1700
|
+
}
|
|
1701
|
+
function formatDedupMarker(result) {
|
|
1702
|
+
if (result.verdict === "unique" || result.matches.length === 0) {
|
|
1692
1703
|
return void 0;
|
|
1693
1704
|
}
|
|
1694
|
-
const
|
|
1695
|
-
return {
|
|
1696
|
-
summary: synthesizedSummary,
|
|
1697
|
-
intent_clues: [],
|
|
1698
|
-
tech_stack: [],
|
|
1699
|
-
impact: [],
|
|
1700
|
-
must_read_if: synthesizedSummary,
|
|
1701
|
-
// v2.0-rc.22: when frontmatter is present, merge its knowledge fields;
|
|
1702
|
-
// when fully absent (no `---` block), all knowledge fields stay
|
|
1703
|
-
// undefined, matching the original heading-only fallback contract.
|
|
1704
|
-
id: knowledge?.id,
|
|
1705
|
-
knowledge_type: knowledge?.knowledge_type,
|
|
1706
|
-
maturity: knowledge?.maturity,
|
|
1707
|
-
knowledge_layer: knowledge?.knowledge_layer,
|
|
1708
|
-
layer_reason: knowledge?.layer_reason,
|
|
1709
|
-
created_at: knowledge?.created_at,
|
|
1710
|
-
tags: knowledge?.tags,
|
|
1711
|
-
// v2.0-rc.5 (C1): default-safe values when there is no frontmatter at all;
|
|
1712
|
-
// when frontmatter exists, honor its declared values (extractKnowledge
|
|
1713
|
-
// FieldsFromFrontmatter already applies the broad-default for missing
|
|
1714
|
-
// or malformed scopes).
|
|
1715
|
-
relevance_scope: knowledge?.relevance_scope ?? "broad",
|
|
1716
|
-
relevance_paths: knowledge?.relevance_paths ?? [],
|
|
1717
|
-
// v2.2 H2-related (W1-T7): graph edges, undefined when absent.
|
|
1718
|
-
related: knowledge?.related
|
|
1719
|
-
};
|
|
1705
|
+
const top = result.matches[0];
|
|
1706
|
+
return `${result.verdict} of ${top.stable_id} (${top.similarity.toFixed(2)})`;
|
|
1720
1707
|
}
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1708
|
+
|
|
1709
|
+
// src/services/extract-knowledge.ts
|
|
1710
|
+
var SLUG_MAX_LENGTH = 40;
|
|
1711
|
+
var INJECTION_PATTERNS = [
|
|
1712
|
+
{
|
|
1713
|
+
name: "ignore-prior-instructions",
|
|
1714
|
+
pattern: /\b(?:ignore|forget|disregard)\s+(?:all\s+)?(?:previous|prior|above|earlier)\s+(?:instructions?|messages?|prompts?|rules?)\b/giu
|
|
1715
|
+
},
|
|
1716
|
+
{
|
|
1717
|
+
name: "forget-your-role",
|
|
1718
|
+
pattern: /\b(?:forget|disregard|ignore)\s+(?:your|the)\s+(?:role|identity|system\s+prompt)\b/giu
|
|
1719
|
+
},
|
|
1720
|
+
{
|
|
1721
|
+
name: "you-are-now",
|
|
1722
|
+
pattern: /\byou\s+are\s+now\s+(?:a|an)\s+\w+\s+(?:assistant|agent|model|bot|persona)\b/giu
|
|
1723
|
+
},
|
|
1724
|
+
{
|
|
1725
|
+
name: "rm-rf-root",
|
|
1726
|
+
pattern: /\brm\s+-rf?\s+(?:--no-preserve-root\s+)?[/~][^\s`'")>}]*?\s*(?:\/[*]?|;|$|\n|\|)/giu
|
|
1727
|
+
},
|
|
1728
|
+
{
|
|
1729
|
+
name: "shell-eval-curl",
|
|
1730
|
+
pattern: /\b(?:eval|sh|bash|zsh)\s+(?:-\w+\s+)?["'`]?\$\(\s*curl\s+[^)]+\)/giu
|
|
1731
|
+
},
|
|
1732
|
+
{
|
|
1733
|
+
name: "chatml-envelope",
|
|
1734
|
+
pattern: /<\|(?:im_start|im_end|system|user|assistant|endoftext|fim_prefix|fim_suffix|fim_middle)\|>/giu
|
|
1735
|
+
},
|
|
1736
|
+
{
|
|
1737
|
+
name: "claude-envelope",
|
|
1738
|
+
pattern: /\b(?:Human:|Assistant:)\s*<.*?>/giu
|
|
1750
1739
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1740
|
+
];
|
|
1741
|
+
var INJECTION_REDACTION_MARKER = "[REDACTED: prompt-injection pattern stripped by fab_propose \u2014 NEW-31]";
|
|
1742
|
+
function sanitizeInjectionPatterns(input) {
|
|
1743
|
+
let sanitized = input;
|
|
1744
|
+
const redactions = [];
|
|
1745
|
+
for (const { name, pattern } of INJECTION_PATTERNS) {
|
|
1746
|
+
const matches = sanitized.match(pattern);
|
|
1747
|
+
if (matches === null || matches.length === 0) continue;
|
|
1748
|
+
redactions.push({ name, matches: matches.length });
|
|
1749
|
+
sanitized = sanitized.replace(pattern, INJECTION_REDACTION_MARKER);
|
|
1754
1750
|
}
|
|
1755
|
-
return
|
|
1751
|
+
return { sanitized, redactions };
|
|
1756
1752
|
}
|
|
1757
|
-
function
|
|
1758
|
-
const
|
|
1759
|
-
|
|
1760
|
-
|
|
1753
|
+
function sanitizeInjectionFields(fields) {
|
|
1754
|
+
const out = { ...fields };
|
|
1755
|
+
const allRedactions = [];
|
|
1756
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
1757
|
+
if (typeof value === "string") {
|
|
1758
|
+
const { sanitized, redactions } = sanitizeInjectionPatterns(value);
|
|
1759
|
+
out[key] = sanitized;
|
|
1760
|
+
for (const r of redactions) {
|
|
1761
|
+
allRedactions.push({ field: key, ...r });
|
|
1762
|
+
}
|
|
1763
|
+
} else if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
|
|
1764
|
+
const cleaned = [];
|
|
1765
|
+
for (const entry of value) {
|
|
1766
|
+
const { sanitized, redactions } = sanitizeInjectionPatterns(entry);
|
|
1767
|
+
cleaned.push(sanitized);
|
|
1768
|
+
for (const r of redactions) {
|
|
1769
|
+
allRedactions.push({ field: key, ...r });
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
out[key] = cleaned;
|
|
1773
|
+
}
|
|
1761
1774
|
}
|
|
1762
|
-
|
|
1763
|
-
return tail ?? ref;
|
|
1775
|
+
return { sanitized: out, allRedactions };
|
|
1764
1776
|
}
|
|
1765
|
-
function
|
|
1766
|
-
const
|
|
1767
|
-
const
|
|
1768
|
-
const
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
`);
|
|
1777
|
+
function redactPiiFields(fields) {
|
|
1778
|
+
const out = { ...fields };
|
|
1779
|
+
const redactedFields = /* @__PURE__ */ new Set();
|
|
1780
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
1781
|
+
if (typeof value === "string") {
|
|
1782
|
+
const cleaned = redactPii(value);
|
|
1783
|
+
out[key] = cleaned;
|
|
1784
|
+
if (cleaned !== value) redactedFields.add(key);
|
|
1785
|
+
} else if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
|
|
1786
|
+
const cleaned = value.map((entry) => redactPii(entry));
|
|
1787
|
+
out[key] = cleaned;
|
|
1788
|
+
if (cleaned.some((entry, index) => entry !== value[index])) {
|
|
1789
|
+
redactedFields.add(key);
|
|
1790
|
+
}
|
|
1780
1791
|
}
|
|
1781
1792
|
}
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1793
|
+
return { redacted: out, redactedFields: [...redactedFields].sort() };
|
|
1794
|
+
}
|
|
1795
|
+
function pendingBase(layer, projectRoot, semanticScope) {
|
|
1796
|
+
return resolveStorePendingBase(layer, projectRoot, semanticScope);
|
|
1797
|
+
}
|
|
1798
|
+
function toPosixPath(path) {
|
|
1799
|
+
return path.replace(/\\/gu, "/");
|
|
1800
|
+
}
|
|
1801
|
+
async function extractKnowledge(projectRoot, input) {
|
|
1802
|
+
const sanitizedInputFields = sanitizeInjectionFields({
|
|
1803
|
+
slug: input.slug ?? "",
|
|
1804
|
+
user_messages_summary: input.user_messages_summary ?? "",
|
|
1805
|
+
session_context: input.session_context ?? "",
|
|
1806
|
+
must_read_if: input.must_read_if ?? "",
|
|
1807
|
+
intent_clues: input.intent_clues ?? []
|
|
1808
|
+
});
|
|
1809
|
+
input = {
|
|
1810
|
+
...input,
|
|
1811
|
+
slug: sanitizedInputFields.sanitized.slug,
|
|
1812
|
+
user_messages_summary: sanitizedInputFields.sanitized.user_messages_summary || void 0,
|
|
1813
|
+
session_context: sanitizedInputFields.sanitized.session_context || void 0,
|
|
1814
|
+
must_read_if: sanitizedInputFields.sanitized.must_read_if || void 0,
|
|
1815
|
+
intent_clues: sanitizedInputFields.sanitized.intent_clues.length > 0 ? sanitizedInputFields.sanitized.intent_clues : void 0
|
|
1788
1816
|
};
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
const
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
`
|
|
1798
|
-
}
|
|
1817
|
+
if (sanitizedInputFields.allRedactions.length > 0) {
|
|
1818
|
+
const summary2 = sanitizedInputFields.allRedactions.map((r) => `${r.field}:${r.name}x${r.matches}`).join(",");
|
|
1819
|
+
const primarySessionForLog = (input.source_sessions ?? [])[0] ?? "";
|
|
1820
|
+
await emitEventBestEffort(projectRoot, {
|
|
1821
|
+
event_type: "knowledge_archive_attempted",
|
|
1822
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1823
|
+
correlation_id: primarySessionForLog,
|
|
1824
|
+
session_id: primarySessionForLog,
|
|
1825
|
+
reason: `extract_knowledge:injection-redacted:${summary2}`
|
|
1826
|
+
});
|
|
1799
1827
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1828
|
+
const piiRedactedInputFields = redactPiiFields({
|
|
1829
|
+
slug: input.slug ?? "",
|
|
1830
|
+
user_messages_summary: input.user_messages_summary ?? "",
|
|
1831
|
+
session_context: input.session_context ?? "",
|
|
1832
|
+
must_read_if: input.must_read_if ?? "",
|
|
1833
|
+
intent_clues: input.intent_clues ?? [],
|
|
1834
|
+
tags: input.tags ?? [],
|
|
1835
|
+
evidence_paths: input.evidence_paths ?? []
|
|
1836
|
+
});
|
|
1837
|
+
input = {
|
|
1838
|
+
...input,
|
|
1839
|
+
slug: piiRedactedInputFields.redacted.slug,
|
|
1840
|
+
user_messages_summary: piiRedactedInputFields.redacted.user_messages_summary || void 0,
|
|
1841
|
+
session_context: piiRedactedInputFields.redacted.session_context || void 0,
|
|
1842
|
+
must_read_if: piiRedactedInputFields.redacted.must_read_if || void 0,
|
|
1843
|
+
intent_clues: piiRedactedInputFields.redacted.intent_clues.length > 0 ? piiRedactedInputFields.redacted.intent_clues : void 0,
|
|
1844
|
+
tags: piiRedactedInputFields.redacted.tags.length > 0 ? piiRedactedInputFields.redacted.tags : void 0,
|
|
1845
|
+
evidence_paths: piiRedactedInputFields.redacted.evidence_paths.length > 0 ? piiRedactedInputFields.redacted.evidence_paths : void 0
|
|
1846
|
+
};
|
|
1847
|
+
if (piiRedactedInputFields.redactedFields.length > 0) {
|
|
1848
|
+
const primarySessionForLog = (input.source_sessions ?? [])[0] ?? "";
|
|
1849
|
+
await emitEventBestEffort(projectRoot, {
|
|
1850
|
+
event_type: "knowledge_archive_attempted",
|
|
1851
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1852
|
+
correlation_id: primarySessionForLog,
|
|
1853
|
+
session_id: primarySessionForLog,
|
|
1854
|
+
reason: `extract_knowledge:pii-redacted:${piiRedactedInputFields.redactedFields.join(",")}`
|
|
1855
|
+
});
|
|
1809
1856
|
}
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
}
|
|
1857
|
+
const sanitizedSlug = sanitizeSlug(input.slug);
|
|
1858
|
+
const sourceSessions = input.source_sessions ?? [];
|
|
1859
|
+
const primarySession = sourceSessions[0] ?? "";
|
|
1860
|
+
const idempotencyKey = sha256(
|
|
1861
|
+
JSON.stringify({
|
|
1862
|
+
source_session: primarySession,
|
|
1863
|
+
type: input.type,
|
|
1864
|
+
slug: sanitizedSlug
|
|
1865
|
+
})
|
|
1866
|
+
);
|
|
1867
|
+
const summary = input.user_messages_summary ?? "";
|
|
1868
|
+
const summaryTrimmed = summary.trim();
|
|
1869
|
+
const summaryIsEmpty = summaryTrimmed.length === 0;
|
|
1870
|
+
const slugIsEmpty = sanitizedSlug.length === 0;
|
|
1871
|
+
const summaryTooShort = !summaryIsEmpty && summaryTrimmed.length < 15;
|
|
1872
|
+
const summaryEqualsSlug = !summaryIsEmpty && summaryTrimmed.toLowerCase() === sanitizedSlug.toLowerCase();
|
|
1873
|
+
const summaryLooksLikeStableId = !summaryIsEmpty && /^K[TP]-[A-Z]{3}-\d{4}$/.test(summaryTrimmed);
|
|
1874
|
+
const summaryIsOpaque = summaryTooShort || summaryEqualsSlug || summaryLooksLikeStableId;
|
|
1875
|
+
if (summaryIsEmpty || slugIsEmpty || summaryIsOpaque) {
|
|
1876
|
+
const reason = summaryIsEmpty ? "empty_summary" : slugIsEmpty ? "empty_slug" : summaryTooShort ? "summary_too_short" : summaryEqualsSlug ? "summary_equals_slug" : "summary_looks_like_stable_id";
|
|
1877
|
+
await emitEventBestEffort(projectRoot, {
|
|
1878
|
+
event_type: "knowledge_archive_attempted",
|
|
1879
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1880
|
+
correlation_id: primarySession,
|
|
1881
|
+
session_id: primarySession,
|
|
1882
|
+
reason: `extract_knowledge:${sanitizedSlug || input.slug}:${reason}`
|
|
1883
|
+
});
|
|
1884
|
+
return {
|
|
1885
|
+
pending_path: "",
|
|
1886
|
+
idempotency_key: idempotencyKey
|
|
1887
|
+
};
|
|
1819
1888
|
}
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1889
|
+
const secretScanTarget = [
|
|
1890
|
+
input.slug ?? "",
|
|
1891
|
+
input.user_messages_summary ?? "",
|
|
1892
|
+
input.session_context ?? "",
|
|
1893
|
+
input.must_read_if ?? "",
|
|
1894
|
+
...input.intent_clues ?? [],
|
|
1895
|
+
...input.tags ?? [],
|
|
1896
|
+
...input.evidence_paths ?? []
|
|
1897
|
+
].join("\n");
|
|
1898
|
+
if (hasSecrets(secretScanTarget)) {
|
|
1899
|
+
await emitEventBestEffort(projectRoot, {
|
|
1900
|
+
event_type: "knowledge_archive_attempted",
|
|
1901
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1902
|
+
correlation_id: primarySession,
|
|
1903
|
+
session_id: primarySession,
|
|
1904
|
+
reason: `extract_knowledge:${sanitizedSlug || input.slug}:secret_detected`
|
|
1905
|
+
});
|
|
1906
|
+
return {
|
|
1907
|
+
pending_path: "",
|
|
1908
|
+
idempotency_key: idempotencyKey
|
|
1909
|
+
};
|
|
1828
1910
|
}
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1911
|
+
const semanticScope = input.audience;
|
|
1912
|
+
const scopeIsPersonal = semanticScope !== void 0 && isPersonalScope(semanticScope);
|
|
1913
|
+
const layer = scopeIsPersonal ? "personal" : "team";
|
|
1914
|
+
let relevancePaths = input.paths;
|
|
1915
|
+
let relevanceScope = relevancePaths === void 0 ? void 0 : relevancePaths.length > 0 ? "narrow" : "broad";
|
|
1916
|
+
const shouldAutoDegrade = layer === "personal" && relevanceScope === "narrow";
|
|
1917
|
+
if (shouldAutoDegrade) {
|
|
1918
|
+
relevanceScope = "broad";
|
|
1919
|
+
relevancePaths = [];
|
|
1920
|
+
await emitEventBestEffort(projectRoot, {
|
|
1921
|
+
event_type: "knowledge_scope_degraded",
|
|
1922
|
+
stable_id: `pending:${idempotencyKey}`,
|
|
1923
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1924
|
+
from_scope: "narrow",
|
|
1925
|
+
to_scope: "broad",
|
|
1926
|
+
reason: "personal-implies-broad"
|
|
1927
|
+
});
|
|
1839
1928
|
}
|
|
1840
|
-
const
|
|
1841
|
-
const
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1929
|
+
const baseDir = pendingBase(layer, projectRoot, semanticScope);
|
|
1930
|
+
const { absolutePath, sanitizedSlug: chosenSlug, idempotencyKey: chosenKey } = await resolveDisambiguatedSlugPath({
|
|
1931
|
+
baseDir,
|
|
1932
|
+
type: input.type,
|
|
1933
|
+
slug: sanitizedSlug,
|
|
1934
|
+
primarySession,
|
|
1935
|
+
baseIdempotencyKey: idempotencyKey
|
|
1936
|
+
});
|
|
1937
|
+
const reportedPath = toPosixPath(absolutePath);
|
|
1938
|
+
const effectiveSanitizedSlug = chosenSlug;
|
|
1939
|
+
const effectiveIdempotencyKey = chosenKey;
|
|
1940
|
+
const writeScopeMeta = resolveWriteScopeMeta(layer, projectRoot, semanticScope);
|
|
1941
|
+
await ensureParentDirectory(absolutePath);
|
|
1942
|
+
if (existsSync3(absolutePath)) {
|
|
1943
|
+
const existing = await readFile4(absolutePath, "utf8");
|
|
1944
|
+
const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
|
|
1945
|
+
if (existingKey === effectiveIdempotencyKey) {
|
|
1946
|
+
const fresh2 = renderFreshEntry({
|
|
1947
|
+
type: input.type,
|
|
1948
|
+
sourceSessions,
|
|
1949
|
+
idempotencyKey: effectiveIdempotencyKey,
|
|
1950
|
+
summary,
|
|
1951
|
+
recentPaths: input.recent_paths,
|
|
1952
|
+
layer,
|
|
1953
|
+
semanticScope: writeScopeMeta.semantic_scope,
|
|
1954
|
+
visibilityStore: writeScopeMeta.visibility_store,
|
|
1955
|
+
proposedReason: input.proposed_reason,
|
|
1956
|
+
sessionContext: input.session_context,
|
|
1957
|
+
relevanceScope,
|
|
1958
|
+
relevancePaths,
|
|
1959
|
+
intentClues: input.intent_clues,
|
|
1960
|
+
techStack: input.tech_stack,
|
|
1961
|
+
impact: input.impact,
|
|
1962
|
+
mustReadIf: input.must_read_if,
|
|
1963
|
+
onboardSlot: input.onboard_slot,
|
|
1964
|
+
// v2.0.0-rc.37 NEW-37: pass-through topic tags.
|
|
1965
|
+
tags: input.tags,
|
|
1966
|
+
// v2.0.0-rc.37 NEW-7: pass-through evidence_paths to frontmatter.
|
|
1967
|
+
evidencePaths: input.evidence_paths
|
|
1968
|
+
});
|
|
1969
|
+
const augmented = mergeEvidenceNotes(existing, fresh2);
|
|
1970
|
+
await atomicWriteText(absolutePath, augmented);
|
|
1971
|
+
await emitEventBestEffort(projectRoot, {
|
|
1972
|
+
event_type: "knowledge_proposed",
|
|
1973
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1974
|
+
correlation_id: primarySession,
|
|
1975
|
+
session_id: primarySession,
|
|
1976
|
+
reason: `extract_knowledge:${effectiveSanitizedSlug}`
|
|
1977
|
+
});
|
|
1978
|
+
return {
|
|
1979
|
+
pending_path: reportedPath,
|
|
1980
|
+
idempotency_key: effectiveIdempotencyKey
|
|
1981
|
+
};
|
|
1853
1982
|
}
|
|
1854
|
-
|
|
1983
|
+
throw new Error(
|
|
1984
|
+
`slug collision (unreachable after rc.37 NEW-6): pending file ${reportedPath} already exists with key ${existingKey ?? "<missing>"} != ${effectiveIdempotencyKey}`
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
const dedupMarker = await computeDedupMarker(projectRoot, {
|
|
1988
|
+
text: [
|
|
1989
|
+
summary,
|
|
1990
|
+
input.must_read_if ?? "",
|
|
1991
|
+
...input.intent_clues ?? [],
|
|
1992
|
+
...input.impact ?? [],
|
|
1993
|
+
...input.tags ?? []
|
|
1994
|
+
].join(" "),
|
|
1995
|
+
knowledge_type: input.type,
|
|
1996
|
+
layer
|
|
1997
|
+
});
|
|
1998
|
+
const fresh = renderFreshEntry({
|
|
1999
|
+
type: input.type,
|
|
2000
|
+
sourceSessions,
|
|
2001
|
+
idempotencyKey: effectiveIdempotencyKey,
|
|
2002
|
+
summary,
|
|
2003
|
+
recentPaths: input.recent_paths,
|
|
2004
|
+
layer,
|
|
2005
|
+
semanticScope: writeScopeMeta.semantic_scope,
|
|
2006
|
+
visibilityStore: writeScopeMeta.visibility_store,
|
|
2007
|
+
proposedReason: input.proposed_reason,
|
|
2008
|
+
sessionContext: input.session_context,
|
|
2009
|
+
relevanceScope,
|
|
2010
|
+
relevancePaths,
|
|
2011
|
+
intentClues: input.intent_clues,
|
|
2012
|
+
techStack: input.tech_stack,
|
|
2013
|
+
impact: input.impact,
|
|
2014
|
+
mustReadIf: input.must_read_if,
|
|
2015
|
+
onboardSlot: input.onboard_slot,
|
|
2016
|
+
tags: input.tags,
|
|
2017
|
+
// v2.0.0-rc.37 NEW-7: pass-through evidence_paths to frontmatter.
|
|
2018
|
+
evidencePaths: input.evidence_paths,
|
|
2019
|
+
dedupMarker
|
|
2020
|
+
});
|
|
2021
|
+
await atomicWriteText(absolutePath, fresh);
|
|
2022
|
+
await emitEventBestEffort(projectRoot, {
|
|
2023
|
+
event_type: "knowledge_proposed",
|
|
2024
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2025
|
+
correlation_id: primarySession,
|
|
2026
|
+
session_id: primarySession,
|
|
2027
|
+
reason: `extract_knowledge:${effectiveSanitizedSlug}`
|
|
1855
2028
|
});
|
|
1856
2029
|
return {
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
maturity,
|
|
1860
|
-
knowledge_layer,
|
|
1861
|
-
layer_reason: rawLayerReason,
|
|
1862
|
-
created_at,
|
|
1863
|
-
tags: tags.length > 0 ? tags : void 0,
|
|
1864
|
-
relevance_scope,
|
|
1865
|
-
relevance_paths,
|
|
1866
|
-
related: related.length > 0 ? related : void 0
|
|
2030
|
+
pending_path: reportedPath,
|
|
2031
|
+
idempotency_key: effectiveIdempotencyKey
|
|
1867
2032
|
};
|
|
1868
2033
|
}
|
|
1869
|
-
function
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
2034
|
+
async function computeDedupMarker(projectRoot, candidate) {
|
|
2035
|
+
try {
|
|
2036
|
+
const corpus = await collectStoreCanonicalEntries(projectRoot);
|
|
2037
|
+
const result = classifyArchiveCandidate(
|
|
2038
|
+
candidate,
|
|
2039
|
+
corpus.map((e) => ({
|
|
2040
|
+
stable_id: e.qualifiedId,
|
|
2041
|
+
knowledge_type: typeof e.description.knowledge_type === "string" && e.description.knowledge_type.length > 0 ? e.description.knowledge_type : e.type,
|
|
2042
|
+
layer: e.layer,
|
|
2043
|
+
text: [
|
|
2044
|
+
e.description.summary ?? "",
|
|
2045
|
+
e.description.must_read_if ?? "",
|
|
2046
|
+
...e.description.intent_clues ?? [],
|
|
2047
|
+
...e.description.impact ?? [],
|
|
2048
|
+
...e.description.tags ?? []
|
|
2049
|
+
].join(" ")
|
|
2050
|
+
}))
|
|
2051
|
+
);
|
|
2052
|
+
return formatDedupMarker(result);
|
|
2053
|
+
} catch {
|
|
1873
2054
|
return void 0;
|
|
1874
2055
|
}
|
|
1875
|
-
return unquote(match[1].trim());
|
|
1876
2056
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
2057
|
+
var SLUG_DISAMBIGUATE_MAX_VARIANTS = 9;
|
|
2058
|
+
async function resolveDisambiguatedSlugPath(args) {
|
|
2059
|
+
for (let n = 1; n <= SLUG_DISAMBIGUATE_MAX_VARIANTS; n += 1) {
|
|
2060
|
+
const candidateSlug = n === 1 ? args.slug : `${args.slug}-${n}`;
|
|
2061
|
+
const candidatePath = join7(args.baseDir, args.type, `${candidateSlug}.md`);
|
|
2062
|
+
const candidateKey = n === 1 ? args.baseIdempotencyKey : sha256(
|
|
2063
|
+
JSON.stringify({
|
|
2064
|
+
source_session: args.primarySession,
|
|
2065
|
+
type: args.type,
|
|
2066
|
+
slug: candidateSlug
|
|
2067
|
+
})
|
|
2068
|
+
);
|
|
2069
|
+
if (!existsSync3(candidatePath)) {
|
|
2070
|
+
return {
|
|
2071
|
+
absolutePath: candidatePath,
|
|
2072
|
+
sanitizedSlug: candidateSlug,
|
|
2073
|
+
idempotencyKey: candidateKey
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
const existing = await readFile4(candidatePath, "utf8");
|
|
2077
|
+
const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
|
|
2078
|
+
if (existingKey === candidateKey) {
|
|
2079
|
+
return {
|
|
2080
|
+
absolutePath: candidatePath,
|
|
2081
|
+
sanitizedSlug: candidateSlug,
|
|
2082
|
+
idempotencyKey: candidateKey
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
1882
2085
|
}
|
|
1883
|
-
|
|
1884
|
-
}
|
|
1885
|
-
function unquote(value) {
|
|
1886
|
-
return value.replace(/^["'](.*)["']$/u, "$1");
|
|
1887
|
-
}
|
|
1888
|
-
function escapeRegExp(value) {
|
|
1889
|
-
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
// src/services/cross-store-recall.ts
|
|
1893
|
-
var readSetWalkCache = /* @__PURE__ */ new Map();
|
|
1894
|
-
var readSetWalkCount = 0;
|
|
1895
|
-
function readSetWalkCacheKey(projectRoot) {
|
|
1896
|
-
return `${projectRoot}\0${process.env.FABRIC_HOME ?? ""}`;
|
|
1897
|
-
}
|
|
1898
|
-
async function readSetFingerprint(refs) {
|
|
1899
|
-
const parts = await Promise.all(
|
|
1900
|
-
refs.map(async (ref) => {
|
|
1901
|
-
try {
|
|
1902
|
-
const fileStat = await stat(ref.file);
|
|
1903
|
-
return `${ref.store_uuid}|${ref.alias}|${ref.file}|${fileStat.size}|${fileStat.mtimeMs}`;
|
|
1904
|
-
} catch {
|
|
1905
|
-
return `${ref.store_uuid}|${ref.alias}|${ref.file}|missing`;
|
|
1906
|
-
}
|
|
1907
|
-
})
|
|
2086
|
+
throw new Error(
|
|
2087
|
+
`slug exhaustion: tried ${args.slug}.md plus -2..-${SLUG_DISAMBIGUATE_MAX_VARIANTS} suffix variants and all slots are taken by entries with different idempotency_keys; rename slug at the caller and retry`
|
|
1908
2088
|
);
|
|
1909
|
-
return parts.sort().join("\n");
|
|
1910
|
-
}
|
|
1911
|
-
var SEMANTIC_SCOPE_LINE = /^semantic_scope:\s*"?([^"\n]+?)"?\s*$/mu;
|
|
1912
|
-
function readSemanticScope(source, layer) {
|
|
1913
|
-
const match = SEMANTIC_SCOPE_LINE.exec(source);
|
|
1914
|
-
return match?.[1] ?? layer;
|
|
1915
2089
|
}
|
|
1916
|
-
function
|
|
1917
|
-
|
|
1918
|
-
|
|
2090
|
+
function sanitizeSlug(raw) {
|
|
2091
|
+
const lower = raw.toLowerCase();
|
|
2092
|
+
const collapsed = lower.replace(/[^a-z0-9]+/g, "-");
|
|
2093
|
+
const trimmed = collapsed.replace(/^-+|-+$/g, "");
|
|
2094
|
+
if (trimmed.length === 0) {
|
|
2095
|
+
return "";
|
|
1919
2096
|
}
|
|
1920
|
-
|
|
1921
|
-
return entries.filter(
|
|
1922
|
-
(e) => scopeRoot(e.semanticScope) !== "project" || e.semanticScope === current
|
|
1923
|
-
);
|
|
1924
|
-
}
|
|
1925
|
-
function activeProjectOf(projectRoot) {
|
|
1926
|
-
const ap = loadProjectConfig2(projectRoot)?.active_project;
|
|
1927
|
-
return ap !== void 0 && ap.length > 0 ? ap : void 0;
|
|
2097
|
+
return trimmed.slice(0, SLUG_MAX_LENGTH).replace(/-+$/g, "");
|
|
1928
2098
|
}
|
|
1929
|
-
|
|
1930
|
-
const
|
|
1931
|
-
|
|
1932
|
-
|
|
2099
|
+
function renderFreshEntry(args) {
|
|
2100
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2101
|
+
const frontmatterLines = [
|
|
2102
|
+
"---",
|
|
2103
|
+
`type: ${args.type}`,
|
|
2104
|
+
"maturity: draft",
|
|
2105
|
+
`layer: ${args.layer}`,
|
|
2106
|
+
// v2.1 global-refactor (W1/A1): scope coordinate (resolution axis) + the
|
|
2107
|
+
// physical store this entry lives in. `layer` is retained for back-compat
|
|
2108
|
+
// during the co-location retirement; `semantic_scope`/`visibility_store` are
|
|
2109
|
+
// the v2.1 source of truth (scope ⊥ store).
|
|
2110
|
+
`semantic_scope: ${args.semanticScope}`,
|
|
2111
|
+
`visibility_store: ${quoteRelevancePath(args.visibilityStore)}`,
|
|
2112
|
+
`created_at: ${createdAt}`,
|
|
2113
|
+
`source_sessions: [${args.sourceSessions.map((s) => JSON.stringify(s)).join(", ")}]`,
|
|
2114
|
+
`proposed_reason: ${args.proposedReason}`,
|
|
2115
|
+
// rc.31 BUG-2.9/2.1: persist the caller-supplied summary in frontmatter so
|
|
2116
|
+
// knowledge-meta-builder.extractDescriptionFromFrontmatter picks it up
|
|
2117
|
+
// directly. Without this, the meta-builder fell back to extractRule
|
|
2118
|
+
// Description's h1-or-stable-id-or-placeholder synthesis (line ~944),
|
|
2119
|
+
// which made user-visible description.summary == stable_id for any
|
|
2120
|
+
// pending file whose body started with h2-only sections (`## Summary` is
|
|
2121
|
+
// the canonical pending shape). The frontmatter `summary:` line is the
|
|
2122
|
+
// canonical source-of-truth: `extractDescriptionFromFrontmatter` reads it
|
|
2123
|
+
// before extractRuleDescription's fallback kicks in.
|
|
2124
|
+
`summary: ${quoteRelevancePath(args.summary)}`,
|
|
2125
|
+
// v2.0.0-rc.37 NEW-37: render caller-supplied tags or fall back to empty
|
|
2126
|
+
// array. Empty array still legal but doctor's knowledge_tags_empty_ratio
|
|
2127
|
+
// lint will warn at the corpus level. Encourages 2-4 kebab-case topic
|
|
2128
|
+
// strings per entry for cross-entry retrieval signal.
|
|
2129
|
+
args.tags !== void 0 && args.tags.length > 0 ? `tags: [${args.tags.map((t) => quoteRelevancePath(t)).join(", ")}]` : "tags: []"
|
|
2130
|
+
];
|
|
2131
|
+
if (args.relevanceScope !== void 0) {
|
|
2132
|
+
frontmatterLines.push(`relevance_scope: ${args.relevanceScope}`);
|
|
1933
2133
|
}
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
2134
|
+
if (args.relevancePaths !== void 0) {
|
|
2135
|
+
const pathsBody = args.relevancePaths.map((p) => quoteRelevancePath(p)).join(", ");
|
|
2136
|
+
frontmatterLines.push(`relevance_paths: [${pathsBody}]`);
|
|
1937
2137
|
}
|
|
1938
|
-
|
|
1939
|
-
|
|
2138
|
+
if (args.intentClues !== void 0) {
|
|
2139
|
+
const body2 = args.intentClues.map((s) => quoteRelevancePath(s)).join(", ");
|
|
2140
|
+
frontmatterLines.push(`intent_clues: [${body2}]`);
|
|
2141
|
+
}
|
|
2142
|
+
if (args.techStack !== void 0) {
|
|
2143
|
+
const body2 = args.techStack.map((s) => quoteRelevancePath(s)).join(", ");
|
|
2144
|
+
frontmatterLines.push(`tech_stack: [${body2}]`);
|
|
2145
|
+
}
|
|
2146
|
+
if (args.impact !== void 0) {
|
|
2147
|
+
const body2 = args.impact.map((s) => quoteRelevancePath(s)).join(", ");
|
|
2148
|
+
frontmatterLines.push(`impact: [${body2}]`);
|
|
2149
|
+
}
|
|
2150
|
+
if (args.mustReadIf !== void 0) {
|
|
2151
|
+
frontmatterLines.push(`must_read_if: ${quoteRelevancePath(args.mustReadIf)}`);
|
|
2152
|
+
}
|
|
2153
|
+
if (args.onboardSlot !== void 0) {
|
|
2154
|
+
frontmatterLines.push(`onboard_slot: ${args.onboardSlot}`);
|
|
2155
|
+
}
|
|
2156
|
+
if (args.evidencePaths !== void 0 && args.evidencePaths.length > 0) {
|
|
2157
|
+
const body2 = args.evidencePaths.map((p) => quoteRelevancePath(p)).join(", ");
|
|
2158
|
+
frontmatterLines.push(`evidence_paths: [${body2}]`);
|
|
2159
|
+
}
|
|
2160
|
+
if (args.dedupMarker !== void 0) {
|
|
2161
|
+
frontmatterLines.push(`x-fabric-dedup: ${quoteRelevancePath(args.dedupMarker)}`);
|
|
2162
|
+
}
|
|
2163
|
+
frontmatterLines.push(
|
|
2164
|
+
`x-fabric-idempotency-key: ${args.idempotencyKey}`,
|
|
2165
|
+
"---"
|
|
1940
2166
|
);
|
|
1941
|
-
const
|
|
1942
|
-
const
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
2167
|
+
const frontmatter = frontmatterLines.join("\n");
|
|
2168
|
+
const reasonExplanation = PROPOSED_REASON_DESCRIPTIONS_BY_LOCALE[resolveGlobalLocale()][args.proposedReason];
|
|
2169
|
+
const body = [
|
|
2170
|
+
"",
|
|
2171
|
+
"## Summary",
|
|
2172
|
+
"",
|
|
2173
|
+
args.summary,
|
|
2174
|
+
"",
|
|
2175
|
+
"## Why proposed",
|
|
2176
|
+
"",
|
|
2177
|
+
`${args.proposedReason} \u2014 ${reasonExplanation}`,
|
|
2178
|
+
"",
|
|
2179
|
+
"## Session context",
|
|
2180
|
+
"",
|
|
2181
|
+
args.sessionContext,
|
|
2182
|
+
"",
|
|
2183
|
+
"## Evidence",
|
|
2184
|
+
"",
|
|
2185
|
+
renderEvidenceBlock(args.summary, args.recentPaths),
|
|
2186
|
+
""
|
|
2187
|
+
].join("\n");
|
|
2188
|
+
return `${frontmatter}
|
|
2189
|
+
${body}`;
|
|
1958
2190
|
}
|
|
1959
|
-
|
|
1960
|
-
const
|
|
1961
|
-
|
|
1962
|
-
|
|
2191
|
+
function quoteRelevancePath(value) {
|
|
2192
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
2193
|
+
return `"${escaped}"`;
|
|
2194
|
+
}
|
|
2195
|
+
function renderEvidenceBlock(summary, recentPaths) {
|
|
2196
|
+
const pathLines = recentPaths.length === 0 ? "_(no recent paths reported)_" : recentPaths.map((p) => `- ${p}`).join("\n");
|
|
2197
|
+
return [
|
|
2198
|
+
"Recent paths:",
|
|
2199
|
+
"",
|
|
2200
|
+
pathLines,
|
|
2201
|
+
"",
|
|
2202
|
+
"Notes:",
|
|
2203
|
+
"",
|
|
2204
|
+
`- ${summary.trim()}`
|
|
2205
|
+
].join("\n");
|
|
2206
|
+
}
|
|
2207
|
+
function mergeEvidenceNotes(existing, fresh) {
|
|
2208
|
+
const freshSplit = splitAtEvidence(fresh);
|
|
2209
|
+
if (freshSplit === null) {
|
|
2210
|
+
return fresh.endsWith("\n") ? fresh : `${fresh}
|
|
2211
|
+
`;
|
|
1963
2212
|
}
|
|
1964
|
-
const
|
|
1965
|
-
const
|
|
1966
|
-
const
|
|
1967
|
-
|
|
1968
|
-
|
|
2213
|
+
const freshHead = freshSplit.head;
|
|
2214
|
+
const oldEvidence = collectEvidenceItems(existing);
|
|
2215
|
+
const freshEvidence = collectEvidenceItems(fresh);
|
|
2216
|
+
const mergedNotes = [];
|
|
2217
|
+
const seenNotes = /* @__PURE__ */ new Set();
|
|
2218
|
+
for (const note of [...oldEvidence.notes, ...freshEvidence.notes]) {
|
|
2219
|
+
const key = note.replace(/\s+/gu, " ").trim();
|
|
2220
|
+
if (key.length === 0) continue;
|
|
2221
|
+
if (seenNotes.has(key)) continue;
|
|
2222
|
+
seenNotes.add(key);
|
|
2223
|
+
mergedNotes.push(note);
|
|
1969
2224
|
}
|
|
1970
|
-
const
|
|
1971
|
-
|
|
1972
|
-
|
|
2225
|
+
const mergedPaths = [];
|
|
2226
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
2227
|
+
for (const p of [...oldEvidence.paths, ...freshEvidence.paths]) {
|
|
2228
|
+
const key = p.trim();
|
|
2229
|
+
if (key.length === 0) continue;
|
|
2230
|
+
if (seenPaths.has(key)) continue;
|
|
2231
|
+
seenPaths.add(key);
|
|
2232
|
+
mergedPaths.push(key);
|
|
2233
|
+
}
|
|
2234
|
+
const pathLines = mergedPaths.length === 0 ? "_(no recent paths reported)_" : mergedPaths.map((p) => `- ${p}`).join("\n");
|
|
2235
|
+
const noteLines = mergedNotes.length === 0 ? "_(no notes recorded)_" : mergedNotes.map((n) => `- ${n}`).join("\n");
|
|
2236
|
+
const evidenceBody = [
|
|
2237
|
+
"Recent paths:",
|
|
2238
|
+
"",
|
|
2239
|
+
pathLines,
|
|
2240
|
+
"",
|
|
2241
|
+
"Notes:",
|
|
2242
|
+
"",
|
|
2243
|
+
noteLines
|
|
2244
|
+
].join("\n");
|
|
2245
|
+
return `${freshHead}
|
|
2246
|
+
## Evidence
|
|
2247
|
+
|
|
2248
|
+
${evidenceBody}
|
|
2249
|
+
`;
|
|
1973
2250
|
}
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
} catch {
|
|
1981
|
-
return null;
|
|
1982
|
-
}
|
|
1983
|
-
const stableId = deriveRuleIdentity(ref.file, source, void 0).stableId;
|
|
1984
|
-
const layer = snapshot.personalUuids.has(ref.store_uuid) ? "personal" : "team";
|
|
1985
|
-
return {
|
|
1986
|
-
qualifiedId: `${ref.alias}:${stableId}`,
|
|
1987
|
-
file: ref.file,
|
|
1988
|
-
type: ref.type,
|
|
1989
|
-
alias: ref.alias,
|
|
1990
|
-
layer,
|
|
1991
|
-
semanticScope: readSemanticScope(source, layer),
|
|
1992
|
-
source
|
|
1993
|
-
};
|
|
1994
|
-
}));
|
|
1995
|
-
return entries.filter((entry) => entry !== null);
|
|
2251
|
+
function splitAtEvidence(content) {
|
|
2252
|
+
const tail = content.endsWith("\n") ? content : `${content}
|
|
2253
|
+
`;
|
|
2254
|
+
const match = /^([\s\S]*?)(\n## Evidence(?:\s*\(call \d+\))?\s*\n)/u.exec(tail);
|
|
2255
|
+
if (match === null) return null;
|
|
2256
|
+
return { head: match[1] ?? "" };
|
|
1996
2257
|
}
|
|
1997
|
-
|
|
1998
|
-
const
|
|
1999
|
-
const
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2258
|
+
function collectEvidenceItems(content) {
|
|
2259
|
+
const notes = [];
|
|
2260
|
+
const paths = [];
|
|
2261
|
+
const evidenceBlockRe = /\n## Evidence(?:\s*\(call \d+\))?\s*\n([\s\S]*?)(?=\n## |$)/gu;
|
|
2262
|
+
let m;
|
|
2263
|
+
while ((m = evidenceBlockRe.exec(`${content}
|
|
2264
|
+
`)) !== null) {
|
|
2265
|
+
const block = m[1] ?? "";
|
|
2266
|
+
const pathSection = /Recent paths:\s*\n([\s\S]*?)(?:\n\s*Notes:|$)/u.exec(block);
|
|
2267
|
+
if (pathSection !== null) {
|
|
2268
|
+
for (const rawLine of (pathSection[1] ?? "").split(/\r?\n/u)) {
|
|
2269
|
+
const t = rawLine.trim();
|
|
2270
|
+
if (t.startsWith("- ")) {
|
|
2271
|
+
paths.push(t.slice(2).trim());
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2004
2274
|
}
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2275
|
+
const notesSection = /Notes:\s*\n([\s\S]*?)$/u.exec(block);
|
|
2276
|
+
const noteBody = (notesSection !== null ? notesSection[1] : block) ?? "";
|
|
2277
|
+
const bulletLines = [];
|
|
2278
|
+
let prose = [];
|
|
2279
|
+
for (const rawLine of noteBody.split(/\r?\n/u)) {
|
|
2280
|
+
const t = rawLine.trim();
|
|
2281
|
+
if (t.length === 0) continue;
|
|
2282
|
+
if (t.startsWith("- ")) {
|
|
2283
|
+
if (prose.length > 0) {
|
|
2284
|
+
notes.push(prose.join(" ").trim());
|
|
2285
|
+
prose = [];
|
|
2286
|
+
}
|
|
2287
|
+
bulletLines.push(t.slice(2).trim());
|
|
2288
|
+
} else {
|
|
2289
|
+
prose.push(t);
|
|
2011
2290
|
}
|
|
2012
|
-
});
|
|
2013
|
-
}
|
|
2014
|
-
return items;
|
|
2015
|
-
}
|
|
2016
|
-
async function buildCrossStoreBodyIndex(projectRoot) {
|
|
2017
|
-
const index = /* @__PURE__ */ new Map();
|
|
2018
|
-
const activeProject = activeProjectOf(projectRoot);
|
|
2019
|
-
for (const entry of filterByActiveProject(await walkReadSetStores(projectRoot), activeProject)) {
|
|
2020
|
-
if (!index.has(entry.qualifiedId)) {
|
|
2021
|
-
index.set(entry.qualifiedId, { file: entry.file, layer: entry.layer });
|
|
2022
2291
|
}
|
|
2292
|
+
if (prose.length > 0) notes.push(prose.join(" ").trim());
|
|
2293
|
+
for (const n of bulletLines) notes.push(n);
|
|
2023
2294
|
}
|
|
2024
|
-
return
|
|
2295
|
+
return { notes, paths };
|
|
2025
2296
|
}
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
const
|
|
2037
|
-
const
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
census.by_type[type] = (census.by_type[type] ?? 0) + 1;
|
|
2043
|
-
}
|
|
2044
|
-
if (scopeRoot(entry.semanticScope) === "project") {
|
|
2045
|
-
census.by_layer.project += 1;
|
|
2046
|
-
} else {
|
|
2047
|
-
census.by_layer[entry.layer] += 1;
|
|
2048
|
-
}
|
|
2049
|
-
census.total += 1;
|
|
2297
|
+
function readFrontmatterKey(content, key) {
|
|
2298
|
+
const match = /^---\n([\s\S]*?)\n---/u.exec(content);
|
|
2299
|
+
if (match === null) {
|
|
2300
|
+
return void 0;
|
|
2301
|
+
}
|
|
2302
|
+
const block = match[1];
|
|
2303
|
+
if (block === void 0) {
|
|
2304
|
+
return void 0;
|
|
2305
|
+
}
|
|
2306
|
+
for (const rawLine of block.split(/\r?\n/u)) {
|
|
2307
|
+
const line = rawLine.trim();
|
|
2308
|
+
const sep5 = line.indexOf(":");
|
|
2309
|
+
if (sep5 === -1) continue;
|
|
2310
|
+
const k = line.slice(0, sep5).trim();
|
|
2311
|
+
if (k === key) {
|
|
2312
|
+
return line.slice(sep5 + 1).trim();
|
|
2050
2313
|
}
|
|
2051
|
-
} catch {
|
|
2052
2314
|
}
|
|
2053
|
-
return
|
|
2315
|
+
return void 0;
|
|
2054
2316
|
}
|
|
2055
|
-
async function
|
|
2056
|
-
const out = [];
|
|
2317
|
+
async function emitEventBestEffort(projectRoot, event) {
|
|
2057
2318
|
try {
|
|
2058
|
-
|
|
2059
|
-
for (const entry of filterByActiveProject(
|
|
2060
|
-
await walkReadSetStores(projectRoot),
|
|
2061
|
-
activeProject
|
|
2062
|
-
)) {
|
|
2063
|
-
const desc = extractRuleDescription(entry.source);
|
|
2064
|
-
if (desc === void 0) continue;
|
|
2065
|
-
const type = desc.knowledge_type;
|
|
2066
|
-
if (typeof type !== "string" || !ALWAYS_ACTIVE_TYPES.has(type)) continue;
|
|
2067
|
-
out.push({
|
|
2068
|
-
stable_id: entry.qualifiedId,
|
|
2069
|
-
type,
|
|
2070
|
-
layer: entry.layer,
|
|
2071
|
-
summary: typeof desc.summary === "string" ? desc.summary : "",
|
|
2072
|
-
body: extractBody(entry.source)
|
|
2073
|
-
});
|
|
2074
|
-
}
|
|
2319
|
+
await appendEventLedgerEvent(projectRoot, event);
|
|
2075
2320
|
} catch {
|
|
2076
|
-
return [];
|
|
2077
2321
|
}
|
|
2078
|
-
return out;
|
|
2079
|
-
}
|
|
2080
|
-
async function computeReadSetRevision(projectRoot) {
|
|
2081
|
-
const revisionSource = (await walkReadSetStores(projectRoot)).filter((entry) => !entry.file.includes("/knowledge/pending/")).map((entry) => `${entry.qualifiedId}|${sha256(entry.source)}`).sort().join("\n");
|
|
2082
|
-
return sha256(revisionSource);
|
|
2083
2322
|
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2323
|
+
|
|
2324
|
+
// src/tools/extract-knowledge.ts
|
|
2325
|
+
function registerExtractKnowledge(server, tracker) {
|
|
2326
|
+
server.registerTool(
|
|
2327
|
+
"fab_propose",
|
|
2328
|
+
{
|
|
2329
|
+
description: "Persist a proposed pending knowledge entry into the resolved write-target store under knowledge/pending/<type>/<slug>.md. Idempotent on (source_sessions[0], type, slug); repeat calls append evidence rather than overwrite. Skill-side tool \u2014 invoked at session-stop.",
|
|
2330
|
+
inputSchema: FabExtractKnowledgeInputShape,
|
|
2331
|
+
outputSchema: FabExtractKnowledgeOutputSchema.shape,
|
|
2332
|
+
annotations: fabExtractKnowledgeAnnotations
|
|
2333
|
+
},
|
|
2334
|
+
async (input) => {
|
|
2335
|
+
const requestId = randomUUID2();
|
|
2336
|
+
tracker?.enter(requestId);
|
|
2337
|
+
try {
|
|
2338
|
+
const gateResult = await awaitFirstReconcileGate();
|
|
2339
|
+
const gateWarn = gateWarning(gateResult);
|
|
2340
|
+
let validated;
|
|
2341
|
+
try {
|
|
2342
|
+
validated = FabExtractKnowledgeInputSchema.parse(input);
|
|
2343
|
+
} catch (parseErr) {
|
|
2344
|
+
if (parseErr instanceof ZodError) {
|
|
2345
|
+
const audienceIssue = parseErr.issues.find((issue) => issue.path.includes("audience"));
|
|
2346
|
+
if (audienceIssue !== void 0) {
|
|
2347
|
+
const hinted = new Error(audienceIssue.message);
|
|
2348
|
+
hinted.code = "scope_coordinate_invalid";
|
|
2349
|
+
hinted.action_hint = SCOPE_COORDINATE_HINT;
|
|
2350
|
+
throw hinted;
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
throw parseErr;
|
|
2354
|
+
}
|
|
2355
|
+
const projectRoot = resolveProjectRoot();
|
|
2356
|
+
const result = await extractKnowledge(projectRoot, validated);
|
|
2357
|
+
const response = { ...result };
|
|
2358
|
+
if (gateWarn) {
|
|
2359
|
+
response.warnings = [gateWarn];
|
|
2360
|
+
}
|
|
2361
|
+
const payloadLimits = readPayloadLimits(projectRoot);
|
|
2362
|
+
const serialized = JSON.stringify(response);
|
|
2363
|
+
const guardResult = enforcePayloadLimit(serialized, payloadLimits);
|
|
2364
|
+
response.warnings = appendPayloadWarning(
|
|
2365
|
+
response.warnings,
|
|
2366
|
+
guardResult,
|
|
2367
|
+
"fab_propose produced an unexpectedly large response \u2014 extract from a smaller span of text."
|
|
2368
|
+
);
|
|
2369
|
+
return {
|
|
2370
|
+
content: [
|
|
2371
|
+
{
|
|
2372
|
+
type: "text",
|
|
2373
|
+
text: `Fabric propose: ${response.pending_path} (see structuredContent)`
|
|
2374
|
+
}
|
|
2375
|
+
],
|
|
2376
|
+
structuredContent: response
|
|
2377
|
+
};
|
|
2378
|
+
} finally {
|
|
2379
|
+
tracker?.exit(requestId);
|
|
2380
|
+
}
|
|
2093
2381
|
}
|
|
2094
|
-
|
|
2095
|
-
stableId: entry.qualifiedId.slice(entry.alias.length + 1),
|
|
2096
|
-
qualifiedId: entry.qualifiedId,
|
|
2097
|
-
file: entry.file,
|
|
2098
|
-
type: entry.type,
|
|
2099
|
-
layer: entry.layer,
|
|
2100
|
-
body: entry.source,
|
|
2101
|
-
description
|
|
2102
|
-
});
|
|
2103
|
-
}
|
|
2104
|
-
return out;
|
|
2382
|
+
);
|
|
2105
2383
|
}
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2384
|
+
|
|
2385
|
+
// src/tools/recall.ts
|
|
2386
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2387
|
+
import {
|
|
2388
|
+
recallAnnotations,
|
|
2389
|
+
recallInputSchema,
|
|
2390
|
+
recallOutputSchema
|
|
2391
|
+
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
2392
|
+
import { enforcePayloadLimit as enforcePayloadLimit2 } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
2393
|
+
|
|
2394
|
+
// src/services/plan-context.ts
|
|
2395
|
+
import {
|
|
2396
|
+
buildStoreResolveInput as buildStoreResolveInput3,
|
|
2397
|
+
createStoreResolver as createStoreResolver3,
|
|
2398
|
+
resolveCandidates,
|
|
2399
|
+
tokenize as tokenize2
|
|
2400
|
+
} from "@fenglimg/fabric-shared";
|
|
2401
|
+
import {
|
|
2402
|
+
trimToPayloadBudget
|
|
2403
|
+
} from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
2404
|
+
|
|
2405
|
+
// src/services/get-knowledge.ts
|
|
2406
|
+
function normalizeKnowledgePath(value) {
|
|
2407
|
+
return value.replaceAll("\\", "/");
|
|
2120
2408
|
}
|
|
2121
2409
|
|
|
2122
2410
|
// src/services/id-redirect.ts
|
|
@@ -2382,61 +2670,6 @@ var LEDGER_DUAL_WRITE_METRIC_NAMES = {
|
|
|
2382
2670
|
edit_intent_checked: METRIC_COUNTER_NAMES.edit_intent_checked
|
|
2383
2671
|
};
|
|
2384
2672
|
|
|
2385
|
-
// src/services/bm25.ts
|
|
2386
|
-
import { tokenize } from "@fenglimg/fabric-shared";
|
|
2387
|
-
var K1 = 1.5;
|
|
2388
|
-
var B = 0.75;
|
|
2389
|
-
function buildBm25Model(docs) {
|
|
2390
|
-
const totalDocs = docs.length;
|
|
2391
|
-
const documentFrequency = /* @__PURE__ */ new Map();
|
|
2392
|
-
const perDoc = /* @__PURE__ */ new Map();
|
|
2393
|
-
let totalLength = 0;
|
|
2394
|
-
for (const doc of docs) {
|
|
2395
|
-
const termFreq = /* @__PURE__ */ new Map();
|
|
2396
|
-
for (const term of doc.tokens) {
|
|
2397
|
-
termFreq.set(term, (termFreq.get(term) ?? 0) + 1);
|
|
2398
|
-
}
|
|
2399
|
-
for (const term of termFreq.keys()) {
|
|
2400
|
-
documentFrequency.set(term, (documentFrequency.get(term) ?? 0) + 1);
|
|
2401
|
-
}
|
|
2402
|
-
totalLength += doc.tokens.length;
|
|
2403
|
-
perDoc.set(doc.id, { termFreq, length: doc.tokens.length });
|
|
2404
|
-
}
|
|
2405
|
-
const avgDocLength = totalDocs > 0 ? totalLength / totalDocs : 0;
|
|
2406
|
-
const idf = (term) => {
|
|
2407
|
-
const n = documentFrequency.get(term) ?? 0;
|
|
2408
|
-
return Math.log(1 + (totalDocs - n + 0.5) / (n + 0.5));
|
|
2409
|
-
};
|
|
2410
|
-
return {
|
|
2411
|
-
scoreDoc(id, queryTerms) {
|
|
2412
|
-
const data = perDoc.get(id);
|
|
2413
|
-
if (data === void 0 || data.length === 0 || queryTerms.length === 0) {
|
|
2414
|
-
return 0;
|
|
2415
|
-
}
|
|
2416
|
-
const normalizer = avgDocLength > 0 ? data.length / avgDocLength : 1;
|
|
2417
|
-
let score = 0;
|
|
2418
|
-
const scoredTerms = /* @__PURE__ */ new Set();
|
|
2419
|
-
for (const term of queryTerms) {
|
|
2420
|
-
if (scoredTerms.has(term)) {
|
|
2421
|
-
continue;
|
|
2422
|
-
}
|
|
2423
|
-
scoredTerms.add(term);
|
|
2424
|
-
const freq = data.termFreq.get(term);
|
|
2425
|
-
if (freq === void 0) {
|
|
2426
|
-
continue;
|
|
2427
|
-
}
|
|
2428
|
-
const numerator = freq * (K1 + 1);
|
|
2429
|
-
const denominator = freq + K1 * (1 - B + B * normalizer);
|
|
2430
|
-
score += idf(term) * (numerator / denominator);
|
|
2431
|
-
}
|
|
2432
|
-
return score;
|
|
2433
|
-
}
|
|
2434
|
-
};
|
|
2435
|
-
}
|
|
2436
|
-
function buildQueryTerms(text) {
|
|
2437
|
-
return tokenize(text);
|
|
2438
|
-
}
|
|
2439
|
-
|
|
2440
2673
|
// src/services/vector-retrieval.ts
|
|
2441
2674
|
var embedderLoad;
|
|
2442
2675
|
var OPTIONAL_EMBED_PACKAGE = "fastembed";
|
|
@@ -2654,7 +2887,9 @@ async function planContext(projectRoot, input) {
|
|
|
2654
2887
|
const relevanceFloor = maxScore * relevanceRatio;
|
|
2655
2888
|
const survivingScored = hasQuery && maxScore > 0 && relevanceRatio > 0 ? cappedScored.filter((entry) => entry.score >= relevanceFloor) : cappedScored;
|
|
2656
2889
|
const topKCandidates = survivingScored.map((entry) => entry.item);
|
|
2657
|
-
const
|
|
2890
|
+
const topKIds = new Set(topKCandidates.map((item) => item.stable_id));
|
|
2891
|
+
const retrievalDropped = rankedCandidates.filter((item) => !topKIds.has(item.stable_id)).map((item) => ({ id: item.stable_id, reason: "retrieval_budget" }));
|
|
2892
|
+
const omittedCandidateCount = retrievalDropped.length;
|
|
2658
2893
|
let candidates = topKCandidates;
|
|
2659
2894
|
const relatedAppended = {};
|
|
2660
2895
|
if (input.include_related === true) {
|
|
@@ -2694,8 +2929,10 @@ async function planContext(projectRoot, input) {
|
|
|
2694
2929
|
const basePreflightDiagnostics = buildPreflightDiagnostics(suppressedStableIds);
|
|
2695
2930
|
let payloadTrimDropped = 0;
|
|
2696
2931
|
let payloadOverBudget = false;
|
|
2932
|
+
let payloadDropped = [];
|
|
2697
2933
|
if (input.payload_budget !== void 0) {
|
|
2698
2934
|
const fullCandidateCount = candidates.length;
|
|
2935
|
+
const preTrimIds = candidates.map((item) => item.stable_id);
|
|
2699
2936
|
const serialize = (candidateSlice) => {
|
|
2700
2937
|
const dropped = fullCandidateCount - candidateSlice.length;
|
|
2701
2938
|
const totalOmitted = omittedCandidateCount + dropped;
|
|
@@ -2706,7 +2943,7 @@ async function planContext(projectRoot, input) {
|
|
|
2706
2943
|
entries,
|
|
2707
2944
|
...input.intent !== void 0 ? { intent: input.intent } : {},
|
|
2708
2945
|
candidates: candidateSlice,
|
|
2709
|
-
...totalOmitted > 0 ? {
|
|
2946
|
+
...totalOmitted > 0 ? { dropped_count: totalOmitted } : {},
|
|
2710
2947
|
preflight_diagnostics: basePreflightDiagnostics,
|
|
2711
2948
|
warnings: dropped > 0 && input.payload_budget?.trim_warning !== void 0 ? [...input.payload_budget.warnings ?? [], input.payload_budget.trim_warning] : input.payload_budget?.warnings,
|
|
2712
2949
|
...Object.keys(relatedAppended).length > 0 ? { related_appended: relatedAppended } : {}
|
|
@@ -2714,6 +2951,8 @@ async function planContext(projectRoot, input) {
|
|
|
2714
2951
|
};
|
|
2715
2952
|
const trim = trimToPayloadBudget(candidates, serialize, input.payload_budget.limits);
|
|
2716
2953
|
if (trim.dropped > 0) {
|
|
2954
|
+
const survivingIds = new Set(trim.items.map((item) => item.stable_id));
|
|
2955
|
+
payloadDropped = preTrimIds.filter((id) => !survivingIds.has(id)).map((id) => ({ id, reason: "payload_budget" }));
|
|
2717
2956
|
candidates = trim.items;
|
|
2718
2957
|
payloadTrimDropped = trim.dropped;
|
|
2719
2958
|
}
|
|
@@ -2737,7 +2976,11 @@ async function planContext(projectRoot, input) {
|
|
|
2737
2976
|
entries,
|
|
2738
2977
|
...input.intent !== void 0 ? { intent: input.intent } : {},
|
|
2739
2978
|
candidates,
|
|
2740
|
-
|
|
2979
|
+
// K6 (W3-K): assemble the structured dropped[] ONCE, here at final result
|
|
2980
|
+
// assembly — the CONSTANT retrieval_budget set (computed pre-trim) followed
|
|
2981
|
+
// by the payload_budget set (computed after the trim settled). Omitted when
|
|
2982
|
+
// nothing was dropped, keeping the steady-state wire shape unchanged.
|
|
2983
|
+
...retrievalDropped.length + payloadDropped.length > 0 ? { dropped: [...retrievalDropped, ...payloadDropped] } : {},
|
|
2741
2984
|
preflight_diagnostics: basePreflightDiagnostics,
|
|
2742
2985
|
// v2.2 W5 R1: the auto_healed / previous_revision_hash pair was tied to the
|
|
2743
2986
|
// co-location loadActiveMetaOrStale read-path auto-heal, which is retired.
|
|
@@ -2845,7 +3088,7 @@ function getOrBuildBm25Model(revision, rawItems, docTexts) {
|
|
|
2845
3088
|
const model = buildBm25Model(
|
|
2846
3089
|
rawItems.map((item) => ({
|
|
2847
3090
|
id: item.stable_id,
|
|
2848
|
-
|
|
3091
|
+
fields: documentFieldsForItem(item.description)
|
|
2849
3092
|
}))
|
|
2850
3093
|
);
|
|
2851
3094
|
bm25ModelCache = { revision, model };
|
|
@@ -2919,6 +3162,16 @@ function documentTextForItem(description) {
|
|
|
2919
3162
|
...description.tags ?? []
|
|
2920
3163
|
].join(" ");
|
|
2921
3164
|
}
|
|
3165
|
+
function documentFieldsForItem(description) {
|
|
3166
|
+
return {
|
|
3167
|
+
title: tokenize2(description.summary),
|
|
3168
|
+
tags: tokenize2(
|
|
3169
|
+
[...description.tags ?? [], ...description.tech_stack, ...description.entities ?? []].join(" ")
|
|
3170
|
+
),
|
|
3171
|
+
summary: tokenize2([description.must_read_if, ...description.intent_clues].join(" ")),
|
|
3172
|
+
body: tokenize2(description.impact.join(" "))
|
|
3173
|
+
};
|
|
3174
|
+
}
|
|
2922
3175
|
function isEmptyShellDescription(description, stableId) {
|
|
2923
3176
|
return description.summary === stableId && description.intent_clues.length === 0 && description.tech_stack.length === 0 && description.impact.length === 0;
|
|
2924
3177
|
}
|
|
@@ -3018,7 +3271,7 @@ function relatedLookupKeys(stableId) {
|
|
|
3018
3271
|
}
|
|
3019
3272
|
|
|
3020
3273
|
// src/services/recall.ts
|
|
3021
|
-
var RECALL_DIRECTIVE = "
|
|
3274
|
+
var RECALL_DIRECTIVE = "These entries are auto-accounted as citations for edits whose paths overlap this recall \u2014 no first-line cite needed. Speak up only to dismiss one you judge inapplicable: `dismissed: <id> (<reason>)`.";
|
|
3022
3275
|
async function recall(projectRoot, input) {
|
|
3023
3276
|
const planResult = await planContext(projectRoot, input);
|
|
3024
3277
|
const { selection_token: _token, payload_trimmed: _pt, payload_over_budget: _pob, ...planRest } = planResult;
|
|
@@ -3067,13 +3320,22 @@ async function recall(projectRoot, input) {
|
|
|
3067
3320
|
paths.push(attachPathStore({ stable_id: candidate.stable_id, path: ref.file }));
|
|
3068
3321
|
}
|
|
3069
3322
|
const nextSteps = buildNextSteps(planResult, paths, candidateById, candidateLookup);
|
|
3070
|
-
const
|
|
3071
|
-
|
|
3072
|
-
|
|
3323
|
+
const pathByStableId = new Map(paths.map((p) => [p.stable_id, p]));
|
|
3324
|
+
const entries = planRest.candidates.map((c, index) => {
|
|
3325
|
+
const readPath = pathByStableId.get(c.stable_id);
|
|
3326
|
+
return {
|
|
3327
|
+
stable_id: c.stable_id,
|
|
3328
|
+
rank: index + 1,
|
|
3329
|
+
description: c.description,
|
|
3330
|
+
...readPath ? { read_path: readPath.path } : {},
|
|
3331
|
+
...readPath?.store ? { store: readPath.store } : {},
|
|
3332
|
+
...isAlwaysActive(c) ? { body_in_context: true } : {}
|
|
3333
|
+
};
|
|
3334
|
+
});
|
|
3335
|
+
const { entries: _reqProfiles, candidates: _candidates, ...planRestNoLists } = planRest;
|
|
3073
3336
|
return {
|
|
3074
|
-
...
|
|
3075
|
-
|
|
3076
|
-
paths,
|
|
3337
|
+
...planRestNoLists,
|
|
3338
|
+
entries,
|
|
3077
3339
|
directive: RECALL_DIRECTIVE,
|
|
3078
3340
|
...nextSteps.length > 0 ? { next_steps: nextSteps } : {}
|
|
3079
3341
|
};
|
|
@@ -3085,7 +3347,7 @@ function isAlwaysActive(candidate) {
|
|
|
3085
3347
|
}
|
|
3086
3348
|
function buildNextSteps(planResult, paths, candidateById, candidateLookup) {
|
|
3087
3349
|
const nextSteps = [];
|
|
3088
|
-
const omitted = planResult.
|
|
3350
|
+
const omitted = (planResult.dropped ?? []).filter((d) => d.reason === "retrieval_budget").length;
|
|
3089
3351
|
if (omitted > 0) {
|
|
3090
3352
|
nextSteps.push(
|
|
3091
3353
|
`${omitted} lower-ranked candidate(s) were omitted by the retrieval budget \u2014 pass a narrower intent (or raise plan_context_top_k) to surface them.`
|
|
@@ -3121,7 +3383,7 @@ function registerRecall(server, tracker) {
|
|
|
3121
3383
|
server.registerTool(
|
|
3122
3384
|
"fab_recall",
|
|
3123
3385
|
{
|
|
3124
|
-
description: "Recall the Fabric knowledge relevant to the files you are about to touch. Pass candidate `paths` (+ optional `intent`) and receive a
|
|
3386
|
+
description: "Recall the Fabric knowledge relevant to the files you are about to touch. Pass candidate `paths` (+ optional `intent`) and receive a single ranked `entries[]` list \u2014 each entry carries its DESCRIPTION (summary / intent_clues / must_read_if / related), a `read_path` (the on-disk knowledge file to Read for the body), `rank` (1 = most relevant), and `body_in_context:true` when the body is already injected at SessionStart. It does NOT return bodies \u2014 Read an entry's `read_path` on demand (cheap, recoverable) instead of paying a permanent per-recall context tax for bodies you may not use. Pass `ids` to scope which entries surface a read_path when you already know which you want; `include_related:true` to also surface one-hop `related` neighbours.",
|
|
3125
3387
|
inputSchema: recallInputSchema,
|
|
3126
3388
|
outputSchema: recallOutputSchema,
|
|
3127
3389
|
annotations: recallAnnotations
|
|
@@ -3176,11 +3438,16 @@ function registerRecall(server, tracker) {
|
|
|
3176
3438
|
response.warnings = appendPayloadWarning(
|
|
3177
3439
|
response.warnings,
|
|
3178
3440
|
guardResult,
|
|
3179
|
-
"Pass an explicit `ids` array (or a narrower `intent`) to scope fab_recall's
|
|
3441
|
+
"Pass an explicit `ids` array (or a narrower `intent`) to scope fab_recall's entries \u2014 each entry carries a `read_path`, so Read it to load any body on demand."
|
|
3180
3442
|
);
|
|
3181
3443
|
payloadBytesOut = Buffer.byteLength(serialized, "utf8");
|
|
3182
3444
|
return {
|
|
3183
|
-
content: [
|
|
3445
|
+
content: [
|
|
3446
|
+
{
|
|
3447
|
+
type: "text",
|
|
3448
|
+
text: `Fabric recall: ${result.entries.length} entries (see structuredContent)`
|
|
3449
|
+
}
|
|
3450
|
+
],
|
|
3184
3451
|
structuredContent: response
|
|
3185
3452
|
};
|
|
3186
3453
|
} catch (error) {
|
|
@@ -3384,7 +3651,12 @@ function registerArchiveScan(server, tracker) {
|
|
|
3384
3651
|
"fab_archive_scan returned a large candidate set \u2014 pass an explicit `range` of session_ids to narrow the scan."
|
|
3385
3652
|
);
|
|
3386
3653
|
return {
|
|
3387
|
-
content: [
|
|
3654
|
+
content: [
|
|
3655
|
+
{
|
|
3656
|
+
type: "text",
|
|
3657
|
+
text: `Fabric archive scan: ${result.session_ids.length} sessions, ${result.dropped.length} dropped (see structuredContent)`
|
|
3658
|
+
}
|
|
3659
|
+
],
|
|
3388
3660
|
structuredContent: result
|
|
3389
3661
|
};
|
|
3390
3662
|
} finally {
|
|
@@ -3406,11 +3678,190 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
|
|
|
3406
3678
|
|
|
3407
3679
|
// src/services/review.ts
|
|
3408
3680
|
import { execFileSync } from "child_process";
|
|
3409
|
-
import { existsSync as
|
|
3410
|
-
import { readFile as
|
|
3681
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3682
|
+
import { readFile as readFile6, readdir as readdir2, stat as stat2, unlink as unlink2 } from "fs/promises";
|
|
3411
3683
|
import { homedir } from "os";
|
|
3412
|
-
import { basename, isAbsolute, join as
|
|
3684
|
+
import { basename, isAbsolute, join as join10, relative, resolve as resolve2, sep as sep2 } from "path";
|
|
3685
|
+
|
|
3686
|
+
// src/services/promotion-gate.ts
|
|
3687
|
+
function toLocalId(id) {
|
|
3688
|
+
const sep5 = id.indexOf(":");
|
|
3689
|
+
return sep5 === -1 ? id : id.slice(sep5 + 1);
|
|
3690
|
+
}
|
|
3691
|
+
async function hasUnresolvedDismissal(projectRoot, id) {
|
|
3692
|
+
const target = toLocalId(id);
|
|
3693
|
+
let events;
|
|
3694
|
+
try {
|
|
3695
|
+
({ events } = await readEventLedger(projectRoot, { event_type: "assistant_turn_observed" }));
|
|
3696
|
+
} catch {
|
|
3697
|
+
return false;
|
|
3698
|
+
}
|
|
3699
|
+
let latestTs = -1;
|
|
3700
|
+
let latestTag;
|
|
3701
|
+
for (const event of events) {
|
|
3702
|
+
if (event.event_type !== "assistant_turn_observed") continue;
|
|
3703
|
+
const ids = event.cite_ids;
|
|
3704
|
+
const tags = event.cite_tags;
|
|
3705
|
+
for (let i = 0; i < ids.length; i += 1) {
|
|
3706
|
+
const cited = ids[i];
|
|
3707
|
+
if (cited === void 0 || toLocalId(cited) !== target) continue;
|
|
3708
|
+
const tag = tags[i];
|
|
3709
|
+
if (tag === void 0) continue;
|
|
3710
|
+
if (event.ts >= latestTs) {
|
|
3711
|
+
latestTs = event.ts;
|
|
3712
|
+
latestTag = tag;
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
return latestTag === "dismissed";
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
// src/services/review.ts
|
|
3413
3720
|
import { allocateStoreKnowledgeId, isPersonalScope as isPersonalScope2 } from "@fenglimg/fabric-shared";
|
|
3721
|
+
|
|
3722
|
+
// src/services/pending-dedupe.ts
|
|
3723
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3724
|
+
import { readdir, readFile as readFile5, unlink } from "fs/promises";
|
|
3725
|
+
import { join as join9 } from "path";
|
|
3726
|
+
var PENDING_TYPES = ["decisions", "pitfalls", "guidelines", "models", "processes"];
|
|
3727
|
+
var DISAMBIGUATION_SUFFIX = /^(.+)-([2-9])\.md$/u;
|
|
3728
|
+
var SOURCE_SESSIONS_LINE = /^source_sessions:\s*\[(.*)\]\s*$/mu;
|
|
3729
|
+
var CREATED_AT_LINE = /^created_at:\s*(.+)$/mu;
|
|
3730
|
+
function parseSourceSessions(content) {
|
|
3731
|
+
const m = SOURCE_SESSIONS_LINE.exec(content);
|
|
3732
|
+
if (!m) return [];
|
|
3733
|
+
try {
|
|
3734
|
+
const arr = JSON.parse(`[${m[1]}]`);
|
|
3735
|
+
return Array.isArray(arr) ? arr.filter((s) => typeof s === "string") : [];
|
|
3736
|
+
} catch {
|
|
3737
|
+
return [];
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
function parseCreatedAt(content) {
|
|
3741
|
+
const m = CREATED_AT_LINE.exec(content);
|
|
3742
|
+
return m ? m[1].trim() : "";
|
|
3743
|
+
}
|
|
3744
|
+
function resolveBaseSlug(name, present) {
|
|
3745
|
+
const m = DISAMBIGUATION_SUFFIX.exec(name);
|
|
3746
|
+
if (m && present.has(`${m[1]}.md`)) return m[1];
|
|
3747
|
+
return name.replace(/\.md$/u, "");
|
|
3748
|
+
}
|
|
3749
|
+
function unionSessions(survivor, twins) {
|
|
3750
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3751
|
+
const out = [];
|
|
3752
|
+
for (const s of [survivor, ...twins]) {
|
|
3753
|
+
for (const sid of s.sourceSessions) {
|
|
3754
|
+
if (sid.length > 0 && !seen.has(sid)) {
|
|
3755
|
+
seen.add(sid);
|
|
3756
|
+
out.push(sid);
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
return out;
|
|
3761
|
+
}
|
|
3762
|
+
function buildMergedContent(survivor, twins) {
|
|
3763
|
+
const union = unionSessions(survivor, twins);
|
|
3764
|
+
let merged = survivor.content.replace(
|
|
3765
|
+
SOURCE_SESSIONS_LINE,
|
|
3766
|
+
`source_sessions: [${union.map((s) => JSON.stringify(s)).join(", ")}]`
|
|
3767
|
+
);
|
|
3768
|
+
if (!merged.endsWith("\n")) merged += "\n";
|
|
3769
|
+
for (const twin of twins) {
|
|
3770
|
+
const body = extractBody(twin.content).trim();
|
|
3771
|
+
if (body.length === 0) continue;
|
|
3772
|
+
merged += `
|
|
3773
|
+
## Evidence (merged from session ${twin.primarySession || "unknown"})
|
|
3774
|
+
|
|
3775
|
+
${body}
|
|
3776
|
+
`;
|
|
3777
|
+
}
|
|
3778
|
+
return merged;
|
|
3779
|
+
}
|
|
3780
|
+
function chooseSurvivor(baseSlug, group) {
|
|
3781
|
+
const exact = group.find((p) => p.name === `${baseSlug}.md`);
|
|
3782
|
+
if (exact) return exact;
|
|
3783
|
+
return [...group].sort((a, b) => {
|
|
3784
|
+
if (a.createdAt !== b.createdAt) return a.createdAt < b.createdAt ? -1 : 1;
|
|
3785
|
+
return a.name < b.name ? -1 : 1;
|
|
3786
|
+
})[0];
|
|
3787
|
+
}
|
|
3788
|
+
async function mergePendingTwins(projectRoot) {
|
|
3789
|
+
const merged = [];
|
|
3790
|
+
for (const layer of ["team", "personal"]) {
|
|
3791
|
+
let pendingBase2;
|
|
3792
|
+
try {
|
|
3793
|
+
pendingBase2 = resolveStorePendingBase(layer, projectRoot);
|
|
3794
|
+
} catch {
|
|
3795
|
+
continue;
|
|
3796
|
+
}
|
|
3797
|
+
for (const type of PENDING_TYPES) {
|
|
3798
|
+
const dir = join9(pendingBase2, type);
|
|
3799
|
+
if (!existsSync4(dir)) continue;
|
|
3800
|
+
let names;
|
|
3801
|
+
try {
|
|
3802
|
+
names = (await readdir(dir)).filter((n) => n.endsWith(".md"));
|
|
3803
|
+
} catch {
|
|
3804
|
+
continue;
|
|
3805
|
+
}
|
|
3806
|
+
if (names.length < 2) continue;
|
|
3807
|
+
const present = new Set(names);
|
|
3808
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3809
|
+
for (const name of names) {
|
|
3810
|
+
const base = resolveBaseSlug(name, present);
|
|
3811
|
+
const arr = groups.get(base);
|
|
3812
|
+
if (arr) arr.push(name);
|
|
3813
|
+
else groups.set(base, [name]);
|
|
3814
|
+
}
|
|
3815
|
+
for (const [baseSlug, groupNames] of groups) {
|
|
3816
|
+
if (groupNames.length < 2) continue;
|
|
3817
|
+
const parsed = [];
|
|
3818
|
+
for (const name of groupNames) {
|
|
3819
|
+
const abs = join9(dir, name);
|
|
3820
|
+
let content;
|
|
3821
|
+
try {
|
|
3822
|
+
content = await readFile5(abs, "utf8");
|
|
3823
|
+
} catch {
|
|
3824
|
+
continue;
|
|
3825
|
+
}
|
|
3826
|
+
const sourceSessions = parseSourceSessions(content);
|
|
3827
|
+
parsed.push({
|
|
3828
|
+
name,
|
|
3829
|
+
abs,
|
|
3830
|
+
content,
|
|
3831
|
+
sourceSessions,
|
|
3832
|
+
primarySession: sourceSessions[0] ?? "",
|
|
3833
|
+
createdAt: parseCreatedAt(content)
|
|
3834
|
+
});
|
|
3835
|
+
}
|
|
3836
|
+
if (parsed.length < 2) continue;
|
|
3837
|
+
const distinctPrimaries = new Set(parsed.map((p) => p.primarySession).filter((s) => s.length > 0));
|
|
3838
|
+
if (distinctPrimaries.size < 2) continue;
|
|
3839
|
+
const survivor = chooseSurvivor(baseSlug, parsed);
|
|
3840
|
+
const twins = parsed.filter((p) => p.abs !== survivor.abs);
|
|
3841
|
+
const mergedContent = buildMergedContent(survivor, twins);
|
|
3842
|
+
try {
|
|
3843
|
+
await atomicWriteText(survivor.abs, mergedContent);
|
|
3844
|
+
for (const twin of twins) {
|
|
3845
|
+
await unlink(twin.abs);
|
|
3846
|
+
}
|
|
3847
|
+
} catch {
|
|
3848
|
+
continue;
|
|
3849
|
+
}
|
|
3850
|
+
merged.push({
|
|
3851
|
+
layer,
|
|
3852
|
+
type,
|
|
3853
|
+
base_slug: baseSlug,
|
|
3854
|
+
survivor: survivor.abs,
|
|
3855
|
+
removed: twins.map((t) => t.abs),
|
|
3856
|
+
source_sessions: unionSessions(survivor, twins)
|
|
3857
|
+
});
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
return { merged };
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
// src/services/review.ts
|
|
3414
3865
|
var PLURAL_TYPES = [
|
|
3415
3866
|
"decisions",
|
|
3416
3867
|
"pitfalls",
|
|
@@ -3420,11 +3871,6 @@ var PLURAL_TYPES = [
|
|
|
3420
3871
|
];
|
|
3421
3872
|
async function reviewKnowledge(projectRoot, input) {
|
|
3422
3873
|
switch (input.action) {
|
|
3423
|
-
case "list":
|
|
3424
|
-
return {
|
|
3425
|
-
action: "list",
|
|
3426
|
-
items: await listPending(projectRoot, input.filters)
|
|
3427
|
-
};
|
|
3428
3874
|
case "approve":
|
|
3429
3875
|
return {
|
|
3430
3876
|
action: "approve",
|
|
@@ -3447,11 +3893,6 @@ async function reviewKnowledge(projectRoot, input) {
|
|
|
3447
3893
|
}
|
|
3448
3894
|
case "modify-layer":
|
|
3449
3895
|
return await modifyEntry(projectRoot, input.pending_path, input.changes);
|
|
3450
|
-
case "search":
|
|
3451
|
-
return {
|
|
3452
|
-
action: "search",
|
|
3453
|
-
items: await searchEntries(projectRoot, input.query, input.filters)
|
|
3454
|
-
};
|
|
3455
3896
|
case "defer":
|
|
3456
3897
|
return {
|
|
3457
3898
|
action: "defer",
|
|
@@ -3468,6 +3909,24 @@ async function reviewKnowledge(projectRoot, input) {
|
|
|
3468
3909
|
}
|
|
3469
3910
|
}
|
|
3470
3911
|
}
|
|
3912
|
+
async function reviewPending(projectRoot, input) {
|
|
3913
|
+
switch (input.action) {
|
|
3914
|
+
case "list":
|
|
3915
|
+
return {
|
|
3916
|
+
action: "list",
|
|
3917
|
+
items: await listPending(projectRoot, input.filters)
|
|
3918
|
+
};
|
|
3919
|
+
case "search":
|
|
3920
|
+
return {
|
|
3921
|
+
action: "search",
|
|
3922
|
+
items: await searchEntries(projectRoot, input.query, input.filters)
|
|
3923
|
+
};
|
|
3924
|
+
default: {
|
|
3925
|
+
const exhaustive = input;
|
|
3926
|
+
throw new Error(`unsupported action: ${JSON.stringify(exhaustive)}`);
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3471
3930
|
function storeKnowledgeRoots(projectRoot) {
|
|
3472
3931
|
const roots = [];
|
|
3473
3932
|
for (const layer of ["team", "personal"]) {
|
|
@@ -3517,6 +3976,10 @@ function isVisibleByLifecycle(fm, filters) {
|
|
|
3517
3976
|
return true;
|
|
3518
3977
|
}
|
|
3519
3978
|
async function listPending(projectRoot, filters) {
|
|
3979
|
+
try {
|
|
3980
|
+
await mergePendingTwins(projectRoot);
|
|
3981
|
+
} catch {
|
|
3982
|
+
}
|
|
3520
3983
|
const items = [];
|
|
3521
3984
|
const typesToScan = filters?.type !== void 0 ? [filters.type] : PLURAL_TYPES;
|
|
3522
3985
|
const sources = [];
|
|
@@ -3536,22 +3999,22 @@ async function listPending(projectRoot, filters) {
|
|
|
3536
3999
|
}
|
|
3537
4000
|
for (const source of sources) {
|
|
3538
4001
|
for (const type of typesToScan) {
|
|
3539
|
-
const dir =
|
|
3540
|
-
if (!
|
|
4002
|
+
const dir = join10(source.root, type);
|
|
4003
|
+
if (!existsSync5(dir)) {
|
|
3541
4004
|
continue;
|
|
3542
4005
|
}
|
|
3543
4006
|
let entries;
|
|
3544
4007
|
try {
|
|
3545
|
-
entries = await
|
|
4008
|
+
entries = await readdir2(dir);
|
|
3546
4009
|
} catch {
|
|
3547
4010
|
continue;
|
|
3548
4011
|
}
|
|
3549
4012
|
for (const name of entries) {
|
|
3550
4013
|
if (!name.endsWith(".md")) continue;
|
|
3551
|
-
const absolutePath =
|
|
4014
|
+
const absolutePath = join10(dir, name);
|
|
3552
4015
|
let content;
|
|
3553
4016
|
try {
|
|
3554
|
-
content = await
|
|
4017
|
+
content = await readFile6(absolutePath, "utf8");
|
|
3555
4018
|
} catch {
|
|
3556
4019
|
continue;
|
|
3557
4020
|
}
|
|
@@ -3661,7 +4124,7 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
3661
4124
|
let targetAbs;
|
|
3662
4125
|
let writtenTarget = false;
|
|
3663
4126
|
try {
|
|
3664
|
-
const content = await
|
|
4127
|
+
const content = await readFile6(sourceAbs, "utf8");
|
|
3665
4128
|
const fm = parseFrontmatter(content);
|
|
3666
4129
|
const pluralType = fm.type;
|
|
3667
4130
|
if (pluralType === void 0 || !PLURAL_TYPES.includes(pluralType)) {
|
|
@@ -3675,14 +4138,17 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
3675
4138
|
);
|
|
3676
4139
|
allocatedId = stableId;
|
|
3677
4140
|
const newFilename = `${stableId}--${slug}.md`;
|
|
3678
|
-
targetAbs =
|
|
4141
|
+
targetAbs = join10(resolveStoreCanonicalBase(layer, projectRoot), pluralType, newFilename);
|
|
3679
4142
|
await ensureParentDirectory(targetAbs);
|
|
3680
|
-
const rewritten =
|
|
4143
|
+
const rewritten = rewriteFrontmatterMerge(
|
|
4144
|
+
rewriteFrontmatterForPromote(content, stableId),
|
|
4145
|
+
{ last_review_confirmed_at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
4146
|
+
);
|
|
3681
4147
|
await atomicWriteText(targetAbs, rewritten);
|
|
3682
4148
|
writtenTarget = true;
|
|
3683
4149
|
if (sourceIsStore) {
|
|
3684
|
-
if (
|
|
3685
|
-
await
|
|
4150
|
+
if (existsSync5(sourceAbs)) {
|
|
4151
|
+
await unlink2(sourceAbs);
|
|
3686
4152
|
}
|
|
3687
4153
|
} else if (sourceOrigin === "team") {
|
|
3688
4154
|
try {
|
|
@@ -3691,13 +4157,13 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
3691
4157
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3692
4158
|
});
|
|
3693
4159
|
} catch {
|
|
3694
|
-
if (
|
|
3695
|
-
await
|
|
4160
|
+
if (existsSync5(sourceAbs)) {
|
|
4161
|
+
await unlink2(sourceAbs);
|
|
3696
4162
|
}
|
|
3697
4163
|
}
|
|
3698
4164
|
} else {
|
|
3699
|
-
if (
|
|
3700
|
-
await
|
|
4165
|
+
if (existsSync5(sourceAbs)) {
|
|
4166
|
+
await unlink2(sourceAbs);
|
|
3701
4167
|
}
|
|
3702
4168
|
}
|
|
3703
4169
|
await emitEventBestEffort2(projectRoot, {
|
|
@@ -3708,9 +4174,9 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
3708
4174
|
});
|
|
3709
4175
|
return { pending_path: pendingPath, stable_id: stableId };
|
|
3710
4176
|
} catch (err) {
|
|
3711
|
-
if (writtenTarget && targetAbs !== void 0 &&
|
|
4177
|
+
if (writtenTarget && targetAbs !== void 0 && existsSync5(targetAbs)) {
|
|
3712
4178
|
try {
|
|
3713
|
-
await
|
|
4179
|
+
await unlink2(targetAbs);
|
|
3714
4180
|
} catch {
|
|
3715
4181
|
}
|
|
3716
4182
|
}
|
|
@@ -3729,14 +4195,14 @@ async function rejectAll(projectRoot, pendingPaths, reason) {
|
|
|
3729
4195
|
for (const pendingPath of pendingPaths) {
|
|
3730
4196
|
try {
|
|
3731
4197
|
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
3732
|
-
if (
|
|
3733
|
-
const content = await
|
|
4198
|
+
if (existsSync5(sandboxed.abs)) {
|
|
4199
|
+
const content = await readFile6(sandboxed.abs, "utf8");
|
|
3734
4200
|
const merged = rewriteFrontmatterMerge(content, { status: "rejected" });
|
|
3735
4201
|
const rejectedAbs = sandboxed.abs.includes(`${sep2}pending${sep2}`) ? sandboxed.abs.replace(`${sep2}pending${sep2}`, `${sep2}rejected${sep2}`) : null;
|
|
3736
4202
|
if (rejectedAbs !== null) {
|
|
3737
4203
|
await ensureParentDirectory(rejectedAbs);
|
|
3738
4204
|
await atomicWriteText(rejectedAbs, merged);
|
|
3739
|
-
await
|
|
4205
|
+
await unlink2(sandboxed.abs);
|
|
3740
4206
|
} else if (merged !== content) {
|
|
3741
4207
|
await atomicWriteText(sandboxed.abs, merged);
|
|
3742
4208
|
}
|
|
@@ -3757,9 +4223,16 @@ async function modifyEntry(projectRoot, pendingPath, changes) {
|
|
|
3757
4223
|
if (target === null) {
|
|
3758
4224
|
throw new Error(`modify target not found: ${pendingPath}`);
|
|
3759
4225
|
}
|
|
3760
|
-
const content = await
|
|
4226
|
+
const content = await readFile6(target.absPath, "utf8");
|
|
3761
4227
|
const fm = parseFrontmatter(content);
|
|
3762
4228
|
const currentLayer = fm.layer ?? "team";
|
|
4229
|
+
if (fm.maturity === "verified" && changes.maturity === "proven" && fm.id !== void 0) {
|
|
4230
|
+
if (await hasUnresolvedDismissal(projectRoot, fm.id)) {
|
|
4231
|
+
throw new Error(
|
|
4232
|
+
`verified\u2192proven promotion blocked for ${fm.id}: an unresolved dismissed cite is on record (rubric necessary gate "0 dismiss"). Re-affirm the entry with an applied cite or address the objection, then retry.`
|
|
4233
|
+
);
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
3763
4236
|
if (changes.semantic_scope !== void 0 && isPersonalScope2(changes.semantic_scope)) {
|
|
3764
4237
|
throw new Error(
|
|
3765
4238
|
`cannot re-scope to personal coordinate '${changes.semantic_scope}' via modify; use action 'modify-layer' with layer 'personal' to move the entry into the personal store (R5#3)`
|
|
@@ -3768,7 +4241,10 @@ async function modifyEntry(projectRoot, pendingPath, changes) {
|
|
|
3768
4241
|
if (changes.layer !== void 0 && changes.layer !== currentLayer) {
|
|
3769
4242
|
return await modifyLayerFlip(projectRoot, target, content, fm, changes);
|
|
3770
4243
|
}
|
|
3771
|
-
const merged = rewriteFrontmatterMerge(content,
|
|
4244
|
+
const merged = rewriteFrontmatterMerge(content, {
|
|
4245
|
+
...changes,
|
|
4246
|
+
last_review_confirmed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4247
|
+
});
|
|
3772
4248
|
await atomicWriteText(target.absPath, merged);
|
|
3773
4249
|
const changedFields = Object.keys(changes).filter(
|
|
3774
4250
|
(field) => changes[field] !== void 0
|
|
@@ -3802,7 +4278,7 @@ function resolveModifyTarget(projectRoot, pendingPath) {
|
|
|
3802
4278
|
} catch {
|
|
3803
4279
|
return null;
|
|
3804
4280
|
}
|
|
3805
|
-
if (
|
|
4281
|
+
if (existsSync5(sandboxed.abs)) {
|
|
3806
4282
|
return {
|
|
3807
4283
|
absPath: sandboxed.abs,
|
|
3808
4284
|
isInProjectTree: sandboxed.isInProjectTree,
|
|
@@ -3849,7 +4325,7 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
|
3849
4325
|
pluralType,
|
|
3850
4326
|
resolveWriteTargetStoreDir(toLayer, projectRoot)
|
|
3851
4327
|
);
|
|
3852
|
-
const toAbs =
|
|
4328
|
+
const toAbs = join10(
|
|
3853
4329
|
resolveStoreCanonicalBase(toLayer, projectRoot),
|
|
3854
4330
|
pluralType,
|
|
3855
4331
|
`${newStableId}--${slug}.md`
|
|
@@ -3867,7 +4343,11 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
|
3867
4343
|
relevance_scope: "broad",
|
|
3868
4344
|
relevance_paths: []
|
|
3869
4345
|
} : { ...changes, layer: toLayer };
|
|
3870
|
-
const rewritten = rewriteFrontmatterMerge(
|
|
4346
|
+
const rewritten = rewriteFrontmatterMerge(
|
|
4347
|
+
content,
|
|
4348
|
+
{ ...effectivePatch, last_review_confirmed_at: (/* @__PURE__ */ new Date()).toISOString() },
|
|
4349
|
+
{ id: newStableId }
|
|
4350
|
+
);
|
|
3871
4351
|
await atomicWriteText(toAbs, rewritten);
|
|
3872
4352
|
if (target.isInProjectTree) {
|
|
3873
4353
|
const relSource = relative(projectRoot, target.absPath);
|
|
@@ -3877,12 +4357,12 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
|
3877
4357
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3878
4358
|
});
|
|
3879
4359
|
} catch {
|
|
3880
|
-
if (
|
|
3881
|
-
await
|
|
4360
|
+
if (existsSync5(target.absPath)) {
|
|
4361
|
+
await unlink2(target.absPath);
|
|
3882
4362
|
}
|
|
3883
4363
|
}
|
|
3884
|
-
} else if (
|
|
3885
|
-
await
|
|
4364
|
+
} else if (existsSync5(target.absPath)) {
|
|
4365
|
+
await unlink2(target.absPath);
|
|
3886
4366
|
}
|
|
3887
4367
|
const flipReason = `layer_flip:${priorStableId ?? "<unassigned>"}->${newStableId}`;
|
|
3888
4368
|
const flipTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -3945,10 +4425,10 @@ function getSearchDirectoryCache(cacheKey) {
|
|
|
3945
4425
|
return created;
|
|
3946
4426
|
}
|
|
3947
4427
|
async function listIndexedSearchEntries(source, type) {
|
|
3948
|
-
const dir =
|
|
4428
|
+
const dir = join10(source.root, type);
|
|
3949
4429
|
let entries;
|
|
3950
4430
|
try {
|
|
3951
|
-
entries = await
|
|
4431
|
+
entries = await readdir2(dir);
|
|
3952
4432
|
} catch {
|
|
3953
4433
|
return [];
|
|
3954
4434
|
}
|
|
@@ -3958,7 +4438,7 @@ async function listIndexedSearchEntries(source, type) {
|
|
|
3958
4438
|
const indexed = [];
|
|
3959
4439
|
for (const name of entries) {
|
|
3960
4440
|
if (!name.endsWith(".md")) continue;
|
|
3961
|
-
const absolutePath =
|
|
4441
|
+
const absolutePath = join10(dir, name);
|
|
3962
4442
|
let fingerprint;
|
|
3963
4443
|
try {
|
|
3964
4444
|
const st = await stat2(absolutePath);
|
|
@@ -3975,7 +4455,7 @@ async function listIndexedSearchEntries(source, type) {
|
|
|
3975
4455
|
}
|
|
3976
4456
|
let content;
|
|
3977
4457
|
try {
|
|
3978
|
-
content = await
|
|
4458
|
+
content = await readFile6(absolutePath, "utf8");
|
|
3979
4459
|
searchEntryIndexContentReads += 1;
|
|
3980
4460
|
} catch {
|
|
3981
4461
|
directoryCache.files.delete(absolutePath);
|
|
@@ -4095,8 +4575,8 @@ async function deferAll(projectRoot, pendingPaths, until, reason) {
|
|
|
4095
4575
|
let stableId;
|
|
4096
4576
|
try {
|
|
4097
4577
|
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
4098
|
-
if (
|
|
4099
|
-
const content = await
|
|
4578
|
+
if (existsSync5(sandboxed.abs)) {
|
|
4579
|
+
const content = await readFile6(sandboxed.abs, "utf8");
|
|
4100
4580
|
stableId = parseFrontmatter(content).id;
|
|
4101
4581
|
const patch = {
|
|
4102
4582
|
status: "deferred",
|
|
@@ -4191,6 +4671,9 @@ function parseFrontmatter(content) {
|
|
|
4191
4671
|
case "deferred_until":
|
|
4192
4672
|
out.deferred_until = stripQuotes(value);
|
|
4193
4673
|
break;
|
|
4674
|
+
case "last_review_confirmed_at":
|
|
4675
|
+
out.last_review_confirmed_at = stripQuotes(value);
|
|
4676
|
+
break;
|
|
4194
4677
|
default:
|
|
4195
4678
|
break;
|
|
4196
4679
|
}
|
|
@@ -4256,6 +4739,7 @@ ${content}`;
|
|
|
4256
4739
|
if (patch.related !== void 0) updates.related = `related: ${flowArray(patch.related)}`;
|
|
4257
4740
|
if (patch.status !== void 0) updates.status = `status: ${patch.status}`;
|
|
4258
4741
|
if (patch.deferred_until !== void 0) updates.deferred_until = `deferred_until: ${quoteIfNeeded(patch.deferred_until)}`;
|
|
4742
|
+
if (patch.last_review_confirmed_at !== void 0) updates.last_review_confirmed_at = `last_review_confirmed_at: ${quoteIfNeeded(patch.last_review_confirmed_at)}`;
|
|
4259
4743
|
const lines = block.split(/\r?\n/u);
|
|
4260
4744
|
const seen = /* @__PURE__ */ new Set();
|
|
4261
4745
|
const newLines = [];
|
|
@@ -4292,6 +4776,7 @@ function appendPatchLines(lines, patch) {
|
|
|
4292
4776
|
if (patch.related !== void 0) lines.push(`related: ${flowArray(patch.related)}`);
|
|
4293
4777
|
if (patch.status !== void 0) lines.push(`status: ${patch.status}`);
|
|
4294
4778
|
if (patch.deferred_until !== void 0) lines.push(`deferred_until: ${quoteIfNeeded(patch.deferred_until)}`);
|
|
4779
|
+
if (patch.last_review_confirmed_at !== void 0) lines.push(`last_review_confirmed_at: ${quoteIfNeeded(patch.last_review_confirmed_at)}`);
|
|
4295
4780
|
}
|
|
4296
4781
|
function flowArrayElement(value) {
|
|
4297
4782
|
if (/[\n\r,\[\]{}"#:]/u.test(value) || /^\s|\s$/u.test(value)) {
|
|
@@ -4326,7 +4811,7 @@ function registerReview(server, tracker) {
|
|
|
4326
4811
|
server.registerTool(
|
|
4327
4812
|
"fab_review",
|
|
4328
4813
|
{
|
|
4329
|
-
description: "Review pending knowledge entries in resolved store-backed knowledge/pending/. Discriminated by `action
|
|
4814
|
+
description: "Review pending knowledge entries in resolved store-backed knowledge/pending/. Discriminated by `action`; required fields per action: list \u2192 (filters optional); approve \u2192 pending_paths[\u22651]; reject \u2192 pending_paths[\u22651] + reason; modify / modify-content \u2192 pending_path + changes; modify-layer \u2192 pending_path + changes.layer(team|personal); search \u2192 query; defer \u2192 pending_paths[\u22651] (until/reason optional). approve allocates a stable_id and promotes to the canonical store knowledge path. Skill-side tool \u2014 invoked by fabric-review.",
|
|
4330
4815
|
// Flat ZodRawShape required by MCP SDK 1.29.0 registerTool. The
|
|
4331
4816
|
// authoritative cross-field contract still lives in FabReviewInputSchema
|
|
4332
4817
|
// (discriminatedUnion) and is enforced inside the handler via
|
|
@@ -4357,7 +4842,71 @@ function registerReview(server, tracker) {
|
|
|
4357
4842
|
"fab_review returned a large result set \u2014 pass a narrower filter (topic / status / id) to reduce response size."
|
|
4358
4843
|
);
|
|
4359
4844
|
return {
|
|
4360
|
-
content: [
|
|
4845
|
+
content: [
|
|
4846
|
+
{
|
|
4847
|
+
type: "text",
|
|
4848
|
+
text: `Fabric review: ${response.action} (see structuredContent)`
|
|
4849
|
+
}
|
|
4850
|
+
],
|
|
4851
|
+
structuredContent: response
|
|
4852
|
+
};
|
|
4853
|
+
} finally {
|
|
4854
|
+
tracker?.exit(requestId);
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
);
|
|
4858
|
+
}
|
|
4859
|
+
|
|
4860
|
+
// src/tools/pending.ts
|
|
4861
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
4862
|
+
import {
|
|
4863
|
+
FabPendingInputSchema,
|
|
4864
|
+
FabPendingInputShape,
|
|
4865
|
+
FabPendingOutputShape,
|
|
4866
|
+
fabPendingAnnotations
|
|
4867
|
+
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
4868
|
+
import { enforcePayloadLimit as enforcePayloadLimit5 } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
4869
|
+
function registerPending(server, tracker) {
|
|
4870
|
+
server.registerTool(
|
|
4871
|
+
"fab_pending",
|
|
4872
|
+
{
|
|
4873
|
+
description: "Browse and search store-backed pending + canonical knowledge (read-only). Discriminated by `action`; required fields per action: list \u2192 (filters optional, returns pending entries with `pending_path`); search \u2192 query (filters optional, ranges over pending + canonical with `area`+`path`). Never mutates state \u2014 pair with the write-only fab_review tool for approve/reject/modify/defer. Skill-side read tool \u2014 invoked by fabric-review / fabric-archive.",
|
|
4874
|
+
// Flat ZodRawShape required by MCP SDK 1.29.0 registerTool. The
|
|
4875
|
+
// authoritative cross-field contract still lives in FabPendingInputSchema
|
|
4876
|
+
// (discriminatedUnion) and is enforced inside the handler via
|
|
4877
|
+
// `FabPendingInputSchema.parse(input)`.
|
|
4878
|
+
inputSchema: FabPendingInputShape,
|
|
4879
|
+
outputSchema: FabPendingOutputShape,
|
|
4880
|
+
annotations: fabPendingAnnotations
|
|
4881
|
+
},
|
|
4882
|
+
async (input) => {
|
|
4883
|
+
const requestId = randomUUID6();
|
|
4884
|
+
tracker?.enter(requestId);
|
|
4885
|
+
try {
|
|
4886
|
+
const gateResult = await awaitFirstReconcileGate();
|
|
4887
|
+
const gateWarn = gateWarning(gateResult);
|
|
4888
|
+
const narrowed = FabPendingInputSchema.parse(input);
|
|
4889
|
+
const projectRoot = resolveProjectRoot();
|
|
4890
|
+
const result = await reviewPending(projectRoot, narrowed);
|
|
4891
|
+
const response = { ...result };
|
|
4892
|
+
if (gateWarn) {
|
|
4893
|
+
response.warnings = [gateWarn];
|
|
4894
|
+
}
|
|
4895
|
+
const payloadLimits = readPayloadLimits(projectRoot);
|
|
4896
|
+
const serialized = JSON.stringify(response);
|
|
4897
|
+
const guardResult = enforcePayloadLimit5(serialized, payloadLimits);
|
|
4898
|
+
response.warnings = appendPayloadWarning(
|
|
4899
|
+
response.warnings,
|
|
4900
|
+
guardResult,
|
|
4901
|
+
"fab_pending returned a large result set \u2014 pass a narrower filter (topic / status / id) to reduce response size."
|
|
4902
|
+
);
|
|
4903
|
+
return {
|
|
4904
|
+
content: [
|
|
4905
|
+
{
|
|
4906
|
+
type: "text",
|
|
4907
|
+
text: `Fabric pending: ${response.action} (see structuredContent)`
|
|
4908
|
+
}
|
|
4909
|
+
],
|
|
4361
4910
|
structuredContent: response
|
|
4362
4911
|
};
|
|
4363
4912
|
} finally {
|
|
@@ -4368,9 +4917,9 @@ function registerReview(server, tracker) {
|
|
|
4368
4917
|
}
|
|
4369
4918
|
|
|
4370
4919
|
// src/services/doctor.ts
|
|
4371
|
-
import { access as access4, readFile as
|
|
4920
|
+
import { access as access4, readFile as readFile16, readdir as readdirAsync, stat as statAsync, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
|
|
4372
4921
|
import { constants as constants2 } from "fs";
|
|
4373
|
-
import { isAbsolute as isAbsolute2, join as
|
|
4922
|
+
import { isAbsolute as isAbsolute2, join as join21, posix as posix5, resolve as resolve3 } from "path";
|
|
4374
4923
|
import {
|
|
4375
4924
|
createTranslator,
|
|
4376
4925
|
forensicReportSchema,
|
|
@@ -4584,8 +5133,8 @@ function createKnowledgeSummaryOpaqueCheck(t, inspection) {
|
|
|
4584
5133
|
}
|
|
4585
5134
|
|
|
4586
5135
|
// src/services/doctor-stable-id-collision.ts
|
|
4587
|
-
import { readFile as
|
|
4588
|
-
import { basename as basename2, join as
|
|
5136
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
5137
|
+
import { basename as basename2, join as join11 } from "path";
|
|
4589
5138
|
import {
|
|
4590
5139
|
buildStoreResolveInput as buildStoreResolveInput4,
|
|
4591
5140
|
createStoreResolver as createStoreResolver4,
|
|
@@ -4612,7 +5161,7 @@ function resolveIntegrityStores(projectRoot) {
|
|
|
4612
5161
|
return {
|
|
4613
5162
|
store_uuid: entry.store_uuid,
|
|
4614
5163
|
alias: entry.alias,
|
|
4615
|
-
dir:
|
|
5164
|
+
dir: join11(globalRoot, storeRelativePathForMount3(mounted ?? { store_uuid: entry.store_uuid }))
|
|
4616
5165
|
};
|
|
4617
5166
|
});
|
|
4618
5167
|
return { dirs, personalUuids };
|
|
@@ -4631,7 +5180,7 @@ async function inspectStoreStableIdIntegrity(projectRoot) {
|
|
|
4631
5180
|
for (const ref of await readKnowledgeAcrossStores2(resolved.dirs)) {
|
|
4632
5181
|
let source;
|
|
4633
5182
|
try {
|
|
4634
|
-
source = await
|
|
5183
|
+
source = await readFile7(ref.file, "utf8");
|
|
4635
5184
|
} catch {
|
|
4636
5185
|
continue;
|
|
4637
5186
|
}
|
|
@@ -4715,8 +5264,8 @@ function createLayerMismatchCheck(t, inspection) {
|
|
|
4715
5264
|
|
|
4716
5265
|
// src/services/doctor-relevance-paths.ts
|
|
4717
5266
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
4718
|
-
import { existsSync as
|
|
4719
|
-
import { join as
|
|
5267
|
+
import { existsSync as existsSync6, readdirSync, statSync as statSync3 } from "fs";
|
|
5268
|
+
import { join as join12, sep as sep3 } from "path";
|
|
4720
5269
|
import { minimatch } from "minimatch";
|
|
4721
5270
|
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
4722
5271
|
var RELEVANCE_PATHS_DRIFT_WINDOW_DAYS = 90;
|
|
@@ -4737,7 +5286,7 @@ function expandGlob(rawGlob) {
|
|
|
4737
5286
|
return rawGlob.endsWith("/") ? `${rawGlob}**` : rawGlob;
|
|
4738
5287
|
}
|
|
4739
5288
|
function collectWorkspacePaths(projectRoot) {
|
|
4740
|
-
if (!
|
|
5289
|
+
if (!existsSync6(projectRoot)) {
|
|
4741
5290
|
return [];
|
|
4742
5291
|
}
|
|
4743
5292
|
try {
|
|
@@ -4759,7 +5308,7 @@ function collectWorkspacePaths(projectRoot) {
|
|
|
4759
5308
|
continue;
|
|
4760
5309
|
}
|
|
4761
5310
|
for (const entry of entries) {
|
|
4762
|
-
const abs =
|
|
5311
|
+
const abs = join12(current, entry.name);
|
|
4763
5312
|
const rel = toPosix(abs.slice(projectRoot.length + 1));
|
|
4764
5313
|
if (rel.length === 0) continue;
|
|
4765
5314
|
if (entry.isDirectory()) {
|
|
@@ -4952,16 +5501,16 @@ function createNarrowNoPathsCheck(t, inspection) {
|
|
|
4952
5501
|
}
|
|
4953
5502
|
|
|
4954
5503
|
// src/services/doctor-broad-index.ts
|
|
4955
|
-
import { readFile as
|
|
4956
|
-
import { join as
|
|
5504
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
5505
|
+
import { join as join13 } from "path";
|
|
4957
5506
|
var DEFAULT_BROAD_INDEX_BACKSTOP = 50;
|
|
4958
5507
|
var BROAD_INDEX_BACKSTOP_MIN = 20;
|
|
4959
5508
|
var BROAD_INDEX_BACKSTOP_MAX = 500;
|
|
4960
5509
|
var BROAD_INDEX_DRIFT_RATIO = 0.8;
|
|
4961
5510
|
async function readBroadIndexBackstop(projectRoot) {
|
|
4962
|
-
const configPath =
|
|
5511
|
+
const configPath = join13(projectRoot, ".fabric", "fabric-config.json");
|
|
4963
5512
|
try {
|
|
4964
|
-
const raw = await
|
|
5513
|
+
const raw = await readFile8(configPath, "utf8");
|
|
4965
5514
|
const parsed = JSON.parse(raw);
|
|
4966
5515
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
4967
5516
|
const v = parsed.broad_index_backstop;
|
|
@@ -5057,6 +5606,9 @@ async function inspectStoreKnowledgeAge(projectRoot, now, lastActiveIndex) {
|
|
|
5057
5606
|
if (maturity === void 0) {
|
|
5058
5607
|
continue;
|
|
5059
5608
|
}
|
|
5609
|
+
if (entry.description.relevance_scope === "broad") {
|
|
5610
|
+
continue;
|
|
5611
|
+
}
|
|
5060
5612
|
const lastActive = lastActiveIndex.get(entry.stableId);
|
|
5061
5613
|
if (lastActive === void 0) {
|
|
5062
5614
|
continue;
|
|
@@ -5141,9 +5693,142 @@ function createStaleArchiveCheck(t, inspection) {
|
|
|
5141
5693
|
};
|
|
5142
5694
|
}
|
|
5143
5695
|
|
|
5696
|
+
// src/services/doctor-knowledge-promotion.ts
|
|
5697
|
+
var PROVEN_RELATED_INDEGREE_THRESHOLD = 3;
|
|
5698
|
+
function toLocalId2(id) {
|
|
5699
|
+
const sep5 = id.indexOf(":");
|
|
5700
|
+
return sep5 === -1 ? id : id.slice(sep5 + 1);
|
|
5701
|
+
}
|
|
5702
|
+
function computeRelatedInDegree(entries) {
|
|
5703
|
+
const indegree = /* @__PURE__ */ new Map();
|
|
5704
|
+
for (const entry of entries) {
|
|
5705
|
+
for (const target of entry.description.related ?? []) {
|
|
5706
|
+
const key = toLocalId2(target);
|
|
5707
|
+
indegree.set(key, (indegree.get(key) ?? 0) + 1);
|
|
5708
|
+
}
|
|
5709
|
+
}
|
|
5710
|
+
return indegree;
|
|
5711
|
+
}
|
|
5712
|
+
async function inspectStoreKnowledgePromotion(projectRoot, indegreeThreshold = PROVEN_RELATED_INDEGREE_THRESHOLD) {
|
|
5713
|
+
const entries = await collectStoreCanonicalEntries(projectRoot);
|
|
5714
|
+
const indegree = computeRelatedInDegree(entries);
|
|
5715
|
+
const candidates = [];
|
|
5716
|
+
for (const entry of entries) {
|
|
5717
|
+
if (entry.description.maturity !== "verified") {
|
|
5718
|
+
continue;
|
|
5719
|
+
}
|
|
5720
|
+
const deg = indegree.get(entry.stableId) ?? 0;
|
|
5721
|
+
if (deg >= indegreeThreshold) {
|
|
5722
|
+
candidates.push({
|
|
5723
|
+
stable_id: entry.qualifiedId,
|
|
5724
|
+
path: `store:${entry.qualifiedId}`,
|
|
5725
|
+
related_indegree: deg
|
|
5726
|
+
});
|
|
5727
|
+
}
|
|
5728
|
+
}
|
|
5729
|
+
candidates.sort((a, b) => a.path.localeCompare(b.path));
|
|
5730
|
+
return { candidates, indegree_threshold: indegreeThreshold };
|
|
5731
|
+
}
|
|
5732
|
+
function createPromotionCandidateCheck(t, inspection) {
|
|
5733
|
+
if (inspection.candidates.length === 0) {
|
|
5734
|
+
return {
|
|
5735
|
+
name: t("doctor.check.promotion_candidate.name"),
|
|
5736
|
+
status: "ok",
|
|
5737
|
+
message: t("doctor.check.promotion_candidate.ok")
|
|
5738
|
+
};
|
|
5739
|
+
}
|
|
5740
|
+
const first = inspection.candidates[0];
|
|
5741
|
+
const detail = `${first.stable_id} (verified, ${first.related_indegree} inbound related \u2192 proven candidate)`;
|
|
5742
|
+
const count = inspection.candidates.length;
|
|
5743
|
+
return {
|
|
5744
|
+
name: t("doctor.check.promotion_candidate.name"),
|
|
5745
|
+
// An opportunity, not a defect — info kind keeps doctor health "ok".
|
|
5746
|
+
status: "ok",
|
|
5747
|
+
kind: "info",
|
|
5748
|
+
code: "knowledge_promotion_candidate",
|
|
5749
|
+
fixable: false,
|
|
5750
|
+
message: t(`doctor.check.promotion_candidate.message.${count === 1 ? "singular" : "plural"}`, {
|
|
5751
|
+
count: String(count),
|
|
5752
|
+
threshold: String(inspection.indegree_threshold),
|
|
5753
|
+
detail
|
|
5754
|
+
}),
|
|
5755
|
+
actionHint: t("doctor.check.promotion_candidate.remediation")
|
|
5756
|
+
};
|
|
5757
|
+
}
|
|
5758
|
+
|
|
5759
|
+
// src/services/doctor-knowledge-review-recheck.ts
|
|
5760
|
+
var MS_PER_DAY3 = 24 * 60 * 60 * 1e3;
|
|
5761
|
+
var LAST_REVIEW_CONFIRMED_LINE = /^last_review_confirmed_at:\s*"?([^"\n]+?)"?\s*$/mu;
|
|
5762
|
+
var CREATED_AT_LINE2 = /^created_at:\s*"?([^"\n]+?)"?\s*$/mu;
|
|
5763
|
+
function resolveClock(source) {
|
|
5764
|
+
const review = LAST_REVIEW_CONFIRMED_LINE.exec(source)?.[1];
|
|
5765
|
+
if (review !== void 0) {
|
|
5766
|
+
const ms = Date.parse(review);
|
|
5767
|
+
if (!Number.isNaN(ms)) return { ms, source: "review" };
|
|
5768
|
+
}
|
|
5769
|
+
const created = CREATED_AT_LINE2.exec(source)?.[1];
|
|
5770
|
+
if (created !== void 0) {
|
|
5771
|
+
const ms = Date.parse(created);
|
|
5772
|
+
if (!Number.isNaN(ms)) return { ms, source: "created" };
|
|
5773
|
+
}
|
|
5774
|
+
return void 0;
|
|
5775
|
+
}
|
|
5776
|
+
async function inspectStoreBroadReviewRecheck(projectRoot, now, thresholdDays = readBroadReviewRecheckThresholdDays(projectRoot)) {
|
|
5777
|
+
const entries = await collectStoreCanonicalEntries(projectRoot);
|
|
5778
|
+
const candidates = [];
|
|
5779
|
+
for (const entry of entries) {
|
|
5780
|
+
if (entry.description.relevance_scope !== "broad") {
|
|
5781
|
+
continue;
|
|
5782
|
+
}
|
|
5783
|
+
const clock = resolveClock(entry.body);
|
|
5784
|
+
if (clock === void 0) {
|
|
5785
|
+
continue;
|
|
5786
|
+
}
|
|
5787
|
+
const ageDays = Math.floor((now - clock.ms) / MS_PER_DAY3);
|
|
5788
|
+
if (ageDays > thresholdDays) {
|
|
5789
|
+
candidates.push({
|
|
5790
|
+
stable_id: entry.qualifiedId,
|
|
5791
|
+
path: `store:${entry.qualifiedId}`,
|
|
5792
|
+
age_days: ageDays,
|
|
5793
|
+
clock_source: clock.source
|
|
5794
|
+
});
|
|
5795
|
+
}
|
|
5796
|
+
}
|
|
5797
|
+
candidates.sort((a, b) => a.path.localeCompare(b.path));
|
|
5798
|
+
return { candidates, threshold_days: thresholdDays };
|
|
5799
|
+
}
|
|
5800
|
+
function createBroadReviewRecheckCheck(t, inspection) {
|
|
5801
|
+
if (inspection.candidates.length === 0) {
|
|
5802
|
+
return {
|
|
5803
|
+
name: t("doctor.check.broad_review_recheck.name"),
|
|
5804
|
+
status: "ok",
|
|
5805
|
+
message: t("doctor.check.broad_review_recheck.ok")
|
|
5806
|
+
};
|
|
5807
|
+
}
|
|
5808
|
+
const first = inspection.candidates[0];
|
|
5809
|
+
const clockLabel = first.clock_source === "review" ? "last reviewed" : "never re-confirmed since creation,";
|
|
5810
|
+
const detail = `${first.stable_id} (broad, ${clockLabel} ${first.age_days}d ago \u2192 recheck)`;
|
|
5811
|
+
const count = inspection.candidates.length;
|
|
5812
|
+
return {
|
|
5813
|
+
name: t("doctor.check.broad_review_recheck.name"),
|
|
5814
|
+
// A re-confirm nudge, not a defect — info kind keeps doctor health "ok"
|
|
5815
|
+
// (broad is exempt from decay warnings; this is the gentler review clock).
|
|
5816
|
+
status: "ok",
|
|
5817
|
+
kind: "info",
|
|
5818
|
+
code: "knowledge_broad_review_recheck",
|
|
5819
|
+
fixable: false,
|
|
5820
|
+
message: t(`doctor.check.broad_review_recheck.message.${count === 1 ? "singular" : "plural"}`, {
|
|
5821
|
+
count: String(count),
|
|
5822
|
+
thresholdDays: String(inspection.threshold_days),
|
|
5823
|
+
detail
|
|
5824
|
+
}),
|
|
5825
|
+
actionHint: t("doctor.check.broad_review_recheck.remediation")
|
|
5826
|
+
};
|
|
5827
|
+
}
|
|
5828
|
+
|
|
5144
5829
|
// src/services/doctor-scope-lint.ts
|
|
5145
|
-
import { readFile as
|
|
5146
|
-
import { join as
|
|
5830
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
5831
|
+
import { join as join14 } from "path";
|
|
5147
5832
|
import {
|
|
5148
5833
|
buildStoreResolveInput as buildStoreResolveInput5,
|
|
5149
5834
|
createStoreResolver as createStoreResolver5,
|
|
@@ -5178,7 +5863,7 @@ async function resolveLintStores(projectRoot) {
|
|
|
5178
5863
|
const globalRoot = resolveGlobalRoot4();
|
|
5179
5864
|
return Promise.all(readSet.stores.map(async (entry) => {
|
|
5180
5865
|
const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
|
|
5181
|
-
const dir =
|
|
5866
|
+
const dir = join14(
|
|
5182
5867
|
globalRoot,
|
|
5183
5868
|
storeRelativePathForMount4(mounted ?? { store_uuid: entry.store_uuid })
|
|
5184
5869
|
);
|
|
@@ -5210,7 +5895,7 @@ async function lintStoreScopes(projectRoot) {
|
|
|
5210
5895
|
}
|
|
5211
5896
|
let source;
|
|
5212
5897
|
try {
|
|
5213
|
-
source = await
|
|
5898
|
+
source = await readFile9(ref.file, "utf8");
|
|
5214
5899
|
} catch {
|
|
5215
5900
|
continue;
|
|
5216
5901
|
}
|
|
@@ -5332,8 +6017,8 @@ function createUnboundProjectCheck(t, violation) {
|
|
|
5332
6017
|
}
|
|
5333
6018
|
|
|
5334
6019
|
// src/services/doctor-store-counters.ts
|
|
5335
|
-
import { existsSync as
|
|
5336
|
-
import { join as
|
|
6020
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
6021
|
+
import { join as join15 } from "path";
|
|
5337
6022
|
import {
|
|
5338
6023
|
buildStoreResolveInput as buildStoreResolveInput6,
|
|
5339
6024
|
createStoreResolver as createStoreResolver6,
|
|
@@ -5360,7 +6045,7 @@ function resolveCounterStores(projectRoot) {
|
|
|
5360
6045
|
return readSet.stores.map((entry) => ({
|
|
5361
6046
|
uuid: entry.store_uuid,
|
|
5362
6047
|
alias: entry.alias,
|
|
5363
|
-
dir:
|
|
6048
|
+
dir: join15(
|
|
5364
6049
|
globalRoot,
|
|
5365
6050
|
storeRelativePathForMount5(
|
|
5366
6051
|
input.mountedStores.find((s) => s.store_uuid === entry.store_uuid) ?? {
|
|
@@ -5388,8 +6073,8 @@ function readEntryId(file) {
|
|
|
5388
6073
|
function computeStoreDiskMax(storeDir) {
|
|
5389
6074
|
const max = defaultAgentsMetaCounters();
|
|
5390
6075
|
for (const type of STORE_KNOWLEDGE_TYPE_DIRS) {
|
|
5391
|
-
const dir =
|
|
5392
|
-
if (!
|
|
6076
|
+
const dir = join15(storeDir, STORE_LAYOUT2.knowledgeDir, type);
|
|
6077
|
+
if (!existsSync7(dir)) {
|
|
5393
6078
|
continue;
|
|
5394
6079
|
}
|
|
5395
6080
|
let names;
|
|
@@ -5402,7 +6087,7 @@ function computeStoreDiskMax(storeDir) {
|
|
|
5402
6087
|
if (!name.endsWith(".md")) {
|
|
5403
6088
|
continue;
|
|
5404
6089
|
}
|
|
5405
|
-
const parsed = parseKnowledgeId2(readEntryId(
|
|
6090
|
+
const parsed = parseKnowledgeId2(readEntryId(join15(dir, name)) ?? "");
|
|
5406
6091
|
if (parsed === null) {
|
|
5407
6092
|
continue;
|
|
5408
6093
|
}
|
|
@@ -5485,7 +6170,7 @@ function createStoreCounterCheck(t, drifts) {
|
|
|
5485
6170
|
|
|
5486
6171
|
// src/services/doctor-store-orphan.ts
|
|
5487
6172
|
import { readdirSync as readdirSync3 } from "fs";
|
|
5488
|
-
import { join as
|
|
6173
|
+
import { join as join16 } from "path";
|
|
5489
6174
|
import {
|
|
5490
6175
|
STORES_ROOT_DIR,
|
|
5491
6176
|
addMountedStore,
|
|
@@ -5509,15 +6194,15 @@ function inspectStoreOrphans(globalRoot = resolveGlobalRoot6()) {
|
|
|
5509
6194
|
return [];
|
|
5510
6195
|
}
|
|
5511
6196
|
const registered = new Set(config.stores.map((s) => s.store_uuid));
|
|
5512
|
-
const storesRoot =
|
|
6197
|
+
const storesRoot = join16(globalRoot, STORES_ROOT_DIR);
|
|
5513
6198
|
const orphans = [];
|
|
5514
6199
|
for (const group of listDir(storesRoot)) {
|
|
5515
6200
|
if (group === STORE_BY_ALIAS_DIR) {
|
|
5516
6201
|
continue;
|
|
5517
6202
|
}
|
|
5518
|
-
const groupDir =
|
|
6203
|
+
const groupDir = join16(storesRoot, group);
|
|
5519
6204
|
for (const mount of listDir(groupDir)) {
|
|
5520
|
-
const dir =
|
|
6205
|
+
const dir = join16(groupDir, mount);
|
|
5521
6206
|
const identity = readStoreIdentity(dir);
|
|
5522
6207
|
if (identity === null || registered.has(identity.store_uuid)) {
|
|
5523
6208
|
continue;
|
|
@@ -5590,7 +6275,7 @@ import {
|
|
|
5590
6275
|
|
|
5591
6276
|
// src/services/events-jsonl-gates.ts
|
|
5592
6277
|
import { promises as fs } from "fs";
|
|
5593
|
-
import { existsSync as
|
|
6278
|
+
import { existsSync as existsSync8 } from "fs";
|
|
5594
6279
|
var EVENTS_JSONL_SIZE_WARN_BYTES = 10 * 1024 * 1024;
|
|
5595
6280
|
var METRICS_STALE_WARN_MS = 10 * 60 * 1e3;
|
|
5596
6281
|
var ROTATION_OVERDUE_WARN_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
@@ -5605,17 +6290,17 @@ async function inspectEventsJsonlGates(projectRoot, options = {}) {
|
|
|
5605
6290
|
let ledgerSizeBytes = 0;
|
|
5606
6291
|
let ledgerStalenessMs = null;
|
|
5607
6292
|
try {
|
|
5608
|
-
const
|
|
5609
|
-
ledgerSizeBytes =
|
|
5610
|
-
ledgerStalenessMs = Math.max(0, now.getTime() -
|
|
6293
|
+
const stat5 = await fs.stat(eventsPath);
|
|
6294
|
+
ledgerSizeBytes = stat5.size;
|
|
6295
|
+
ledgerStalenessMs = Math.max(0, now.getTime() - stat5.mtimeMs);
|
|
5611
6296
|
} catch (error) {
|
|
5612
6297
|
if (!(isNodeError(error) && error.code === "ENOENT")) throw error;
|
|
5613
6298
|
}
|
|
5614
6299
|
let metricsStalenessMs = null;
|
|
5615
|
-
if (
|
|
6300
|
+
if (existsSync8(metricsPath)) {
|
|
5616
6301
|
try {
|
|
5617
|
-
const
|
|
5618
|
-
metricsStalenessMs = Math.max(0, now.getTime() -
|
|
6302
|
+
const stat5 = await fs.stat(metricsPath);
|
|
6303
|
+
metricsStalenessMs = Math.max(0, now.getTime() - stat5.mtimeMs);
|
|
5619
6304
|
} catch {
|
|
5620
6305
|
}
|
|
5621
6306
|
}
|
|
@@ -5654,18 +6339,9 @@ async function inspectEventsJsonlGates(projectRoot, options = {}) {
|
|
|
5654
6339
|
}
|
|
5655
6340
|
|
|
5656
6341
|
// src/services/doctor-skill-lints.ts
|
|
5657
|
-
import { readdir as
|
|
5658
|
-
import { join as
|
|
5659
|
-
var FABRIC_SKILL_SLUGS = ["fabric-archive", "fabric-review"
|
|
5660
|
-
var ROUTER_VALID_LEAF_SLUGS = /* @__PURE__ */ new Set([
|
|
5661
|
-
"fabric-archive",
|
|
5662
|
-
"fabric-review",
|
|
5663
|
-
"fabric-import",
|
|
5664
|
-
"fabric-sync",
|
|
5665
|
-
"fabric-store",
|
|
5666
|
-
"fabric-audit",
|
|
5667
|
-
"fabric-connect"
|
|
5668
|
-
]);
|
|
6342
|
+
import { readdir as readdir3, readFile as readFile10 } from "fs/promises";
|
|
6343
|
+
import { join as join17, posix as posix2 } from "path";
|
|
6344
|
+
var FABRIC_SKILL_SLUGS = ["fabric-archive", "fabric-review"];
|
|
5669
6345
|
var SKILL_MD_FRONTMATTER_ROOTS = [".claude/skills", ".codex/skills"];
|
|
5670
6346
|
var SKILL_FRONTMATTER_KEY_PATTERN = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]+(.+?)[ \t]*$/u;
|
|
5671
6347
|
var SKILL_QUOTED_VALUE_LEADS = /* @__PURE__ */ new Set(['"', "'", "[", "{", ">", "|"]);
|
|
@@ -5686,7 +6362,7 @@ function issueCheck(name, status, kind, code, message, actionHint, audience) {
|
|
|
5686
6362
|
}
|
|
5687
6363
|
async function listMarkdownFiles(dir) {
|
|
5688
6364
|
try {
|
|
5689
|
-
return (await
|
|
6365
|
+
return (await readdir3(dir)).filter((name) => name.endsWith(".md"));
|
|
5690
6366
|
} catch {
|
|
5691
6367
|
return null;
|
|
5692
6368
|
}
|
|
@@ -5694,8 +6370,8 @@ async function listMarkdownFiles(dir) {
|
|
|
5694
6370
|
async function inspectSkillRefMirror(projectRoot) {
|
|
5695
6371
|
const driftedPaths = [];
|
|
5696
6372
|
for (const slug of FABRIC_SKILL_SLUGS) {
|
|
5697
|
-
const claudeRef =
|
|
5698
|
-
const codexRef =
|
|
6373
|
+
const claudeRef = join17(projectRoot, ".claude", "skills", slug, "ref");
|
|
6374
|
+
const codexRef = join17(projectRoot, ".codex", "skills", slug, "ref");
|
|
5699
6375
|
const [claudeFiles, codexFiles] = await Promise.all([listMarkdownFiles(claudeRef), listMarkdownFiles(codexRef)]);
|
|
5700
6376
|
if (claudeFiles === null || codexFiles === null) continue;
|
|
5701
6377
|
const claudeSet = new Set(claudeFiles);
|
|
@@ -5712,8 +6388,8 @@ async function inspectSkillRefMirror(projectRoot) {
|
|
|
5712
6388
|
let codexBody;
|
|
5713
6389
|
try {
|
|
5714
6390
|
[claudeBody, codexBody] = await Promise.all([
|
|
5715
|
-
|
|
5716
|
-
|
|
6391
|
+
readFile10(join17(claudeRef, fname), "utf8"),
|
|
6392
|
+
readFile10(join17(codexRef, fname), "utf8")
|
|
5717
6393
|
]);
|
|
5718
6394
|
} catch {
|
|
5719
6395
|
continue;
|
|
@@ -5732,10 +6408,10 @@ async function inspectSkillTokenBudget(projectRoot) {
|
|
|
5732
6408
|
const overSize = [];
|
|
5733
6409
|
let highestSeverity = "ok";
|
|
5734
6410
|
for (const slug of FABRIC_SKILL_SLUGS) {
|
|
5735
|
-
const skillMdPath =
|
|
6411
|
+
const skillMdPath = join17(projectRoot, ".claude", "skills", slug, "SKILL.md");
|
|
5736
6412
|
let body;
|
|
5737
6413
|
try {
|
|
5738
|
-
body = await
|
|
6414
|
+
body = await readFile10(skillMdPath, "utf8");
|
|
5739
6415
|
} catch {
|
|
5740
6416
|
continue;
|
|
5741
6417
|
}
|
|
@@ -5756,10 +6432,10 @@ async function inspectSkillDescription(projectRoot) {
|
|
|
5756
6432
|
const CJK_PATTERN = /[\u3400-\u4dbf\u4e00-\u9fff]/u;
|
|
5757
6433
|
const ASCII_PATTERN = /[a-zA-Z]{2,}/u;
|
|
5758
6434
|
for (const slug of FABRIC_SKILL_SLUGS) {
|
|
5759
|
-
const skillMdPath =
|
|
6435
|
+
const skillMdPath = join17(projectRoot, ".claude", "skills", slug, "SKILL.md");
|
|
5760
6436
|
let body;
|
|
5761
6437
|
try {
|
|
5762
|
-
body = await
|
|
6438
|
+
body = await readFile10(skillMdPath, "utf8");
|
|
5763
6439
|
} catch {
|
|
5764
6440
|
continue;
|
|
5765
6441
|
}
|
|
@@ -5790,19 +6466,19 @@ async function inspectSkillDescription(projectRoot) {
|
|
|
5790
6466
|
async function inspectSkillMdYamlInvalid(projectRoot) {
|
|
5791
6467
|
const candidates = [];
|
|
5792
6468
|
for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
|
|
5793
|
-
const rootAbs =
|
|
6469
|
+
const rootAbs = join17(projectRoot, rootRel);
|
|
5794
6470
|
let dirEntries;
|
|
5795
6471
|
try {
|
|
5796
|
-
dirEntries = await
|
|
6472
|
+
dirEntries = await readdir3(rootAbs, { withFileTypes: true });
|
|
5797
6473
|
} catch {
|
|
5798
6474
|
continue;
|
|
5799
6475
|
}
|
|
5800
6476
|
for (const dirEntry of dirEntries) {
|
|
5801
6477
|
if (!dirEntry.isDirectory()) continue;
|
|
5802
|
-
const skillFile =
|
|
6478
|
+
const skillFile = join17(rootAbs, dirEntry.name, "SKILL.md");
|
|
5803
6479
|
let raw;
|
|
5804
6480
|
try {
|
|
5805
|
-
raw = await
|
|
6481
|
+
raw = await readFile10(skillFile, "utf8");
|
|
5806
6482
|
} catch {
|
|
5807
6483
|
continue;
|
|
5808
6484
|
}
|
|
@@ -5850,68 +6526,6 @@ function extractSkillFrontmatterLines(raw) {
|
|
|
5850
6526
|
}
|
|
5851
6527
|
return null;
|
|
5852
6528
|
}
|
|
5853
|
-
function extractMarkdownSectionBody(markdown, sectionName) {
|
|
5854
|
-
const lines = markdown.split(/\r?\n/u);
|
|
5855
|
-
const headingRe = /^(#{2,3})\s+(.+?)\s*$/u;
|
|
5856
|
-
let start = -1;
|
|
5857
|
-
for (let i = 0; i < lines.length; i++) {
|
|
5858
|
-
const h = headingRe.exec(lines[i]);
|
|
5859
|
-
if (h && h[2] === sectionName) {
|
|
5860
|
-
start = i + 1;
|
|
5861
|
-
break;
|
|
5862
|
-
}
|
|
5863
|
-
}
|
|
5864
|
-
if (start === -1) return null;
|
|
5865
|
-
const out = [];
|
|
5866
|
-
for (let i = start; i < lines.length; i++) {
|
|
5867
|
-
if (headingRe.test(lines[i])) break;
|
|
5868
|
-
out.push(lines[i]);
|
|
5869
|
-
}
|
|
5870
|
-
return out.join("\n");
|
|
5871
|
-
}
|
|
5872
|
-
async function inspectRouterChainRef(projectRoot) {
|
|
5873
|
-
const candidatePaths = [
|
|
5874
|
-
join16(projectRoot, ".claude", "skills", "fabric", "SKILL.md"),
|
|
5875
|
-
join16(projectRoot, ".codex", "skills", "fabric", "SKILL.md")
|
|
5876
|
-
];
|
|
5877
|
-
let body = null;
|
|
5878
|
-
for (const candidate of candidatePaths) {
|
|
5879
|
-
try {
|
|
5880
|
-
body = await readFile9(candidate, "utf8");
|
|
5881
|
-
break;
|
|
5882
|
-
} catch {
|
|
5883
|
-
}
|
|
5884
|
-
}
|
|
5885
|
-
if (body === null) return { status: "ok", unknownRefs: [] };
|
|
5886
|
-
const chainSection = extractMarkdownSectionBody(body, "S_CHAIN");
|
|
5887
|
-
if (chainSection === null) return { status: "ok", unknownRefs: [] };
|
|
5888
|
-
const refs = /* @__PURE__ */ new Set();
|
|
5889
|
-
const tokenRe = /`(fabric-[a-z]+)`/gu;
|
|
5890
|
-
let match;
|
|
5891
|
-
while ((match = tokenRe.exec(chainSection)) !== null) {
|
|
5892
|
-
refs.add(match[1]);
|
|
5893
|
-
}
|
|
5894
|
-
const unknownRefs = [...refs].filter((slug) => !ROUTER_VALID_LEAF_SLUGS.has(slug)).sort();
|
|
5895
|
-
return unknownRefs.length === 0 ? { status: "ok", unknownRefs: [] } : { status: "drift", unknownRefs };
|
|
5896
|
-
}
|
|
5897
|
-
function createRouterChainRefCheck(t, inspection) {
|
|
5898
|
-
if (inspection.status === "ok") {
|
|
5899
|
-
return okCheck(t("doctor.check.router_chain_ref.name"), t("doctor.check.router_chain_ref.ok"));
|
|
5900
|
-
}
|
|
5901
|
-
const count = inspection.unknownRefs.length;
|
|
5902
|
-
return issueCheck(
|
|
5903
|
-
t("doctor.check.router_chain_ref.name"),
|
|
5904
|
-
"warn",
|
|
5905
|
-
"warning",
|
|
5906
|
-
"router_chain_ref_drift",
|
|
5907
|
-
t(`doctor.check.router_chain_ref.message.${count === 1 ? "singular" : "plural"}`, {
|
|
5908
|
-
count: String(count),
|
|
5909
|
-
list: inspection.unknownRefs.join(", ")
|
|
5910
|
-
}),
|
|
5911
|
-
t("doctor.check.router_chain_ref.remediation"),
|
|
5912
|
-
"maintainer"
|
|
5913
|
-
);
|
|
5914
|
-
}
|
|
5915
6529
|
function createSkillRefMirrorCheck(t, inspection) {
|
|
5916
6530
|
if (inspection.status === "ok") {
|
|
5917
6531
|
return okCheck(t("doctor.check.skill_ref_mirror.name"), t("doctor.check.skill_ref_mirror.ok"));
|
|
@@ -5986,10 +6600,130 @@ function createSkillMdYamlInvalidCheck(t, inspection) {
|
|
|
5986
6600
|
);
|
|
5987
6601
|
}
|
|
5988
6602
|
|
|
6603
|
+
// src/services/doctor-retired-references-lint.ts
|
|
6604
|
+
import { readdir as readdir4, readFile as readFile11, stat as stat3 } from "fs/promises";
|
|
6605
|
+
import { join as join18, posix as posix3, relative as relative2 } from "path";
|
|
6606
|
+
var RETIRED_TOKENS = [
|
|
6607
|
+
{ token: "fab_plan_context", replacement: "fab_recall", reason: "retrieval collapsed to one lean fab_recall (KT-DEC-0026)" },
|
|
6608
|
+
{ token: "fab_get_knowledge_sections", replacement: "fab_recall", reason: "two-step fetch retired (KT-DEC-0026)" },
|
|
6609
|
+
{ token: "fab_extract_knowledge", replacement: "fab_propose", reason: "tool renamed to match propose/write semantics (ux-w1-1)" },
|
|
6610
|
+
{ token: "hint_broad_budget_chars", replacement: null, reason: "index-only SessionStart sink has no body budget (ux-w1-5)" },
|
|
6611
|
+
{ token: "cite_evict_interval", replacement: "cite_recall_nudge", reason: "turn-counter superseded by recall-aware nudge (ux-w1-5)" },
|
|
6612
|
+
{ token: "reverse_unarchive_enabled", replacement: null, reason: "never-wired opt-in flag deleted (ux-w1-5)" },
|
|
6613
|
+
{ token: "reverse_unarchive_dry_run", replacement: null, reason: "unarchive dryRun comes from the caller, not config (ux-w1-5)" },
|
|
6614
|
+
{ token: "doctor --cite-coverage", replacement: "audit cite", reason: "cite audit moved to the audit group (W3-D)" },
|
|
6615
|
+
{ token: "doctor --fix-knowledge", replacement: "doctor --fix", reason: "fix-knowledge merged into a single --fix (W3-D)" },
|
|
6616
|
+
{ token: "store add", replacement: "store mount", reason: "de-synonymised: add \u2192 mount (W3-E)" },
|
|
6617
|
+
{ token: "store route-write", replacement: "store switch-write --scope", reason: "route-write folded into switch-write --scope (W3-E)" },
|
|
6618
|
+
{ token: "store re-scope", replacement: "store migrate scope", reason: "scope-rewrite ops grouped under `store migrate` (W3-E)" },
|
|
6619
|
+
{ token: "store backfill-scope", replacement: "store migrate backfill", reason: "scope-rewrite ops grouped under `store migrate` (W3-E)" },
|
|
6620
|
+
{ token: "store promote", replacement: "store migrate promote", reason: "scope-rewrite ops grouped under `store migrate` (W3-E)" },
|
|
6621
|
+
{ token: "fabric scope-explain", replacement: "fabric info scope", reason: "scope-explain command merged into the `info scope` subcommand (W3-F)" },
|
|
6622
|
+
{ token: "fabric context", replacement: "fabric inspect", reason: "renamed: `context` of what? \u2192 `inspect` the injection (W3-F / NS-01 \xA71)" },
|
|
6623
|
+
{ token: "fabric metrics", replacement: "fabric audit metrics", reason: "top-level metrics retired; reachable as `audit metrics` (W3-F)" },
|
|
6624
|
+
{ token: "hint_broad_top_k", replacement: null, reason: "W2-1 retired the broad hard cap; broad_index_backstop is the sole guard (W3-J)" }
|
|
6625
|
+
];
|
|
6626
|
+
var HOOK_DIRS = [".claude/hooks", ".codex/hooks"];
|
|
6627
|
+
var SKILL_DIRS = [".claude/skills", ".codex/skills"];
|
|
6628
|
+
var BOOTSTRAP_FILES = ["AGENTS.md", "CLAUDE.md", join18(".fabric", "AGENTS.md")];
|
|
6629
|
+
function isCommentLine(line) {
|
|
6630
|
+
const t = line.trim();
|
|
6631
|
+
return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*");
|
|
6632
|
+
}
|
|
6633
|
+
async function walkFiles(dir, exts) {
|
|
6634
|
+
let entries;
|
|
6635
|
+
try {
|
|
6636
|
+
entries = await readdir4(dir, { withFileTypes: true });
|
|
6637
|
+
} catch {
|
|
6638
|
+
return [];
|
|
6639
|
+
}
|
|
6640
|
+
const out = [];
|
|
6641
|
+
for (const e of entries) {
|
|
6642
|
+
const full = join18(dir, e.name);
|
|
6643
|
+
if (e.isDirectory()) {
|
|
6644
|
+
out.push(...await walkFiles(full, exts));
|
|
6645
|
+
} else if (exts.some((ext) => e.name.endsWith(ext))) {
|
|
6646
|
+
out.push(full);
|
|
6647
|
+
}
|
|
6648
|
+
}
|
|
6649
|
+
return out;
|
|
6650
|
+
}
|
|
6651
|
+
function scanText(rel, text, skipComments, hits) {
|
|
6652
|
+
const lines = text.split("\n");
|
|
6653
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
6654
|
+
const line = lines[i];
|
|
6655
|
+
if (skipComments && isCommentLine(line)) continue;
|
|
6656
|
+
for (const { token, replacement } of RETIRED_TOKENS) {
|
|
6657
|
+
if (line.includes(token)) {
|
|
6658
|
+
hits.push({ path: rel, token, line: i + 1, replacement });
|
|
6659
|
+
}
|
|
6660
|
+
}
|
|
6661
|
+
}
|
|
6662
|
+
}
|
|
6663
|
+
async function inspectRetiredReferences(projectRoot) {
|
|
6664
|
+
const hits = [];
|
|
6665
|
+
let scannedFiles = 0;
|
|
6666
|
+
const toRel = (p) => posix3.normalize(relative2(projectRoot, p).split("\\").join("/"));
|
|
6667
|
+
for (const rel of BOOTSTRAP_FILES) {
|
|
6668
|
+
const abs = join18(projectRoot, rel);
|
|
6669
|
+
try {
|
|
6670
|
+
if ((await stat3(abs)).isFile()) {
|
|
6671
|
+
scannedFiles += 1;
|
|
6672
|
+
scanText(toRel(abs), await readFile11(abs, "utf8"), false, hits);
|
|
6673
|
+
}
|
|
6674
|
+
} catch {
|
|
6675
|
+
}
|
|
6676
|
+
}
|
|
6677
|
+
for (const dir of SKILL_DIRS) {
|
|
6678
|
+
for (const file of await walkFiles(join18(projectRoot, dir), [".md"])) {
|
|
6679
|
+
scannedFiles += 1;
|
|
6680
|
+
try {
|
|
6681
|
+
scanText(toRel(file), await readFile11(file, "utf8"), false, hits);
|
|
6682
|
+
} catch {
|
|
6683
|
+
}
|
|
6684
|
+
}
|
|
6685
|
+
}
|
|
6686
|
+
for (const dir of HOOK_DIRS) {
|
|
6687
|
+
for (const file of await walkFiles(join18(projectRoot, dir), [".cjs"])) {
|
|
6688
|
+
scannedFiles += 1;
|
|
6689
|
+
try {
|
|
6690
|
+
scanText(toRel(file), await readFile11(file, "utf8"), true, hits);
|
|
6691
|
+
} catch {
|
|
6692
|
+
}
|
|
6693
|
+
}
|
|
6694
|
+
}
|
|
6695
|
+
if (scannedFiles === 0) {
|
|
6696
|
+
return { status: "skipped", scannedFiles, hits: [] };
|
|
6697
|
+
}
|
|
6698
|
+
hits.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line);
|
|
6699
|
+
return { status: hits.length > 0 ? "warn" : "ok", scannedFiles, hits };
|
|
6700
|
+
}
|
|
6701
|
+
function createRetiredReferenceCheck(t, inspection) {
|
|
6702
|
+
const name = t("doctor.check.retired_reference.name");
|
|
6703
|
+
if (inspection.status !== "warn") {
|
|
6704
|
+
return { name, status: "ok", message: t("doctor.check.retired_reference.ok") };
|
|
6705
|
+
}
|
|
6706
|
+
const sample = inspection.hits.slice(0, 5).map(
|
|
6707
|
+
(h) => h.replacement ? `${h.path}:${h.line} \`${h.token}\` \u2192 \`${h.replacement}\`` : `${h.path}:${h.line} \`${h.token}\` (removed)`
|
|
6708
|
+
).join("; ");
|
|
6709
|
+
return {
|
|
6710
|
+
name,
|
|
6711
|
+
status: "warn",
|
|
6712
|
+
kind: "warning",
|
|
6713
|
+
code: "retired_reference",
|
|
6714
|
+
audience: "maintainer",
|
|
6715
|
+
message: t("doctor.check.retired_reference.message", {
|
|
6716
|
+
count: String(inspection.hits.length),
|
|
6717
|
+
sample
|
|
6718
|
+
}),
|
|
6719
|
+
actionHint: t("doctor.check.retired_reference.remediation")
|
|
6720
|
+
};
|
|
6721
|
+
}
|
|
6722
|
+
|
|
5989
6723
|
// src/services/doctor-hooks-lints.ts
|
|
5990
6724
|
import { constants } from "fs";
|
|
5991
|
-
import { access, readdir as
|
|
5992
|
-
import { join as
|
|
6725
|
+
import { access, readdir as readdir5, readFile as readFile12, stat as stat4 } from "fs/promises";
|
|
6726
|
+
import { join as join19, posix as posix4 } from "path";
|
|
5993
6727
|
import { Script } from "vm";
|
|
5994
6728
|
var HOOKS_RUNTIME_CLIENT_DIRS = [
|
|
5995
6729
|
{ client: "claude", dir: ".claude/hooks" },
|
|
@@ -6013,7 +6747,7 @@ function isRecord(value) {
|
|
|
6013
6747
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6014
6748
|
}
|
|
6015
6749
|
function normalizePath(path) {
|
|
6016
|
-
return
|
|
6750
|
+
return posix4.normalize(path.split("\\").join("/"));
|
|
6017
6751
|
}
|
|
6018
6752
|
function isNodeMissingPathError(error) {
|
|
6019
6753
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
@@ -6038,27 +6772,27 @@ function isHookWiredForEvent(hooks, event, hookFile) {
|
|
|
6038
6772
|
}
|
|
6039
6773
|
async function readDirectoryFileNames(dir) {
|
|
6040
6774
|
try {
|
|
6041
|
-
return await
|
|
6775
|
+
return await readdir5(dir);
|
|
6042
6776
|
} catch {
|
|
6043
6777
|
return null;
|
|
6044
6778
|
}
|
|
6045
6779
|
}
|
|
6046
6780
|
async function isFile(absPath) {
|
|
6047
6781
|
try {
|
|
6048
|
-
return (await
|
|
6782
|
+
return (await stat4(absPath)).isFile();
|
|
6049
6783
|
} catch {
|
|
6050
6784
|
return false;
|
|
6051
6785
|
}
|
|
6052
6786
|
}
|
|
6053
6787
|
async function inspectHooksWired(projectRoot) {
|
|
6054
|
-
const claudeEntries = await readDirectoryFileNames(
|
|
6788
|
+
const claudeEntries = await readDirectoryFileNames(join19(projectRoot, ".claude"));
|
|
6055
6789
|
if (claudeEntries === null) {
|
|
6056
6790
|
return { status: "skipped", missingHooks: [] };
|
|
6057
6791
|
}
|
|
6058
|
-
const settingsPath =
|
|
6792
|
+
const settingsPath = join19(projectRoot, ".claude", "settings.json");
|
|
6059
6793
|
let parsed;
|
|
6060
6794
|
try {
|
|
6061
|
-
parsed = JSON.parse(await
|
|
6795
|
+
parsed = JSON.parse(await readFile12(settingsPath, "utf8"));
|
|
6062
6796
|
} catch {
|
|
6063
6797
|
return { status: "missing-settings", missingHooks: [] };
|
|
6064
6798
|
}
|
|
@@ -6080,12 +6814,12 @@ async function inspectHooksWired(projectRoot) {
|
|
|
6080
6814
|
return { status: "incomplete", missingHooks: missing };
|
|
6081
6815
|
}
|
|
6082
6816
|
async function inspectHookCacheWritability(projectRoot) {
|
|
6083
|
-
const relPath =
|
|
6084
|
-
const fabricDir =
|
|
6085
|
-
const cacheDir =
|
|
6817
|
+
const relPath = posix4.join(".fabric", ".cache");
|
|
6818
|
+
const fabricDir = join19(projectRoot, ".fabric");
|
|
6819
|
+
const cacheDir = join19(projectRoot, ".fabric", ".cache");
|
|
6086
6820
|
try {
|
|
6087
6821
|
try {
|
|
6088
|
-
const cacheStats = await
|
|
6822
|
+
const cacheStats = await stat4(cacheDir);
|
|
6089
6823
|
if (!cacheStats.isDirectory()) {
|
|
6090
6824
|
return {
|
|
6091
6825
|
writable: false,
|
|
@@ -6102,14 +6836,14 @@ async function inspectHookCacheWritability(projectRoot) {
|
|
|
6102
6836
|
}
|
|
6103
6837
|
let parent = fabricDir;
|
|
6104
6838
|
try {
|
|
6105
|
-
await
|
|
6839
|
+
await stat4(fabricDir);
|
|
6106
6840
|
} catch (error) {
|
|
6107
6841
|
if (!isNodeMissingPathError(error)) {
|
|
6108
6842
|
throw error;
|
|
6109
6843
|
}
|
|
6110
6844
|
parent = projectRoot;
|
|
6111
6845
|
}
|
|
6112
|
-
const parentStats = await
|
|
6846
|
+
const parentStats = await stat4(parent);
|
|
6113
6847
|
if (!parentStats.isDirectory()) {
|
|
6114
6848
|
return {
|
|
6115
6849
|
writable: false,
|
|
@@ -6131,12 +6865,12 @@ async function inspectHookCacheWritability(projectRoot) {
|
|
|
6131
6865
|
async function inspectHooksContentDrift(projectRoot) {
|
|
6132
6866
|
const hookFilesByBasename = /* @__PURE__ */ new Map();
|
|
6133
6867
|
for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
|
|
6134
|
-
const absDir =
|
|
6868
|
+
const absDir = join19(projectRoot, dir);
|
|
6135
6869
|
const entries = await readDirectoryFileNames(absDir);
|
|
6136
6870
|
if (entries === null) continue;
|
|
6137
6871
|
for (const name of entries) {
|
|
6138
6872
|
if (!name.endsWith(".cjs")) continue;
|
|
6139
|
-
const abs =
|
|
6873
|
+
const abs = join19(absDir, name);
|
|
6140
6874
|
if (!await isFile(abs)) continue;
|
|
6141
6875
|
const arr = hookFilesByBasename.get(name) ?? [];
|
|
6142
6876
|
arr.push({ client, abs });
|
|
@@ -6145,13 +6879,13 @@ async function inspectHooksContentDrift(projectRoot) {
|
|
|
6145
6879
|
}
|
|
6146
6880
|
const drifts = [];
|
|
6147
6881
|
let scanned = 0;
|
|
6148
|
-
for (const [
|
|
6882
|
+
for (const [basename4, copies] of hookFilesByBasename) {
|
|
6149
6883
|
if (copies.length < 2) continue;
|
|
6150
6884
|
scanned += copies.length;
|
|
6151
6885
|
const hashes = [];
|
|
6152
6886
|
for (const { client, abs } of copies) {
|
|
6153
6887
|
try {
|
|
6154
|
-
const body = await
|
|
6888
|
+
const body = await readFile12(abs, "utf8");
|
|
6155
6889
|
hashes.push({ client, sha: sha256(body) });
|
|
6156
6890
|
} catch {
|
|
6157
6891
|
}
|
|
@@ -6160,7 +6894,7 @@ async function inspectHooksContentDrift(projectRoot) {
|
|
|
6160
6894
|
const first = hashes[0].sha;
|
|
6161
6895
|
if (hashes.some((h) => h.sha !== first)) {
|
|
6162
6896
|
drifts.push({
|
|
6163
|
-
basename:
|
|
6897
|
+
basename: basename4,
|
|
6164
6898
|
clients: copies.map((copy) => copy.client),
|
|
6165
6899
|
hashes
|
|
6166
6900
|
});
|
|
@@ -6173,18 +6907,18 @@ async function inspectHooksRuntime(projectRoot) {
|
|
|
6173
6907
|
const issues = [];
|
|
6174
6908
|
let scanned = 0;
|
|
6175
6909
|
for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
|
|
6176
|
-
const absDir =
|
|
6910
|
+
const absDir = join19(projectRoot, dir);
|
|
6177
6911
|
const entries = await readDirectoryFileNames(absDir);
|
|
6178
6912
|
if (entries === null) continue;
|
|
6179
6913
|
for (const name of entries) {
|
|
6180
6914
|
if (!name.endsWith(".cjs")) continue;
|
|
6181
|
-
const abs =
|
|
6915
|
+
const abs = join19(absDir, name);
|
|
6182
6916
|
const displayPath = `${dir}/${name}`;
|
|
6183
6917
|
if (!await isFile(abs)) continue;
|
|
6184
6918
|
scanned += 1;
|
|
6185
6919
|
let body;
|
|
6186
6920
|
try {
|
|
6187
|
-
body = await
|
|
6921
|
+
body = await readFile12(abs, "utf8");
|
|
6188
6922
|
} catch (err) {
|
|
6189
6923
|
issues.push({
|
|
6190
6924
|
path: displayPath,
|
|
@@ -6321,8 +7055,8 @@ function createHookCacheWritabilityCheck(t, inspection) {
|
|
|
6321
7055
|
}
|
|
6322
7056
|
|
|
6323
7057
|
// src/services/doctor-bootstrap-lints.ts
|
|
6324
|
-
import { access as access2, readFile as
|
|
6325
|
-
import { join as
|
|
7058
|
+
import { access as access2, readFile as readFile13 } from "fs/promises";
|
|
7059
|
+
import { join as join20 } from "path";
|
|
6326
7060
|
import {
|
|
6327
7061
|
BOOTSTRAP_MARKER_BEGIN,
|
|
6328
7062
|
BOOTSTRAP_MARKER_END,
|
|
@@ -6354,17 +7088,17 @@ async function fileExists(path) {
|
|
|
6354
7088
|
}
|
|
6355
7089
|
async function inspectBootstrapAnchor(projectRoot) {
|
|
6356
7090
|
const [hasAgentsMd, hasClaudeMd] = await Promise.all([
|
|
6357
|
-
fileExists(
|
|
6358
|
-
fileExists(
|
|
7091
|
+
fileExists(join20(projectRoot, "AGENTS.md")),
|
|
7092
|
+
fileExists(join20(projectRoot, "CLAUDE.md"))
|
|
6359
7093
|
]);
|
|
6360
7094
|
return { hasAgentsMd, hasClaudeMd };
|
|
6361
7095
|
}
|
|
6362
7096
|
async function inspectL1BootstrapSnapshotDrift(target) {
|
|
6363
|
-
const abs =
|
|
7097
|
+
const abs = join20(target, ".fabric", "AGENTS.md");
|
|
6364
7098
|
const canonical = resolveBootstrapCanonical();
|
|
6365
7099
|
let onDisk;
|
|
6366
7100
|
try {
|
|
6367
|
-
onDisk = await
|
|
7101
|
+
onDisk = await readFile13(abs, "utf8");
|
|
6368
7102
|
} catch {
|
|
6369
7103
|
return { status: "missing", canonical, onDisk: null };
|
|
6370
7104
|
}
|
|
@@ -6390,17 +7124,17 @@ function createL1BootstrapSnapshotDriftCheck(t, inspection) {
|
|
|
6390
7124
|
);
|
|
6391
7125
|
}
|
|
6392
7126
|
async function inspectL2ManagedBlockDrift(target) {
|
|
6393
|
-
const snapshotPath =
|
|
7127
|
+
const snapshotPath = join20(target, ".fabric", "AGENTS.md");
|
|
6394
7128
|
let snapshot;
|
|
6395
7129
|
try {
|
|
6396
|
-
snapshot = await
|
|
7130
|
+
snapshot = await readFile13(snapshotPath, "utf8");
|
|
6397
7131
|
} catch {
|
|
6398
7132
|
return { status: "ok", drifted: [] };
|
|
6399
7133
|
}
|
|
6400
|
-
const projectRulesPath =
|
|
7134
|
+
const projectRulesPath = join20(target, ".fabric", "project-rules.md");
|
|
6401
7135
|
let expectedBody = snapshot;
|
|
6402
7136
|
try {
|
|
6403
|
-
const projectRules = await
|
|
7137
|
+
const projectRules = await readFile13(projectRulesPath, "utf8");
|
|
6404
7138
|
expectedBody = `${snapshot}
|
|
6405
7139
|
---
|
|
6406
7140
|
${projectRules}`;
|
|
@@ -6409,12 +7143,12 @@ ${projectRules}`;
|
|
|
6409
7143
|
const drifted = [];
|
|
6410
7144
|
let anyManagedBlockFound = false;
|
|
6411
7145
|
const blockTargets = [
|
|
6412
|
-
|
|
7146
|
+
join20(target, "AGENTS.md")
|
|
6413
7147
|
];
|
|
6414
7148
|
for (const abs of blockTargets) {
|
|
6415
7149
|
let content;
|
|
6416
7150
|
try {
|
|
6417
|
-
content = await
|
|
7151
|
+
content = await readFile13(abs, "utf8");
|
|
6418
7152
|
} catch {
|
|
6419
7153
|
continue;
|
|
6420
7154
|
}
|
|
@@ -6437,9 +7171,9 @@ ${projectRules}`;
|
|
|
6437
7171
|
drifted.push({ path: abs, expected: expectedBody, actual: body });
|
|
6438
7172
|
}
|
|
6439
7173
|
}
|
|
6440
|
-
const claudeMdPath =
|
|
7174
|
+
const claudeMdPath = join20(target, "CLAUDE.md");
|
|
6441
7175
|
try {
|
|
6442
|
-
const claudeContent = await
|
|
7176
|
+
const claudeContent = await readFile13(claudeMdPath, "utf8");
|
|
6443
7177
|
anyManagedBlockFound = true;
|
|
6444
7178
|
const lines = claudeContent.split(/\r?\n/u);
|
|
6445
7179
|
const hasAtImport = lines.some((line) => line.trim() === "@.fabric/AGENTS.md");
|
|
@@ -6507,7 +7241,7 @@ import { appendFile as appendFile3 } from "fs/promises";
|
|
|
6507
7241
|
import { minimatch as minimatch2 } from "minimatch";
|
|
6508
7242
|
|
|
6509
7243
|
// src/services/cite-rollup.ts
|
|
6510
|
-
import { readFile as
|
|
7244
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
6511
7245
|
import { createLedgerWriteQueue as createLedgerWriteQueue3 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
6512
7246
|
var citeRollupQueue = createLedgerWriteQueue3();
|
|
6513
7247
|
async function appendCiteRollupRow(projectRoot, row) {
|
|
@@ -6519,7 +7253,7 @@ async function readCiteRollup(projectRoot) {
|
|
|
6519
7253
|
const path = getCiteRollupPath(projectRoot);
|
|
6520
7254
|
let raw;
|
|
6521
7255
|
try {
|
|
6522
|
-
raw = await
|
|
7256
|
+
raw = await readFile14(path, "utf8");
|
|
6523
7257
|
} catch (error) {
|
|
6524
7258
|
if (isNodeError(error) && error.code === "ENOENT") return [];
|
|
6525
7259
|
throw error;
|
|
@@ -7527,13 +8261,13 @@ var DEFAULT_UNDERSEED_NODE_THRESHOLD = 10;
|
|
|
7527
8261
|
var SESSION_HINTS_STALE_DAYS = 7;
|
|
7528
8262
|
var SESSION_HINTS_FILE_PREFIX = "session-hints-";
|
|
7529
8263
|
var SESSION_HINTS_FILE_SUFFIX = ".json";
|
|
7530
|
-
var EDIT_COUNTER_FILE_REL =
|
|
7531
|
-
var HINT_SILENCE_COUNTER_FILE_REL =
|
|
8264
|
+
var EDIT_COUNTER_FILE_REL = posix5.join(".fabric", ".cache", "edit-counter");
|
|
8265
|
+
var HINT_SILENCE_COUNTER_FILE_REL = posix5.join(
|
|
7532
8266
|
".fabric",
|
|
7533
8267
|
".cache",
|
|
7534
8268
|
"hint-silence-counter"
|
|
7535
8269
|
);
|
|
7536
|
-
var
|
|
8270
|
+
var MS_PER_DAY4 = 24 * 60 * 60 * 1e3;
|
|
7537
8271
|
var KNOWLEDGE_CANONICAL_TYPE_DIRS = [
|
|
7538
8272
|
"decisions",
|
|
7539
8273
|
"pitfalls",
|
|
@@ -7581,7 +8315,8 @@ async function runDoctorReport(target) {
|
|
|
7581
8315
|
l2ManagedBlockDrift,
|
|
7582
8316
|
skillRefMirror,
|
|
7583
8317
|
skillTokenBudget,
|
|
7584
|
-
skillDescription
|
|
8318
|
+
skillDescription,
|
|
8319
|
+
retiredReferences
|
|
7585
8320
|
] = await Promise.all([
|
|
7586
8321
|
inspectForensic(projectRoot),
|
|
7587
8322
|
// v2.2 W5 R4 (agents.meta decolo): `inspectMeta` (read co-location
|
|
@@ -7601,7 +8336,10 @@ async function runDoctorReport(target) {
|
|
|
7601
8336
|
inspectL2ManagedBlockDrift(projectRoot),
|
|
7602
8337
|
inspectSkillRefMirror(projectRoot),
|
|
7603
8338
|
inspectSkillTokenBudget(projectRoot),
|
|
7604
|
-
inspectSkillDescription(projectRoot)
|
|
8339
|
+
inspectSkillDescription(projectRoot),
|
|
8340
|
+
// ux-w2-2: registry-driven stale-pointer scan over the agent-consumed
|
|
8341
|
+
// surface (bootstrap + SKILL.md + installed hooks).
|
|
8342
|
+
inspectRetiredReferences(projectRoot)
|
|
7605
8343
|
]);
|
|
7606
8344
|
const citeGoodhart = await inspectCiteGoodhart(projectRoot);
|
|
7607
8345
|
const storeKnowledgeSummaries = await collectStoreKnowledgeSummaries(projectRoot);
|
|
@@ -7622,6 +8360,8 @@ async function runDoctorReport(target) {
|
|
|
7622
8360
|
const lintNow = Date.now();
|
|
7623
8361
|
const lastActiveIndex = await buildLastActiveIndex(projectRoot);
|
|
7624
8362
|
const knowledgeAge = await inspectStoreKnowledgeAge(projectRoot, lintNow, lastActiveIndex);
|
|
8363
|
+
const knowledgePromotion = await inspectStoreKnowledgePromotion(projectRoot);
|
|
8364
|
+
const broadReviewRecheck = await inspectStoreBroadReviewRecheck(projectRoot, lintNow);
|
|
7625
8365
|
const preexistingRootFiles = await inspectPreexistingRootFiles(projectRoot);
|
|
7626
8366
|
const underseedThreshold = await readUnderseedThresholdFromConfig(projectRoot);
|
|
7627
8367
|
const underseeded = {
|
|
@@ -7633,7 +8373,6 @@ async function runDoctorReport(target) {
|
|
|
7633
8373
|
const hookCacheWritability = await inspectHookCacheWritability(projectRoot);
|
|
7634
8374
|
const staleServeLock = inspectStaleServeLock(projectRoot, lintNow);
|
|
7635
8375
|
const skillMdYamlInvalid = await inspectSkillMdYamlInvalid(projectRoot);
|
|
7636
|
-
const routerChainRef = await inspectRouterChainRef(projectRoot);
|
|
7637
8376
|
const onboardCoverage = await inspectOnboardCoverage(projectRoot);
|
|
7638
8377
|
const [hooksWired, hooksRuntime, hooksContentDrift] = await Promise.all([
|
|
7639
8378
|
inspectHooksWired(projectRoot),
|
|
@@ -7644,7 +8383,7 @@ async function runDoctorReport(target) {
|
|
|
7644
8383
|
const globalCliVersion = process.env.VITEST === "true" ? { status: "ok", version: "test-skipped" } : inspectGlobalCliVersion();
|
|
7645
8384
|
const targetFiles = Object.fromEntries(
|
|
7646
8385
|
await Promise.all(
|
|
7647
|
-
TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(
|
|
8386
|
+
TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(join21(projectRoot, path))])
|
|
7648
8387
|
)
|
|
7649
8388
|
);
|
|
7650
8389
|
const checks = [
|
|
@@ -7683,6 +8422,8 @@ async function runDoctorReport(target) {
|
|
|
7683
8422
|
createSkillRefMirrorCheck(t, skillRefMirror),
|
|
7684
8423
|
createSkillTokenBudgetCheck(t, skillTokenBudget),
|
|
7685
8424
|
createSkillDescriptionCheck(t, skillDescription),
|
|
8425
|
+
// ux-w2-2: retired-reference (stale-pointer) lint — registry-driven.
|
|
8426
|
+
createRetiredReferenceCheck(t, retiredReferences),
|
|
7686
8427
|
createCiteGoodhartCheck(t, citeGoodhart),
|
|
7687
8428
|
createDraftBacklogCheck(t, draftBacklog),
|
|
7688
8429
|
createKnowledgeTagsEmptyCheck(t, knowledgeTagsEmpty),
|
|
@@ -7710,9 +8451,6 @@ async function runDoctorReport(target) {
|
|
|
7710
8451
|
// rc.12 lint #29: skill_md_yaml_invalid. Warning kind — surfaces
|
|
7711
8452
|
// SKILL.md frontmatter that Codex CLI silently drops at load.
|
|
7712
8453
|
createSkillMdYamlInvalidCheck(t, skillMdYamlInvalid),
|
|
7713
|
-
// B2 skill-router (A4): S_CHAIN reference backstop. Warning kind — flags an
|
|
7714
|
-
// S_CHAIN `fabric-*` reference to a leaf no longer in the install set.
|
|
7715
|
-
createRouterChainRefCheck(t, routerChainRef),
|
|
7716
8454
|
// v2.0.0-rc.23 TASK-014 (F8c): Onboard coverage advisory. Info kind.
|
|
7717
8455
|
// Surfaces uncovered S5 onboard slots and recommends /fabric-archive
|
|
7718
8456
|
// first-run phase. Sits adjacent to Skill markdown YAML — both are
|
|
@@ -7764,6 +8502,11 @@ async function runDoctorReport(target) {
|
|
|
7764
8502
|
// thresholds 90/30/14d per maturity tier (KT-DEC-0008).
|
|
7765
8503
|
createOrphanDemoteCheck(t, knowledgeAge.orphanDemote),
|
|
7766
8504
|
createStaleArchiveCheck(t, knowledgeAge.staleArchive),
|
|
8505
|
+
// v2.2 C1: knowledge promotion candidate (info kind — opportunity, not defect).
|
|
8506
|
+
createPromotionCandidateCheck(t, knowledgePromotion),
|
|
8507
|
+
// v2.2 C1: broad review-recheck nudge (info kind — broad's review-clock
|
|
8508
|
+
// counterpart to the usage-age decay it is exempt from).
|
|
8509
|
+
createBroadReviewRecheckCheck(t, broadReviewRecheck),
|
|
7767
8510
|
// project-scope binding backfill lint — a store bound as the write target
|
|
7768
8511
|
// but with no project_id / active_project parks the project axis. The
|
|
7769
8512
|
// fresh-install hole is sealed in store.stage.ts; this covers existing repos
|
|
@@ -7844,7 +8587,7 @@ async function runDoctorFix(target) {
|
|
|
7844
8587
|
const fixed = [];
|
|
7845
8588
|
const ledgerWarnings = [];
|
|
7846
8589
|
if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
|
|
7847
|
-
const snapshotPath =
|
|
8590
|
+
const snapshotPath = join21(projectRoot, ".fabric", "AGENTS.md");
|
|
7848
8591
|
await ensureParentDirectory(snapshotPath);
|
|
7849
8592
|
await atomicWriteText4(snapshotPath, resolveBootstrapCanonical2());
|
|
7850
8593
|
fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
|
|
@@ -7917,9 +8660,9 @@ async function runDoctorFix(target) {
|
|
|
7917
8660
|
if (before.infos.some((issue) => issue.code === "stale_serve_lock")) {
|
|
7918
8661
|
const lockInspection = inspectStaleServeLock(projectRoot, Date.now());
|
|
7919
8662
|
if (lockInspection.present && !lockInspection.pidAlive) {
|
|
7920
|
-
const lockFilePath =
|
|
8663
|
+
const lockFilePath = join21(projectRoot, ".fabric", ".serve.lock");
|
|
7921
8664
|
try {
|
|
7922
|
-
await
|
|
8665
|
+
await unlink4(lockFilePath);
|
|
7923
8666
|
} catch (err) {
|
|
7924
8667
|
const errno = err;
|
|
7925
8668
|
if (errno.code !== "ENOENT") throw err;
|
|
@@ -8034,10 +8777,10 @@ function createApplyLintMessage(succeeded, failed, manualErrorCount) {
|
|
|
8034
8777
|
}
|
|
8035
8778
|
async function applySessionHintsStaleCleanup(projectRoot, candidate) {
|
|
8036
8779
|
const detail = `deleted (${candidate.age_days}d old)`;
|
|
8037
|
-
const absPath =
|
|
8780
|
+
const absPath = join21(projectRoot, candidate.path);
|
|
8038
8781
|
try {
|
|
8039
|
-
const { unlink:
|
|
8040
|
-
await
|
|
8782
|
+
const { unlink: unlink5 } = await import("fs/promises");
|
|
8783
|
+
await unlink5(absPath);
|
|
8041
8784
|
return {
|
|
8042
8785
|
kind: "knowledge_session_hints_stale_cleanup",
|
|
8043
8786
|
path: candidate.path,
|
|
@@ -8059,9 +8802,9 @@ function truncateErrorMessage(error) {
|
|
|
8059
8802
|
return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
|
|
8060
8803
|
}
|
|
8061
8804
|
async function inspectForensic(projectRoot) {
|
|
8062
|
-
const path =
|
|
8805
|
+
const path = join21(projectRoot, ".fabric", "forensic.json");
|
|
8063
8806
|
try {
|
|
8064
|
-
const parsed = forensicReportSchema.parse(JSON.parse(await
|
|
8807
|
+
const parsed = forensicReportSchema.parse(JSON.parse(await readFile16(path, "utf8")));
|
|
8065
8808
|
return { present: true, valid: true, report: parsed };
|
|
8066
8809
|
} catch (error) {
|
|
8067
8810
|
if (isMissingFileError(error)) {
|
|
@@ -8091,7 +8834,7 @@ async function inspectEventLedger(projectRoot) {
|
|
|
8091
8834
|
try {
|
|
8092
8835
|
await access4(path, constants2.W_OK);
|
|
8093
8836
|
const { warnings } = await readEventLedger(projectRoot);
|
|
8094
|
-
const raw = await
|
|
8837
|
+
const raw = await readFile16(path, "utf8");
|
|
8095
8838
|
const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
|
|
8096
8839
|
const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
|
|
8097
8840
|
const schemaVersionSamples = [];
|
|
@@ -8636,7 +9379,7 @@ async function inspectPreexistingRootFiles(projectRoot) {
|
|
|
8636
9379
|
const candidates = ["CLAUDE.md", "AGENTS.md"];
|
|
8637
9380
|
const detected = [];
|
|
8638
9381
|
for (const name of candidates) {
|
|
8639
|
-
if (await pathExists(
|
|
9382
|
+
if (await pathExists(join21(projectRoot, name))) {
|
|
8640
9383
|
detected.push(name);
|
|
8641
9384
|
}
|
|
8642
9385
|
}
|
|
@@ -8721,7 +9464,7 @@ async function buildLastActiveIndex(projectRoot) {
|
|
|
8721
9464
|
return map;
|
|
8722
9465
|
}
|
|
8723
9466
|
async function inspectSessionHintsStale(projectRoot, now) {
|
|
8724
|
-
const cacheDir =
|
|
9467
|
+
const cacheDir = join21(projectRoot, ".fabric", ".cache");
|
|
8725
9468
|
let entries;
|
|
8726
9469
|
try {
|
|
8727
9470
|
entries = await readdirAsync(cacheDir, { withFileTypes: true });
|
|
@@ -8733,17 +9476,17 @@ async function inspectSessionHintsStale(projectRoot, now) {
|
|
|
8733
9476
|
if (!entry.isFile()) continue;
|
|
8734
9477
|
if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
|
|
8735
9478
|
if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
|
|
8736
|
-
const absPath =
|
|
9479
|
+
const absPath = join21(cacheDir, entry.name);
|
|
8737
9480
|
let mtimeMs = 0;
|
|
8738
9481
|
try {
|
|
8739
9482
|
mtimeMs = (await statAsync(absPath)).mtimeMs;
|
|
8740
9483
|
} catch {
|
|
8741
9484
|
continue;
|
|
8742
9485
|
}
|
|
8743
|
-
const ageDays = Math.floor((now - mtimeMs) /
|
|
9486
|
+
const ageDays = Math.floor((now - mtimeMs) / MS_PER_DAY4);
|
|
8744
9487
|
if (ageDays < SESSION_HINTS_STALE_DAYS) continue;
|
|
8745
9488
|
candidates.push({
|
|
8746
|
-
path:
|
|
9489
|
+
path: posix5.join(".fabric", ".cache", entry.name),
|
|
8747
9490
|
age_days: ageDays
|
|
8748
9491
|
});
|
|
8749
9492
|
}
|
|
@@ -8765,9 +9508,9 @@ function inspectStaleServeLock(projectRoot, now) {
|
|
|
8765
9508
|
};
|
|
8766
9509
|
}
|
|
8767
9510
|
async function readUnderseedThresholdFromConfig(projectRoot) {
|
|
8768
|
-
const configPath =
|
|
9511
|
+
const configPath = join21(projectRoot, ".fabric", "fabric-config.json");
|
|
8769
9512
|
try {
|
|
8770
|
-
const raw = await
|
|
9513
|
+
const raw = await readFile16(configPath, "utf8");
|
|
8771
9514
|
const parsed = JSON.parse(raw);
|
|
8772
9515
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
8773
9516
|
const v = parsed.underseed_node_threshold;
|
|
@@ -8841,7 +9584,7 @@ function createStaleServeLockCheck(t, inspection) {
|
|
|
8841
9584
|
})
|
|
8842
9585
|
);
|
|
8843
9586
|
}
|
|
8844
|
-
const days = Math.floor(inspection.ageMs /
|
|
9587
|
+
const days = Math.floor(inspection.ageMs / MS_PER_DAY4);
|
|
8845
9588
|
const hours = Math.floor(inspection.ageMs / (60 * 60 * 1e3));
|
|
8846
9589
|
const acquiredAgo = days >= 1 ? t(`doctor.check.stale_serve_lock.age.day.${days === 1 ? "singular" : "plural"}`, {
|
|
8847
9590
|
count: String(days)
|
|
@@ -8883,10 +9626,10 @@ async function inspectOnboardCoverage(projectRoot) {
|
|
|
8883
9626
|
return { filled, missing, opted_out: optedOut };
|
|
8884
9627
|
}
|
|
8885
9628
|
async function readOnboardOptedOut(projectRoot) {
|
|
8886
|
-
const path =
|
|
9629
|
+
const path = join21(projectRoot, ".fabric", "fabric-config.json");
|
|
8887
9630
|
let raw;
|
|
8888
9631
|
try {
|
|
8889
|
-
raw = await
|
|
9632
|
+
raw = await readFile16(path, "utf8");
|
|
8890
9633
|
} catch {
|
|
8891
9634
|
return [];
|
|
8892
9635
|
}
|
|
@@ -8971,7 +9714,7 @@ async function* iterateCanonicalFilenames(projectRoot) {
|
|
|
8971
9714
|
if (!KNOWLEDGE_CANONICAL_TYPE_DIRS.includes(entry.type)) {
|
|
8972
9715
|
continue;
|
|
8973
9716
|
}
|
|
8974
|
-
const filename =
|
|
9717
|
+
const filename = posix5.basename(normalizePath2(entry.file));
|
|
8975
9718
|
const parsed = parseStableIdFromCanonicalFilename(filename);
|
|
8976
9719
|
if (parsed === null) {
|
|
8977
9720
|
continue;
|
|
@@ -8988,22 +9731,22 @@ async function* iterateCanonicalFilenames(projectRoot) {
|
|
|
8988
9731
|
}
|
|
8989
9732
|
}
|
|
8990
9733
|
async function rewriteThreeEndManagedBlocks(projectRoot) {
|
|
8991
|
-
const snapshotPath =
|
|
9734
|
+
const snapshotPath = join21(projectRoot, ".fabric", "AGENTS.md");
|
|
8992
9735
|
if (!await pathExists(snapshotPath)) {
|
|
8993
9736
|
return;
|
|
8994
9737
|
}
|
|
8995
9738
|
let snapshot;
|
|
8996
9739
|
try {
|
|
8997
|
-
snapshot = await
|
|
9740
|
+
snapshot = await readFile16(snapshotPath, "utf8");
|
|
8998
9741
|
} catch {
|
|
8999
9742
|
return;
|
|
9000
9743
|
}
|
|
9001
|
-
const projectRulesPath =
|
|
9744
|
+
const projectRulesPath = join21(projectRoot, ".fabric", "project-rules.md");
|
|
9002
9745
|
const hasProjectRules = await pathExists(projectRulesPath);
|
|
9003
9746
|
let expectedBody = snapshot;
|
|
9004
9747
|
if (hasProjectRules) {
|
|
9005
9748
|
try {
|
|
9006
|
-
const projectRules = await
|
|
9749
|
+
const projectRules = await readFile16(projectRulesPath, "utf8");
|
|
9007
9750
|
expectedBody = `${snapshot}
|
|
9008
9751
|
---
|
|
9009
9752
|
${projectRules}`;
|
|
@@ -9014,7 +9757,7 @@ ${projectRules}`;
|
|
|
9014
9757
|
${expectedBody}
|
|
9015
9758
|
${BOOTSTRAP_MARKER_END2}`;
|
|
9016
9759
|
const blockTargets = [
|
|
9017
|
-
|
|
9760
|
+
join21(projectRoot, "AGENTS.md")
|
|
9018
9761
|
];
|
|
9019
9762
|
for (const abs of blockTargets) {
|
|
9020
9763
|
if (!await pathExists(abs)) {
|
|
@@ -9022,7 +9765,7 @@ ${BOOTSTRAP_MARKER_END2}`;
|
|
|
9022
9765
|
}
|
|
9023
9766
|
let existing;
|
|
9024
9767
|
try {
|
|
9025
|
-
existing = await
|
|
9768
|
+
existing = await readFile16(abs, "utf8");
|
|
9026
9769
|
} catch {
|
|
9027
9770
|
continue;
|
|
9028
9771
|
}
|
|
@@ -9047,11 +9790,11 @@ ${managedBlock}
|
|
|
9047
9790
|
}
|
|
9048
9791
|
await atomicWriteText4(abs, next);
|
|
9049
9792
|
}
|
|
9050
|
-
const claudeMdPath =
|
|
9793
|
+
const claudeMdPath = join21(projectRoot, "CLAUDE.md");
|
|
9051
9794
|
if (await pathExists(claudeMdPath)) {
|
|
9052
9795
|
let claudeContent;
|
|
9053
9796
|
try {
|
|
9054
|
-
claudeContent = await
|
|
9797
|
+
claudeContent = await readFile16(claudeMdPath, "utf8");
|
|
9055
9798
|
} catch {
|
|
9056
9799
|
return;
|
|
9057
9800
|
}
|
|
@@ -9097,7 +9840,7 @@ function normalizeTarget(targetInput) {
|
|
|
9097
9840
|
return isAbsolute2(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
|
|
9098
9841
|
}
|
|
9099
9842
|
function normalizePath2(path) {
|
|
9100
|
-
return
|
|
9843
|
+
return posix5.normalize(path.split("\\").join("/"));
|
|
9101
9844
|
}
|
|
9102
9845
|
async function collectEntryPoints(root) {
|
|
9103
9846
|
let rootStat;
|
|
@@ -9117,7 +9860,7 @@ async function collectEntryPoints(root) {
|
|
|
9117
9860
|
continue;
|
|
9118
9861
|
}
|
|
9119
9862
|
for (const entry of await readdirAsync(current, { withFileTypes: true })) {
|
|
9120
|
-
const absolutePath =
|
|
9863
|
+
const absolutePath = join21(current, entry.name);
|
|
9121
9864
|
const relativePath = normalizePath2(absolutePath.slice(root.length + 1));
|
|
9122
9865
|
if (relativePath.length === 0) {
|
|
9123
9866
|
continue;
|
|
@@ -9144,8 +9887,8 @@ function getEntryPointReason(relativePath) {
|
|
|
9144
9887
|
if (!SCRIPT_EXTENSIONS.has(extension)) {
|
|
9145
9888
|
return null;
|
|
9146
9889
|
}
|
|
9147
|
-
const directory =
|
|
9148
|
-
const fileName =
|
|
9890
|
+
const directory = posix5.dirname(relativePath);
|
|
9891
|
+
const fileName = posix5.basename(relativePath);
|
|
9149
9892
|
const fileBase = fileName.slice(0, Math.max(fileName.lastIndexOf("."), 0));
|
|
9150
9893
|
if (directory === "assets/scripts" || directory === "scripts") {
|
|
9151
9894
|
return "top-level script";
|
|
@@ -9193,7 +9936,7 @@ async function enrichDescriptions(projectRoot, opts = {}) {
|
|
|
9193
9936
|
scanned += 1;
|
|
9194
9937
|
let source;
|
|
9195
9938
|
try {
|
|
9196
|
-
source = await
|
|
9939
|
+
source = await readFile16(absPath, "utf8");
|
|
9197
9940
|
} catch {
|
|
9198
9941
|
continue;
|
|
9199
9942
|
}
|
|
@@ -9290,88 +10033,6 @@ function yamlQuoteIfNeeded(value) {
|
|
|
9290
10033
|
return value;
|
|
9291
10034
|
}
|
|
9292
10035
|
|
|
9293
|
-
// src/services/conflict-lint.ts
|
|
9294
|
-
var DEFAULT_CONFLICT_SIMILARITY_THRESHOLD = 0.5;
|
|
9295
|
-
function groupKey(entry) {
|
|
9296
|
-
return `${entry.layer}\0${entry.knowledge_type}`;
|
|
9297
|
-
}
|
|
9298
|
-
function pairSimilarity(model, a, b) {
|
|
9299
|
-
const selfA = model.scoreDoc(a.id, a.tokens);
|
|
9300
|
-
const selfB = model.scoreDoc(b.id, b.tokens);
|
|
9301
|
-
if (selfA <= 0 || selfB <= 0) return 0;
|
|
9302
|
-
const aToB = model.scoreDoc(b.id, a.tokens) / selfB;
|
|
9303
|
-
const bToA = model.scoreDoc(a.id, b.tokens) / selfA;
|
|
9304
|
-
const sim = Math.min(aToB, bToA);
|
|
9305
|
-
return sim < 0 ? 0 : sim > 1 ? 1 : sim;
|
|
9306
|
-
}
|
|
9307
|
-
function findConflictCandidates(entries, opts = {}) {
|
|
9308
|
-
const threshold = typeof opts.threshold === "number" && opts.threshold >= 0 && opts.threshold <= 1 ? opts.threshold : DEFAULT_CONFLICT_SIMILARITY_THRESHOLD;
|
|
9309
|
-
const groups = /* @__PURE__ */ new Map();
|
|
9310
|
-
for (const entry of entries) {
|
|
9311
|
-
if (typeof entry.stable_id !== "string" || entry.stable_id.length === 0) continue;
|
|
9312
|
-
const list = groups.get(groupKey(entry)) ?? [];
|
|
9313
|
-
list.push(entry);
|
|
9314
|
-
groups.set(groupKey(entry), list);
|
|
9315
|
-
}
|
|
9316
|
-
const pairs = [];
|
|
9317
|
-
for (const group of groups.values()) {
|
|
9318
|
-
if (group.length < 2) continue;
|
|
9319
|
-
const docs = group.map((e) => ({ id: e.stable_id, tokens: buildQueryTerms(e.text) }));
|
|
9320
|
-
const tokensById = new Map(docs.map((d) => [d.id, d.tokens]));
|
|
9321
|
-
const model = buildBm25Model(docs);
|
|
9322
|
-
for (let i = 0; i < group.length; i += 1) {
|
|
9323
|
-
for (let j = i + 1; j < group.length; j += 1) {
|
|
9324
|
-
const ea = group[i];
|
|
9325
|
-
const eb = group[j];
|
|
9326
|
-
const sim = pairSimilarity(
|
|
9327
|
-
model,
|
|
9328
|
-
{ id: ea.stable_id, tokens: tokensById.get(ea.stable_id) ?? [] },
|
|
9329
|
-
{ id: eb.stable_id, tokens: tokensById.get(eb.stable_id) ?? [] }
|
|
9330
|
-
);
|
|
9331
|
-
if (sim < threshold) continue;
|
|
9332
|
-
const [a, b] = ea.stable_id <= eb.stable_id ? [ea.stable_id, eb.stable_id] : [eb.stable_id, ea.stable_id];
|
|
9333
|
-
pairs.push({
|
|
9334
|
-
a,
|
|
9335
|
-
b,
|
|
9336
|
-
knowledge_type: ea.knowledge_type,
|
|
9337
|
-
layer: ea.layer,
|
|
9338
|
-
similarity: sim,
|
|
9339
|
-
verdict: "unknown"
|
|
9340
|
-
});
|
|
9341
|
-
}
|
|
9342
|
-
}
|
|
9343
|
-
}
|
|
9344
|
-
pairs.sort((x, y) => y.similarity - x.similarity || (x.a < y.a ? -1 : x.a > y.a ? 1 : x.b < y.b ? -1 : 1));
|
|
9345
|
-
return pairs;
|
|
9346
|
-
}
|
|
9347
|
-
async function lintConflicts(entries, opts = {}) {
|
|
9348
|
-
const candidates = findConflictCandidates(entries, { threshold: opts.threshold });
|
|
9349
|
-
if (opts.judge === void 0 || candidates.length === 0) {
|
|
9350
|
-
return candidates;
|
|
9351
|
-
}
|
|
9352
|
-
const byId = new Map(entries.map((e) => [e.stable_id, e]));
|
|
9353
|
-
const judged = [];
|
|
9354
|
-
for (const pair of candidates) {
|
|
9355
|
-
const ea = byId.get(pair.a);
|
|
9356
|
-
const eb = byId.get(pair.b);
|
|
9357
|
-
if (ea === void 0 || eb === void 0) {
|
|
9358
|
-
judged.push(pair);
|
|
9359
|
-
continue;
|
|
9360
|
-
}
|
|
9361
|
-
try {
|
|
9362
|
-
const verdict = await opts.judge(ea, eb);
|
|
9363
|
-
judged.push({
|
|
9364
|
-
...pair,
|
|
9365
|
-
verdict: verdict.isConflict ? "conflict" : "similar",
|
|
9366
|
-
rationale: verdict.rationale
|
|
9367
|
-
});
|
|
9368
|
-
} catch {
|
|
9369
|
-
judged.push(pair);
|
|
9370
|
-
}
|
|
9371
|
-
}
|
|
9372
|
-
return judged;
|
|
9373
|
-
}
|
|
9374
|
-
|
|
9375
10036
|
// src/services/doctor-conflict.ts
|
|
9376
10037
|
function stripFrontmatter(content) {
|
|
9377
10038
|
if (!content.startsWith("---")) return content;
|
|
@@ -9412,6 +10073,91 @@ async function runDoctorConflictLint(projectRoot, opts = {}) {
|
|
|
9412
10073
|
};
|
|
9413
10074
|
}
|
|
9414
10075
|
|
|
10076
|
+
// src/services/why-not-surfaced.ts
|
|
10077
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
10078
|
+
import { basename as basename3, join as join22 } from "path";
|
|
10079
|
+
import {
|
|
10080
|
+
buildStoreResolveInput as buildStoreResolveInput7,
|
|
10081
|
+
createStoreResolver as createStoreResolver7,
|
|
10082
|
+
loadProjectConfig as loadProjectConfig4,
|
|
10083
|
+
readKnowledgeAcrossStores as readKnowledgeAcrossStores4,
|
|
10084
|
+
resolveGlobalRoot as resolveGlobalRoot7,
|
|
10085
|
+
scopeRoot as scopeRoot3,
|
|
10086
|
+
storeRelativePathForMount as storeRelativePathForMount6
|
|
10087
|
+
} from "@fenglimg/fabric-shared";
|
|
10088
|
+
var SEMANTIC_SCOPE_LINE3 = /^semantic_scope:\s*"?([^"\n]+?)"?\s*$/mu;
|
|
10089
|
+
var RELEVANCE_SCOPE_LINE = /^relevance_scope:\s*"?(broad|narrow)"?\s*$/mu;
|
|
10090
|
+
function toLocalId3(query) {
|
|
10091
|
+
const i = query.indexOf(":");
|
|
10092
|
+
return i === -1 ? query : query.slice(i + 1);
|
|
10093
|
+
}
|
|
10094
|
+
function fileMatchesId(file, localId) {
|
|
10095
|
+
const base = basename3(file);
|
|
10096
|
+
return base === `${localId}.md` || base.startsWith(`${localId}--`);
|
|
10097
|
+
}
|
|
10098
|
+
async function explainWhyNotSurfaced(projectRoot, query) {
|
|
10099
|
+
const localId = toLocalId3(query.trim());
|
|
10100
|
+
const base = {
|
|
10101
|
+
query,
|
|
10102
|
+
localId,
|
|
10103
|
+
verdict: "not_found",
|
|
10104
|
+
storeAlias: null,
|
|
10105
|
+
storeBound: null,
|
|
10106
|
+
semanticScope: null,
|
|
10107
|
+
activeProject: null,
|
|
10108
|
+
relevanceScope: null
|
|
10109
|
+
};
|
|
10110
|
+
const input = buildStoreResolveInput7(projectRoot);
|
|
10111
|
+
if (input === null) {
|
|
10112
|
+
return base;
|
|
10113
|
+
}
|
|
10114
|
+
const globalRoot = resolveGlobalRoot7();
|
|
10115
|
+
const allStores = input.mountedStores.map((s) => ({
|
|
10116
|
+
store_uuid: s.store_uuid,
|
|
10117
|
+
alias: s.alias,
|
|
10118
|
+
dir: join22(globalRoot, storeRelativePathForMount6(s))
|
|
10119
|
+
}));
|
|
10120
|
+
const refs = await readKnowledgeAcrossStores4(allStores);
|
|
10121
|
+
const candidate = refs.find((ref) => fileMatchesId(ref.file, localId));
|
|
10122
|
+
if (candidate === void 0) {
|
|
10123
|
+
return base;
|
|
10124
|
+
}
|
|
10125
|
+
let source;
|
|
10126
|
+
try {
|
|
10127
|
+
source = await readFile17(candidate.file, "utf8");
|
|
10128
|
+
} catch {
|
|
10129
|
+
return base;
|
|
10130
|
+
}
|
|
10131
|
+
if (deriveRuleIdentity(candidate.file, source, void 0).stableId !== localId) {
|
|
10132
|
+
return base;
|
|
10133
|
+
}
|
|
10134
|
+
const semanticScope = SEMANTIC_SCOPE_LINE3.exec(source)?.[1] ?? null;
|
|
10135
|
+
const relevanceScope = RELEVANCE_SCOPE_LINE.exec(source)?.[1] ?? "broad";
|
|
10136
|
+
const activeProject = loadProjectConfig4(projectRoot)?.active_project ?? null;
|
|
10137
|
+
const boundUuids = new Set(
|
|
10138
|
+
createStoreResolver7().resolveReadSet(input).stores.map((s) => s.store_uuid)
|
|
10139
|
+
);
|
|
10140
|
+
const storeBound = boundUuids.has(candidate.store_uuid);
|
|
10141
|
+
const found = {
|
|
10142
|
+
...base,
|
|
10143
|
+
storeAlias: candidate.alias,
|
|
10144
|
+
storeBound,
|
|
10145
|
+
semanticScope,
|
|
10146
|
+
activeProject,
|
|
10147
|
+
relevanceScope
|
|
10148
|
+
};
|
|
10149
|
+
if (!storeBound) {
|
|
10150
|
+
return { ...found, verdict: "store_unbound" };
|
|
10151
|
+
}
|
|
10152
|
+
if (semanticScope !== null && scopeRoot3(semanticScope) === "project" && activeProject !== null && semanticScope !== `project:${activeProject}`) {
|
|
10153
|
+
return { ...found, verdict: "project_mismatch" };
|
|
10154
|
+
}
|
|
10155
|
+
if (relevanceScope === "narrow") {
|
|
10156
|
+
return { ...found, verdict: "narrow_timing" };
|
|
10157
|
+
}
|
|
10158
|
+
return { ...found, verdict: "should_surface" };
|
|
10159
|
+
}
|
|
10160
|
+
|
|
9415
10161
|
// src/services/summary-cold-eval.ts
|
|
9416
10162
|
var COLD_EVAL_RUBRIC = [
|
|
9417
10163
|
"You are a ZERO-CONTEXT judge. You are shown ONLY a one-line knowledge summary \u2014",
|
|
@@ -9465,8 +10211,8 @@ import { agentsMetaSchema as agentsMetaSchema3 } from "@fenglimg/fabric-shared";
|
|
|
9465
10211
|
import { IOFabricError as IOFabricError2, RuleError } from "@fenglimg/fabric-shared/errors";
|
|
9466
10212
|
|
|
9467
10213
|
// src/services/read-ledger.ts
|
|
9468
|
-
import { randomUUID as
|
|
9469
|
-
import { access as access5, copyFile, readFile as
|
|
10214
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
10215
|
+
import { access as access5, copyFile, readFile as readFile18, rm } from "fs/promises";
|
|
9470
10216
|
import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
|
|
9471
10217
|
async function resolveLedgerPaths(projectRoot) {
|
|
9472
10218
|
const primaryPath = getLedgerPath(projectRoot);
|
|
@@ -9494,7 +10240,7 @@ async function readLegacyLedger(projectRoot) {
|
|
|
9494
10240
|
const { readPath } = await resolveLedgerPaths(projectRoot);
|
|
9495
10241
|
let raw;
|
|
9496
10242
|
try {
|
|
9497
|
-
raw = await
|
|
10243
|
+
raw = await readFile18(readPath, "utf8");
|
|
9498
10244
|
} catch (error) {
|
|
9499
10245
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
9500
10246
|
return [];
|
|
@@ -9749,26 +10495,27 @@ function formatError(error) {
|
|
|
9749
10495
|
}
|
|
9750
10496
|
function formatPreexistingRootMessage(projectRoot) {
|
|
9751
10497
|
const preexisting = [];
|
|
9752
|
-
if (
|
|
9753
|
-
if (
|
|
10498
|
+
if (existsSync9(join23(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
|
|
10499
|
+
if (existsSync9(join23(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
|
|
9754
10500
|
if (preexisting.length === 0) return null;
|
|
9755
10501
|
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.`;
|
|
9756
10502
|
}
|
|
9757
10503
|
var FABRIC_SERVER_INSTRUCTIONS = [
|
|
9758
10504
|
"Fabric is a cross-client knowledge layer: durable team/personal decisions, pitfalls, guidelines, models, and processes this server surfaces so you do not re-learn them each session.",
|
|
9759
10505
|
"",
|
|
10506
|
+
"AGENT-DIRECT tools \u2014 call these yourself, inline, as you work:",
|
|
9760
10507
|
"Retrieval \u2014 do this BEFORE you edit code or commit to a decision:",
|
|
9761
|
-
"-
|
|
9762
|
-
"- It does NOT return bodies. To load an entry's full content, Read
|
|
10508
|
+
"- `fab_recall(paths)` \u2014 one-shot KB recall for the files you are about to touch. Returns a single ranked `entries[]` \u2014 each entry carries its DESCRIPTION, a `read_path` (the on-disk knowledge file), `rank` (1 = most relevant), and `body_in_context:true` when the body is already in context from SessionStart.",
|
|
10509
|
+
"- It does NOT return bodies. To load an entry's full content, Read its `read_path` (native file read) \u2014 that is observed as a `knowledge_body_read`. Reading on demand keeps the index lean; a needed body is one cheap Read away.",
|
|
9763
10510
|
"",
|
|
9764
|
-
"
|
|
9765
|
-
"- `
|
|
9766
|
-
"- `fab_extract_knowledge` \u2014 extract structured knowledge from text you supply.",
|
|
10511
|
+
"SKILL-DRIVEN tools \u2014 invoked by the Fabric skills (fabric-archive / fabric-review) at the right lifecycle moment, not called ad-hoc:",
|
|
10512
|
+
"- `fab_propose` \u2014 propose/persist a pending knowledge entry into the write-target store for later review.",
|
|
9767
10513
|
"- `fab_archive_scan` \u2014 scan recent work for archive-worthy knowledge candidates.",
|
|
9768
|
-
"- `
|
|
10514
|
+
"- `fab_pending` \u2014 read-only browse/search of pending + canonical knowledge (list / search).",
|
|
10515
|
+
"- `fab_review` \u2014 write-only triage of pending knowledge entries (approve / reject / modify / defer).",
|
|
9769
10516
|
"",
|
|
9770
10517
|
"Conventions:",
|
|
9771
|
-
"- Candidate lists are ranked best-first (content relevance) and bounded; `
|
|
10518
|
+
"- Candidate lists are ranked best-first (content relevance) and bounded; a non-empty `dropped[]` (each entry `{ id, reason }`, reason `retrieval_budget` | `payload_budget`) means more exist \u2014 narrow your intent to surface them.",
|
|
9772
10519
|
"- Pass the client `session_id` to `fab_recall` so cross-session knowledge-debt tracking stays accurate.",
|
|
9773
10520
|
"- Cite the KB id you applied or dismissed before edits, per the project's cite policy."
|
|
9774
10521
|
].join("\n");
|
|
@@ -9776,7 +10523,7 @@ function createFabricServer(tracker) {
|
|
|
9776
10523
|
const server = new McpServer(
|
|
9777
10524
|
{
|
|
9778
10525
|
name: "fabric-knowledge-server",
|
|
9779
|
-
version: "2.
|
|
10526
|
+
version: "2.3.0-rc.1"
|
|
9780
10527
|
},
|
|
9781
10528
|
{
|
|
9782
10529
|
instructions: FABRIC_SERVER_INSTRUCTIONS
|
|
@@ -9786,6 +10533,7 @@ function createFabricServer(tracker) {
|
|
|
9786
10533
|
registerArchiveScan(server, tracker);
|
|
9787
10534
|
registerExtractKnowledge(server, tracker);
|
|
9788
10535
|
registerReview(server, tracker);
|
|
10536
|
+
registerPending(server, tracker);
|
|
9789
10537
|
server.registerResource(
|
|
9790
10538
|
"bootstrap README",
|
|
9791
10539
|
AGENTS_MD_RESOURCE_URI,
|
|
@@ -9795,10 +10543,10 @@ function createFabricServer(tracker) {
|
|
|
9795
10543
|
},
|
|
9796
10544
|
async (_uri) => {
|
|
9797
10545
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
9798
|
-
const path =
|
|
10546
|
+
const path = join23(projectRoot, ".fabric", "bootstrap", "README.md");
|
|
9799
10547
|
let text = "";
|
|
9800
|
-
if (
|
|
9801
|
-
text = await
|
|
10548
|
+
if (existsSync9(path)) {
|
|
10549
|
+
text = await readFile19(path, "utf8");
|
|
9802
10550
|
}
|
|
9803
10551
|
return {
|
|
9804
10552
|
contents: [
|
|
@@ -9896,6 +10644,7 @@ export {
|
|
|
9896
10644
|
LEGACY_LEDGER_PATH,
|
|
9897
10645
|
METRICS_LEDGER_PATH,
|
|
9898
10646
|
METRIC_COUNTER_NAMES,
|
|
10647
|
+
RETIRED_TOKENS,
|
|
9899
10648
|
appendEventLedgerEvent,
|
|
9900
10649
|
buildAlwaysActiveBodies,
|
|
9901
10650
|
buildColdEvalBatch,
|
|
@@ -9908,6 +10657,7 @@ export {
|
|
|
9908
10657
|
detectUnboundProject,
|
|
9909
10658
|
drainCounters,
|
|
9910
10659
|
enrichDescriptions,
|
|
10660
|
+
explainWhyNotSurfaced,
|
|
9911
10661
|
extractKnowledge,
|
|
9912
10662
|
findConflictCandidates,
|
|
9913
10663
|
flushAndSyncEventLedger,
|
|
@@ -9917,6 +10667,7 @@ export {
|
|
|
9917
10667
|
getLedgerPath,
|
|
9918
10668
|
getLegacyLedgerPath,
|
|
9919
10669
|
getMetricsLedgerPath,
|
|
10670
|
+
inspectRetiredReferences,
|
|
9920
10671
|
lintConflicts,
|
|
9921
10672
|
loadConflictEntries,
|
|
9922
10673
|
pairSimilarity,
|
|
@@ -9929,6 +10680,7 @@ export {
|
|
|
9929
10680
|
rehydrateAgentsMetaAt,
|
|
9930
10681
|
resolveLedgerPaths,
|
|
9931
10682
|
reviewKnowledge,
|
|
10683
|
+
reviewPending,
|
|
9932
10684
|
runDoctorApplyLint,
|
|
9933
10685
|
runDoctorArchiveHistory,
|
|
9934
10686
|
runDoctorCiteCoverage,
|