@fenglimg/fabric-server 2.2.0 → 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 +84 -20
- package/dist/index.js +2012 -1417
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { existsSync as existsSync9 } from "fs";
|
|
3
|
-
import { readFile as
|
|
4
|
-
import { join as
|
|
3
|
+
import { readFile as 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,1178 +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
|
-
const sourceSessions = input.source_sessions ?? [];
|
|
1107
|
-
const primarySession = sourceSessions[0] ?? "";
|
|
1108
|
-
const idempotencyKey = sha256(
|
|
1109
|
-
JSON.stringify({
|
|
1110
|
-
source_session: primarySession,
|
|
1111
|
-
type: input.type,
|
|
1112
|
-
slug: sanitizedSlug
|
|
1113
|
-
})
|
|
1114
|
-
);
|
|
1115
|
-
const summary = input.user_messages_summary ?? "";
|
|
1116
|
-
const summaryTrimmed = summary.trim();
|
|
1117
|
-
const summaryIsEmpty = summaryTrimmed.length === 0;
|
|
1118
|
-
const slugIsEmpty = sanitizedSlug.length === 0;
|
|
1119
|
-
const summaryTooShort = !summaryIsEmpty && summaryTrimmed.length < 15;
|
|
1120
|
-
const summaryEqualsSlug = !summaryIsEmpty && summaryTrimmed.toLowerCase() === sanitizedSlug.toLowerCase();
|
|
1121
|
-
const summaryLooksLikeStableId = !summaryIsEmpty && /^K[TP]-[A-Z]{3}-\d{4}$/.test(summaryTrimmed);
|
|
1122
|
-
const summaryIsOpaque = summaryTooShort || summaryEqualsSlug || summaryLooksLikeStableId;
|
|
1123
|
-
if (summaryIsEmpty || slugIsEmpty || summaryIsOpaque) {
|
|
1124
|
-
const reason = summaryIsEmpty ? "empty_summary" : slugIsEmpty ? "empty_slug" : summaryTooShort ? "summary_too_short" : summaryEqualsSlug ? "summary_equals_slug" : "summary_looks_like_stable_id";
|
|
1125
|
-
await emitEventBestEffort(projectRoot, {
|
|
1126
|
-
event_type: "knowledge_archive_attempted",
|
|
1127
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1128
|
-
correlation_id: primarySession,
|
|
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
|
-
};
|
|
1158
|
-
}
|
|
1159
|
-
const semanticScope = input.semantic_scope;
|
|
1160
|
-
const scopeIsPersonal = semanticScope !== void 0 && isPersonalScope(semanticScope);
|
|
1161
|
-
if (semanticScope !== void 0 && input.layer !== void 0 && scopeIsPersonal !== (input.layer === "personal")) {
|
|
1162
|
-
throw new Error(
|
|
1163
|
-
`semantic_scope '${semanticScope}' conflicts with compatibility layer '${input.layer}'`
|
|
1164
|
-
);
|
|
1165
|
-
}
|
|
1166
|
-
const layer = scopeIsPersonal ? "personal" : input.layer ?? "team";
|
|
1167
|
-
let relevanceScope = input.relevance_scope;
|
|
1168
|
-
let relevancePaths = input.relevance_paths;
|
|
1169
|
-
const shouldAutoDegrade = layer === "personal" && relevanceScope === "narrow";
|
|
1170
|
-
if (shouldAutoDegrade) {
|
|
1171
|
-
relevanceScope = "broad";
|
|
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
|
-
});
|
|
1181
|
-
}
|
|
1182
|
-
const baseDir = pendingBase(layer, projectRoot, semanticScope);
|
|
1183
|
-
const { absolutePath, sanitizedSlug: chosenSlug, idempotencyKey: chosenKey } = await resolveDisambiguatedSlugPath({
|
|
1184
|
-
baseDir,
|
|
1185
|
-
type: input.type,
|
|
1186
|
-
slug: sanitizedSlug,
|
|
1187
|
-
primarySession,
|
|
1188
|
-
baseIdempotencyKey: idempotencyKey
|
|
1189
|
-
});
|
|
1190
|
-
const reportedPath = toPosixPath(absolutePath);
|
|
1191
|
-
const effectiveSanitizedSlug = chosenSlug;
|
|
1192
|
-
const effectiveIdempotencyKey = chosenKey;
|
|
1193
|
-
const writeScopeMeta = resolveWriteScopeMeta(layer, projectRoot, semanticScope);
|
|
1194
|
-
await ensureParentDirectory(absolutePath);
|
|
1195
|
-
if (existsSync3(absolutePath)) {
|
|
1196
|
-
const existing = await readFile3(absolutePath, "utf8");
|
|
1197
|
-
const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
|
|
1198
|
-
if (existingKey === effectiveIdempotencyKey) {
|
|
1199
|
-
const fresh2 = renderFreshEntry({
|
|
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
|
-
};
|
|
1235
|
-
}
|
|
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
|
-
}
|
|
1240
|
-
const fresh = renderFreshEntry({
|
|
1241
|
-
type: input.type,
|
|
1242
|
-
sourceSessions,
|
|
1243
|
-
idempotencyKey: effectiveIdempotencyKey,
|
|
1244
|
-
summary,
|
|
1245
|
-
recentPaths: input.recent_paths,
|
|
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
|
-
});
|
|
1091
|
+
const knowledge = extractKnowledgeFieldsFromFrontmatter(frontmatter);
|
|
1270
1092
|
return {
|
|
1271
|
-
|
|
1272
|
-
|
|
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
|
|
1273
1110
|
};
|
|
1274
1111
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
const candidateSlug = n === 1 ? args.slug : `${args.slug}-${n}`;
|
|
1279
|
-
const candidatePath = join6(args.baseDir, args.type, `${candidateSlug}.md`);
|
|
1280
|
-
const candidateKey = n === 1 ? args.baseIdempotencyKey : sha256(
|
|
1281
|
-
JSON.stringify({
|
|
1282
|
-
source_session: args.primarySession,
|
|
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
|
-
};
|
|
1293
|
-
}
|
|
1294
|
-
const existing = await readFile3(candidatePath, "utf8");
|
|
1295
|
-
const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
|
|
1296
|
-
if (existingKey === candidateKey) {
|
|
1297
|
-
return {
|
|
1298
|
-
absolutePath: candidatePath,
|
|
1299
|
-
sanitizedSlug: candidateSlug,
|
|
1300
|
-
idempotencyKey: candidateKey
|
|
1301
|
-
};
|
|
1302
|
-
}
|
|
1112
|
+
function isForbiddenCrossLayerEdge(sourceLayer, targetId) {
|
|
1113
|
+
if (sourceLayer !== "team") {
|
|
1114
|
+
return false;
|
|
1303
1115
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1116
|
+
const decoded = parseKnowledgeId(localKnowledgeIdFromReference(targetId));
|
|
1117
|
+
if (decoded === null) {
|
|
1118
|
+
return false;
|
|
1119
|
+
}
|
|
1120
|
+
return decoded.layer === "personal";
|
|
1307
1121
|
}
|
|
1308
|
-
function
|
|
1309
|
-
const
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
if (trimmed.length === 0) {
|
|
1313
|
-
return "";
|
|
1122
|
+
function localKnowledgeIdFromReference(ref) {
|
|
1123
|
+
const direct = parseKnowledgeId(ref);
|
|
1124
|
+
if (direct !== null) {
|
|
1125
|
+
return ref;
|
|
1314
1126
|
}
|
|
1315
|
-
|
|
1127
|
+
const tail = ref.split(":").at(-1);
|
|
1128
|
+
return tail ?? ref;
|
|
1316
1129
|
}
|
|
1317
|
-
function
|
|
1318
|
-
const
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
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}`);
|
|
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
|
+
`);
|
|
1145
|
+
}
|
|
1351
1146
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1147
|
+
const SINGULAR_TO_PLURAL = {
|
|
1148
|
+
model: "models",
|
|
1149
|
+
decision: "decisions",
|
|
1150
|
+
guideline: "guidelines",
|
|
1151
|
+
pitfall: "pitfalls",
|
|
1152
|
+
process: "processes"
|
|
1153
|
+
};
|
|
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
|
+
`);
|
|
1163
|
+
}
|
|
1355
1164
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
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
|
+
`);
|
|
1173
|
+
}
|
|
1359
1174
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
-
|
|
1380
|
-
|
|
1248
|
+
return match[1].split(",").map((item) => unquote(item.trim())).filter((item) => item.length > 0);
|
|
1249
|
+
}
|
|
1250
|
+
function unquote(value) {
|
|
1251
|
+
return value.replace(/^["'](.*)["']$/u, "$1");
|
|
1252
|
+
}
|
|
1253
|
+
function escapeRegExp(value) {
|
|
1254
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
1255
|
+
}
|
|
1256
|
+
|
|
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 ?? ""}`;
|
|
1262
|
+
}
|
|
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
|
+
})
|
|
1381
1273
|
);
|
|
1382
|
-
|
|
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}`;
|
|
1274
|
+
return parts.sort().join("\n");
|
|
1405
1275
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
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;
|
|
1409
1280
|
}
|
|
1410
|
-
function
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
""
|
|
1417
|
-
|
|
1418
|
-
"",
|
|
1419
|
-
`- ${summary.trim()}`
|
|
1420
|
-
].join("\n");
|
|
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
|
+
);
|
|
1421
1289
|
}
|
|
1422
|
-
function
|
|
1423
|
-
const
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
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;
|
|
1427
1298
|
}
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1430
|
-
|
|
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);
|
|
1299
|
+
const readSet = createStoreResolver2().resolveReadSet(resolveInput);
|
|
1300
|
+
if (readSet.stores.length === 0) {
|
|
1301
|
+
return null;
|
|
1439
1302
|
}
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
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
|
|
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 [];
|
|
1448
1328
|
}
|
|
1449
|
-
const
|
|
1450
|
-
const
|
|
1451
|
-
const
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
noteLines
|
|
1459
|
-
].join("\n");
|
|
1460
|
-
return `${freshHead}
|
|
1461
|
-
## Evidence
|
|
1462
|
-
|
|
1463
|
-
${evidenceBody}
|
|
1464
|
-
`;
|
|
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();
|
|
1465
1338
|
}
|
|
1466
|
-
function
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
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);
|
|
1472
1361
|
}
|
|
1473
|
-
function
|
|
1474
|
-
const
|
|
1475
|
-
const
|
|
1476
|
-
const
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
const block = m[1] ?? "";
|
|
1481
|
-
const pathSection = /Recent paths:\s*\n([\s\S]*?)(?:\n\s*Notes:|$)/u.exec(block);
|
|
1482
|
-
if (pathSection !== null) {
|
|
1483
|
-
for (const rawLine of (pathSection[1] ?? "").split(/\r?\n/u)) {
|
|
1484
|
-
const t = rawLine.trim();
|
|
1485
|
-
if (t.startsWith("- ")) {
|
|
1486
|
-
paths.push(t.slice(2).trim());
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
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;
|
|
1489
1369
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
if (t.length === 0) continue;
|
|
1497
|
-
if (t.startsWith("- ")) {
|
|
1498
|
-
if (prose.length > 0) {
|
|
1499
|
-
notes.push(prose.join(" ").trim());
|
|
1500
|
-
prose = [];
|
|
1501
|
-
}
|
|
1502
|
-
bulletLines.push(t.slice(2).trim());
|
|
1503
|
-
} else {
|
|
1504
|
-
prose.push(t);
|
|
1370
|
+
items.push({
|
|
1371
|
+
stable_id: entry.qualifiedId,
|
|
1372
|
+
description: {
|
|
1373
|
+
...baseDescription,
|
|
1374
|
+
knowledge_layer: entry.layer,
|
|
1375
|
+
semantic_scope: entry.semanticScope
|
|
1505
1376
|
}
|
|
1506
|
-
}
|
|
1507
|
-
if (prose.length > 0) notes.push(prose.join(" ").trim());
|
|
1508
|
-
for (const n of bulletLines) notes.push(n);
|
|
1377
|
+
});
|
|
1509
1378
|
}
|
|
1510
|
-
return
|
|
1379
|
+
return items;
|
|
1511
1380
|
}
|
|
1512
|
-
function
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
if (block === void 0) {
|
|
1519
|
-
return void 0;
|
|
1520
|
-
}
|
|
1521
|
-
for (const rawLine of block.split(/\r?\n/u)) {
|
|
1522
|
-
const line = rawLine.trim();
|
|
1523
|
-
const sep5 = line.indexOf(":");
|
|
1524
|
-
if (sep5 === -1) continue;
|
|
1525
|
-
const k = line.slice(0, sep5).trim();
|
|
1526
|
-
if (k === key) {
|
|
1527
|
-
return line.slice(sep5 + 1).trim();
|
|
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 });
|
|
1528
1387
|
}
|
|
1529
1388
|
}
|
|
1530
|
-
return
|
|
1389
|
+
return index;
|
|
1531
1390
|
}
|
|
1532
|
-
|
|
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
|
+
};
|
|
1533
1401
|
try {
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
outputSchema: FabExtractKnowledgeOutputSchema.shape,
|
|
1547
|
-
annotations: fabExtractKnowledgeAnnotations
|
|
1548
|
-
},
|
|
1549
|
-
async (input) => {
|
|
1550
|
-
const requestId = randomUUID2();
|
|
1551
|
-
tracker?.enter(requestId);
|
|
1552
|
-
try {
|
|
1553
|
-
const gateResult = await awaitFirstReconcileGate();
|
|
1554
|
-
const gateWarn = gateWarning(gateResult);
|
|
1555
|
-
const validated = FabExtractKnowledgeInputSchema.parse(input);
|
|
1556
|
-
const projectRoot = resolveProjectRoot();
|
|
1557
|
-
const result = await extractKnowledge(projectRoot, validated);
|
|
1558
|
-
const response = { ...result };
|
|
1559
|
-
if (gateWarn) {
|
|
1560
|
-
response.warnings = [gateWarn];
|
|
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;
|
|
1561
1414
|
}
|
|
1562
|
-
const payloadLimits = readPayloadLimits(projectRoot);
|
|
1563
|
-
const serialized = JSON.stringify(response);
|
|
1564
|
-
const guardResult = enforcePayloadLimit(serialized, payloadLimits);
|
|
1565
|
-
response.warnings = appendPayloadWarning(
|
|
1566
|
-
response.warnings,
|
|
1567
|
-
guardResult,
|
|
1568
|
-
"fab_extract_knowledge produced an unexpectedly large response \u2014 extract from a smaller span of text."
|
|
1569
|
-
);
|
|
1570
|
-
return {
|
|
1571
|
-
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
1572
|
-
structuredContent: response
|
|
1573
|
-
};
|
|
1574
|
-
} finally {
|
|
1575
|
-
tracker?.exit(requestId);
|
|
1576
1415
|
}
|
|
1416
|
+
if (isNarrow) census.narrow_total += 1;
|
|
1417
|
+
if (scopeRoot(entry.semanticScope) === "project") {
|
|
1418
|
+
census.by_layer.project += 1;
|
|
1419
|
+
} else {
|
|
1420
|
+
census.by_layer[entry.layer] += 1;
|
|
1421
|
+
}
|
|
1422
|
+
census.total += 1;
|
|
1577
1423
|
}
|
|
1578
|
-
|
|
1424
|
+
} catch {
|
|
1425
|
+
}
|
|
1426
|
+
return census;
|
|
1579
1427
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
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 [];
|
|
1451
|
+
}
|
|
1452
|
+
return out;
|
|
1604
1453
|
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
import { join as join7 } from "path";
|
|
1609
|
-
import {
|
|
1610
|
-
buildStoreResolveInput as buildStoreResolveInput2,
|
|
1611
|
-
createStoreResolver as createStoreResolver2,
|
|
1612
|
-
loadProjectConfig as loadProjectConfig2,
|
|
1613
|
-
readKnowledgeAcrossStores,
|
|
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/");
|
|
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);
|
|
1631
1457
|
}
|
|
1632
|
-
function
|
|
1633
|
-
const
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
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;
|
|
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
|
+
});
|
|
1639
1477
|
}
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1478
|
+
return out;
|
|
1479
|
+
}
|
|
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
|
+
});
|
|
1645
1492
|
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1493
|
+
return out;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
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);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
for (const term of docTerms) {
|
|
1529
|
+
documentFrequency.set(term, (documentFrequency.get(term) ?? 0) + 1);
|
|
1530
|
+
}
|
|
1531
|
+
perDoc.set(doc.id, { fieldTermFreq, fieldLength });
|
|
1653
1532
|
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
identitySource: "declared"
|
|
1658
|
-
};
|
|
1533
|
+
const avgFieldLength = emptyFieldRecord(() => 0);
|
|
1534
|
+
for (const field of BM25_FIELDS) {
|
|
1535
|
+
avgFieldLength[field] = totalDocs > 0 ? totalFieldLength[field] / totalDocs : 0;
|
|
1659
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
|
+
};
|
|
1660
1541
|
return {
|
|
1661
|
-
|
|
1662
|
-
|
|
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
|
+
}
|
|
1663
1575
|
};
|
|
1664
1576
|
}
|
|
1665
|
-
function
|
|
1666
|
-
|
|
1667
|
-
return match?.[1];
|
|
1577
|
+
function buildQueryTerms(text) {
|
|
1578
|
+
return tokenize(text);
|
|
1668
1579
|
}
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1580
|
+
|
|
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 };
|
|
1589
|
+
}
|
|
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);
|
|
1673
1611
|
}
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1676
|
-
|
|
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
|
+
}
|
|
1677
1639
|
}
|
|
1678
|
-
|
|
1679
|
-
return
|
|
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;
|
|
1680
1642
|
}
|
|
1681
|
-
function
|
|
1682
|
-
const
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
return description;
|
|
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;
|
|
1686
1647
|
}
|
|
1687
|
-
const
|
|
1688
|
-
const
|
|
1689
|
-
const
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
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
|
+
}
|
|
1693
1667
|
}
|
|
1694
|
-
|
|
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
|
-
};
|
|
1668
|
+
return judged;
|
|
1720
1669
|
}
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
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: [] };
|
|
1683
|
+
}
|
|
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) {
|
|
1724
1703
|
return void 0;
|
|
1725
1704
|
}
|
|
1726
|
-
const
|
|
1727
|
-
return {
|
|
1728
|
-
summary,
|
|
1729
|
-
intent_clues: extractInlineArray(frontmatter, "intent_clues"),
|
|
1730
|
-
tech_stack: extractInlineArray(frontmatter, "tech_stack"),
|
|
1731
|
-
impact: extractInlineArray(frontmatter, "impact"),
|
|
1732
|
-
must_read_if: extractScalar(frontmatter, "must_read_if") ?? summary,
|
|
1733
|
-
entities: extractInlineArray(frontmatter, "entities"),
|
|
1734
|
-
id: knowledge.id,
|
|
1735
|
-
knowledge_type: knowledge.knowledge_type,
|
|
1736
|
-
maturity: knowledge.maturity,
|
|
1737
|
-
knowledge_layer: knowledge.knowledge_layer,
|
|
1738
|
-
layer_reason: knowledge.layer_reason,
|
|
1739
|
-
created_at: knowledge.created_at,
|
|
1740
|
-
tags: knowledge.tags,
|
|
1741
|
-
relevance_scope: knowledge.relevance_scope,
|
|
1742
|
-
relevance_paths: knowledge.relevance_paths,
|
|
1743
|
-
// v2.2 H2-related (W1-T7): graph edges parsed from frontmatter.
|
|
1744
|
-
related: knowledge.related
|
|
1745
|
-
};
|
|
1705
|
+
const top = result.matches[0];
|
|
1706
|
+
return `${result.verdict} of ${top.stable_id} (${top.similarity.toFixed(2)})`;
|
|
1746
1707
|
}
|
|
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);
|
|
1750
|
+
}
|
|
1751
|
+
return { sanitized, redactions };
|
|
1752
|
+
}
|
|
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
|
+
}
|
|
1754
1774
|
}
|
|
1755
|
-
return
|
|
1775
|
+
return { sanitized: out, allRedactions };
|
|
1756
1776
|
}
|
|
1757
|
-
function
|
|
1758
|
-
const
|
|
1759
|
-
|
|
1760
|
-
|
|
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
|
+
}
|
|
1791
|
+
}
|
|
1761
1792
|
}
|
|
1762
|
-
|
|
1763
|
-
return tail ?? ref;
|
|
1793
|
+
return { redacted: out, redactedFields: [...redactedFields].sort() };
|
|
1764
1794
|
}
|
|
1765
|
-
function
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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
|
|
1816
|
+
};
|
|
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
|
+
});
|
|
1781
1827
|
}
|
|
1782
|
-
const
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
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
|
|
1788
1846
|
};
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
}
|
|
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
|
+
});
|
|
1799
1856
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
}
|
|
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
|
+
};
|
|
1809
1888
|
}
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
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
|
+
};
|
|
1819
1910
|
}
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
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
|
+
});
|
|
1828
1928
|
}
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
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
|
+
};
|
|
1838
1982
|
}
|
|
1983
|
+
throw new Error(
|
|
1984
|
+
`slug collision (unreachable after rc.37 NEW-6): pending file ${reportedPath} already exists with key ${existingKey ?? "<missing>"} != ${effectiveIdempotencyKey}`
|
|
1985
|
+
);
|
|
1839
1986
|
}
|
|
1840
|
-
const
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
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
|
-
|
|
1886
|
-
return value.replace(/^["'](.*)["']$/u, "$1");
|
|
1887
|
-
}
|
|
1888
|
-
function escapeRegExp(value) {
|
|
1889
|
-
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
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`
|
|
2088
|
+
);
|
|
1890
2089
|
}
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
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 "";
|
|
2096
|
+
}
|
|
2097
|
+
return trimmed.slice(0, SLUG_MAX_LENGTH).replace(/-+$/g, "");
|
|
1897
2098
|
}
|
|
1898
|
-
|
|
1899
|
-
const
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
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}`);
|
|
2133
|
+
}
|
|
2134
|
+
if (args.relevancePaths !== void 0) {
|
|
2135
|
+
const pathsBody = args.relevancePaths.map((p) => quoteRelevancePath(p)).join(", ");
|
|
2136
|
+
frontmatterLines.push(`relevance_paths: [${pathsBody}]`);
|
|
2137
|
+
}
|
|
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
|
+
"---"
|
|
1908
2166
|
);
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
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}`;
|
|
1915
2190
|
}
|
|
1916
|
-
function
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
}
|
|
1920
|
-
const current = `project:${activeProject}`;
|
|
1921
|
-
return entries.filter(
|
|
1922
|
-
(e) => scopeRoot(e.semanticScope) !== "project" || e.semanticScope === current
|
|
1923
|
-
);
|
|
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}"`;
|
|
1924
2194
|
}
|
|
1925
|
-
function
|
|
1926
|
-
const
|
|
1927
|
-
return
|
|
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");
|
|
1928
2206
|
}
|
|
1929
|
-
|
|
1930
|
-
const
|
|
1931
|
-
if (
|
|
1932
|
-
return
|
|
1933
|
-
|
|
1934
|
-
const readSet = createStoreResolver2().resolveReadSet(resolveInput);
|
|
1935
|
-
if (readSet.stores.length === 0) {
|
|
1936
|
-
return null;
|
|
2207
|
+
function mergeEvidenceNotes(existing, fresh) {
|
|
2208
|
+
const freshSplit = splitAtEvidence(fresh);
|
|
2209
|
+
if (freshSplit === null) {
|
|
2210
|
+
return fresh.endsWith("\n") ? fresh : `${fresh}
|
|
2211
|
+
`;
|
|
1937
2212
|
}
|
|
1938
|
-
const
|
|
1939
|
-
|
|
1940
|
-
);
|
|
1941
|
-
const
|
|
1942
|
-
const
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
store_uuid: entry.store_uuid
|
|
1950
|
-
}
|
|
1951
|
-
)
|
|
1952
|
-
)
|
|
1953
|
-
}));
|
|
1954
|
-
return {
|
|
1955
|
-
refs: await readKnowledgeAcrossStores(dirs),
|
|
1956
|
-
personalUuids
|
|
1957
|
-
};
|
|
1958
|
-
}
|
|
1959
|
-
async function walkReadSetStores(projectRoot) {
|
|
1960
|
-
const snapshot = await resolveReadSetSnapshot(projectRoot);
|
|
1961
|
-
if (snapshot === null) {
|
|
1962
|
-
return [];
|
|
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);
|
|
1963
2224
|
}
|
|
1964
|
-
const
|
|
1965
|
-
const
|
|
1966
|
-
const
|
|
1967
|
-
|
|
1968
|
-
|
|
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);
|
|
1969
2233
|
}
|
|
1970
|
-
const
|
|
1971
|
-
|
|
1972
|
-
|
|
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
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
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
|
+
}
|
|
2011
2273
|
}
|
|
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
2274
|
}
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
total: 0
|
|
2035
|
-
};
|
|
2036
|
-
try {
|
|
2037
|
-
const activeProject = activeProjectOf(projectRoot);
|
|
2038
|
-
const all = await walkReadSetStores(projectRoot);
|
|
2039
|
-
const kept = filterByActiveProject(all, activeProject);
|
|
2040
|
-
census.dropped_other_project = all.length - kept.length;
|
|
2041
|
-
for (const entry of kept) {
|
|
2042
|
-
const desc = extractRuleDescription(entry.source);
|
|
2043
|
-
const type = desc?.knowledge_type;
|
|
2044
|
-
const isNarrow = desc?.relevance_scope === "narrow";
|
|
2045
|
-
if (typeof type === "string") {
|
|
2046
|
-
census.by_type[type] = (census.by_type[type] ?? 0) + 1;
|
|
2047
|
-
if (!isNarrow) {
|
|
2048
|
-
census.broad_by_type[type] = (census.broad_by_type[type] ?? 0) + 1;
|
|
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 = [];
|
|
2049
2286
|
}
|
|
2050
|
-
|
|
2051
|
-
if (isNarrow) census.narrow_total += 1;
|
|
2052
|
-
if (scopeRoot(entry.semanticScope) === "project") {
|
|
2053
|
-
census.by_layer.project += 1;
|
|
2287
|
+
bulletLines.push(t.slice(2).trim());
|
|
2054
2288
|
} else {
|
|
2055
|
-
|
|
2289
|
+
prose.push(t);
|
|
2056
2290
|
}
|
|
2057
|
-
census.total += 1;
|
|
2058
2291
|
}
|
|
2059
|
-
|
|
2292
|
+
if (prose.length > 0) notes.push(prose.join(" ").trim());
|
|
2293
|
+
for (const n of bulletLines) notes.push(n);
|
|
2060
2294
|
}
|
|
2061
|
-
return
|
|
2295
|
+
return { notes, paths };
|
|
2062
2296
|
}
|
|
2063
|
-
|
|
2064
|
-
const
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
layer: entry.layer,
|
|
2080
|
-
summary: typeof desc.summary === "string" ? desc.summary : "",
|
|
2081
|
-
body: extractBody(entry.source)
|
|
2082
|
-
});
|
|
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();
|
|
2083
2313
|
}
|
|
2084
|
-
} catch {
|
|
2085
|
-
return [];
|
|
2086
2314
|
}
|
|
2087
|
-
return
|
|
2088
|
-
}
|
|
2089
|
-
async function computeReadSetRevision(projectRoot) {
|
|
2090
|
-
const revisionSource = (await walkReadSetStores(projectRoot)).filter((entry) => !entry.file.includes("/knowledge/pending/")).map((entry) => `${entry.qualifiedId}|${sha256(entry.source)}`).sort().join("\n");
|
|
2091
|
-
return sha256(revisionSource);
|
|
2315
|
+
return void 0;
|
|
2092
2316
|
}
|
|
2093
|
-
async function
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
continue;
|
|
2098
|
-
}
|
|
2099
|
-
const description = extractRuleDescription(entry.source);
|
|
2100
|
-
if (description === void 0) {
|
|
2101
|
-
continue;
|
|
2102
|
-
}
|
|
2103
|
-
out.push({
|
|
2104
|
-
stableId: entry.qualifiedId.slice(entry.alias.length + 1),
|
|
2105
|
-
qualifiedId: entry.qualifiedId,
|
|
2106
|
-
file: entry.file,
|
|
2107
|
-
type: entry.type,
|
|
2108
|
-
layer: entry.layer,
|
|
2109
|
-
body: entry.source,
|
|
2110
|
-
description
|
|
2111
|
-
});
|
|
2317
|
+
async function emitEventBestEffort(projectRoot, event) {
|
|
2318
|
+
try {
|
|
2319
|
+
await appendEventLedgerEvent(projectRoot, event);
|
|
2320
|
+
} catch {
|
|
2112
2321
|
}
|
|
2113
|
-
return out;
|
|
2114
2322
|
}
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
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
|
+
}
|
|
2121
2381
|
}
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2382
|
+
);
|
|
2383
|
+
}
|
|
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("\\", "/");
|
|
2129
2408
|
}
|
|
2130
2409
|
|
|
2131
2410
|
// src/services/id-redirect.ts
|
|
@@ -2391,61 +2670,6 @@ var LEDGER_DUAL_WRITE_METRIC_NAMES = {
|
|
|
2391
2670
|
edit_intent_checked: METRIC_COUNTER_NAMES.edit_intent_checked
|
|
2392
2671
|
};
|
|
2393
2672
|
|
|
2394
|
-
// src/services/bm25.ts
|
|
2395
|
-
import { tokenize } from "@fenglimg/fabric-shared";
|
|
2396
|
-
var K1 = 1.5;
|
|
2397
|
-
var B = 0.75;
|
|
2398
|
-
function buildBm25Model(docs) {
|
|
2399
|
-
const totalDocs = docs.length;
|
|
2400
|
-
const documentFrequency = /* @__PURE__ */ new Map();
|
|
2401
|
-
const perDoc = /* @__PURE__ */ new Map();
|
|
2402
|
-
let totalLength = 0;
|
|
2403
|
-
for (const doc of docs) {
|
|
2404
|
-
const termFreq = /* @__PURE__ */ new Map();
|
|
2405
|
-
for (const term of doc.tokens) {
|
|
2406
|
-
termFreq.set(term, (termFreq.get(term) ?? 0) + 1);
|
|
2407
|
-
}
|
|
2408
|
-
for (const term of termFreq.keys()) {
|
|
2409
|
-
documentFrequency.set(term, (documentFrequency.get(term) ?? 0) + 1);
|
|
2410
|
-
}
|
|
2411
|
-
totalLength += doc.tokens.length;
|
|
2412
|
-
perDoc.set(doc.id, { termFreq, length: doc.tokens.length });
|
|
2413
|
-
}
|
|
2414
|
-
const avgDocLength = totalDocs > 0 ? totalLength / totalDocs : 0;
|
|
2415
|
-
const idf = (term) => {
|
|
2416
|
-
const n = documentFrequency.get(term) ?? 0;
|
|
2417
|
-
return Math.log(1 + (totalDocs - n + 0.5) / (n + 0.5));
|
|
2418
|
-
};
|
|
2419
|
-
return {
|
|
2420
|
-
scoreDoc(id, queryTerms) {
|
|
2421
|
-
const data = perDoc.get(id);
|
|
2422
|
-
if (data === void 0 || data.length === 0 || queryTerms.length === 0) {
|
|
2423
|
-
return 0;
|
|
2424
|
-
}
|
|
2425
|
-
const normalizer = avgDocLength > 0 ? data.length / avgDocLength : 1;
|
|
2426
|
-
let score = 0;
|
|
2427
|
-
const scoredTerms = /* @__PURE__ */ new Set();
|
|
2428
|
-
for (const term of queryTerms) {
|
|
2429
|
-
if (scoredTerms.has(term)) {
|
|
2430
|
-
continue;
|
|
2431
|
-
}
|
|
2432
|
-
scoredTerms.add(term);
|
|
2433
|
-
const freq = data.termFreq.get(term);
|
|
2434
|
-
if (freq === void 0) {
|
|
2435
|
-
continue;
|
|
2436
|
-
}
|
|
2437
|
-
const numerator = freq * (K1 + 1);
|
|
2438
|
-
const denominator = freq + K1 * (1 - B + B * normalizer);
|
|
2439
|
-
score += idf(term) * (numerator / denominator);
|
|
2440
|
-
}
|
|
2441
|
-
return score;
|
|
2442
|
-
}
|
|
2443
|
-
};
|
|
2444
|
-
}
|
|
2445
|
-
function buildQueryTerms(text) {
|
|
2446
|
-
return tokenize(text);
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
2673
|
// src/services/vector-retrieval.ts
|
|
2450
2674
|
var embedderLoad;
|
|
2451
2675
|
var OPTIONAL_EMBED_PACKAGE = "fastembed";
|
|
@@ -2663,7 +2887,9 @@ async function planContext(projectRoot, input) {
|
|
|
2663
2887
|
const relevanceFloor = maxScore * relevanceRatio;
|
|
2664
2888
|
const survivingScored = hasQuery && maxScore > 0 && relevanceRatio > 0 ? cappedScored.filter((entry) => entry.score >= relevanceFloor) : cappedScored;
|
|
2665
2889
|
const topKCandidates = survivingScored.map((entry) => entry.item);
|
|
2666
|
-
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;
|
|
2667
2893
|
let candidates = topKCandidates;
|
|
2668
2894
|
const relatedAppended = {};
|
|
2669
2895
|
if (input.include_related === true) {
|
|
@@ -2703,8 +2929,10 @@ async function planContext(projectRoot, input) {
|
|
|
2703
2929
|
const basePreflightDiagnostics = buildPreflightDiagnostics(suppressedStableIds);
|
|
2704
2930
|
let payloadTrimDropped = 0;
|
|
2705
2931
|
let payloadOverBudget = false;
|
|
2932
|
+
let payloadDropped = [];
|
|
2706
2933
|
if (input.payload_budget !== void 0) {
|
|
2707
2934
|
const fullCandidateCount = candidates.length;
|
|
2935
|
+
const preTrimIds = candidates.map((item) => item.stable_id);
|
|
2708
2936
|
const serialize = (candidateSlice) => {
|
|
2709
2937
|
const dropped = fullCandidateCount - candidateSlice.length;
|
|
2710
2938
|
const totalOmitted = omittedCandidateCount + dropped;
|
|
@@ -2715,7 +2943,7 @@ async function planContext(projectRoot, input) {
|
|
|
2715
2943
|
entries,
|
|
2716
2944
|
...input.intent !== void 0 ? { intent: input.intent } : {},
|
|
2717
2945
|
candidates: candidateSlice,
|
|
2718
|
-
...totalOmitted > 0 ? {
|
|
2946
|
+
...totalOmitted > 0 ? { dropped_count: totalOmitted } : {},
|
|
2719
2947
|
preflight_diagnostics: basePreflightDiagnostics,
|
|
2720
2948
|
warnings: dropped > 0 && input.payload_budget?.trim_warning !== void 0 ? [...input.payload_budget.warnings ?? [], input.payload_budget.trim_warning] : input.payload_budget?.warnings,
|
|
2721
2949
|
...Object.keys(relatedAppended).length > 0 ? { related_appended: relatedAppended } : {}
|
|
@@ -2723,6 +2951,8 @@ async function planContext(projectRoot, input) {
|
|
|
2723
2951
|
};
|
|
2724
2952
|
const trim = trimToPayloadBudget(candidates, serialize, input.payload_budget.limits);
|
|
2725
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" }));
|
|
2726
2956
|
candidates = trim.items;
|
|
2727
2957
|
payloadTrimDropped = trim.dropped;
|
|
2728
2958
|
}
|
|
@@ -2746,7 +2976,11 @@ async function planContext(projectRoot, input) {
|
|
|
2746
2976
|
entries,
|
|
2747
2977
|
...input.intent !== void 0 ? { intent: input.intent } : {},
|
|
2748
2978
|
candidates,
|
|
2749
|
-
|
|
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] } : {},
|
|
2750
2984
|
preflight_diagnostics: basePreflightDiagnostics,
|
|
2751
2985
|
// v2.2 W5 R1: the auto_healed / previous_revision_hash pair was tied to the
|
|
2752
2986
|
// co-location loadActiveMetaOrStale read-path auto-heal, which is retired.
|
|
@@ -2854,7 +3088,7 @@ function getOrBuildBm25Model(revision, rawItems, docTexts) {
|
|
|
2854
3088
|
const model = buildBm25Model(
|
|
2855
3089
|
rawItems.map((item) => ({
|
|
2856
3090
|
id: item.stable_id,
|
|
2857
|
-
|
|
3091
|
+
fields: documentFieldsForItem(item.description)
|
|
2858
3092
|
}))
|
|
2859
3093
|
);
|
|
2860
3094
|
bm25ModelCache = { revision, model };
|
|
@@ -2928,6 +3162,16 @@ function documentTextForItem(description) {
|
|
|
2928
3162
|
...description.tags ?? []
|
|
2929
3163
|
].join(" ");
|
|
2930
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
|
+
}
|
|
2931
3175
|
function isEmptyShellDescription(description, stableId) {
|
|
2932
3176
|
return description.summary === stableId && description.intent_clues.length === 0 && description.tech_stack.length === 0 && description.impact.length === 0;
|
|
2933
3177
|
}
|
|
@@ -3027,7 +3271,7 @@ function relatedLookupKeys(stableId) {
|
|
|
3027
3271
|
}
|
|
3028
3272
|
|
|
3029
3273
|
// src/services/recall.ts
|
|
3030
|
-
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>)`.";
|
|
3031
3275
|
async function recall(projectRoot, input) {
|
|
3032
3276
|
const planResult = await planContext(projectRoot, input);
|
|
3033
3277
|
const { selection_token: _token, payload_trimmed: _pt, payload_over_budget: _pob, ...planRest } = planResult;
|
|
@@ -3076,13 +3320,22 @@ async function recall(projectRoot, input) {
|
|
|
3076
3320
|
paths.push(attachPathStore({ stable_id: candidate.stable_id, path: ref.file }));
|
|
3077
3321
|
}
|
|
3078
3322
|
const nextSteps = buildNextSteps(planResult, paths, candidateById, candidateLookup);
|
|
3079
|
-
const
|
|
3080
|
-
|
|
3081
|
-
|
|
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;
|
|
3082
3336
|
return {
|
|
3083
|
-
...
|
|
3084
|
-
|
|
3085
|
-
paths,
|
|
3337
|
+
...planRestNoLists,
|
|
3338
|
+
entries,
|
|
3086
3339
|
directive: RECALL_DIRECTIVE,
|
|
3087
3340
|
...nextSteps.length > 0 ? { next_steps: nextSteps } : {}
|
|
3088
3341
|
};
|
|
@@ -3094,7 +3347,7 @@ function isAlwaysActive(candidate) {
|
|
|
3094
3347
|
}
|
|
3095
3348
|
function buildNextSteps(planResult, paths, candidateById, candidateLookup) {
|
|
3096
3349
|
const nextSteps = [];
|
|
3097
|
-
const omitted = planResult.
|
|
3350
|
+
const omitted = (planResult.dropped ?? []).filter((d) => d.reason === "retrieval_budget").length;
|
|
3098
3351
|
if (omitted > 0) {
|
|
3099
3352
|
nextSteps.push(
|
|
3100
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.`
|
|
@@ -3130,7 +3383,7 @@ function registerRecall(server, tracker) {
|
|
|
3130
3383
|
server.registerTool(
|
|
3131
3384
|
"fab_recall",
|
|
3132
3385
|
{
|
|
3133
|
-
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.",
|
|
3134
3387
|
inputSchema: recallInputSchema,
|
|
3135
3388
|
outputSchema: recallOutputSchema,
|
|
3136
3389
|
annotations: recallAnnotations
|
|
@@ -3185,11 +3438,16 @@ function registerRecall(server, tracker) {
|
|
|
3185
3438
|
response.warnings = appendPayloadWarning(
|
|
3186
3439
|
response.warnings,
|
|
3187
3440
|
guardResult,
|
|
3188
|
-
"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."
|
|
3189
3442
|
);
|
|
3190
3443
|
payloadBytesOut = Buffer.byteLength(serialized, "utf8");
|
|
3191
3444
|
return {
|
|
3192
|
-
content: [
|
|
3445
|
+
content: [
|
|
3446
|
+
{
|
|
3447
|
+
type: "text",
|
|
3448
|
+
text: `Fabric recall: ${result.entries.length} entries (see structuredContent)`
|
|
3449
|
+
}
|
|
3450
|
+
],
|
|
3193
3451
|
structuredContent: response
|
|
3194
3452
|
};
|
|
3195
3453
|
} catch (error) {
|
|
@@ -3393,7 +3651,12 @@ function registerArchiveScan(server, tracker) {
|
|
|
3393
3651
|
"fab_archive_scan returned a large candidate set \u2014 pass an explicit `range` of session_ids to narrow the scan."
|
|
3394
3652
|
);
|
|
3395
3653
|
return {
|
|
3396
|
-
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
|
+
],
|
|
3397
3660
|
structuredContent: result
|
|
3398
3661
|
};
|
|
3399
3662
|
} finally {
|
|
@@ -3419,6 +3682,41 @@ import { existsSync as existsSync5 } from "fs";
|
|
|
3419
3682
|
import { readFile as readFile6, readdir as readdir2, stat as stat2, unlink as unlink2 } from "fs/promises";
|
|
3420
3683
|
import { homedir } from "os";
|
|
3421
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
|
|
3422
3720
|
import { allocateStoreKnowledgeId, isPersonalScope as isPersonalScope2 } from "@fenglimg/fabric-shared";
|
|
3423
3721
|
|
|
3424
3722
|
// src/services/pending-dedupe.ts
|
|
@@ -3573,11 +3871,6 @@ var PLURAL_TYPES = [
|
|
|
3573
3871
|
];
|
|
3574
3872
|
async function reviewKnowledge(projectRoot, input) {
|
|
3575
3873
|
switch (input.action) {
|
|
3576
|
-
case "list":
|
|
3577
|
-
return {
|
|
3578
|
-
action: "list",
|
|
3579
|
-
items: await listPending(projectRoot, input.filters)
|
|
3580
|
-
};
|
|
3581
3874
|
case "approve":
|
|
3582
3875
|
return {
|
|
3583
3876
|
action: "approve",
|
|
@@ -3600,11 +3893,6 @@ async function reviewKnowledge(projectRoot, input) {
|
|
|
3600
3893
|
}
|
|
3601
3894
|
case "modify-layer":
|
|
3602
3895
|
return await modifyEntry(projectRoot, input.pending_path, input.changes);
|
|
3603
|
-
case "search":
|
|
3604
|
-
return {
|
|
3605
|
-
action: "search",
|
|
3606
|
-
items: await searchEntries(projectRoot, input.query, input.filters)
|
|
3607
|
-
};
|
|
3608
3896
|
case "defer":
|
|
3609
3897
|
return {
|
|
3610
3898
|
action: "defer",
|
|
@@ -3621,6 +3909,24 @@ async function reviewKnowledge(projectRoot, input) {
|
|
|
3621
3909
|
}
|
|
3622
3910
|
}
|
|
3623
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
|
+
}
|
|
3624
3930
|
function storeKnowledgeRoots(projectRoot) {
|
|
3625
3931
|
const roots = [];
|
|
3626
3932
|
for (const layer of ["team", "personal"]) {
|
|
@@ -3834,7 +4140,10 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
3834
4140
|
const newFilename = `${stableId}--${slug}.md`;
|
|
3835
4141
|
targetAbs = join10(resolveStoreCanonicalBase(layer, projectRoot), pluralType, newFilename);
|
|
3836
4142
|
await ensureParentDirectory(targetAbs);
|
|
3837
|
-
const rewritten =
|
|
4143
|
+
const rewritten = rewriteFrontmatterMerge(
|
|
4144
|
+
rewriteFrontmatterForPromote(content, stableId),
|
|
4145
|
+
{ last_review_confirmed_at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
4146
|
+
);
|
|
3838
4147
|
await atomicWriteText(targetAbs, rewritten);
|
|
3839
4148
|
writtenTarget = true;
|
|
3840
4149
|
if (sourceIsStore) {
|
|
@@ -3917,6 +4226,13 @@ async function modifyEntry(projectRoot, pendingPath, changes) {
|
|
|
3917
4226
|
const content = await readFile6(target.absPath, "utf8");
|
|
3918
4227
|
const fm = parseFrontmatter(content);
|
|
3919
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
|
+
}
|
|
3920
4236
|
if (changes.semantic_scope !== void 0 && isPersonalScope2(changes.semantic_scope)) {
|
|
3921
4237
|
throw new Error(
|
|
3922
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)`
|
|
@@ -3925,7 +4241,10 @@ async function modifyEntry(projectRoot, pendingPath, changes) {
|
|
|
3925
4241
|
if (changes.layer !== void 0 && changes.layer !== currentLayer) {
|
|
3926
4242
|
return await modifyLayerFlip(projectRoot, target, content, fm, changes);
|
|
3927
4243
|
}
|
|
3928
|
-
const merged = rewriteFrontmatterMerge(content,
|
|
4244
|
+
const merged = rewriteFrontmatterMerge(content, {
|
|
4245
|
+
...changes,
|
|
4246
|
+
last_review_confirmed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4247
|
+
});
|
|
3929
4248
|
await atomicWriteText(target.absPath, merged);
|
|
3930
4249
|
const changedFields = Object.keys(changes).filter(
|
|
3931
4250
|
(field) => changes[field] !== void 0
|
|
@@ -4024,7 +4343,11 @@ async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
|
4024
4343
|
relevance_scope: "broad",
|
|
4025
4344
|
relevance_paths: []
|
|
4026
4345
|
} : { ...changes, layer: toLayer };
|
|
4027
|
-
const rewritten = rewriteFrontmatterMerge(
|
|
4346
|
+
const rewritten = rewriteFrontmatterMerge(
|
|
4347
|
+
content,
|
|
4348
|
+
{ ...effectivePatch, last_review_confirmed_at: (/* @__PURE__ */ new Date()).toISOString() },
|
|
4349
|
+
{ id: newStableId }
|
|
4350
|
+
);
|
|
4028
4351
|
await atomicWriteText(toAbs, rewritten);
|
|
4029
4352
|
if (target.isInProjectTree) {
|
|
4030
4353
|
const relSource = relative(projectRoot, target.absPath);
|
|
@@ -4348,6 +4671,9 @@ function parseFrontmatter(content) {
|
|
|
4348
4671
|
case "deferred_until":
|
|
4349
4672
|
out.deferred_until = stripQuotes(value);
|
|
4350
4673
|
break;
|
|
4674
|
+
case "last_review_confirmed_at":
|
|
4675
|
+
out.last_review_confirmed_at = stripQuotes(value);
|
|
4676
|
+
break;
|
|
4351
4677
|
default:
|
|
4352
4678
|
break;
|
|
4353
4679
|
}
|
|
@@ -4413,6 +4739,7 @@ ${content}`;
|
|
|
4413
4739
|
if (patch.related !== void 0) updates.related = `related: ${flowArray(patch.related)}`;
|
|
4414
4740
|
if (patch.status !== void 0) updates.status = `status: ${patch.status}`;
|
|
4415
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)}`;
|
|
4416
4743
|
const lines = block.split(/\r?\n/u);
|
|
4417
4744
|
const seen = /* @__PURE__ */ new Set();
|
|
4418
4745
|
const newLines = [];
|
|
@@ -4449,6 +4776,7 @@ function appendPatchLines(lines, patch) {
|
|
|
4449
4776
|
if (patch.related !== void 0) lines.push(`related: ${flowArray(patch.related)}`);
|
|
4450
4777
|
if (patch.status !== void 0) lines.push(`status: ${patch.status}`);
|
|
4451
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)}`);
|
|
4452
4780
|
}
|
|
4453
4781
|
function flowArrayElement(value) {
|
|
4454
4782
|
if (/[\n\r,\[\]{}"#:]/u.test(value) || /^\s|\s$/u.test(value)) {
|
|
@@ -4481,40 +4809,104 @@ async function emitEventBestEffort2(projectRoot, event) {
|
|
|
4481
4809
|
// src/tools/review.ts
|
|
4482
4810
|
function registerReview(server, tracker) {
|
|
4483
4811
|
server.registerTool(
|
|
4484
|
-
"fab_review",
|
|
4812
|
+
"fab_review",
|
|
4813
|
+
{
|
|
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.",
|
|
4815
|
+
// Flat ZodRawShape required by MCP SDK 1.29.0 registerTool. The
|
|
4816
|
+
// authoritative cross-field contract still lives in FabReviewInputSchema
|
|
4817
|
+
// (discriminatedUnion) and is enforced inside the handler via
|
|
4818
|
+
// `FabReviewInputSchema.parse(input)`.
|
|
4819
|
+
inputSchema: FabReviewInputShape,
|
|
4820
|
+
outputSchema: FabReviewOutputShape,
|
|
4821
|
+
annotations: fabReviewAnnotations
|
|
4822
|
+
},
|
|
4823
|
+
async (input) => {
|
|
4824
|
+
const requestId = randomUUID5();
|
|
4825
|
+
tracker?.enter(requestId);
|
|
4826
|
+
try {
|
|
4827
|
+
const gateResult = await awaitFirstReconcileGate();
|
|
4828
|
+
const gateWarn = gateWarning(gateResult);
|
|
4829
|
+
const narrowed = FabReviewInputSchema.parse(input);
|
|
4830
|
+
const projectRoot = resolveProjectRoot();
|
|
4831
|
+
const result = await reviewKnowledge(projectRoot, narrowed);
|
|
4832
|
+
const response = { ...result };
|
|
4833
|
+
if (gateWarn) {
|
|
4834
|
+
response.warnings = [gateWarn];
|
|
4835
|
+
}
|
|
4836
|
+
const payloadLimits = readPayloadLimits(projectRoot);
|
|
4837
|
+
const serialized = JSON.stringify(response);
|
|
4838
|
+
const guardResult = enforcePayloadLimit4(serialized, payloadLimits);
|
|
4839
|
+
response.warnings = appendPayloadWarning(
|
|
4840
|
+
response.warnings,
|
|
4841
|
+
guardResult,
|
|
4842
|
+
"fab_review returned a large result set \u2014 pass a narrower filter (topic / status / id) to reduce response size."
|
|
4843
|
+
);
|
|
4844
|
+
return {
|
|
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",
|
|
4485
4872
|
{
|
|
4486
|
-
description: "
|
|
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.",
|
|
4487
4874
|
// Flat ZodRawShape required by MCP SDK 1.29.0 registerTool. The
|
|
4488
|
-
// authoritative cross-field contract still lives in
|
|
4875
|
+
// authoritative cross-field contract still lives in FabPendingInputSchema
|
|
4489
4876
|
// (discriminatedUnion) and is enforced inside the handler via
|
|
4490
|
-
// `
|
|
4491
|
-
inputSchema:
|
|
4492
|
-
outputSchema:
|
|
4493
|
-
annotations:
|
|
4877
|
+
// `FabPendingInputSchema.parse(input)`.
|
|
4878
|
+
inputSchema: FabPendingInputShape,
|
|
4879
|
+
outputSchema: FabPendingOutputShape,
|
|
4880
|
+
annotations: fabPendingAnnotations
|
|
4494
4881
|
},
|
|
4495
4882
|
async (input) => {
|
|
4496
|
-
const requestId =
|
|
4883
|
+
const requestId = randomUUID6();
|
|
4497
4884
|
tracker?.enter(requestId);
|
|
4498
4885
|
try {
|
|
4499
4886
|
const gateResult = await awaitFirstReconcileGate();
|
|
4500
4887
|
const gateWarn = gateWarning(gateResult);
|
|
4501
|
-
const narrowed =
|
|
4888
|
+
const narrowed = FabPendingInputSchema.parse(input);
|
|
4502
4889
|
const projectRoot = resolveProjectRoot();
|
|
4503
|
-
const result = await
|
|
4890
|
+
const result = await reviewPending(projectRoot, narrowed);
|
|
4504
4891
|
const response = { ...result };
|
|
4505
4892
|
if (gateWarn) {
|
|
4506
4893
|
response.warnings = [gateWarn];
|
|
4507
4894
|
}
|
|
4508
4895
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
4509
4896
|
const serialized = JSON.stringify(response);
|
|
4510
|
-
const guardResult =
|
|
4897
|
+
const guardResult = enforcePayloadLimit5(serialized, payloadLimits);
|
|
4511
4898
|
response.warnings = appendPayloadWarning(
|
|
4512
4899
|
response.warnings,
|
|
4513
4900
|
guardResult,
|
|
4514
|
-
"
|
|
4901
|
+
"fab_pending returned a large result set \u2014 pass a narrower filter (topic / status / id) to reduce response size."
|
|
4515
4902
|
);
|
|
4516
4903
|
return {
|
|
4517
|
-
content: [
|
|
4904
|
+
content: [
|
|
4905
|
+
{
|
|
4906
|
+
type: "text",
|
|
4907
|
+
text: `Fabric pending: ${response.action} (see structuredContent)`
|
|
4908
|
+
}
|
|
4909
|
+
],
|
|
4518
4910
|
structuredContent: response
|
|
4519
4911
|
};
|
|
4520
4912
|
} finally {
|
|
@@ -4525,9 +4917,9 @@ function registerReview(server, tracker) {
|
|
|
4525
4917
|
}
|
|
4526
4918
|
|
|
4527
4919
|
// src/services/doctor.ts
|
|
4528
|
-
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";
|
|
4529
4921
|
import { constants as constants2 } from "fs";
|
|
4530
|
-
import { isAbsolute as isAbsolute2, join as
|
|
4922
|
+
import { isAbsolute as isAbsolute2, join as join21, posix as posix5, resolve as resolve3 } from "path";
|
|
4531
4923
|
import {
|
|
4532
4924
|
createTranslator,
|
|
4533
4925
|
forensicReportSchema,
|
|
@@ -5214,6 +5606,9 @@ async function inspectStoreKnowledgeAge(projectRoot, now, lastActiveIndex) {
|
|
|
5214
5606
|
if (maturity === void 0) {
|
|
5215
5607
|
continue;
|
|
5216
5608
|
}
|
|
5609
|
+
if (entry.description.relevance_scope === "broad") {
|
|
5610
|
+
continue;
|
|
5611
|
+
}
|
|
5217
5612
|
const lastActive = lastActiveIndex.get(entry.stableId);
|
|
5218
5613
|
if (lastActive === void 0) {
|
|
5219
5614
|
continue;
|
|
@@ -5298,6 +5693,139 @@ function createStaleArchiveCheck(t, inspection) {
|
|
|
5298
5693
|
};
|
|
5299
5694
|
}
|
|
5300
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
|
+
|
|
5301
5829
|
// src/services/doctor-scope-lint.ts
|
|
5302
5830
|
import { readFile as readFile9 } from "fs/promises";
|
|
5303
5831
|
import { join as join14 } from "path";
|
|
@@ -5762,17 +6290,17 @@ async function inspectEventsJsonlGates(projectRoot, options = {}) {
|
|
|
5762
6290
|
let ledgerSizeBytes = 0;
|
|
5763
6291
|
let ledgerStalenessMs = null;
|
|
5764
6292
|
try {
|
|
5765
|
-
const
|
|
5766
|
-
ledgerSizeBytes =
|
|
5767
|
-
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);
|
|
5768
6296
|
} catch (error) {
|
|
5769
6297
|
if (!(isNodeError(error) && error.code === "ENOENT")) throw error;
|
|
5770
6298
|
}
|
|
5771
6299
|
let metricsStalenessMs = null;
|
|
5772
6300
|
if (existsSync8(metricsPath)) {
|
|
5773
6301
|
try {
|
|
5774
|
-
const
|
|
5775
|
-
metricsStalenessMs = Math.max(0, now.getTime() -
|
|
6302
|
+
const stat5 = await fs.stat(metricsPath);
|
|
6303
|
+
metricsStalenessMs = Math.max(0, now.getTime() - stat5.mtimeMs);
|
|
5776
6304
|
} catch {
|
|
5777
6305
|
}
|
|
5778
6306
|
}
|
|
@@ -5813,16 +6341,7 @@ async function inspectEventsJsonlGates(projectRoot, options = {}) {
|
|
|
5813
6341
|
// src/services/doctor-skill-lints.ts
|
|
5814
6342
|
import { readdir as readdir3, readFile as readFile10 } from "fs/promises";
|
|
5815
6343
|
import { join as join17, posix as posix2 } from "path";
|
|
5816
|
-
var FABRIC_SKILL_SLUGS = ["fabric-archive", "fabric-review"
|
|
5817
|
-
var ROUTER_VALID_LEAF_SLUGS = /* @__PURE__ */ new Set([
|
|
5818
|
-
"fabric-archive",
|
|
5819
|
-
"fabric-review",
|
|
5820
|
-
"fabric-import",
|
|
5821
|
-
"fabric-sync",
|
|
5822
|
-
"fabric-store",
|
|
5823
|
-
"fabric-audit",
|
|
5824
|
-
"fabric-connect"
|
|
5825
|
-
]);
|
|
6344
|
+
var FABRIC_SKILL_SLUGS = ["fabric-archive", "fabric-review"];
|
|
5826
6345
|
var SKILL_MD_FRONTMATTER_ROOTS = [".claude/skills", ".codex/skills"];
|
|
5827
6346
|
var SKILL_FRONTMATTER_KEY_PATTERN = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]+(.+?)[ \t]*$/u;
|
|
5828
6347
|
var SKILL_QUOTED_VALUE_LEADS = /* @__PURE__ */ new Set(['"', "'", "[", "{", ">", "|"]);
|
|
@@ -6007,68 +6526,6 @@ function extractSkillFrontmatterLines(raw) {
|
|
|
6007
6526
|
}
|
|
6008
6527
|
return null;
|
|
6009
6528
|
}
|
|
6010
|
-
function extractMarkdownSectionBody(markdown, sectionName) {
|
|
6011
|
-
const lines = markdown.split(/\r?\n/u);
|
|
6012
|
-
const headingRe = /^(#{2,3})\s+(.+?)\s*$/u;
|
|
6013
|
-
let start = -1;
|
|
6014
|
-
for (let i = 0; i < lines.length; i++) {
|
|
6015
|
-
const h = headingRe.exec(lines[i]);
|
|
6016
|
-
if (h && h[2] === sectionName) {
|
|
6017
|
-
start = i + 1;
|
|
6018
|
-
break;
|
|
6019
|
-
}
|
|
6020
|
-
}
|
|
6021
|
-
if (start === -1) return null;
|
|
6022
|
-
const out = [];
|
|
6023
|
-
for (let i = start; i < lines.length; i++) {
|
|
6024
|
-
if (headingRe.test(lines[i])) break;
|
|
6025
|
-
out.push(lines[i]);
|
|
6026
|
-
}
|
|
6027
|
-
return out.join("\n");
|
|
6028
|
-
}
|
|
6029
|
-
async function inspectRouterChainRef(projectRoot) {
|
|
6030
|
-
const candidatePaths = [
|
|
6031
|
-
join17(projectRoot, ".claude", "skills", "fabric", "SKILL.md"),
|
|
6032
|
-
join17(projectRoot, ".codex", "skills", "fabric", "SKILL.md")
|
|
6033
|
-
];
|
|
6034
|
-
let body = null;
|
|
6035
|
-
for (const candidate of candidatePaths) {
|
|
6036
|
-
try {
|
|
6037
|
-
body = await readFile10(candidate, "utf8");
|
|
6038
|
-
break;
|
|
6039
|
-
} catch {
|
|
6040
|
-
}
|
|
6041
|
-
}
|
|
6042
|
-
if (body === null) return { status: "ok", unknownRefs: [] };
|
|
6043
|
-
const chainSection = extractMarkdownSectionBody(body, "S_CHAIN");
|
|
6044
|
-
if (chainSection === null) return { status: "ok", unknownRefs: [] };
|
|
6045
|
-
const refs = /* @__PURE__ */ new Set();
|
|
6046
|
-
const tokenRe = /`(fabric-[a-z]+)`/gu;
|
|
6047
|
-
let match;
|
|
6048
|
-
while ((match = tokenRe.exec(chainSection)) !== null) {
|
|
6049
|
-
refs.add(match[1]);
|
|
6050
|
-
}
|
|
6051
|
-
const unknownRefs = [...refs].filter((slug) => !ROUTER_VALID_LEAF_SLUGS.has(slug)).sort();
|
|
6052
|
-
return unknownRefs.length === 0 ? { status: "ok", unknownRefs: [] } : { status: "drift", unknownRefs };
|
|
6053
|
-
}
|
|
6054
|
-
function createRouterChainRefCheck(t, inspection) {
|
|
6055
|
-
if (inspection.status === "ok") {
|
|
6056
|
-
return okCheck(t("doctor.check.router_chain_ref.name"), t("doctor.check.router_chain_ref.ok"));
|
|
6057
|
-
}
|
|
6058
|
-
const count = inspection.unknownRefs.length;
|
|
6059
|
-
return issueCheck(
|
|
6060
|
-
t("doctor.check.router_chain_ref.name"),
|
|
6061
|
-
"warn",
|
|
6062
|
-
"warning",
|
|
6063
|
-
"router_chain_ref_drift",
|
|
6064
|
-
t(`doctor.check.router_chain_ref.message.${count === 1 ? "singular" : "plural"}`, {
|
|
6065
|
-
count: String(count),
|
|
6066
|
-
list: inspection.unknownRefs.join(", ")
|
|
6067
|
-
}),
|
|
6068
|
-
t("doctor.check.router_chain_ref.remediation"),
|
|
6069
|
-
"maintainer"
|
|
6070
|
-
);
|
|
6071
|
-
}
|
|
6072
6529
|
function createSkillRefMirrorCheck(t, inspection) {
|
|
6073
6530
|
if (inspection.status === "ok") {
|
|
6074
6531
|
return okCheck(t("doctor.check.skill_ref_mirror.name"), t("doctor.check.skill_ref_mirror.ok"));
|
|
@@ -6143,10 +6600,130 @@ function createSkillMdYamlInvalidCheck(t, inspection) {
|
|
|
6143
6600
|
);
|
|
6144
6601
|
}
|
|
6145
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
|
+
|
|
6146
6723
|
// src/services/doctor-hooks-lints.ts
|
|
6147
6724
|
import { constants } from "fs";
|
|
6148
|
-
import { access, readdir as
|
|
6149
|
-
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";
|
|
6150
6727
|
import { Script } from "vm";
|
|
6151
6728
|
var HOOKS_RUNTIME_CLIENT_DIRS = [
|
|
6152
6729
|
{ client: "claude", dir: ".claude/hooks" },
|
|
@@ -6170,7 +6747,7 @@ function isRecord(value) {
|
|
|
6170
6747
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6171
6748
|
}
|
|
6172
6749
|
function normalizePath(path) {
|
|
6173
|
-
return
|
|
6750
|
+
return posix4.normalize(path.split("\\").join("/"));
|
|
6174
6751
|
}
|
|
6175
6752
|
function isNodeMissingPathError(error) {
|
|
6176
6753
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
@@ -6195,27 +6772,27 @@ function isHookWiredForEvent(hooks, event, hookFile) {
|
|
|
6195
6772
|
}
|
|
6196
6773
|
async function readDirectoryFileNames(dir) {
|
|
6197
6774
|
try {
|
|
6198
|
-
return await
|
|
6775
|
+
return await readdir5(dir);
|
|
6199
6776
|
} catch {
|
|
6200
6777
|
return null;
|
|
6201
6778
|
}
|
|
6202
6779
|
}
|
|
6203
6780
|
async function isFile(absPath) {
|
|
6204
6781
|
try {
|
|
6205
|
-
return (await
|
|
6782
|
+
return (await stat4(absPath)).isFile();
|
|
6206
6783
|
} catch {
|
|
6207
6784
|
return false;
|
|
6208
6785
|
}
|
|
6209
6786
|
}
|
|
6210
6787
|
async function inspectHooksWired(projectRoot) {
|
|
6211
|
-
const claudeEntries = await readDirectoryFileNames(
|
|
6788
|
+
const claudeEntries = await readDirectoryFileNames(join19(projectRoot, ".claude"));
|
|
6212
6789
|
if (claudeEntries === null) {
|
|
6213
6790
|
return { status: "skipped", missingHooks: [] };
|
|
6214
6791
|
}
|
|
6215
|
-
const settingsPath =
|
|
6792
|
+
const settingsPath = join19(projectRoot, ".claude", "settings.json");
|
|
6216
6793
|
let parsed;
|
|
6217
6794
|
try {
|
|
6218
|
-
parsed = JSON.parse(await
|
|
6795
|
+
parsed = JSON.parse(await readFile12(settingsPath, "utf8"));
|
|
6219
6796
|
} catch {
|
|
6220
6797
|
return { status: "missing-settings", missingHooks: [] };
|
|
6221
6798
|
}
|
|
@@ -6237,12 +6814,12 @@ async function inspectHooksWired(projectRoot) {
|
|
|
6237
6814
|
return { status: "incomplete", missingHooks: missing };
|
|
6238
6815
|
}
|
|
6239
6816
|
async function inspectHookCacheWritability(projectRoot) {
|
|
6240
|
-
const relPath =
|
|
6241
|
-
const fabricDir =
|
|
6242
|
-
const cacheDir =
|
|
6817
|
+
const relPath = posix4.join(".fabric", ".cache");
|
|
6818
|
+
const fabricDir = join19(projectRoot, ".fabric");
|
|
6819
|
+
const cacheDir = join19(projectRoot, ".fabric", ".cache");
|
|
6243
6820
|
try {
|
|
6244
6821
|
try {
|
|
6245
|
-
const cacheStats = await
|
|
6822
|
+
const cacheStats = await stat4(cacheDir);
|
|
6246
6823
|
if (!cacheStats.isDirectory()) {
|
|
6247
6824
|
return {
|
|
6248
6825
|
writable: false,
|
|
@@ -6259,14 +6836,14 @@ async function inspectHookCacheWritability(projectRoot) {
|
|
|
6259
6836
|
}
|
|
6260
6837
|
let parent = fabricDir;
|
|
6261
6838
|
try {
|
|
6262
|
-
await
|
|
6839
|
+
await stat4(fabricDir);
|
|
6263
6840
|
} catch (error) {
|
|
6264
6841
|
if (!isNodeMissingPathError(error)) {
|
|
6265
6842
|
throw error;
|
|
6266
6843
|
}
|
|
6267
6844
|
parent = projectRoot;
|
|
6268
6845
|
}
|
|
6269
|
-
const parentStats = await
|
|
6846
|
+
const parentStats = await stat4(parent);
|
|
6270
6847
|
if (!parentStats.isDirectory()) {
|
|
6271
6848
|
return {
|
|
6272
6849
|
writable: false,
|
|
@@ -6288,12 +6865,12 @@ async function inspectHookCacheWritability(projectRoot) {
|
|
|
6288
6865
|
async function inspectHooksContentDrift(projectRoot) {
|
|
6289
6866
|
const hookFilesByBasename = /* @__PURE__ */ new Map();
|
|
6290
6867
|
for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
|
|
6291
|
-
const absDir =
|
|
6868
|
+
const absDir = join19(projectRoot, dir);
|
|
6292
6869
|
const entries = await readDirectoryFileNames(absDir);
|
|
6293
6870
|
if (entries === null) continue;
|
|
6294
6871
|
for (const name of entries) {
|
|
6295
6872
|
if (!name.endsWith(".cjs")) continue;
|
|
6296
|
-
const abs =
|
|
6873
|
+
const abs = join19(absDir, name);
|
|
6297
6874
|
if (!await isFile(abs)) continue;
|
|
6298
6875
|
const arr = hookFilesByBasename.get(name) ?? [];
|
|
6299
6876
|
arr.push({ client, abs });
|
|
@@ -6302,13 +6879,13 @@ async function inspectHooksContentDrift(projectRoot) {
|
|
|
6302
6879
|
}
|
|
6303
6880
|
const drifts = [];
|
|
6304
6881
|
let scanned = 0;
|
|
6305
|
-
for (const [
|
|
6882
|
+
for (const [basename4, copies] of hookFilesByBasename) {
|
|
6306
6883
|
if (copies.length < 2) continue;
|
|
6307
6884
|
scanned += copies.length;
|
|
6308
6885
|
const hashes = [];
|
|
6309
6886
|
for (const { client, abs } of copies) {
|
|
6310
6887
|
try {
|
|
6311
|
-
const body = await
|
|
6888
|
+
const body = await readFile12(abs, "utf8");
|
|
6312
6889
|
hashes.push({ client, sha: sha256(body) });
|
|
6313
6890
|
} catch {
|
|
6314
6891
|
}
|
|
@@ -6317,7 +6894,7 @@ async function inspectHooksContentDrift(projectRoot) {
|
|
|
6317
6894
|
const first = hashes[0].sha;
|
|
6318
6895
|
if (hashes.some((h) => h.sha !== first)) {
|
|
6319
6896
|
drifts.push({
|
|
6320
|
-
basename:
|
|
6897
|
+
basename: basename4,
|
|
6321
6898
|
clients: copies.map((copy) => copy.client),
|
|
6322
6899
|
hashes
|
|
6323
6900
|
});
|
|
@@ -6330,18 +6907,18 @@ async function inspectHooksRuntime(projectRoot) {
|
|
|
6330
6907
|
const issues = [];
|
|
6331
6908
|
let scanned = 0;
|
|
6332
6909
|
for (const { client, dir } of HOOKS_RUNTIME_CLIENT_DIRS) {
|
|
6333
|
-
const absDir =
|
|
6910
|
+
const absDir = join19(projectRoot, dir);
|
|
6334
6911
|
const entries = await readDirectoryFileNames(absDir);
|
|
6335
6912
|
if (entries === null) continue;
|
|
6336
6913
|
for (const name of entries) {
|
|
6337
6914
|
if (!name.endsWith(".cjs")) continue;
|
|
6338
|
-
const abs =
|
|
6915
|
+
const abs = join19(absDir, name);
|
|
6339
6916
|
const displayPath = `${dir}/${name}`;
|
|
6340
6917
|
if (!await isFile(abs)) continue;
|
|
6341
6918
|
scanned += 1;
|
|
6342
6919
|
let body;
|
|
6343
6920
|
try {
|
|
6344
|
-
body = await
|
|
6921
|
+
body = await readFile12(abs, "utf8");
|
|
6345
6922
|
} catch (err) {
|
|
6346
6923
|
issues.push({
|
|
6347
6924
|
path: displayPath,
|
|
@@ -6478,8 +7055,8 @@ function createHookCacheWritabilityCheck(t, inspection) {
|
|
|
6478
7055
|
}
|
|
6479
7056
|
|
|
6480
7057
|
// src/services/doctor-bootstrap-lints.ts
|
|
6481
|
-
import { access as access2, readFile as
|
|
6482
|
-
import { join as
|
|
7058
|
+
import { access as access2, readFile as readFile13 } from "fs/promises";
|
|
7059
|
+
import { join as join20 } from "path";
|
|
6483
7060
|
import {
|
|
6484
7061
|
BOOTSTRAP_MARKER_BEGIN,
|
|
6485
7062
|
BOOTSTRAP_MARKER_END,
|
|
@@ -6511,17 +7088,17 @@ async function fileExists(path) {
|
|
|
6511
7088
|
}
|
|
6512
7089
|
async function inspectBootstrapAnchor(projectRoot) {
|
|
6513
7090
|
const [hasAgentsMd, hasClaudeMd] = await Promise.all([
|
|
6514
|
-
fileExists(
|
|
6515
|
-
fileExists(
|
|
7091
|
+
fileExists(join20(projectRoot, "AGENTS.md")),
|
|
7092
|
+
fileExists(join20(projectRoot, "CLAUDE.md"))
|
|
6516
7093
|
]);
|
|
6517
7094
|
return { hasAgentsMd, hasClaudeMd };
|
|
6518
7095
|
}
|
|
6519
7096
|
async function inspectL1BootstrapSnapshotDrift(target) {
|
|
6520
|
-
const abs =
|
|
7097
|
+
const abs = join20(target, ".fabric", "AGENTS.md");
|
|
6521
7098
|
const canonical = resolveBootstrapCanonical();
|
|
6522
7099
|
let onDisk;
|
|
6523
7100
|
try {
|
|
6524
|
-
onDisk = await
|
|
7101
|
+
onDisk = await readFile13(abs, "utf8");
|
|
6525
7102
|
} catch {
|
|
6526
7103
|
return { status: "missing", canonical, onDisk: null };
|
|
6527
7104
|
}
|
|
@@ -6547,17 +7124,17 @@ function createL1BootstrapSnapshotDriftCheck(t, inspection) {
|
|
|
6547
7124
|
);
|
|
6548
7125
|
}
|
|
6549
7126
|
async function inspectL2ManagedBlockDrift(target) {
|
|
6550
|
-
const snapshotPath =
|
|
7127
|
+
const snapshotPath = join20(target, ".fabric", "AGENTS.md");
|
|
6551
7128
|
let snapshot;
|
|
6552
7129
|
try {
|
|
6553
|
-
snapshot = await
|
|
7130
|
+
snapshot = await readFile13(snapshotPath, "utf8");
|
|
6554
7131
|
} catch {
|
|
6555
7132
|
return { status: "ok", drifted: [] };
|
|
6556
7133
|
}
|
|
6557
|
-
const projectRulesPath =
|
|
7134
|
+
const projectRulesPath = join20(target, ".fabric", "project-rules.md");
|
|
6558
7135
|
let expectedBody = snapshot;
|
|
6559
7136
|
try {
|
|
6560
|
-
const projectRules = await
|
|
7137
|
+
const projectRules = await readFile13(projectRulesPath, "utf8");
|
|
6561
7138
|
expectedBody = `${snapshot}
|
|
6562
7139
|
---
|
|
6563
7140
|
${projectRules}`;
|
|
@@ -6566,12 +7143,12 @@ ${projectRules}`;
|
|
|
6566
7143
|
const drifted = [];
|
|
6567
7144
|
let anyManagedBlockFound = false;
|
|
6568
7145
|
const blockTargets = [
|
|
6569
|
-
|
|
7146
|
+
join20(target, "AGENTS.md")
|
|
6570
7147
|
];
|
|
6571
7148
|
for (const abs of blockTargets) {
|
|
6572
7149
|
let content;
|
|
6573
7150
|
try {
|
|
6574
|
-
content = await
|
|
7151
|
+
content = await readFile13(abs, "utf8");
|
|
6575
7152
|
} catch {
|
|
6576
7153
|
continue;
|
|
6577
7154
|
}
|
|
@@ -6594,9 +7171,9 @@ ${projectRules}`;
|
|
|
6594
7171
|
drifted.push({ path: abs, expected: expectedBody, actual: body });
|
|
6595
7172
|
}
|
|
6596
7173
|
}
|
|
6597
|
-
const claudeMdPath =
|
|
7174
|
+
const claudeMdPath = join20(target, "CLAUDE.md");
|
|
6598
7175
|
try {
|
|
6599
|
-
const claudeContent = await
|
|
7176
|
+
const claudeContent = await readFile13(claudeMdPath, "utf8");
|
|
6600
7177
|
anyManagedBlockFound = true;
|
|
6601
7178
|
const lines = claudeContent.split(/\r?\n/u);
|
|
6602
7179
|
const hasAtImport = lines.some((line) => line.trim() === "@.fabric/AGENTS.md");
|
|
@@ -6664,7 +7241,7 @@ import { appendFile as appendFile3 } from "fs/promises";
|
|
|
6664
7241
|
import { minimatch as minimatch2 } from "minimatch";
|
|
6665
7242
|
|
|
6666
7243
|
// src/services/cite-rollup.ts
|
|
6667
|
-
import { readFile as
|
|
7244
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
6668
7245
|
import { createLedgerWriteQueue as createLedgerWriteQueue3 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
6669
7246
|
var citeRollupQueue = createLedgerWriteQueue3();
|
|
6670
7247
|
async function appendCiteRollupRow(projectRoot, row) {
|
|
@@ -6676,7 +7253,7 @@ async function readCiteRollup(projectRoot) {
|
|
|
6676
7253
|
const path = getCiteRollupPath(projectRoot);
|
|
6677
7254
|
let raw;
|
|
6678
7255
|
try {
|
|
6679
|
-
raw = await
|
|
7256
|
+
raw = await readFile14(path, "utf8");
|
|
6680
7257
|
} catch (error) {
|
|
6681
7258
|
if (isNodeError(error) && error.code === "ENOENT") return [];
|
|
6682
7259
|
throw error;
|
|
@@ -7684,13 +8261,13 @@ var DEFAULT_UNDERSEED_NODE_THRESHOLD = 10;
|
|
|
7684
8261
|
var SESSION_HINTS_STALE_DAYS = 7;
|
|
7685
8262
|
var SESSION_HINTS_FILE_PREFIX = "session-hints-";
|
|
7686
8263
|
var SESSION_HINTS_FILE_SUFFIX = ".json";
|
|
7687
|
-
var EDIT_COUNTER_FILE_REL =
|
|
7688
|
-
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(
|
|
7689
8266
|
".fabric",
|
|
7690
8267
|
".cache",
|
|
7691
8268
|
"hint-silence-counter"
|
|
7692
8269
|
);
|
|
7693
|
-
var
|
|
8270
|
+
var MS_PER_DAY4 = 24 * 60 * 60 * 1e3;
|
|
7694
8271
|
var KNOWLEDGE_CANONICAL_TYPE_DIRS = [
|
|
7695
8272
|
"decisions",
|
|
7696
8273
|
"pitfalls",
|
|
@@ -7738,7 +8315,8 @@ async function runDoctorReport(target) {
|
|
|
7738
8315
|
l2ManagedBlockDrift,
|
|
7739
8316
|
skillRefMirror,
|
|
7740
8317
|
skillTokenBudget,
|
|
7741
|
-
skillDescription
|
|
8318
|
+
skillDescription,
|
|
8319
|
+
retiredReferences
|
|
7742
8320
|
] = await Promise.all([
|
|
7743
8321
|
inspectForensic(projectRoot),
|
|
7744
8322
|
// v2.2 W5 R4 (agents.meta decolo): `inspectMeta` (read co-location
|
|
@@ -7758,7 +8336,10 @@ async function runDoctorReport(target) {
|
|
|
7758
8336
|
inspectL2ManagedBlockDrift(projectRoot),
|
|
7759
8337
|
inspectSkillRefMirror(projectRoot),
|
|
7760
8338
|
inspectSkillTokenBudget(projectRoot),
|
|
7761
|
-
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)
|
|
7762
8343
|
]);
|
|
7763
8344
|
const citeGoodhart = await inspectCiteGoodhart(projectRoot);
|
|
7764
8345
|
const storeKnowledgeSummaries = await collectStoreKnowledgeSummaries(projectRoot);
|
|
@@ -7779,6 +8360,8 @@ async function runDoctorReport(target) {
|
|
|
7779
8360
|
const lintNow = Date.now();
|
|
7780
8361
|
const lastActiveIndex = await buildLastActiveIndex(projectRoot);
|
|
7781
8362
|
const knowledgeAge = await inspectStoreKnowledgeAge(projectRoot, lintNow, lastActiveIndex);
|
|
8363
|
+
const knowledgePromotion = await inspectStoreKnowledgePromotion(projectRoot);
|
|
8364
|
+
const broadReviewRecheck = await inspectStoreBroadReviewRecheck(projectRoot, lintNow);
|
|
7782
8365
|
const preexistingRootFiles = await inspectPreexistingRootFiles(projectRoot);
|
|
7783
8366
|
const underseedThreshold = await readUnderseedThresholdFromConfig(projectRoot);
|
|
7784
8367
|
const underseeded = {
|
|
@@ -7790,7 +8373,6 @@ async function runDoctorReport(target) {
|
|
|
7790
8373
|
const hookCacheWritability = await inspectHookCacheWritability(projectRoot);
|
|
7791
8374
|
const staleServeLock = inspectStaleServeLock(projectRoot, lintNow);
|
|
7792
8375
|
const skillMdYamlInvalid = await inspectSkillMdYamlInvalid(projectRoot);
|
|
7793
|
-
const routerChainRef = await inspectRouterChainRef(projectRoot);
|
|
7794
8376
|
const onboardCoverage = await inspectOnboardCoverage(projectRoot);
|
|
7795
8377
|
const [hooksWired, hooksRuntime, hooksContentDrift] = await Promise.all([
|
|
7796
8378
|
inspectHooksWired(projectRoot),
|
|
@@ -7801,7 +8383,7 @@ async function runDoctorReport(target) {
|
|
|
7801
8383
|
const globalCliVersion = process.env.VITEST === "true" ? { status: "ok", version: "test-skipped" } : inspectGlobalCliVersion();
|
|
7802
8384
|
const targetFiles = Object.fromEntries(
|
|
7803
8385
|
await Promise.all(
|
|
7804
|
-
TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(
|
|
8386
|
+
TARGET_FILE_PATHS.map(async (path) => [path, await pathExists(join21(projectRoot, path))])
|
|
7805
8387
|
)
|
|
7806
8388
|
);
|
|
7807
8389
|
const checks = [
|
|
@@ -7840,6 +8422,8 @@ async function runDoctorReport(target) {
|
|
|
7840
8422
|
createSkillRefMirrorCheck(t, skillRefMirror),
|
|
7841
8423
|
createSkillTokenBudgetCheck(t, skillTokenBudget),
|
|
7842
8424
|
createSkillDescriptionCheck(t, skillDescription),
|
|
8425
|
+
// ux-w2-2: retired-reference (stale-pointer) lint — registry-driven.
|
|
8426
|
+
createRetiredReferenceCheck(t, retiredReferences),
|
|
7843
8427
|
createCiteGoodhartCheck(t, citeGoodhart),
|
|
7844
8428
|
createDraftBacklogCheck(t, draftBacklog),
|
|
7845
8429
|
createKnowledgeTagsEmptyCheck(t, knowledgeTagsEmpty),
|
|
@@ -7867,9 +8451,6 @@ async function runDoctorReport(target) {
|
|
|
7867
8451
|
// rc.12 lint #29: skill_md_yaml_invalid. Warning kind — surfaces
|
|
7868
8452
|
// SKILL.md frontmatter that Codex CLI silently drops at load.
|
|
7869
8453
|
createSkillMdYamlInvalidCheck(t, skillMdYamlInvalid),
|
|
7870
|
-
// B2 skill-router (A4): S_CHAIN reference backstop. Warning kind — flags an
|
|
7871
|
-
// S_CHAIN `fabric-*` reference to a leaf no longer in the install set.
|
|
7872
|
-
createRouterChainRefCheck(t, routerChainRef),
|
|
7873
8454
|
// v2.0.0-rc.23 TASK-014 (F8c): Onboard coverage advisory. Info kind.
|
|
7874
8455
|
// Surfaces uncovered S5 onboard slots and recommends /fabric-archive
|
|
7875
8456
|
// first-run phase. Sits adjacent to Skill markdown YAML — both are
|
|
@@ -7921,6 +8502,11 @@ async function runDoctorReport(target) {
|
|
|
7921
8502
|
// thresholds 90/30/14d per maturity tier (KT-DEC-0008).
|
|
7922
8503
|
createOrphanDemoteCheck(t, knowledgeAge.orphanDemote),
|
|
7923
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),
|
|
7924
8510
|
// project-scope binding backfill lint — a store bound as the write target
|
|
7925
8511
|
// but with no project_id / active_project parks the project axis. The
|
|
7926
8512
|
// fresh-install hole is sealed in store.stage.ts; this covers existing repos
|
|
@@ -8001,7 +8587,7 @@ async function runDoctorFix(target) {
|
|
|
8001
8587
|
const fixed = [];
|
|
8002
8588
|
const ledgerWarnings = [];
|
|
8003
8589
|
if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
|
|
8004
|
-
const snapshotPath =
|
|
8590
|
+
const snapshotPath = join21(projectRoot, ".fabric", "AGENTS.md");
|
|
8005
8591
|
await ensureParentDirectory(snapshotPath);
|
|
8006
8592
|
await atomicWriteText4(snapshotPath, resolveBootstrapCanonical2());
|
|
8007
8593
|
fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
|
|
@@ -8074,7 +8660,7 @@ async function runDoctorFix(target) {
|
|
|
8074
8660
|
if (before.infos.some((issue) => issue.code === "stale_serve_lock")) {
|
|
8075
8661
|
const lockInspection = inspectStaleServeLock(projectRoot, Date.now());
|
|
8076
8662
|
if (lockInspection.present && !lockInspection.pidAlive) {
|
|
8077
|
-
const lockFilePath =
|
|
8663
|
+
const lockFilePath = join21(projectRoot, ".fabric", ".serve.lock");
|
|
8078
8664
|
try {
|
|
8079
8665
|
await unlink4(lockFilePath);
|
|
8080
8666
|
} catch (err) {
|
|
@@ -8191,7 +8777,7 @@ function createApplyLintMessage(succeeded, failed, manualErrorCount) {
|
|
|
8191
8777
|
}
|
|
8192
8778
|
async function applySessionHintsStaleCleanup(projectRoot, candidate) {
|
|
8193
8779
|
const detail = `deleted (${candidate.age_days}d old)`;
|
|
8194
|
-
const absPath =
|
|
8780
|
+
const absPath = join21(projectRoot, candidate.path);
|
|
8195
8781
|
try {
|
|
8196
8782
|
const { unlink: unlink5 } = await import("fs/promises");
|
|
8197
8783
|
await unlink5(absPath);
|
|
@@ -8216,9 +8802,9 @@ function truncateErrorMessage(error) {
|
|
|
8216
8802
|
return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
|
|
8217
8803
|
}
|
|
8218
8804
|
async function inspectForensic(projectRoot) {
|
|
8219
|
-
const path =
|
|
8805
|
+
const path = join21(projectRoot, ".fabric", "forensic.json");
|
|
8220
8806
|
try {
|
|
8221
|
-
const parsed = forensicReportSchema.parse(JSON.parse(await
|
|
8807
|
+
const parsed = forensicReportSchema.parse(JSON.parse(await readFile16(path, "utf8")));
|
|
8222
8808
|
return { present: true, valid: true, report: parsed };
|
|
8223
8809
|
} catch (error) {
|
|
8224
8810
|
if (isMissingFileError(error)) {
|
|
@@ -8248,7 +8834,7 @@ async function inspectEventLedger(projectRoot) {
|
|
|
8248
8834
|
try {
|
|
8249
8835
|
await access4(path, constants2.W_OK);
|
|
8250
8836
|
const { warnings } = await readEventLedger(projectRoot);
|
|
8251
|
-
const raw = await
|
|
8837
|
+
const raw = await readFile16(path, "utf8");
|
|
8252
8838
|
const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
|
|
8253
8839
|
const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
|
|
8254
8840
|
const schemaVersionSamples = [];
|
|
@@ -8793,7 +9379,7 @@ async function inspectPreexistingRootFiles(projectRoot) {
|
|
|
8793
9379
|
const candidates = ["CLAUDE.md", "AGENTS.md"];
|
|
8794
9380
|
const detected = [];
|
|
8795
9381
|
for (const name of candidates) {
|
|
8796
|
-
if (await pathExists(
|
|
9382
|
+
if (await pathExists(join21(projectRoot, name))) {
|
|
8797
9383
|
detected.push(name);
|
|
8798
9384
|
}
|
|
8799
9385
|
}
|
|
@@ -8878,7 +9464,7 @@ async function buildLastActiveIndex(projectRoot) {
|
|
|
8878
9464
|
return map;
|
|
8879
9465
|
}
|
|
8880
9466
|
async function inspectSessionHintsStale(projectRoot, now) {
|
|
8881
|
-
const cacheDir =
|
|
9467
|
+
const cacheDir = join21(projectRoot, ".fabric", ".cache");
|
|
8882
9468
|
let entries;
|
|
8883
9469
|
try {
|
|
8884
9470
|
entries = await readdirAsync(cacheDir, { withFileTypes: true });
|
|
@@ -8890,17 +9476,17 @@ async function inspectSessionHintsStale(projectRoot, now) {
|
|
|
8890
9476
|
if (!entry.isFile()) continue;
|
|
8891
9477
|
if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
|
|
8892
9478
|
if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
|
|
8893
|
-
const absPath =
|
|
9479
|
+
const absPath = join21(cacheDir, entry.name);
|
|
8894
9480
|
let mtimeMs = 0;
|
|
8895
9481
|
try {
|
|
8896
9482
|
mtimeMs = (await statAsync(absPath)).mtimeMs;
|
|
8897
9483
|
} catch {
|
|
8898
9484
|
continue;
|
|
8899
9485
|
}
|
|
8900
|
-
const ageDays = Math.floor((now - mtimeMs) /
|
|
9486
|
+
const ageDays = Math.floor((now - mtimeMs) / MS_PER_DAY4);
|
|
8901
9487
|
if (ageDays < SESSION_HINTS_STALE_DAYS) continue;
|
|
8902
9488
|
candidates.push({
|
|
8903
|
-
path:
|
|
9489
|
+
path: posix5.join(".fabric", ".cache", entry.name),
|
|
8904
9490
|
age_days: ageDays
|
|
8905
9491
|
});
|
|
8906
9492
|
}
|
|
@@ -8922,9 +9508,9 @@ function inspectStaleServeLock(projectRoot, now) {
|
|
|
8922
9508
|
};
|
|
8923
9509
|
}
|
|
8924
9510
|
async function readUnderseedThresholdFromConfig(projectRoot) {
|
|
8925
|
-
const configPath =
|
|
9511
|
+
const configPath = join21(projectRoot, ".fabric", "fabric-config.json");
|
|
8926
9512
|
try {
|
|
8927
|
-
const raw = await
|
|
9513
|
+
const raw = await readFile16(configPath, "utf8");
|
|
8928
9514
|
const parsed = JSON.parse(raw);
|
|
8929
9515
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
8930
9516
|
const v = parsed.underseed_node_threshold;
|
|
@@ -8998,7 +9584,7 @@ function createStaleServeLockCheck(t, inspection) {
|
|
|
8998
9584
|
})
|
|
8999
9585
|
);
|
|
9000
9586
|
}
|
|
9001
|
-
const days = Math.floor(inspection.ageMs /
|
|
9587
|
+
const days = Math.floor(inspection.ageMs / MS_PER_DAY4);
|
|
9002
9588
|
const hours = Math.floor(inspection.ageMs / (60 * 60 * 1e3));
|
|
9003
9589
|
const acquiredAgo = days >= 1 ? t(`doctor.check.stale_serve_lock.age.day.${days === 1 ? "singular" : "plural"}`, {
|
|
9004
9590
|
count: String(days)
|
|
@@ -9040,10 +9626,10 @@ async function inspectOnboardCoverage(projectRoot) {
|
|
|
9040
9626
|
return { filled, missing, opted_out: optedOut };
|
|
9041
9627
|
}
|
|
9042
9628
|
async function readOnboardOptedOut(projectRoot) {
|
|
9043
|
-
const path =
|
|
9629
|
+
const path = join21(projectRoot, ".fabric", "fabric-config.json");
|
|
9044
9630
|
let raw;
|
|
9045
9631
|
try {
|
|
9046
|
-
raw = await
|
|
9632
|
+
raw = await readFile16(path, "utf8");
|
|
9047
9633
|
} catch {
|
|
9048
9634
|
return [];
|
|
9049
9635
|
}
|
|
@@ -9128,7 +9714,7 @@ async function* iterateCanonicalFilenames(projectRoot) {
|
|
|
9128
9714
|
if (!KNOWLEDGE_CANONICAL_TYPE_DIRS.includes(entry.type)) {
|
|
9129
9715
|
continue;
|
|
9130
9716
|
}
|
|
9131
|
-
const filename =
|
|
9717
|
+
const filename = posix5.basename(normalizePath2(entry.file));
|
|
9132
9718
|
const parsed = parseStableIdFromCanonicalFilename(filename);
|
|
9133
9719
|
if (parsed === null) {
|
|
9134
9720
|
continue;
|
|
@@ -9145,22 +9731,22 @@ async function* iterateCanonicalFilenames(projectRoot) {
|
|
|
9145
9731
|
}
|
|
9146
9732
|
}
|
|
9147
9733
|
async function rewriteThreeEndManagedBlocks(projectRoot) {
|
|
9148
|
-
const snapshotPath =
|
|
9734
|
+
const snapshotPath = join21(projectRoot, ".fabric", "AGENTS.md");
|
|
9149
9735
|
if (!await pathExists(snapshotPath)) {
|
|
9150
9736
|
return;
|
|
9151
9737
|
}
|
|
9152
9738
|
let snapshot;
|
|
9153
9739
|
try {
|
|
9154
|
-
snapshot = await
|
|
9740
|
+
snapshot = await readFile16(snapshotPath, "utf8");
|
|
9155
9741
|
} catch {
|
|
9156
9742
|
return;
|
|
9157
9743
|
}
|
|
9158
|
-
const projectRulesPath =
|
|
9744
|
+
const projectRulesPath = join21(projectRoot, ".fabric", "project-rules.md");
|
|
9159
9745
|
const hasProjectRules = await pathExists(projectRulesPath);
|
|
9160
9746
|
let expectedBody = snapshot;
|
|
9161
9747
|
if (hasProjectRules) {
|
|
9162
9748
|
try {
|
|
9163
|
-
const projectRules = await
|
|
9749
|
+
const projectRules = await readFile16(projectRulesPath, "utf8");
|
|
9164
9750
|
expectedBody = `${snapshot}
|
|
9165
9751
|
---
|
|
9166
9752
|
${projectRules}`;
|
|
@@ -9171,7 +9757,7 @@ ${projectRules}`;
|
|
|
9171
9757
|
${expectedBody}
|
|
9172
9758
|
${BOOTSTRAP_MARKER_END2}`;
|
|
9173
9759
|
const blockTargets = [
|
|
9174
|
-
|
|
9760
|
+
join21(projectRoot, "AGENTS.md")
|
|
9175
9761
|
];
|
|
9176
9762
|
for (const abs of blockTargets) {
|
|
9177
9763
|
if (!await pathExists(abs)) {
|
|
@@ -9179,7 +9765,7 @@ ${BOOTSTRAP_MARKER_END2}`;
|
|
|
9179
9765
|
}
|
|
9180
9766
|
let existing;
|
|
9181
9767
|
try {
|
|
9182
|
-
existing = await
|
|
9768
|
+
existing = await readFile16(abs, "utf8");
|
|
9183
9769
|
} catch {
|
|
9184
9770
|
continue;
|
|
9185
9771
|
}
|
|
@@ -9204,11 +9790,11 @@ ${managedBlock}
|
|
|
9204
9790
|
}
|
|
9205
9791
|
await atomicWriteText4(abs, next);
|
|
9206
9792
|
}
|
|
9207
|
-
const claudeMdPath =
|
|
9793
|
+
const claudeMdPath = join21(projectRoot, "CLAUDE.md");
|
|
9208
9794
|
if (await pathExists(claudeMdPath)) {
|
|
9209
9795
|
let claudeContent;
|
|
9210
9796
|
try {
|
|
9211
|
-
claudeContent = await
|
|
9797
|
+
claudeContent = await readFile16(claudeMdPath, "utf8");
|
|
9212
9798
|
} catch {
|
|
9213
9799
|
return;
|
|
9214
9800
|
}
|
|
@@ -9254,7 +9840,7 @@ function normalizeTarget(targetInput) {
|
|
|
9254
9840
|
return isAbsolute2(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
|
|
9255
9841
|
}
|
|
9256
9842
|
function normalizePath2(path) {
|
|
9257
|
-
return
|
|
9843
|
+
return posix5.normalize(path.split("\\").join("/"));
|
|
9258
9844
|
}
|
|
9259
9845
|
async function collectEntryPoints(root) {
|
|
9260
9846
|
let rootStat;
|
|
@@ -9274,7 +9860,7 @@ async function collectEntryPoints(root) {
|
|
|
9274
9860
|
continue;
|
|
9275
9861
|
}
|
|
9276
9862
|
for (const entry of await readdirAsync(current, { withFileTypes: true })) {
|
|
9277
|
-
const absolutePath =
|
|
9863
|
+
const absolutePath = join21(current, entry.name);
|
|
9278
9864
|
const relativePath = normalizePath2(absolutePath.slice(root.length + 1));
|
|
9279
9865
|
if (relativePath.length === 0) {
|
|
9280
9866
|
continue;
|
|
@@ -9301,8 +9887,8 @@ function getEntryPointReason(relativePath) {
|
|
|
9301
9887
|
if (!SCRIPT_EXTENSIONS.has(extension)) {
|
|
9302
9888
|
return null;
|
|
9303
9889
|
}
|
|
9304
|
-
const directory =
|
|
9305
|
-
const fileName =
|
|
9890
|
+
const directory = posix5.dirname(relativePath);
|
|
9891
|
+
const fileName = posix5.basename(relativePath);
|
|
9306
9892
|
const fileBase = fileName.slice(0, Math.max(fileName.lastIndexOf("."), 0));
|
|
9307
9893
|
if (directory === "assets/scripts" || directory === "scripts") {
|
|
9308
9894
|
return "top-level script";
|
|
@@ -9350,7 +9936,7 @@ async function enrichDescriptions(projectRoot, opts = {}) {
|
|
|
9350
9936
|
scanned += 1;
|
|
9351
9937
|
let source;
|
|
9352
9938
|
try {
|
|
9353
|
-
source = await
|
|
9939
|
+
source = await readFile16(absPath, "utf8");
|
|
9354
9940
|
} catch {
|
|
9355
9941
|
continue;
|
|
9356
9942
|
}
|
|
@@ -9447,88 +10033,6 @@ function yamlQuoteIfNeeded(value) {
|
|
|
9447
10033
|
return value;
|
|
9448
10034
|
}
|
|
9449
10035
|
|
|
9450
|
-
// src/services/conflict-lint.ts
|
|
9451
|
-
var DEFAULT_CONFLICT_SIMILARITY_THRESHOLD = 0.5;
|
|
9452
|
-
function groupKey(entry) {
|
|
9453
|
-
return `${entry.layer}\0${entry.knowledge_type}`;
|
|
9454
|
-
}
|
|
9455
|
-
function pairSimilarity(model, a, b) {
|
|
9456
|
-
const selfA = model.scoreDoc(a.id, a.tokens);
|
|
9457
|
-
const selfB = model.scoreDoc(b.id, b.tokens);
|
|
9458
|
-
if (selfA <= 0 || selfB <= 0) return 0;
|
|
9459
|
-
const aToB = model.scoreDoc(b.id, a.tokens) / selfB;
|
|
9460
|
-
const bToA = model.scoreDoc(a.id, b.tokens) / selfA;
|
|
9461
|
-
const sim = Math.min(aToB, bToA);
|
|
9462
|
-
return sim < 0 ? 0 : sim > 1 ? 1 : sim;
|
|
9463
|
-
}
|
|
9464
|
-
function findConflictCandidates(entries, opts = {}) {
|
|
9465
|
-
const threshold = typeof opts.threshold === "number" && opts.threshold >= 0 && opts.threshold <= 1 ? opts.threshold : DEFAULT_CONFLICT_SIMILARITY_THRESHOLD;
|
|
9466
|
-
const groups = /* @__PURE__ */ new Map();
|
|
9467
|
-
for (const entry of entries) {
|
|
9468
|
-
if (typeof entry.stable_id !== "string" || entry.stable_id.length === 0) continue;
|
|
9469
|
-
const list = groups.get(groupKey(entry)) ?? [];
|
|
9470
|
-
list.push(entry);
|
|
9471
|
-
groups.set(groupKey(entry), list);
|
|
9472
|
-
}
|
|
9473
|
-
const pairs = [];
|
|
9474
|
-
for (const group of groups.values()) {
|
|
9475
|
-
if (group.length < 2) continue;
|
|
9476
|
-
const docs = group.map((e) => ({ id: e.stable_id, tokens: buildQueryTerms(e.text) }));
|
|
9477
|
-
const tokensById = new Map(docs.map((d) => [d.id, d.tokens]));
|
|
9478
|
-
const model = buildBm25Model(docs);
|
|
9479
|
-
for (let i = 0; i < group.length; i += 1) {
|
|
9480
|
-
for (let j = i + 1; j < group.length; j += 1) {
|
|
9481
|
-
const ea = group[i];
|
|
9482
|
-
const eb = group[j];
|
|
9483
|
-
const sim = pairSimilarity(
|
|
9484
|
-
model,
|
|
9485
|
-
{ id: ea.stable_id, tokens: tokensById.get(ea.stable_id) ?? [] },
|
|
9486
|
-
{ id: eb.stable_id, tokens: tokensById.get(eb.stable_id) ?? [] }
|
|
9487
|
-
);
|
|
9488
|
-
if (sim < threshold) continue;
|
|
9489
|
-
const [a, b] = ea.stable_id <= eb.stable_id ? [ea.stable_id, eb.stable_id] : [eb.stable_id, ea.stable_id];
|
|
9490
|
-
pairs.push({
|
|
9491
|
-
a,
|
|
9492
|
-
b,
|
|
9493
|
-
knowledge_type: ea.knowledge_type,
|
|
9494
|
-
layer: ea.layer,
|
|
9495
|
-
similarity: sim,
|
|
9496
|
-
verdict: "unknown"
|
|
9497
|
-
});
|
|
9498
|
-
}
|
|
9499
|
-
}
|
|
9500
|
-
}
|
|
9501
|
-
pairs.sort((x, y) => y.similarity - x.similarity || (x.a < y.a ? -1 : x.a > y.a ? 1 : x.b < y.b ? -1 : 1));
|
|
9502
|
-
return pairs;
|
|
9503
|
-
}
|
|
9504
|
-
async function lintConflicts(entries, opts = {}) {
|
|
9505
|
-
const candidates = findConflictCandidates(entries, { threshold: opts.threshold });
|
|
9506
|
-
if (opts.judge === void 0 || candidates.length === 0) {
|
|
9507
|
-
return candidates;
|
|
9508
|
-
}
|
|
9509
|
-
const byId = new Map(entries.map((e) => [e.stable_id, e]));
|
|
9510
|
-
const judged = [];
|
|
9511
|
-
for (const pair of candidates) {
|
|
9512
|
-
const ea = byId.get(pair.a);
|
|
9513
|
-
const eb = byId.get(pair.b);
|
|
9514
|
-
if (ea === void 0 || eb === void 0) {
|
|
9515
|
-
judged.push(pair);
|
|
9516
|
-
continue;
|
|
9517
|
-
}
|
|
9518
|
-
try {
|
|
9519
|
-
const verdict = await opts.judge(ea, eb);
|
|
9520
|
-
judged.push({
|
|
9521
|
-
...pair,
|
|
9522
|
-
verdict: verdict.isConflict ? "conflict" : "similar",
|
|
9523
|
-
rationale: verdict.rationale
|
|
9524
|
-
});
|
|
9525
|
-
} catch {
|
|
9526
|
-
judged.push(pair);
|
|
9527
|
-
}
|
|
9528
|
-
}
|
|
9529
|
-
return judged;
|
|
9530
|
-
}
|
|
9531
|
-
|
|
9532
10036
|
// src/services/doctor-conflict.ts
|
|
9533
10037
|
function stripFrontmatter(content) {
|
|
9534
10038
|
if (!content.startsWith("---")) return content;
|
|
@@ -9569,6 +10073,91 @@ async function runDoctorConflictLint(projectRoot, opts = {}) {
|
|
|
9569
10073
|
};
|
|
9570
10074
|
}
|
|
9571
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
|
+
|
|
9572
10161
|
// src/services/summary-cold-eval.ts
|
|
9573
10162
|
var COLD_EVAL_RUBRIC = [
|
|
9574
10163
|
"You are a ZERO-CONTEXT judge. You are shown ONLY a one-line knowledge summary \u2014",
|
|
@@ -9622,8 +10211,8 @@ import { agentsMetaSchema as agentsMetaSchema3 } from "@fenglimg/fabric-shared";
|
|
|
9622
10211
|
import { IOFabricError as IOFabricError2, RuleError } from "@fenglimg/fabric-shared/errors";
|
|
9623
10212
|
|
|
9624
10213
|
// src/services/read-ledger.ts
|
|
9625
|
-
import { randomUUID as
|
|
9626
|
-
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";
|
|
9627
10216
|
import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
|
|
9628
10217
|
async function resolveLedgerPaths(projectRoot) {
|
|
9629
10218
|
const primaryPath = getLedgerPath(projectRoot);
|
|
@@ -9651,7 +10240,7 @@ async function readLegacyLedger(projectRoot) {
|
|
|
9651
10240
|
const { readPath } = await resolveLedgerPaths(projectRoot);
|
|
9652
10241
|
let raw;
|
|
9653
10242
|
try {
|
|
9654
|
-
raw = await
|
|
10243
|
+
raw = await readFile18(readPath, "utf8");
|
|
9655
10244
|
} catch (error) {
|
|
9656
10245
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
9657
10246
|
return [];
|
|
@@ -9906,26 +10495,27 @@ function formatError(error) {
|
|
|
9906
10495
|
}
|
|
9907
10496
|
function formatPreexistingRootMessage(projectRoot) {
|
|
9908
10497
|
const preexisting = [];
|
|
9909
|
-
if (existsSync9(
|
|
9910
|
-
if (existsSync9(
|
|
10498
|
+
if (existsSync9(join23(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
|
|
10499
|
+
if (existsSync9(join23(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
|
|
9911
10500
|
if (preexisting.length === 0) return null;
|
|
9912
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.`;
|
|
9913
10502
|
}
|
|
9914
10503
|
var FABRIC_SERVER_INSTRUCTIONS = [
|
|
9915
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.",
|
|
9916
10505
|
"",
|
|
10506
|
+
"AGENT-DIRECT tools \u2014 call these yourself, inline, as you work:",
|
|
9917
10507
|
"Retrieval \u2014 do this BEFORE you edit code or commit to a decision:",
|
|
9918
|
-
"-
|
|
9919
|
-
"- 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.",
|
|
9920
10510
|
"",
|
|
9921
|
-
"
|
|
9922
|
-
"- `
|
|
9923
|
-
"- `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.",
|
|
9924
10513
|
"- `fab_archive_scan` \u2014 scan recent work for archive-worthy knowledge candidates.",
|
|
9925
|
-
"- `
|
|
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).",
|
|
9926
10516
|
"",
|
|
9927
10517
|
"Conventions:",
|
|
9928
|
-
"- 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.",
|
|
9929
10519
|
"- Pass the client `session_id` to `fab_recall` so cross-session knowledge-debt tracking stays accurate.",
|
|
9930
10520
|
"- Cite the KB id you applied or dismissed before edits, per the project's cite policy."
|
|
9931
10521
|
].join("\n");
|
|
@@ -9933,7 +10523,7 @@ function createFabricServer(tracker) {
|
|
|
9933
10523
|
const server = new McpServer(
|
|
9934
10524
|
{
|
|
9935
10525
|
name: "fabric-knowledge-server",
|
|
9936
|
-
version: "2.
|
|
10526
|
+
version: "2.3.0-rc.1"
|
|
9937
10527
|
},
|
|
9938
10528
|
{
|
|
9939
10529
|
instructions: FABRIC_SERVER_INSTRUCTIONS
|
|
@@ -9943,6 +10533,7 @@ function createFabricServer(tracker) {
|
|
|
9943
10533
|
registerArchiveScan(server, tracker);
|
|
9944
10534
|
registerExtractKnowledge(server, tracker);
|
|
9945
10535
|
registerReview(server, tracker);
|
|
10536
|
+
registerPending(server, tracker);
|
|
9946
10537
|
server.registerResource(
|
|
9947
10538
|
"bootstrap README",
|
|
9948
10539
|
AGENTS_MD_RESOURCE_URI,
|
|
@@ -9952,10 +10543,10 @@ function createFabricServer(tracker) {
|
|
|
9952
10543
|
},
|
|
9953
10544
|
async (_uri) => {
|
|
9954
10545
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
9955
|
-
const path =
|
|
10546
|
+
const path = join23(projectRoot, ".fabric", "bootstrap", "README.md");
|
|
9956
10547
|
let text = "";
|
|
9957
10548
|
if (existsSync9(path)) {
|
|
9958
|
-
text = await
|
|
10549
|
+
text = await readFile19(path, "utf8");
|
|
9959
10550
|
}
|
|
9960
10551
|
return {
|
|
9961
10552
|
contents: [
|
|
@@ -10053,6 +10644,7 @@ export {
|
|
|
10053
10644
|
LEGACY_LEDGER_PATH,
|
|
10054
10645
|
METRICS_LEDGER_PATH,
|
|
10055
10646
|
METRIC_COUNTER_NAMES,
|
|
10647
|
+
RETIRED_TOKENS,
|
|
10056
10648
|
appendEventLedgerEvent,
|
|
10057
10649
|
buildAlwaysActiveBodies,
|
|
10058
10650
|
buildColdEvalBatch,
|
|
@@ -10065,6 +10657,7 @@ export {
|
|
|
10065
10657
|
detectUnboundProject,
|
|
10066
10658
|
drainCounters,
|
|
10067
10659
|
enrichDescriptions,
|
|
10660
|
+
explainWhyNotSurfaced,
|
|
10068
10661
|
extractKnowledge,
|
|
10069
10662
|
findConflictCandidates,
|
|
10070
10663
|
flushAndSyncEventLedger,
|
|
@@ -10074,6 +10667,7 @@ export {
|
|
|
10074
10667
|
getLedgerPath,
|
|
10075
10668
|
getLegacyLedgerPath,
|
|
10076
10669
|
getMetricsLedgerPath,
|
|
10670
|
+
inspectRetiredReferences,
|
|
10077
10671
|
lintConflicts,
|
|
10078
10672
|
loadConflictEntries,
|
|
10079
10673
|
pairSimilarity,
|
|
@@ -10086,6 +10680,7 @@ export {
|
|
|
10086
10680
|
rehydrateAgentsMetaAt,
|
|
10087
10681
|
resolveLedgerPaths,
|
|
10088
10682
|
reviewKnowledge,
|
|
10683
|
+
reviewPending,
|
|
10089
10684
|
runDoctorApplyLint,
|
|
10090
10685
|
runDoctorArchiveHistory,
|
|
10091
10686
|
runDoctorCiteCoverage,
|