@fenglimg/fabric-server 2.0.0-rc.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-NRWDWAVO.js → chunk-AR2HV5JT.js} +1762 -273
- package/dist/{http-CHCOF6DJ.js → http-74Z4HGXM.js} +12 -151
- package/dist/index.d.ts +131 -30
- package/dist/index.js +1273 -263
- package/package.json +4 -5
- package/dist/static/assets/index-DNSKn-El.js +0 -10
- package/dist/static/assets/index-FoBU5Kta.css +0 -1
- package/dist/static/index.html +0 -16
|
@@ -6,7 +6,7 @@ var ContextCache = class {
|
|
|
6
6
|
defaultTtlMs;
|
|
7
7
|
// Slot 1: raw AgentsMeta keyed by projectRoot
|
|
8
8
|
metaSlot = /* @__PURE__ */ new Map();
|
|
9
|
-
// Slot 2:
|
|
9
|
+
// Slot 2: GetKnowledgeContext keyed by projectRoot
|
|
10
10
|
contextSlot = /* @__PURE__ */ new Map();
|
|
11
11
|
// Slot 3: audit sliding-window cursor keyed by projectRoot
|
|
12
12
|
auditSlot = /* @__PURE__ */ new Map();
|
|
@@ -276,20 +276,20 @@ function flushAndSyncEventLedger(projectRoot) {
|
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
// src/services/
|
|
279
|
+
// src/services/knowledge-meta-builder.ts
|
|
280
280
|
import { mkdir as mkdir2, readdir, readFile as readFile3 } from "fs/promises";
|
|
281
281
|
import { existsSync as existsSync2, statSync } from "fs";
|
|
282
282
|
import { homedir } from "os";
|
|
283
283
|
import { isAbsolute, join as join3, relative, resolve as resolve2, sep as sep2 } from "path";
|
|
284
284
|
import {
|
|
285
|
-
|
|
285
|
+
KNOWLEDGE_TEST_INDEX_SCHEMA_VERSION,
|
|
286
286
|
agentsMetaSchema as agentsMetaSchema3,
|
|
287
287
|
defaultAgentsMetaCounters,
|
|
288
288
|
deriveAgentsMetaLayer,
|
|
289
289
|
deriveAgentsMetaStableId,
|
|
290
290
|
deriveAgentsMetaTopologyType,
|
|
291
291
|
isKnowledgeStableId,
|
|
292
|
-
|
|
292
|
+
knowledgeTestIndexSchema,
|
|
293
293
|
KnowledgeTypeSchema,
|
|
294
294
|
MaturitySchema,
|
|
295
295
|
LayerSchema,
|
|
@@ -297,34 +297,35 @@ import {
|
|
|
297
297
|
parseKnowledgeId
|
|
298
298
|
} from "@fenglimg/fabric-shared";
|
|
299
299
|
import { atomicWriteText as atomicWriteText2 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
300
|
-
async function
|
|
300
|
+
async function buildKnowledgeMeta(projectRootInput) {
|
|
301
301
|
const projectRoot = normalizeProjectRoot(projectRootInput);
|
|
302
302
|
assertExistingDirectory(projectRoot);
|
|
303
303
|
const metaPath = join3(projectRoot, ".fabric", "agents.meta.json");
|
|
304
|
-
const
|
|
304
|
+
const knowledgeTestIndexPath = join3(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
|
|
305
305
|
const existingMeta = await readExistingMeta(metaPath);
|
|
306
|
-
const
|
|
307
|
-
const meta = await
|
|
308
|
-
const
|
|
306
|
+
const existingKnowledgeTestIndex = await readExistingKnowledgeTestIndex(knowledgeTestIndexPath);
|
|
307
|
+
const meta = await computeKnowledgeBasedAgentsMeta(projectRoot, existingMeta);
|
|
308
|
+
const knowledgeTestIndex = await computeKnowledgeTestIndex(projectRoot, meta, existingKnowledgeTestIndex);
|
|
309
309
|
return {
|
|
310
310
|
meta,
|
|
311
|
-
|
|
312
|
-
changed: existingMeta === void 0 || stableStringify(existingMeta) !== stableStringify(meta) ||
|
|
311
|
+
knowledgeTestIndex,
|
|
312
|
+
changed: existingMeta === void 0 || stableStringify(existingMeta) !== stableStringify(meta) || existingKnowledgeTestIndex === void 0 || !isSameKnowledgeTestIndex(existingKnowledgeTestIndex, knowledgeTestIndex)
|
|
313
313
|
};
|
|
314
314
|
}
|
|
315
|
-
async function
|
|
315
|
+
async function writeKnowledgeMeta(projectRootInput, options) {
|
|
316
316
|
const projectRoot = normalizeProjectRoot(projectRootInput);
|
|
317
317
|
const metaPath = join3(projectRoot, ".fabric", "agents.meta.json");
|
|
318
|
-
const
|
|
318
|
+
const knowledgeTestIndexPath = join3(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
|
|
319
319
|
const existingMeta = await readExistingMeta(metaPath);
|
|
320
|
-
const result = await
|
|
320
|
+
const result = await buildKnowledgeMeta(projectRoot);
|
|
321
321
|
if (!result.changed) {
|
|
322
322
|
return result;
|
|
323
323
|
}
|
|
324
324
|
await ensureParentDirectory(metaPath);
|
|
325
325
|
await atomicWriteText2(metaPath, `${JSON.stringify(result.meta, null, 2)}
|
|
326
326
|
`);
|
|
327
|
-
await
|
|
327
|
+
await ensureParentDirectory(knowledgeTestIndexPath);
|
|
328
|
+
await atomicWriteText2(knowledgeTestIndexPath, `${JSON.stringify(result.knowledgeTestIndex, null, 2)}
|
|
328
329
|
`);
|
|
329
330
|
if (existingMeta === void 0 || stableStringify(existingMeta) !== stableStringify(result.meta)) {
|
|
330
331
|
await recordBaselineSynced(projectRoot, {
|
|
@@ -338,7 +339,7 @@ async function writeRuleMeta(projectRootInput, options) {
|
|
|
338
339
|
}
|
|
339
340
|
return result;
|
|
340
341
|
}
|
|
341
|
-
async function
|
|
342
|
+
async function computeKnowledgeBasedAgentsMeta(projectRootInput, existingMeta) {
|
|
342
343
|
const projectRoot = normalizeProjectRoot(projectRootInput);
|
|
343
344
|
assertExistingDirectory(projectRoot);
|
|
344
345
|
const previousMeta = existingMeta ?? await readExistingMeta(join3(projectRoot, ".fabric", "agents.meta.json"));
|
|
@@ -372,7 +373,7 @@ async function computeRulesBasedAgentsMeta(projectRootInput, existingMeta) {
|
|
|
372
373
|
counters
|
|
373
374
|
};
|
|
374
375
|
}
|
|
375
|
-
async function
|
|
376
|
+
async function computeKnowledgeTestIndex(projectRootInput, computedMeta, previousIndex) {
|
|
376
377
|
const projectRoot = normalizeProjectRoot(projectRootInput);
|
|
377
378
|
assertExistingDirectory(projectRoot);
|
|
378
379
|
const previousLinks = indexPreviousRuleTestEntries(previousIndex?.links ?? []);
|
|
@@ -408,7 +409,7 @@ async function computeRuleTestIndex(projectRootInput, computedMeta, previousInde
|
|
|
408
409
|
});
|
|
409
410
|
}
|
|
410
411
|
return {
|
|
411
|
-
schema_version:
|
|
412
|
+
schema_version: KNOWLEDGE_TEST_INDEX_SCHEMA_VERSION,
|
|
412
413
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
413
414
|
revision: computedMeta.revision,
|
|
414
415
|
previous_revision: previousIndex?.revision !== void 0 && previousIndex.revision !== computedMeta.revision ? previousIndex.revision : previousIndex?.previous_revision,
|
|
@@ -416,14 +417,14 @@ async function computeRuleTestIndex(projectRootInput, computedMeta, previousInde
|
|
|
416
417
|
orphan_annotations: orphanAnnotations.sort(compareRuleTestEntries)
|
|
417
418
|
};
|
|
418
419
|
}
|
|
419
|
-
function
|
|
420
|
+
function deriveKnowledgeMetaLayer(relativePath) {
|
|
420
421
|
return deriveAgentsMetaLayer(toAgentsCompatiblePath(relativePath));
|
|
421
422
|
}
|
|
422
|
-
function
|
|
423
|
+
function deriveKnowledgeMetaTopologyType(relativePath) {
|
|
423
424
|
return deriveAgentsMetaTopologyType(toAgentsCompatiblePath(relativePath));
|
|
424
425
|
}
|
|
425
|
-
function
|
|
426
|
-
return stableStringify(
|
|
426
|
+
function isSameKnowledgeTestIndex(left, right) {
|
|
427
|
+
return stableStringify(toComparableKnowledgeTestIndex(left)) === stableStringify(toComparableKnowledgeTestIndex(right));
|
|
427
428
|
}
|
|
428
429
|
function stableStringify(value) {
|
|
429
430
|
return JSON.stringify(value, Object.keys(flattenKeys(value)).sort());
|
|
@@ -452,7 +453,7 @@ async function readExistingMeta(metaPath) {
|
|
|
452
453
|
return void 0;
|
|
453
454
|
}
|
|
454
455
|
}
|
|
455
|
-
async function
|
|
456
|
+
async function readExistingKnowledgeTestIndex(indexPath) {
|
|
456
457
|
let raw;
|
|
457
458
|
try {
|
|
458
459
|
raw = await readFile3(indexPath, "utf8");
|
|
@@ -463,7 +464,7 @@ async function readExistingRuleTestIndex(indexPath) {
|
|
|
463
464
|
throw error;
|
|
464
465
|
}
|
|
465
466
|
try {
|
|
466
|
-
return
|
|
467
|
+
return knowledgeTestIndexSchema.parse(JSON.parse(raw));
|
|
467
468
|
} catch {
|
|
468
469
|
return void 0;
|
|
469
470
|
}
|
|
@@ -615,7 +616,7 @@ function compareRuleTestEntries(left, right) {
|
|
|
615
616
|
function compareAnnotationEntries(left, right) {
|
|
616
617
|
return left.stableId.localeCompare(right.stableId) || left.testFile.localeCompare(right.testFile) || left.line - right.line;
|
|
617
618
|
}
|
|
618
|
-
function
|
|
619
|
+
function toComparableKnowledgeTestIndex(index) {
|
|
619
620
|
const { generated_at: _generatedAt, ...comparable } = index;
|
|
620
621
|
return comparable;
|
|
621
622
|
}
|
|
@@ -627,7 +628,7 @@ function indexExistingNodesByContentRef(existingMeta) {
|
|
|
627
628
|
return byContentRef;
|
|
628
629
|
}
|
|
629
630
|
function deriveNodeId(file) {
|
|
630
|
-
const layer =
|
|
631
|
+
const layer = deriveKnowledgeMetaLayer(file);
|
|
631
632
|
const relativeStem = getRuleRelativeStem(file);
|
|
632
633
|
if (file.startsWith(PERSONAL_CONTENT_REF_PREFIX)) {
|
|
633
634
|
return `${layer}/personal/${relativeStem}`;
|
|
@@ -638,8 +639,8 @@ function deriveNodeId(file) {
|
|
|
638
639
|
return `${layer}/${relativeStem}`;
|
|
639
640
|
}
|
|
640
641
|
function createDefaultNodeMeta(contentRef) {
|
|
641
|
-
const layer =
|
|
642
|
-
const topologyType =
|
|
642
|
+
const layer = deriveKnowledgeMetaLayer(contentRef);
|
|
643
|
+
const topologyType = deriveKnowledgeMetaTopologyType(contentRef);
|
|
643
644
|
return {
|
|
644
645
|
file: contentRef,
|
|
645
646
|
content_ref: contentRef,
|
|
@@ -676,8 +677,12 @@ function toAgentsCompatiblePath(contentRef) {
|
|
|
676
677
|
function sortNodes(nodes) {
|
|
677
678
|
return Object.fromEntries(Object.entries(nodes).sort(([left], [right]) => left.localeCompare(right)));
|
|
678
679
|
}
|
|
680
|
+
function isPendingNode(node) {
|
|
681
|
+
const ref = node.content_ref ?? node.file ?? "";
|
|
682
|
+
return ref.startsWith(".fabric/knowledge/pending/") || ref.startsWith("~/.fabric/knowledge/pending/");
|
|
683
|
+
}
|
|
679
684
|
function computeRevision(nodes) {
|
|
680
|
-
const revisionSource = Object.entries(sortNodes(nodes)).map(([id, node]) => [id, node.hash, node.stable_id ?? "", node.identity_source ?? ""].join("|")).join("\n");
|
|
685
|
+
const revisionSource = Object.entries(sortNodes(nodes)).filter(([, node]) => !isPendingNode(node)).map(([id, node]) => [id, node.hash, node.stable_id ?? "", node.identity_source ?? ""].join("|")).join("\n");
|
|
681
686
|
return sha256(revisionSource);
|
|
682
687
|
}
|
|
683
688
|
function collectSyncedFiles(existingMeta, computedMeta) {
|
|
@@ -810,7 +815,10 @@ function extractRuleDescription(source) {
|
|
|
810
815
|
knowledge_layer: void 0,
|
|
811
816
|
layer_reason: void 0,
|
|
812
817
|
created_at: void 0,
|
|
813
|
-
tags: void 0
|
|
818
|
+
tags: void 0,
|
|
819
|
+
// v2.0-rc.5 (C1): default-safe values when there is no frontmatter at all.
|
|
820
|
+
relevance_scope: "broad",
|
|
821
|
+
relevance_paths: []
|
|
814
822
|
};
|
|
815
823
|
}
|
|
816
824
|
function extractRuleSections(source) {
|
|
@@ -836,7 +844,9 @@ function extractDescriptionFromFrontmatter(frontmatter) {
|
|
|
836
844
|
knowledge_layer: knowledge.knowledge_layer,
|
|
837
845
|
layer_reason: knowledge.layer_reason,
|
|
838
846
|
created_at: knowledge.created_at,
|
|
839
|
-
tags: knowledge.tags
|
|
847
|
+
tags: knowledge.tags,
|
|
848
|
+
relevance_scope: knowledge.relevance_scope,
|
|
849
|
+
relevance_paths: knowledge.relevance_paths
|
|
840
850
|
};
|
|
841
851
|
}
|
|
842
852
|
function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
|
|
@@ -907,6 +917,9 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
|
|
|
907
917
|
}
|
|
908
918
|
}
|
|
909
919
|
const tags = extractInlineArray(frontmatter, "tags");
|
|
920
|
+
const rawRelevanceScope = extractScalar(frontmatter, "relevance_scope");
|
|
921
|
+
const relevance_scope = rawRelevanceScope === "narrow" || rawRelevanceScope === "broad" ? rawRelevanceScope : "broad";
|
|
922
|
+
const relevance_paths = extractInlineArray(frontmatter, "relevance_paths");
|
|
910
923
|
return {
|
|
911
924
|
id,
|
|
912
925
|
knowledge_type,
|
|
@@ -914,7 +927,9 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
|
|
|
914
927
|
knowledge_layer,
|
|
915
928
|
layer_reason: rawLayerReason,
|
|
916
929
|
created_at,
|
|
917
|
-
tags: tags.length > 0 ? tags : void 0
|
|
930
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
931
|
+
relevance_scope,
|
|
932
|
+
relevance_paths
|
|
918
933
|
};
|
|
919
934
|
}
|
|
920
935
|
function extractScalar(frontmatter, key) {
|
|
@@ -943,7 +958,7 @@ function isNodeError3(error) {
|
|
|
943
958
|
return error instanceof Error;
|
|
944
959
|
}
|
|
945
960
|
|
|
946
|
-
// src/services/
|
|
961
|
+
// src/services/knowledge-sync.ts
|
|
947
962
|
import { readdir as readdir2, readFile as readFile4, stat } from "fs/promises";
|
|
948
963
|
import { existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
949
964
|
import { join as join4, relative as relative2, sep as sep3 } from "path";
|
|
@@ -951,7 +966,7 @@ import { RuleValidationError } from "@fenglimg/fabric-shared/errors";
|
|
|
951
966
|
var lastSyncState = /* @__PURE__ */ new Map();
|
|
952
967
|
var freshSyncCooldown = /* @__PURE__ */ new Map();
|
|
953
968
|
var SYNC_COOLDOWN_MS = 500;
|
|
954
|
-
function
|
|
969
|
+
function invalidateKnowledgeSyncCooldown(projectRoot) {
|
|
955
970
|
freshSyncCooldown.delete(projectRoot);
|
|
956
971
|
}
|
|
957
972
|
async function readMetaEntries(projectRoot) {
|
|
@@ -1125,14 +1140,14 @@ async function appendRuleSyncEvents(projectRoot, events) {
|
|
|
1125
1140
|
});
|
|
1126
1141
|
}
|
|
1127
1142
|
}
|
|
1128
|
-
async function
|
|
1143
|
+
async function ensureKnowledgeFresh(projectRoot, opts) {
|
|
1129
1144
|
const mode = opts?.mode ?? "incremental";
|
|
1130
1145
|
const cooldownExpiry = freshSyncCooldown.get(projectRoot);
|
|
1131
1146
|
if (cooldownExpiry !== void 0 && Date.now() < cooldownExpiry && mode !== "full") {
|
|
1132
1147
|
return { status: "fresh", events: [], warnings: [] };
|
|
1133
1148
|
}
|
|
1134
1149
|
const throwOnInvalidFrontmatter = opts?.throwOnInvalidFrontmatter ?? false;
|
|
1135
|
-
const source = "
|
|
1150
|
+
const source = "ensureKnowledgeFresh";
|
|
1136
1151
|
const events = [];
|
|
1137
1152
|
const warnings = [];
|
|
1138
1153
|
const metaEntries = await readMetaEntries(projectRoot);
|
|
@@ -1181,11 +1196,11 @@ async function ensureRulesFresh(projectRoot, opts) {
|
|
|
1181
1196
|
reconciled_files: events.map((e) => e.path)
|
|
1182
1197
|
};
|
|
1183
1198
|
}
|
|
1184
|
-
async function
|
|
1199
|
+
async function reconcileKnowledge(projectRoot, opts) {
|
|
1185
1200
|
freshSyncCooldown.delete(projectRoot);
|
|
1186
1201
|
const trigger = opts?.trigger;
|
|
1187
1202
|
const startTime = Date.now();
|
|
1188
|
-
const source = "
|
|
1203
|
+
const source = "reconcileKnowledge";
|
|
1189
1204
|
const events = [];
|
|
1190
1205
|
const warnings = [];
|
|
1191
1206
|
const metaEntries = await readMetaEntries(projectRoot);
|
|
@@ -1217,7 +1232,7 @@ async function reconcileRules(projectRoot, opts) {
|
|
|
1217
1232
|
}
|
|
1218
1233
|
}
|
|
1219
1234
|
if (events.length > 0) {
|
|
1220
|
-
await
|
|
1235
|
+
await writeKnowledgeMeta(projectRoot, { source: "sync_meta" });
|
|
1221
1236
|
await appendRuleSyncEvents(projectRoot, events);
|
|
1222
1237
|
contextCache.invalidate("file_watch", projectRoot);
|
|
1223
1238
|
}
|
|
@@ -1229,7 +1244,7 @@ async function reconcileRules(projectRoot, opts) {
|
|
|
1229
1244
|
event_type: "meta_reconciled_on_startup",
|
|
1230
1245
|
reconciled_files: reconciledFiles,
|
|
1231
1246
|
duration_ms,
|
|
1232
|
-
source: "
|
|
1247
|
+
source: "reconcileKnowledge"
|
|
1233
1248
|
});
|
|
1234
1249
|
} else {
|
|
1235
1250
|
await appendEventLedgerEvent(projectRoot, {
|
|
@@ -1237,7 +1252,7 @@ async function reconcileRules(projectRoot, opts) {
|
|
|
1237
1252
|
reconciled_files: reconciledFiles,
|
|
1238
1253
|
duration_ms,
|
|
1239
1254
|
trigger,
|
|
1240
|
-
source: "
|
|
1255
|
+
source: "reconcileKnowledge"
|
|
1241
1256
|
});
|
|
1242
1257
|
}
|
|
1243
1258
|
}
|
|
@@ -1254,19 +1269,59 @@ async function reconcileRules(projectRoot, opts) {
|
|
|
1254
1269
|
}
|
|
1255
1270
|
|
|
1256
1271
|
// src/services/doctor.ts
|
|
1272
|
+
import { execFileSync } from "child_process";
|
|
1257
1273
|
import { existsSync as existsSync4, readdirSync, readFileSync, statSync as statSync3 } from "fs";
|
|
1258
|
-
import { access, mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
|
|
1274
|
+
import { access, mkdir as mkdir3, readFile as readFile5, rename, writeFile as writeFile2 } from "fs/promises";
|
|
1259
1275
|
import { constants } from "fs";
|
|
1260
|
-
import {
|
|
1276
|
+
import { homedir as homedir2 } from "os";
|
|
1277
|
+
import { isAbsolute as isAbsolute2, join as join5, posix, relative as nodeRelative, resolve as resolve3, sep as sep4 } from "path";
|
|
1278
|
+
import { minimatch } from "minimatch";
|
|
1261
1279
|
import {
|
|
1262
1280
|
agentsMetaSchema as agentsMetaSchema4,
|
|
1263
1281
|
AgentsMetaCountersSchema,
|
|
1264
1282
|
forensicReportSchema,
|
|
1265
1283
|
parseKnowledgeId as parseKnowledgeId2,
|
|
1266
|
-
|
|
1284
|
+
knowledgeTestIndexSchema as knowledgeTestIndexSchema2
|
|
1267
1285
|
} from "@fenglimg/fabric-shared";
|
|
1268
1286
|
import { detectFramework } from "@fenglimg/fabric-shared/node";
|
|
1269
|
-
import { atomicWriteJson as atomicWriteJson2 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
1287
|
+
import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText3 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
1288
|
+
var ORPHAN_DEMOTE_THRESHOLD_DAYS = {
|
|
1289
|
+
stable: 90,
|
|
1290
|
+
endorsed: 30,
|
|
1291
|
+
draft: 14
|
|
1292
|
+
};
|
|
1293
|
+
var STALE_ARCHIVE_ADDITIONAL_DAYS = 90;
|
|
1294
|
+
var PENDING_OVERDUE_THRESHOLD_DAYS = 14;
|
|
1295
|
+
var PENDING_AUTO_ARCHIVE_THRESHOLD_DAYS = 30;
|
|
1296
|
+
var DEFAULT_UNDERSEED_NODE_THRESHOLD = 10;
|
|
1297
|
+
var SESSION_HINTS_STALE_DAYS = 7;
|
|
1298
|
+
var SESSION_HINTS_FILE_PREFIX = "session-hints-";
|
|
1299
|
+
var SESSION_HINTS_FILE_SUFFIX = ".json";
|
|
1300
|
+
var NARROW_RATIO_THRESHOLD = 0.2;
|
|
1301
|
+
var NARROW_MIN_TOTAL = 10;
|
|
1302
|
+
var SILENCE_RATE_THRESHOLD = 0.95;
|
|
1303
|
+
var SILENCE_WINDOW_DAYS = 30;
|
|
1304
|
+
var EDIT_COUNTER_FILE_REL = posix.join(".fabric", ".cache", "edit-counter");
|
|
1305
|
+
var HINT_SILENCE_COUNTER_FILE_REL = posix.join(
|
|
1306
|
+
".fabric",
|
|
1307
|
+
".cache",
|
|
1308
|
+
"hint-silence-counter"
|
|
1309
|
+
);
|
|
1310
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
1311
|
+
var MATURITY_LINE_PATTERN = /^maturity:\s*("?)(stable|endorsed|draft)\1\s*$/mu;
|
|
1312
|
+
var CREATED_AT_LINE_PATTERN = /^created_at:\s*("?)([^"\n]+)\1\s*$/mu;
|
|
1313
|
+
var RELEVANCE_SCOPE_LINE_PATTERN = /^relevance_scope:\s*("?)(narrow|broad)\1\s*$/mu;
|
|
1314
|
+
var RELEVANCE_PATHS_LINE_PATTERN = /^relevance_paths:\s*\[([^\]]*)\]\s*$/mu;
|
|
1315
|
+
var RELEVANCE_PATHS_DRIFT_WINDOW_DAYS = 90;
|
|
1316
|
+
var SYNTHESIZED_PROMOTED_REASON = "[synthesized] filesystem-edit-fallback";
|
|
1317
|
+
var KNOWLEDGE_CANONICAL_TYPE_DIRS = [
|
|
1318
|
+
"decisions",
|
|
1319
|
+
"pitfalls",
|
|
1320
|
+
"guidelines",
|
|
1321
|
+
"models",
|
|
1322
|
+
"processes"
|
|
1323
|
+
];
|
|
1324
|
+
var CANONICAL_KNOWLEDGE_FILENAME_PATTERN = /^(K[PT]-(?:MOD|DEC|GLD|PIT|PRO)-\d{4,})--[a-z0-9][a-z0-9-]*\.md$/u;
|
|
1270
1325
|
var KNOWLEDGE_SUBDIRS2 = ["decisions", "pitfalls", "guidelines", "models", "processes", "pending"];
|
|
1271
1326
|
var COUNTER_TYPE_CODES = ["MOD", "DEC", "GLD", "PIT", "PRO"];
|
|
1272
1327
|
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
@@ -1285,7 +1340,7 @@ var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
|
1285
1340
|
var TARGET_FILE_PATHS = [
|
|
1286
1341
|
".fabric/forensic.json",
|
|
1287
1342
|
".fabric/agents.meta.json",
|
|
1288
|
-
".fabric/
|
|
1343
|
+
".fabric/.cache/knowledge-test.index.json",
|
|
1289
1344
|
".fabric/events.jsonl",
|
|
1290
1345
|
".fabric/knowledge"
|
|
1291
1346
|
];
|
|
@@ -1297,12 +1352,12 @@ async function runDoctorReport(target) {
|
|
|
1297
1352
|
forensic,
|
|
1298
1353
|
meta,
|
|
1299
1354
|
eventLedger,
|
|
1300
|
-
|
|
1355
|
+
knowledgeTestIndex
|
|
1301
1356
|
] = await Promise.all([
|
|
1302
1357
|
inspectForensic(projectRoot),
|
|
1303
1358
|
inspectMeta(projectRoot),
|
|
1304
1359
|
inspectEventLedger(projectRoot),
|
|
1305
|
-
|
|
1360
|
+
inspectKnowledgeTestIndex(projectRoot)
|
|
1306
1361
|
]);
|
|
1307
1362
|
const mcpConfigInWrongFile = inspectMcpConfigInWrongFile(projectRoot);
|
|
1308
1363
|
const metaManuallyDiverged = await inspectMetaManuallyDiverged(projectRoot);
|
|
@@ -1312,6 +1367,20 @@ async function runDoctorReport(target) {
|
|
|
1312
1367
|
const counterDesync = inspectCounterDesync(meta);
|
|
1313
1368
|
const preexistingRootFiles = inspectPreexistingRootFiles(projectRoot);
|
|
1314
1369
|
const bootstrapAnchor = inspectBootstrapAnchor(projectRoot);
|
|
1370
|
+
const filesystemEditFallback = eventLedger.exists && eventLedger.writable && eventLedger.parseable ? await inspectFilesystemEditFallback(projectRoot) : { synthesized: 0, synthesizedStableIds: [] };
|
|
1371
|
+
const lintNow = Date.now();
|
|
1372
|
+
const orphanDemote = await inspectOrphanDemote(projectRoot, lintNow);
|
|
1373
|
+
const staleArchive = await inspectStaleArchive(projectRoot, lintNow);
|
|
1374
|
+
const pendingOverdue = inspectPendingOverdue(projectRoot, lintNow);
|
|
1375
|
+
const stableIdDuplicate = inspectStableIdDuplicate(projectRoot);
|
|
1376
|
+
const layerMismatch = inspectLayerMismatch(projectRoot);
|
|
1377
|
+
const indexDrift = inspectIndexDrift(projectRoot, meta);
|
|
1378
|
+
const underseeded = inspectUnderseeded(projectRoot);
|
|
1379
|
+
const narrowNoPaths = inspectNarrowNoPaths(projectRoot);
|
|
1380
|
+
const relevancePathsDangling = inspectRelevancePathsDangling(projectRoot);
|
|
1381
|
+
const relevancePathsDrift = inspectRelevancePathsDrift(projectRoot);
|
|
1382
|
+
const narrowTooFew = inspectNarrowTooFew(projectRoot, lintNow);
|
|
1383
|
+
const sessionHintsStale = inspectSessionHintsStale(projectRoot, lintNow);
|
|
1315
1384
|
const checks = [
|
|
1316
1385
|
createBootstrapAnchorCheck(bootstrapAnchor),
|
|
1317
1386
|
createKnowledgeDirMissingCheck(knowledgeDirMissing),
|
|
@@ -1327,7 +1396,7 @@ async function runDoctorReport(target) {
|
|
|
1327
1396
|
// [MANDATORY_INJECTION] sections out of legacy rule files, a structural
|
|
1328
1397
|
// concept that has no v2 equivalent. rc.4 will introduce a dedicated v2
|
|
1329
1398
|
// lint suite for the new knowledge frontmatter contract.
|
|
1330
|
-
|
|
1399
|
+
createKnowledgeTestIndexCheck(knowledgeTestIndex),
|
|
1331
1400
|
createEventLedgerCheck(eventLedger),
|
|
1332
1401
|
createEventLedgerPartialWriteCheck(eventLedger),
|
|
1333
1402
|
createMcpConfigInWrongFileCheck(mcpConfigInWrongFile),
|
|
@@ -1335,6 +1404,38 @@ async function runDoctorReport(target) {
|
|
|
1335
1404
|
createKnowledgeDirUnindexedCheck(knowledgeDirUnindexed),
|
|
1336
1405
|
createStableIdCollisionCheck(stableIdCollision),
|
|
1337
1406
|
createCounterDesyncCheck(counterDesync),
|
|
1407
|
+
createFilesystemEditFallbackCheck(filesystemEditFallback),
|
|
1408
|
+
// rc.4 TASK-001: read-side lint checks #16-18. Findings only — mutation
|
|
1409
|
+
// + event emission lands in TASK-003 behind --apply-lint.
|
|
1410
|
+
createOrphanDemoteCheck(orphanDemote),
|
|
1411
|
+
createStaleArchiveCheck(staleArchive),
|
|
1412
|
+
createPendingOverdueCheck(pendingOverdue),
|
|
1413
|
+
// rc.4 TASK-002: read-side integrity checks #19-21. Stable_id duplicate
|
|
1414
|
+
// runs first in this trio — it is the most critical integrity break and
|
|
1415
|
+
// surfaces ahead of layer-mismatch / index-drift in the report so a
|
|
1416
|
+
// human operator triages the collision before reasoning about counter
|
|
1417
|
+
// state. Index drift is the only fixable_error of the three; stable_id
|
|
1418
|
+
// duplicate and layer mismatch require manual triage (rename / move).
|
|
1419
|
+
createStableIdDuplicateCheck(stableIdDuplicate),
|
|
1420
|
+
createLayerMismatchCheck(layerMismatch),
|
|
1421
|
+
createIndexDriftCheck(indexDrift),
|
|
1422
|
+
// rc.5 TASK-010: read-side underseeded-corpus check (#22). Info kind —
|
|
1423
|
+
// does not bump report status. Recommends running the fabric-import skill
|
|
1424
|
+
// to backfill knowledge when the corpus is below the threshold floor.
|
|
1425
|
+
createUnderseededCheck(underseeded),
|
|
1426
|
+
// rc.5 TASK-013 (C4): relevance_paths hygiene checks #23/#24/#25.
|
|
1427
|
+
// All three are flag-only in rc.5 (no apply-lint mutations).
|
|
1428
|
+
// #23 narrow_no_paths — warning kind (silent recall risk)
|
|
1429
|
+
// #24 relevance_paths_dangling — warning kind (glob → zero matches)
|
|
1430
|
+
// #25 relevance_paths_drift — info kind (git-log heuristic; noisy)
|
|
1431
|
+
createNarrowNoPathsCheck(narrowNoPaths),
|
|
1432
|
+
createRelevancePathsDanglingCheck(relevancePathsDangling),
|
|
1433
|
+
createRelevancePathsDriftCheck(relevancePathsDrift),
|
|
1434
|
+
// rc.6 TASK-023 (E6): narrow_too_few (lint #26). Info kind; both arms
|
|
1435
|
+
// (structural + telemetry) recommend the same fabric-import action.
|
|
1436
|
+
createNarrowTooFewCheck(narrowTooFew),
|
|
1437
|
+
// rc.6 TASK-021 (E3): session-hints cache hygiene (lint #27). Info kind.
|
|
1438
|
+
createSessionHintsStaleCheck(sessionHintsStale),
|
|
1338
1439
|
createPreexistingRootFilesCheck(preexistingRootFiles)
|
|
1339
1440
|
// v2.0 / rc.2: `createLegacyClientPathCheck` removed. The schema now
|
|
1340
1441
|
// rejects retired clientPaths keys (windsurf/rooCode/geminiCLI) at Zod
|
|
@@ -1401,19 +1502,19 @@ async function runDoctorFix(target) {
|
|
|
1401
1502
|
(issue) => [
|
|
1402
1503
|
"agents_meta_missing",
|
|
1403
1504
|
"agents_meta_stale",
|
|
1404
|
-
"
|
|
1405
|
-
"
|
|
1505
|
+
"knowledge_test_index_missing",
|
|
1506
|
+
"knowledge_test_index_stale",
|
|
1406
1507
|
"content_ref_missing",
|
|
1407
1508
|
"knowledge_dir_unindexed"
|
|
1408
1509
|
].includes(issue.code)
|
|
1409
1510
|
)) {
|
|
1410
|
-
await
|
|
1511
|
+
await reconcileKnowledge(projectRoot, { trigger: "doctor" });
|
|
1411
1512
|
for (const issue of before.fixable_errors.filter(
|
|
1412
1513
|
(candidate) => [
|
|
1413
1514
|
"agents_meta_missing",
|
|
1414
1515
|
"agents_meta_stale",
|
|
1415
|
-
"
|
|
1416
|
-
"
|
|
1516
|
+
"knowledge_test_index_missing",
|
|
1517
|
+
"knowledge_test_index_stale",
|
|
1417
1518
|
"content_ref_missing",
|
|
1418
1519
|
"knowledge_dir_unindexed"
|
|
1419
1520
|
].includes(candidate.code)
|
|
@@ -1449,6 +1550,376 @@ async function runDoctorFix(target) {
|
|
|
1449
1550
|
report
|
|
1450
1551
|
};
|
|
1451
1552
|
}
|
|
1553
|
+
var MANUAL_LINT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1554
|
+
"knowledge_stable_id_duplicate",
|
|
1555
|
+
"knowledge_layer_mismatch"
|
|
1556
|
+
]);
|
|
1557
|
+
async function runDoctorApplyLint(target) {
|
|
1558
|
+
const projectRoot = normalizeTarget(target);
|
|
1559
|
+
const before = await runDoctorReport(projectRoot);
|
|
1560
|
+
const mutations = [];
|
|
1561
|
+
const blockingManual = before.manual_errors.find(
|
|
1562
|
+
(issue) => MANUAL_LINT_ERROR_CODES.has(issue.code)
|
|
1563
|
+
);
|
|
1564
|
+
if (blockingManual !== void 0) {
|
|
1565
|
+
return {
|
|
1566
|
+
changed: false,
|
|
1567
|
+
mutations: [],
|
|
1568
|
+
manual_errors: before.manual_errors,
|
|
1569
|
+
aborted: true,
|
|
1570
|
+
abort_reason: `Manual repair required for ${blockingManual.code}: ${blockingManual.message} - apply-lint cannot resolve this safely; triage by hand.`,
|
|
1571
|
+
message: `apply-lint aborted: ${blockingManual.code} requires manual repair.`,
|
|
1572
|
+
report: before
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
const now = Date.now();
|
|
1576
|
+
const orphanDemote = await inspectOrphanDemote(projectRoot, now);
|
|
1577
|
+
for (const candidate of orphanDemote.candidates) {
|
|
1578
|
+
if (candidate.next_maturity === null) {
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
mutations.push(await applyOrphanDemote(projectRoot, candidate, now));
|
|
1582
|
+
}
|
|
1583
|
+
const staleArchive = await inspectStaleArchive(projectRoot, now);
|
|
1584
|
+
for (const candidate of staleArchive.candidates) {
|
|
1585
|
+
mutations.push(await applyStaleArchive(projectRoot, candidate, now));
|
|
1586
|
+
}
|
|
1587
|
+
const pendingAutoArchive = inspectPendingAutoArchive(projectRoot, now);
|
|
1588
|
+
for (const candidate of pendingAutoArchive.candidates) {
|
|
1589
|
+
mutations.push(await applyPendingAutoArchive(projectRoot, candidate, now));
|
|
1590
|
+
}
|
|
1591
|
+
const sessionHintsStale = inspectSessionHintsStale(projectRoot, now);
|
|
1592
|
+
for (const candidate of sessionHintsStale.candidates) {
|
|
1593
|
+
mutations.push(await applySessionHintsStaleCleanup(projectRoot, candidate));
|
|
1594
|
+
}
|
|
1595
|
+
const meta = await inspectMeta(projectRoot);
|
|
1596
|
+
const indexDrift = inspectIndexDrift(projectRoot, meta);
|
|
1597
|
+
if (indexDrift.drifts.length > 0) {
|
|
1598
|
+
mutations.push(await applyIndexDriftFix(projectRoot, indexDrift));
|
|
1599
|
+
}
|
|
1600
|
+
contextCache.invalidate("meta_write", projectRoot);
|
|
1601
|
+
const after = await runDoctorReport(projectRoot);
|
|
1602
|
+
const successCount = mutations.filter((m) => m.applied).length;
|
|
1603
|
+
const failureCount = mutations.length - successCount;
|
|
1604
|
+
return {
|
|
1605
|
+
changed: successCount > 0,
|
|
1606
|
+
mutations,
|
|
1607
|
+
manual_errors: after.manual_errors,
|
|
1608
|
+
aborted: false,
|
|
1609
|
+
message: createApplyLintMessage(successCount, failureCount, after.manual_errors.length),
|
|
1610
|
+
report: after
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
function createApplyLintMessage(succeeded, failed, manualErrorCount) {
|
|
1614
|
+
const parts = [];
|
|
1615
|
+
if (succeeded === 0 && failed === 0) {
|
|
1616
|
+
parts.push("No apply-lint mutations were needed.");
|
|
1617
|
+
} else {
|
|
1618
|
+
parts.push(`Applied ${succeeded} apply-lint mutation${succeeded === 1 ? "" : "s"}.`);
|
|
1619
|
+
if (failed > 0) {
|
|
1620
|
+
parts.push(`${failed} mutation${failed === 1 ? "" : "s"} failed.`);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
parts.push(
|
|
1624
|
+
manualErrorCount === 0 ? "No manual errors remain." : `${manualErrorCount} manual error${manualErrorCount === 1 ? "" : "s"} remain.`
|
|
1625
|
+
);
|
|
1626
|
+
return parts.join(" ");
|
|
1627
|
+
}
|
|
1628
|
+
function rewriteFrontmatterMaturity(source, newMaturity) {
|
|
1629
|
+
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
1630
|
+
const fm = FM_PATTERN.exec(source);
|
|
1631
|
+
if (fm === null) {
|
|
1632
|
+
return null;
|
|
1633
|
+
}
|
|
1634
|
+
const block = fm[1];
|
|
1635
|
+
if (!MATURITY_LINE_PATTERN.test(block)) {
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
const replacedBlock = block.replace(
|
|
1639
|
+
MATURITY_LINE_PATTERN,
|
|
1640
|
+
(line) => line.replace(/(stable|endorsed|draft)/u, newMaturity)
|
|
1641
|
+
);
|
|
1642
|
+
const blockStart = source.indexOf(block);
|
|
1643
|
+
if (blockStart < 0) {
|
|
1644
|
+
return null;
|
|
1645
|
+
}
|
|
1646
|
+
return source.slice(0, blockStart) + replacedBlock + source.slice(blockStart + block.length);
|
|
1647
|
+
}
|
|
1648
|
+
async function applyOrphanDemote(projectRoot, candidate, now) {
|
|
1649
|
+
const next = candidate.next_maturity;
|
|
1650
|
+
if (next === null) {
|
|
1651
|
+
return {
|
|
1652
|
+
kind: "knowledge_orphan_demote_required",
|
|
1653
|
+
path: candidate.path,
|
|
1654
|
+
detail: `${candidate.maturity} -> (none, already at terminal tier)`,
|
|
1655
|
+
applied: false,
|
|
1656
|
+
error: "next_maturity is null; orphan-demote not applicable"
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
const detail = `${candidate.maturity} -> ${next}`;
|
|
1660
|
+
const absPath = join5(projectRoot, candidate.path);
|
|
1661
|
+
try {
|
|
1662
|
+
const source = await readFile5(absPath, "utf8");
|
|
1663
|
+
const rewritten = rewriteFrontmatterMaturity(source, next);
|
|
1664
|
+
if (rewritten === null) {
|
|
1665
|
+
return {
|
|
1666
|
+
kind: "knowledge_orphan_demote_required",
|
|
1667
|
+
path: candidate.path,
|
|
1668
|
+
detail,
|
|
1669
|
+
applied: false,
|
|
1670
|
+
error: "frontmatter missing maturity field; cannot rewrite"
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
if (rewritten === source) {
|
|
1674
|
+
return {
|
|
1675
|
+
kind: "knowledge_orphan_demote_required",
|
|
1676
|
+
path: candidate.path,
|
|
1677
|
+
detail,
|
|
1678
|
+
applied: false,
|
|
1679
|
+
error: "rewrite produced byte-identical output"
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
1682
|
+
await atomicWriteText3(absPath, rewritten);
|
|
1683
|
+
try {
|
|
1684
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1685
|
+
event_type: "knowledge_demoted",
|
|
1686
|
+
stable_id: candidate.stable_id,
|
|
1687
|
+
timestamp: new Date(now).toISOString(),
|
|
1688
|
+
reason: `lint:orphan_demote ${candidate.maturity}->${next} after ${candidate.age_days}d inactive`
|
|
1689
|
+
});
|
|
1690
|
+
} catch (ledgerError) {
|
|
1691
|
+
try {
|
|
1692
|
+
await atomicWriteText3(absPath, source);
|
|
1693
|
+
} catch (rollbackError) {
|
|
1694
|
+
return {
|
|
1695
|
+
kind: "knowledge_orphan_demote_required",
|
|
1696
|
+
path: candidate.path,
|
|
1697
|
+
detail,
|
|
1698
|
+
applied: false,
|
|
1699
|
+
error: `ledger append failed (${truncateErrorMessage(ledgerError)}); rollback also failed (${truncateErrorMessage(rollbackError)}); disk may be in inconsistent state`
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
return {
|
|
1703
|
+
kind: "knowledge_orphan_demote_required",
|
|
1704
|
+
path: candidate.path,
|
|
1705
|
+
detail,
|
|
1706
|
+
applied: false,
|
|
1707
|
+
error: `ledger append failed (${truncateErrorMessage(ledgerError)}); frontmatter rolled back`
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
return {
|
|
1711
|
+
kind: "knowledge_orphan_demote_required",
|
|
1712
|
+
path: candidate.path,
|
|
1713
|
+
detail,
|
|
1714
|
+
applied: true
|
|
1715
|
+
};
|
|
1716
|
+
} catch (error) {
|
|
1717
|
+
return {
|
|
1718
|
+
kind: "knowledge_orphan_demote_required",
|
|
1719
|
+
path: candidate.path,
|
|
1720
|
+
detail,
|
|
1721
|
+
applied: false,
|
|
1722
|
+
error: truncateErrorMessage(error)
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
async function applyStaleArchive(projectRoot, candidate, now) {
|
|
1727
|
+
const sourceAbs = join5(projectRoot, candidate.path);
|
|
1728
|
+
const destAbs = join5(projectRoot, candidate.archive_path);
|
|
1729
|
+
const detail = `${candidate.path} -> ${candidate.archive_path}`;
|
|
1730
|
+
try {
|
|
1731
|
+
await mkdir3(join5(destAbs, ".."), { recursive: true });
|
|
1732
|
+
try {
|
|
1733
|
+
await rename(sourceAbs, destAbs);
|
|
1734
|
+
} catch (renameError) {
|
|
1735
|
+
if (renameError instanceof Error && "code" in renameError && renameError.code === "EXDEV") {
|
|
1736
|
+
const data = await readFile5(sourceAbs);
|
|
1737
|
+
await writeFile2(destAbs, data);
|
|
1738
|
+
const { unlink } = await import("fs/promises");
|
|
1739
|
+
await unlink(sourceAbs);
|
|
1740
|
+
} else {
|
|
1741
|
+
throw renameError;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
try {
|
|
1745
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1746
|
+
event_type: "knowledge_archived",
|
|
1747
|
+
stable_id: candidate.stable_id,
|
|
1748
|
+
timestamp: new Date(now).toISOString(),
|
|
1749
|
+
reason: `lint:stale_archive ${candidate.path} -> ${candidate.archive_path} after ${candidate.age_days}d inactive`
|
|
1750
|
+
});
|
|
1751
|
+
} catch (ledgerError) {
|
|
1752
|
+
try {
|
|
1753
|
+
await rename(destAbs, sourceAbs);
|
|
1754
|
+
} catch (rollbackError) {
|
|
1755
|
+
return {
|
|
1756
|
+
kind: "knowledge_stale_archive_required",
|
|
1757
|
+
path: candidate.path,
|
|
1758
|
+
detail,
|
|
1759
|
+
applied: false,
|
|
1760
|
+
error: `ledger append failed (${truncateErrorMessage(ledgerError)}); rollback also failed (${truncateErrorMessage(rollbackError)}); file may be stranded at ${candidate.archive_path}`
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
return {
|
|
1764
|
+
kind: "knowledge_stale_archive_required",
|
|
1765
|
+
path: candidate.path,
|
|
1766
|
+
detail,
|
|
1767
|
+
applied: false,
|
|
1768
|
+
error: `ledger append failed (${truncateErrorMessage(ledgerError)}); archive rolled back`
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
return {
|
|
1772
|
+
kind: "knowledge_stale_archive_required",
|
|
1773
|
+
path: candidate.path,
|
|
1774
|
+
detail,
|
|
1775
|
+
applied: true
|
|
1776
|
+
};
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
return {
|
|
1779
|
+
kind: "knowledge_stale_archive_required",
|
|
1780
|
+
path: candidate.path,
|
|
1781
|
+
detail,
|
|
1782
|
+
applied: false,
|
|
1783
|
+
error: truncateErrorMessage(error)
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
async function applyPendingAutoArchive(projectRoot, candidate, now) {
|
|
1788
|
+
const detail = `${candidate.pending_path} -> ${candidate.archived_to}`;
|
|
1789
|
+
try {
|
|
1790
|
+
await mkdir3(join5(candidate.archived_to_abs, ".."), { recursive: true });
|
|
1791
|
+
let moved = false;
|
|
1792
|
+
if (candidate.layer === "team") {
|
|
1793
|
+
try {
|
|
1794
|
+
const relSource = relativePosix(projectRoot, candidate.pending_path_abs);
|
|
1795
|
+
const relDest = relativePosix(projectRoot, candidate.archived_to_abs);
|
|
1796
|
+
execFileSync("git", ["mv", "-f", relSource, relDest], {
|
|
1797
|
+
cwd: projectRoot,
|
|
1798
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1799
|
+
});
|
|
1800
|
+
moved = true;
|
|
1801
|
+
} catch {
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
if (!moved) {
|
|
1805
|
+
try {
|
|
1806
|
+
await rename(candidate.pending_path_abs, candidate.archived_to_abs);
|
|
1807
|
+
} catch (renameError) {
|
|
1808
|
+
if (renameError instanceof Error && "code" in renameError && renameError.code === "EXDEV") {
|
|
1809
|
+
const data = await readFile5(candidate.pending_path_abs);
|
|
1810
|
+
await writeFile2(candidate.archived_to_abs, data);
|
|
1811
|
+
const { unlink } = await import("fs/promises");
|
|
1812
|
+
await unlink(candidate.pending_path_abs);
|
|
1813
|
+
} else {
|
|
1814
|
+
throw renameError;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
try {
|
|
1819
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1820
|
+
event_type: "pending_auto_archived",
|
|
1821
|
+
pending_path: candidate.pending_path,
|
|
1822
|
+
archived_to: candidate.archived_to,
|
|
1823
|
+
reason: "auto_archive_30d"
|
|
1824
|
+
});
|
|
1825
|
+
} catch (ledgerError) {
|
|
1826
|
+
try {
|
|
1827
|
+
await rename(candidate.archived_to_abs, candidate.pending_path_abs);
|
|
1828
|
+
} catch (rollbackError) {
|
|
1829
|
+
return {
|
|
1830
|
+
kind: "knowledge_pending_auto_archive",
|
|
1831
|
+
path: candidate.pending_path,
|
|
1832
|
+
detail,
|
|
1833
|
+
applied: false,
|
|
1834
|
+
error: `ledger append failed (${truncateErrorMessage(ledgerError)}); rollback also failed (${truncateErrorMessage(rollbackError)}); file may be stranded at ${candidate.archived_to}`
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
return {
|
|
1838
|
+
kind: "knowledge_pending_auto_archive",
|
|
1839
|
+
path: candidate.pending_path,
|
|
1840
|
+
detail,
|
|
1841
|
+
applied: false,
|
|
1842
|
+
error: `ledger append failed (${truncateErrorMessage(ledgerError)}); archive rolled back`
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
return {
|
|
1846
|
+
kind: "knowledge_pending_auto_archive",
|
|
1847
|
+
path: candidate.pending_path,
|
|
1848
|
+
detail,
|
|
1849
|
+
applied: true
|
|
1850
|
+
};
|
|
1851
|
+
} catch (error) {
|
|
1852
|
+
return {
|
|
1853
|
+
kind: "knowledge_pending_auto_archive",
|
|
1854
|
+
path: candidate.pending_path,
|
|
1855
|
+
detail,
|
|
1856
|
+
applied: false,
|
|
1857
|
+
error: truncateErrorMessage(error)
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
function relativePosix(projectRoot, absolutePath) {
|
|
1862
|
+
const rel = nodeRelative(projectRoot, absolutePath);
|
|
1863
|
+
return rel.split(sep4).join("/");
|
|
1864
|
+
}
|
|
1865
|
+
async function applySessionHintsStaleCleanup(projectRoot, candidate) {
|
|
1866
|
+
const detail = `deleted (${candidate.age_days}d old)`;
|
|
1867
|
+
const absPath = join5(projectRoot, candidate.path);
|
|
1868
|
+
try {
|
|
1869
|
+
const { unlink } = await import("fs/promises");
|
|
1870
|
+
await unlink(absPath);
|
|
1871
|
+
return {
|
|
1872
|
+
kind: "knowledge_session_hints_stale_cleanup",
|
|
1873
|
+
path: candidate.path,
|
|
1874
|
+
detail,
|
|
1875
|
+
applied: true
|
|
1876
|
+
};
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
return {
|
|
1879
|
+
kind: "knowledge_session_hints_stale_cleanup",
|
|
1880
|
+
path: candidate.path,
|
|
1881
|
+
detail,
|
|
1882
|
+
applied: false,
|
|
1883
|
+
error: truncateErrorMessage(error)
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
async function applyIndexDriftFix(projectRoot, inspection) {
|
|
1888
|
+
const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
|
|
1889
|
+
const detailParts = [];
|
|
1890
|
+
try {
|
|
1891
|
+
const meta = agentsMetaSchema4.parse(JSON.parse(await readFile5(metaPath, "utf8")));
|
|
1892
|
+
const baseCounters = AgentsMetaCountersSchema.parse(meta.counters ?? void 0);
|
|
1893
|
+
const updatedCounters = {
|
|
1894
|
+
KP: { ...baseCounters.KP },
|
|
1895
|
+
KT: { ...baseCounters.KT }
|
|
1896
|
+
};
|
|
1897
|
+
for (const drift of inspection.drifts) {
|
|
1898
|
+
updatedCounters[drift.layer][drift.type] = drift.proposed_after;
|
|
1899
|
+
detailParts.push(`${drift.layer}.${drift.type}: ${drift.counter} -> ${drift.proposed_after}`);
|
|
1900
|
+
}
|
|
1901
|
+
const updated = { ...meta, counters: updatedCounters };
|
|
1902
|
+
await atomicWriteJson2(metaPath, updated, { indent: 2 });
|
|
1903
|
+
return {
|
|
1904
|
+
kind: "knowledge_index_drift",
|
|
1905
|
+
path: "agents.meta.json#counters",
|
|
1906
|
+
detail: detailParts.join("; "),
|
|
1907
|
+
applied: true
|
|
1908
|
+
};
|
|
1909
|
+
} catch (error) {
|
|
1910
|
+
return {
|
|
1911
|
+
kind: "knowledge_index_drift",
|
|
1912
|
+
path: "agents.meta.json#counters",
|
|
1913
|
+
detail: detailParts.join("; ") || "(no counters processed)",
|
|
1914
|
+
applied: false,
|
|
1915
|
+
error: truncateErrorMessage(error)
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
function truncateErrorMessage(error) {
|
|
1920
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
1921
|
+
return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
|
|
1922
|
+
}
|
|
1452
1923
|
async function inspectForensic(projectRoot) {
|
|
1453
1924
|
const path = join5(projectRoot, ".fabric", "forensic.json");
|
|
1454
1925
|
try {
|
|
@@ -1537,7 +2008,7 @@ async function inspectMeta(projectRoot) {
|
|
|
1537
2008
|
}
|
|
1538
2009
|
async function tryBuildRuleMeta(projectRoot) {
|
|
1539
2010
|
try {
|
|
1540
|
-
return await
|
|
2011
|
+
return await buildKnowledgeMeta(projectRoot);
|
|
1541
2012
|
} catch {
|
|
1542
2013
|
return null;
|
|
1543
2014
|
}
|
|
@@ -1597,15 +2068,15 @@ async function inspectEventLedger(projectRoot) {
|
|
|
1597
2068
|
};
|
|
1598
2069
|
}
|
|
1599
2070
|
}
|
|
1600
|
-
async function
|
|
1601
|
-
const path = join5(projectRoot, ".fabric", "
|
|
2071
|
+
async function inspectKnowledgeTestIndex(projectRoot) {
|
|
2072
|
+
const path = join5(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
|
|
1602
2073
|
const built = await tryBuildRuleMeta(projectRoot);
|
|
1603
2074
|
try {
|
|
1604
|
-
const index =
|
|
2075
|
+
const index = knowledgeTestIndexSchema2.parse(JSON.parse(await readFile5(path, "utf8")));
|
|
1605
2076
|
return {
|
|
1606
2077
|
present: true,
|
|
1607
2078
|
valid: true,
|
|
1608
|
-
stale: built === null ? false : !
|
|
2079
|
+
stale: built === null ? false : !isSameKnowledgeTestIndex(index, built.knowledgeTestIndex),
|
|
1609
2080
|
linkCount: index.links.length,
|
|
1610
2081
|
orphanCount: index.orphan_annotations.length
|
|
1611
2082
|
};
|
|
@@ -1616,7 +2087,7 @@ async function inspectRuleTestIndex(projectRoot) {
|
|
|
1616
2087
|
stale: true,
|
|
1617
2088
|
linkCount: 0,
|
|
1618
2089
|
orphanCount: 0,
|
|
1619
|
-
error: isMissingFileError(error) ? ".fabric/
|
|
2090
|
+
error: isMissingFileError(error) ? ".fabric/.cache/knowledge-test.index.json is missing." : error instanceof Error ? error.message : String(error)
|
|
1620
2091
|
};
|
|
1621
2092
|
}
|
|
1622
2093
|
}
|
|
@@ -1732,17 +2203,17 @@ function createRuleContentRefCheck(meta) {
|
|
|
1732
2203
|
}
|
|
1733
2204
|
return okCheck("Rule content refs", "All content_ref entries resolve to .fabric/knowledge files.");
|
|
1734
2205
|
}
|
|
1735
|
-
function
|
|
2206
|
+
function createKnowledgeTestIndexCheck(index) {
|
|
1736
2207
|
if (!index.present) {
|
|
1737
|
-
return issueCheck("
|
|
2208
|
+
return issueCheck("Knowledge-test index", "error", "fixable_error", "knowledge_test_index_missing", index.error, "Run `fab doctor --fix` to rebuild .fabric/.cache/knowledge-test.index.json.");
|
|
1738
2209
|
}
|
|
1739
2210
|
if (!index.valid) {
|
|
1740
|
-
return issueCheck("
|
|
2211
|
+
return issueCheck("Knowledge-test index", "error", "manual_error", "knowledge_test_index_invalid", index.error, "Delete .fabric/.cache/knowledge-test.index.json and run `fab doctor --fix` to regenerate it.");
|
|
1741
2212
|
}
|
|
1742
2213
|
if (index.stale) {
|
|
1743
|
-
return issueCheck("
|
|
2214
|
+
return issueCheck("Knowledge-test index", "error", "fixable_error", "knowledge_test_index_stale", ".fabric/.cache/knowledge-test.index.json is stale.", "Run `fab doctor --fix` to rebuild the knowledge-test index.");
|
|
1744
2215
|
}
|
|
1745
|
-
return okCheck("
|
|
2216
|
+
return okCheck("Knowledge-test index", `${index.linkCount} link${index.linkCount === 1 ? "" : "s"} and ${index.orphanCount} orphan annotation${index.orphanCount === 1 ? "" : "s"} indexed.`);
|
|
1746
2217
|
}
|
|
1747
2218
|
function createEventLedgerCheck(ledger) {
|
|
1748
2219
|
if (!ledger.exists) {
|
|
@@ -2071,6 +2542,83 @@ function inspectPreexistingRootFiles(projectRoot) {
|
|
|
2071
2542
|
const detected = candidates.filter((name) => existsSync4(join5(projectRoot, name)));
|
|
2072
2543
|
return { detected };
|
|
2073
2544
|
}
|
|
2545
|
+
async function inspectFilesystemEditFallback(projectRoot) {
|
|
2546
|
+
const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
|
|
2547
|
+
if (!existsSync4(knowledgeRoot)) {
|
|
2548
|
+
return { synthesized: 0, synthesizedStableIds: [] };
|
|
2549
|
+
}
|
|
2550
|
+
const canonicalIds = /* @__PURE__ */ new Set();
|
|
2551
|
+
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
2552
|
+
const dir = join5(knowledgeRoot, typeDir);
|
|
2553
|
+
if (!existsSync4(dir)) {
|
|
2554
|
+
continue;
|
|
2555
|
+
}
|
|
2556
|
+
let entries;
|
|
2557
|
+
try {
|
|
2558
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
2559
|
+
} catch {
|
|
2560
|
+
continue;
|
|
2561
|
+
}
|
|
2562
|
+
for (const entry of entries) {
|
|
2563
|
+
if (!entry.isFile()) {
|
|
2564
|
+
continue;
|
|
2565
|
+
}
|
|
2566
|
+
const match = CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(entry.name);
|
|
2567
|
+
if (match === null) {
|
|
2568
|
+
continue;
|
|
2569
|
+
}
|
|
2570
|
+
canonicalIds.add(match[1]);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
if (canonicalIds.size === 0) {
|
|
2574
|
+
return { synthesized: 0, synthesizedStableIds: [] };
|
|
2575
|
+
}
|
|
2576
|
+
let promotedIds = /* @__PURE__ */ new Set();
|
|
2577
|
+
try {
|
|
2578
|
+
const { events } = await readEventLedger(projectRoot, { event_type: "knowledge_promoted" });
|
|
2579
|
+
promotedIds = new Set(
|
|
2580
|
+
events.map((event) => event.event_type === "knowledge_promoted" ? event.stable_id : void 0).filter((id) => typeof id === "string")
|
|
2581
|
+
);
|
|
2582
|
+
} catch {
|
|
2583
|
+
promotedIds = /* @__PURE__ */ new Set();
|
|
2584
|
+
}
|
|
2585
|
+
const orphanIds = [];
|
|
2586
|
+
for (const id of canonicalIds) {
|
|
2587
|
+
if (!promotedIds.has(id)) {
|
|
2588
|
+
orphanIds.push(id);
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
orphanIds.sort();
|
|
2592
|
+
for (const stable_id of orphanIds) {
|
|
2593
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
2594
|
+
event_type: "knowledge_promoted",
|
|
2595
|
+
stable_id,
|
|
2596
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2597
|
+
reason: SYNTHESIZED_PROMOTED_REASON,
|
|
2598
|
+
correlation_id: "doctor-synthesized",
|
|
2599
|
+
session_id: "doctor-synthesized"
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
return { synthesized: orphanIds.length, synthesizedStableIds: orphanIds };
|
|
2603
|
+
}
|
|
2604
|
+
function createFilesystemEditFallbackCheck(inspection) {
|
|
2605
|
+
if (inspection.synthesized === 0) {
|
|
2606
|
+
return okCheck(
|
|
2607
|
+
"Filesystem-edit fallback",
|
|
2608
|
+
"No orphan canonical knowledge entries detected; events.jsonl promotion trail is complete."
|
|
2609
|
+
);
|
|
2610
|
+
}
|
|
2611
|
+
const sample = inspection.synthesizedStableIds.slice(0, 3).join(", ");
|
|
2612
|
+
return {
|
|
2613
|
+
name: "Filesystem-edit fallback",
|
|
2614
|
+
status: "ok",
|
|
2615
|
+
kind: "info",
|
|
2616
|
+
code: "knowledge_promoted_synthesized",
|
|
2617
|
+
fixable: false,
|
|
2618
|
+
message: `Synthesized ${inspection.synthesized} knowledge_promoted event${inspection.synthesized === 1 ? "" : "s"} for orphan canonical entries (${sample}${inspection.synthesizedStableIds.length > 3 ? ", ..." : ""}). Reason='${SYNTHESIZED_PROMOTED_REASON}'.`,
|
|
2619
|
+
actionHint: "These entries were moved into .fabric/knowledge/<type>/ outside fab_review.approve. The synthesized events restore audit-trail completeness."
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2074
2622
|
function createPreexistingRootFilesCheck(inspection) {
|
|
2075
2623
|
if (inspection.detected.length === 0) {
|
|
2076
2624
|
return okCheck("Preexisting root markdown", "No CLAUDE.md or AGENTS.md detected at project root.");
|
|
@@ -2085,85 +2633,1130 @@ function createPreexistingRootFilesCheck(inspection) {
|
|
|
2085
2633
|
actionHint: "Move knowledge content to `.fabric/knowledge/{type}/` if you want it available in MCP responses."
|
|
2086
2634
|
};
|
|
2087
2635
|
}
|
|
2088
|
-
async function
|
|
2089
|
-
const
|
|
2090
|
-
|
|
2091
|
-
return;
|
|
2092
|
-
}
|
|
2093
|
-
let settings;
|
|
2636
|
+
async function buildLastConsumedIndex(projectRoot) {
|
|
2637
|
+
const map = /* @__PURE__ */ new Map();
|
|
2638
|
+
let events;
|
|
2094
2639
|
try {
|
|
2095
|
-
|
|
2096
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2097
|
-
return;
|
|
2098
|
-
}
|
|
2099
|
-
settings = parsed;
|
|
2640
|
+
({ events } = await readEventLedger(projectRoot));
|
|
2100
2641
|
} catch {
|
|
2101
|
-
return;
|
|
2102
|
-
}
|
|
2103
|
-
const mcpServers = settings.mcpServers;
|
|
2104
|
-
if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
|
|
2105
|
-
return;
|
|
2106
|
-
}
|
|
2107
|
-
const { fabric: _removed, ...remainingServers } = mcpServers;
|
|
2108
|
-
const cleaned = { ...settings };
|
|
2109
|
-
if (Object.keys(remainingServers).length === 0) {
|
|
2110
|
-
delete cleaned.mcpServers;
|
|
2111
|
-
} else {
|
|
2112
|
-
cleaned.mcpServers = remainingServers;
|
|
2642
|
+
return map;
|
|
2113
2643
|
}
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2644
|
+
for (const event of events) {
|
|
2645
|
+
if (event.event_type !== "knowledge_consumed" && event.event_type !== "knowledge_demoted" && event.event_type !== "knowledge_archived") {
|
|
2646
|
+
continue;
|
|
2647
|
+
}
|
|
2648
|
+
const ts = event.ts;
|
|
2649
|
+
if (typeof ts !== "number" || !Number.isFinite(ts)) {
|
|
2650
|
+
continue;
|
|
2651
|
+
}
|
|
2652
|
+
const stableId = event.stable_id;
|
|
2653
|
+
if (typeof stableId !== "string" || stableId.length === 0) {
|
|
2654
|
+
continue;
|
|
2655
|
+
}
|
|
2656
|
+
const prev = map.get(stableId);
|
|
2657
|
+
if (prev === void 0 || ts > prev) {
|
|
2658
|
+
map.set(stableId, ts);
|
|
2659
|
+
}
|
|
2124
2660
|
}
|
|
2661
|
+
return map;
|
|
2125
2662
|
}
|
|
2126
|
-
async function
|
|
2127
|
-
const
|
|
2128
|
-
|
|
2129
|
-
return;
|
|
2130
|
-
}
|
|
2131
|
-
let meta;
|
|
2663
|
+
async function buildLastActiveIndex(projectRoot) {
|
|
2664
|
+
const map = /* @__PURE__ */ new Map();
|
|
2665
|
+
let events;
|
|
2132
2666
|
try {
|
|
2133
|
-
|
|
2667
|
+
({ events } = await readEventLedger(projectRoot));
|
|
2134
2668
|
} catch {
|
|
2135
|
-
return;
|
|
2669
|
+
return map;
|
|
2136
2670
|
}
|
|
2137
|
-
const
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2671
|
+
for (const event of events) {
|
|
2672
|
+
const ts = event.ts;
|
|
2673
|
+
if (typeof ts !== "number" || !Number.isFinite(ts)) {
|
|
2674
|
+
continue;
|
|
2675
|
+
}
|
|
2676
|
+
const ids = [];
|
|
2677
|
+
switch (event.event_type) {
|
|
2678
|
+
case "knowledge_proposed":
|
|
2679
|
+
case "knowledge_promote_started":
|
|
2680
|
+
case "knowledge_promoted":
|
|
2681
|
+
case "knowledge_promote_failed":
|
|
2682
|
+
case "knowledge_layer_changed":
|
|
2683
|
+
case "knowledge_slug_renamed":
|
|
2684
|
+
case "knowledge_demoted":
|
|
2685
|
+
case "knowledge_archived":
|
|
2686
|
+
case "knowledge_archive_attempted":
|
|
2687
|
+
case "knowledge_deferred":
|
|
2688
|
+
case "knowledge_rejected": {
|
|
2689
|
+
if (typeof event.stable_id === "string" && event.stable_id.length > 0) {
|
|
2690
|
+
ids.push(event.stable_id);
|
|
2691
|
+
}
|
|
2692
|
+
break;
|
|
2693
|
+
}
|
|
2694
|
+
case "knowledge_context_planned": {
|
|
2695
|
+
ids.push(...event.required_stable_ids, ...event.ai_selectable_stable_ids, ...event.final_stable_ids);
|
|
2696
|
+
break;
|
|
2697
|
+
}
|
|
2698
|
+
case "knowledge_selection": {
|
|
2699
|
+
ids.push(
|
|
2700
|
+
...event.required_stable_ids,
|
|
2701
|
+
...event.ai_selectable_stable_ids,
|
|
2702
|
+
...event.ai_selected_stable_ids,
|
|
2703
|
+
...event.final_stable_ids
|
|
2704
|
+
);
|
|
2705
|
+
break;
|
|
2706
|
+
}
|
|
2707
|
+
case "knowledge_sections_fetched": {
|
|
2708
|
+
ids.push(...event.final_stable_ids, ...event.ai_selected_stable_ids);
|
|
2709
|
+
break;
|
|
2710
|
+
}
|
|
2711
|
+
default:
|
|
2712
|
+
break;
|
|
2713
|
+
}
|
|
2714
|
+
for (const id of ids) {
|
|
2715
|
+
const prev = map.get(id);
|
|
2716
|
+
if (prev === void 0 || ts > prev) {
|
|
2717
|
+
map.set(id, ts);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2152
2720
|
}
|
|
2153
|
-
|
|
2154
|
-
await atomicWriteJson2(metaPath, updated, { indent: 2 });
|
|
2721
|
+
return map;
|
|
2155
2722
|
}
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
await ensureParentDirectory(path);
|
|
2159
|
-
await writeFile2(path, "", { encoding: "utf8", flag: "a" });
|
|
2723
|
+
function maturityThresholdDays(maturity) {
|
|
2724
|
+
return ORPHAN_DEMOTE_THRESHOLD_DAYS[maturity];
|
|
2160
2725
|
}
|
|
2161
|
-
function
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
return
|
|
2726
|
+
function nextLowerMaturity(current) {
|
|
2727
|
+
if (current === "stable") return "endorsed";
|
|
2728
|
+
if (current === "endorsed") return "draft";
|
|
2729
|
+
return null;
|
|
2165
2730
|
}
|
|
2166
|
-
function
|
|
2731
|
+
function extractKnowledgeFrontmatterMaturity(source) {
|
|
2732
|
+
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
2733
|
+
const fm = FM_PATTERN.exec(source);
|
|
2734
|
+
if (fm === null) {
|
|
2735
|
+
return null;
|
|
2736
|
+
}
|
|
2737
|
+
const match = MATURITY_LINE_PATTERN.exec(fm[1]);
|
|
2738
|
+
return match === null ? null : match[2];
|
|
2739
|
+
}
|
|
2740
|
+
function extractKnowledgeFrontmatterCreatedAt(source) {
|
|
2741
|
+
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
2742
|
+
const fm = FM_PATTERN.exec(source);
|
|
2743
|
+
if (fm === null) {
|
|
2744
|
+
return null;
|
|
2745
|
+
}
|
|
2746
|
+
const match = CREATED_AT_LINE_PATTERN.exec(fm[1]);
|
|
2747
|
+
if (match === null) {
|
|
2748
|
+
return null;
|
|
2749
|
+
}
|
|
2750
|
+
const parsed = Date.parse(match[2]);
|
|
2751
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2752
|
+
}
|
|
2753
|
+
function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
|
|
2754
|
+
const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
|
|
2755
|
+
if (!existsSync4(knowledgeRoot)) {
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
2759
|
+
const dir = join5(knowledgeRoot, typeDir);
|
|
2760
|
+
if (!existsSync4(dir)) {
|
|
2761
|
+
continue;
|
|
2762
|
+
}
|
|
2763
|
+
let entries;
|
|
2764
|
+
try {
|
|
2765
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
2766
|
+
} catch {
|
|
2767
|
+
continue;
|
|
2768
|
+
}
|
|
2769
|
+
for (const entry of entries) {
|
|
2770
|
+
if (!entry.isFile()) {
|
|
2771
|
+
continue;
|
|
2772
|
+
}
|
|
2773
|
+
const match = CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(entry.name);
|
|
2774
|
+
if (match === null) {
|
|
2775
|
+
continue;
|
|
2776
|
+
}
|
|
2777
|
+
const stableId = match[1];
|
|
2778
|
+
const absPath = join5(dir, entry.name);
|
|
2779
|
+
let source;
|
|
2780
|
+
try {
|
|
2781
|
+
source = readFileSync(absPath, "utf8");
|
|
2782
|
+
} catch {
|
|
2783
|
+
continue;
|
|
2784
|
+
}
|
|
2785
|
+
const maturity = extractKnowledgeFrontmatterMaturity(source);
|
|
2786
|
+
if (maturity === null) {
|
|
2787
|
+
continue;
|
|
2788
|
+
}
|
|
2789
|
+
const createdAt = extractKnowledgeFrontmatterCreatedAt(source);
|
|
2790
|
+
const eventTs = lastActiveIndex.get(stableId) ?? 0;
|
|
2791
|
+
let lastReferenceMs = Math.max(createdAt ?? 0, eventTs);
|
|
2792
|
+
if (lastReferenceMs === 0) {
|
|
2793
|
+
try {
|
|
2794
|
+
lastReferenceMs = statSync3(absPath).mtimeMs;
|
|
2795
|
+
} catch {
|
|
2796
|
+
lastReferenceMs = 0;
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
const relPath = posix.join(
|
|
2800
|
+
".fabric/knowledge",
|
|
2801
|
+
typeDir,
|
|
2802
|
+
entry.name
|
|
2803
|
+
);
|
|
2804
|
+
yield { stable_id: stableId, maturity, type: typeDir, absPath, relPath, lastReferenceMs };
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
async function inspectOrphanDemote(projectRoot, now) {
|
|
2809
|
+
const lastConsumedIndex = await buildLastConsumedIndex(projectRoot);
|
|
2810
|
+
const candidates = [];
|
|
2811
|
+
for (const entry of iterateCanonicalEntries(projectRoot, lastConsumedIndex)) {
|
|
2812
|
+
const ageMs = entry.lastReferenceMs > 0 ? now - entry.lastReferenceMs : now;
|
|
2813
|
+
const ageDays = Math.floor(ageMs / MS_PER_DAY);
|
|
2814
|
+
const threshold = maturityThresholdDays(entry.maturity);
|
|
2815
|
+
if (ageDays <= threshold) {
|
|
2816
|
+
continue;
|
|
2817
|
+
}
|
|
2818
|
+
candidates.push({
|
|
2819
|
+
stable_id: entry.stable_id,
|
|
2820
|
+
path: entry.relPath,
|
|
2821
|
+
age_days: ageDays,
|
|
2822
|
+
maturity: entry.maturity,
|
|
2823
|
+
next_maturity: nextLowerMaturity(entry.maturity)
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
candidates.sort((a, b) => a.path.localeCompare(b.path));
|
|
2827
|
+
return { candidates };
|
|
2828
|
+
}
|
|
2829
|
+
async function inspectStaleArchive(projectRoot, now) {
|
|
2830
|
+
const lastActiveIndex = await buildLastActiveIndex(projectRoot);
|
|
2831
|
+
const candidates = [];
|
|
2832
|
+
for (const entry of iterateCanonicalEntries(projectRoot, lastActiveIndex)) {
|
|
2833
|
+
if (entry.maturity !== "draft") {
|
|
2834
|
+
continue;
|
|
2835
|
+
}
|
|
2836
|
+
const ageMs = entry.lastReferenceMs > 0 ? now - entry.lastReferenceMs : now;
|
|
2837
|
+
const ageDays = Math.floor(ageMs / MS_PER_DAY);
|
|
2838
|
+
const requiredQuiet = ORPHAN_DEMOTE_THRESHOLD_DAYS.draft + STALE_ARCHIVE_ADDITIONAL_DAYS;
|
|
2839
|
+
if (ageDays <= requiredQuiet) {
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
const filename = posix.basename(entry.relPath);
|
|
2843
|
+
candidates.push({
|
|
2844
|
+
stable_id: entry.stable_id,
|
|
2845
|
+
path: entry.relPath,
|
|
2846
|
+
age_days: ageDays,
|
|
2847
|
+
archive_path: posix.join(".fabric/.archive", entry.type, filename)
|
|
2848
|
+
});
|
|
2849
|
+
}
|
|
2850
|
+
candidates.sort((a, b) => a.path.localeCompare(b.path));
|
|
2851
|
+
return { candidates };
|
|
2852
|
+
}
|
|
2853
|
+
function* iteratePendingFiles(projectRoot, now) {
|
|
2854
|
+
const teamRoot = join5(projectRoot, ".fabric", "knowledge", "pending");
|
|
2855
|
+
const personalRoot = join5(resolvePersonalRootForPending(), ".fabric", "knowledge", "pending");
|
|
2856
|
+
for (const [layer, root, displayPrefix] of [
|
|
2857
|
+
["team", teamRoot, ".fabric/knowledge/pending"],
|
|
2858
|
+
["personal", personalRoot, "~/.fabric/knowledge/pending"]
|
|
2859
|
+
]) {
|
|
2860
|
+
if (!existsSync4(root)) {
|
|
2861
|
+
continue;
|
|
2862
|
+
}
|
|
2863
|
+
let typeDirs = [];
|
|
2864
|
+
try {
|
|
2865
|
+
typeDirs = readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
2866
|
+
} catch {
|
|
2867
|
+
continue;
|
|
2868
|
+
}
|
|
2869
|
+
for (const typeDir of typeDirs) {
|
|
2870
|
+
const dir = join5(root, typeDir);
|
|
2871
|
+
let entries;
|
|
2872
|
+
try {
|
|
2873
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
2874
|
+
} catch {
|
|
2875
|
+
continue;
|
|
2876
|
+
}
|
|
2877
|
+
for (const entry of entries) {
|
|
2878
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
2879
|
+
continue;
|
|
2880
|
+
}
|
|
2881
|
+
const absPath = join5(dir, entry.name);
|
|
2882
|
+
let source = "";
|
|
2883
|
+
try {
|
|
2884
|
+
source = readFileSync(absPath, "utf8");
|
|
2885
|
+
} catch {
|
|
2886
|
+
continue;
|
|
2887
|
+
}
|
|
2888
|
+
const createdAt = extractKnowledgeFrontmatterCreatedAt(source);
|
|
2889
|
+
let mtimeMs = 0;
|
|
2890
|
+
try {
|
|
2891
|
+
mtimeMs = statSync3(absPath).mtimeMs;
|
|
2892
|
+
} catch {
|
|
2893
|
+
mtimeMs = 0;
|
|
2894
|
+
}
|
|
2895
|
+
const referenceMs = createdAt ?? mtimeMs;
|
|
2896
|
+
const displayPath = posix.join(displayPrefix, typeDir, entry.name);
|
|
2897
|
+
if (referenceMs === 0) {
|
|
2898
|
+
yield {
|
|
2899
|
+
layer,
|
|
2900
|
+
type: typeDir,
|
|
2901
|
+
filename: entry.name,
|
|
2902
|
+
pending_path: displayPath,
|
|
2903
|
+
pending_path_abs: absPath,
|
|
2904
|
+
stable_id: void 0,
|
|
2905
|
+
age_days: PENDING_OVERDUE_THRESHOLD_DAYS + 1
|
|
2906
|
+
};
|
|
2907
|
+
continue;
|
|
2908
|
+
}
|
|
2909
|
+
const ageDays = Math.floor((now - referenceMs) / MS_PER_DAY);
|
|
2910
|
+
const stableId = extractKnowledgeFrontmatterId(source) ?? void 0;
|
|
2911
|
+
yield {
|
|
2912
|
+
layer,
|
|
2913
|
+
type: typeDir,
|
|
2914
|
+
filename: entry.name,
|
|
2915
|
+
pending_path: displayPath,
|
|
2916
|
+
pending_path_abs: absPath,
|
|
2917
|
+
stable_id: stableId,
|
|
2918
|
+
age_days: ageDays
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
function resolvePersonalRootForPending() {
|
|
2925
|
+
return process.env.FABRIC_HOME ?? homedir2();
|
|
2926
|
+
}
|
|
2927
|
+
function inspectPendingOverdue(projectRoot, now) {
|
|
2928
|
+
const candidates = [];
|
|
2929
|
+
for (const visit of iteratePendingFiles(projectRoot, now)) {
|
|
2930
|
+
if (visit.age_days <= PENDING_OVERDUE_THRESHOLD_DAYS) {
|
|
2931
|
+
continue;
|
|
2932
|
+
}
|
|
2933
|
+
candidates.push({
|
|
2934
|
+
stable_id: visit.stable_id,
|
|
2935
|
+
path: visit.pending_path,
|
|
2936
|
+
age_days: visit.age_days
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
candidates.sort((a, b) => a.path.localeCompare(b.path));
|
|
2940
|
+
return { candidates };
|
|
2941
|
+
}
|
|
2942
|
+
function inspectPendingAutoArchive(projectRoot, now) {
|
|
2943
|
+
const candidates = [];
|
|
2944
|
+
for (const visit of iteratePendingFiles(projectRoot, now)) {
|
|
2945
|
+
if (visit.age_days <= PENDING_AUTO_ARCHIVE_THRESHOLD_DAYS) {
|
|
2946
|
+
continue;
|
|
2947
|
+
}
|
|
2948
|
+
if (visit.layer === "team") {
|
|
2949
|
+
const archivedToRel = posix.join(".fabric/.archive/pending", visit.type, visit.filename);
|
|
2950
|
+
candidates.push({
|
|
2951
|
+
layer: "team",
|
|
2952
|
+
type: visit.type,
|
|
2953
|
+
pending_path: visit.pending_path,
|
|
2954
|
+
pending_path_abs: visit.pending_path_abs,
|
|
2955
|
+
archived_to: archivedToRel,
|
|
2956
|
+
archived_to_abs: join5(projectRoot, archivedToRel),
|
|
2957
|
+
age_days: visit.age_days
|
|
2958
|
+
});
|
|
2959
|
+
} else {
|
|
2960
|
+
const archivedToDisplay = posix.join(
|
|
2961
|
+
"~/.fabric/.archive/pending",
|
|
2962
|
+
visit.type,
|
|
2963
|
+
visit.filename
|
|
2964
|
+
);
|
|
2965
|
+
const archivedToAbs = join5(
|
|
2966
|
+
resolvePersonalRootForPending(),
|
|
2967
|
+
".fabric",
|
|
2968
|
+
".archive",
|
|
2969
|
+
"pending",
|
|
2970
|
+
visit.type,
|
|
2971
|
+
visit.filename
|
|
2972
|
+
);
|
|
2973
|
+
candidates.push({
|
|
2974
|
+
layer: "personal",
|
|
2975
|
+
type: visit.type,
|
|
2976
|
+
pending_path: visit.pending_path,
|
|
2977
|
+
pending_path_abs: visit.pending_path_abs,
|
|
2978
|
+
archived_to: archivedToDisplay,
|
|
2979
|
+
archived_to_abs: archivedToAbs,
|
|
2980
|
+
age_days: visit.age_days
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
candidates.sort((a, b) => a.pending_path.localeCompare(b.pending_path));
|
|
2985
|
+
return { candidates };
|
|
2986
|
+
}
|
|
2987
|
+
function inspectUnderseeded(projectRoot) {
|
|
2988
|
+
const threshold = readUnderseedThresholdFromConfig(projectRoot);
|
|
2989
|
+
const knowledgeRoot = join5(projectRoot, ".fabric", "knowledge");
|
|
2990
|
+
let nodeCount = 0;
|
|
2991
|
+
if (existsSync4(knowledgeRoot)) {
|
|
2992
|
+
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
2993
|
+
const dir = join5(knowledgeRoot, typeDir);
|
|
2994
|
+
if (!existsSync4(dir)) continue;
|
|
2995
|
+
let entries;
|
|
2996
|
+
try {
|
|
2997
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
2998
|
+
} catch {
|
|
2999
|
+
continue;
|
|
3000
|
+
}
|
|
3001
|
+
for (const entry of entries) {
|
|
3002
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
3003
|
+
nodeCount += 1;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
return {
|
|
3009
|
+
node_count: nodeCount,
|
|
3010
|
+
threshold,
|
|
3011
|
+
underseeded: nodeCount < threshold
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
function inspectSessionHintsStale(projectRoot, now) {
|
|
3015
|
+
const cacheDir = join5(projectRoot, ".fabric", ".cache");
|
|
3016
|
+
if (!existsSync4(cacheDir)) {
|
|
3017
|
+
return { candidates: [] };
|
|
3018
|
+
}
|
|
3019
|
+
let entries;
|
|
3020
|
+
try {
|
|
3021
|
+
entries = readdirSync(cacheDir, { withFileTypes: true });
|
|
3022
|
+
} catch {
|
|
3023
|
+
return { candidates: [] };
|
|
3024
|
+
}
|
|
3025
|
+
const candidates = [];
|
|
3026
|
+
for (const entry of entries) {
|
|
3027
|
+
if (!entry.isFile()) continue;
|
|
3028
|
+
if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
|
|
3029
|
+
if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
|
|
3030
|
+
const absPath = join5(cacheDir, entry.name);
|
|
3031
|
+
let mtimeMs = 0;
|
|
3032
|
+
try {
|
|
3033
|
+
mtimeMs = statSync3(absPath).mtimeMs;
|
|
3034
|
+
} catch {
|
|
3035
|
+
continue;
|
|
3036
|
+
}
|
|
3037
|
+
const ageDays = Math.floor((now - mtimeMs) / MS_PER_DAY);
|
|
3038
|
+
if (ageDays < SESSION_HINTS_STALE_DAYS) continue;
|
|
3039
|
+
candidates.push({
|
|
3040
|
+
path: posix.join(".fabric", ".cache", entry.name),
|
|
3041
|
+
age_days: ageDays
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
3044
|
+
candidates.sort((a, b) => a.path.localeCompare(b.path));
|
|
3045
|
+
return { candidates };
|
|
3046
|
+
}
|
|
3047
|
+
function inspectNarrowTooFew(projectRoot, now) {
|
|
3048
|
+
let total = 0;
|
|
3049
|
+
let narrowWithPaths = 0;
|
|
3050
|
+
for (const { scope, paths } of iterateRelevanceFrontmatter(projectRoot)) {
|
|
3051
|
+
total += 1;
|
|
3052
|
+
if (scope === "narrow" && paths.length > 0) {
|
|
3053
|
+
narrowWithPaths += 1;
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
const narrowRatio = total === 0 ? 0 : narrowWithPaths / total;
|
|
3057
|
+
const structuralFlagged = total >= NARROW_MIN_TOTAL && narrowRatio < NARROW_RATIO_THRESHOLD;
|
|
3058
|
+
const windowStartMs = now - SILENCE_WINDOW_DAYS * MS_PER_DAY;
|
|
3059
|
+
const editFires = readCounterTimestamps(
|
|
3060
|
+
join5(projectRoot, EDIT_COUNTER_FILE_REL),
|
|
3061
|
+
windowStartMs
|
|
3062
|
+
);
|
|
3063
|
+
const silenceFires = readCounterTimestamps(
|
|
3064
|
+
join5(projectRoot, HINT_SILENCE_COUNTER_FILE_REL),
|
|
3065
|
+
windowStartMs
|
|
3066
|
+
);
|
|
3067
|
+
const telemetrySkipped = editFires === 0;
|
|
3068
|
+
const silenceRate = editFires === 0 ? 0 : silenceFires / editFires;
|
|
3069
|
+
const telemetryFlagged = !telemetrySkipped && silenceRate > SILENCE_RATE_THRESHOLD;
|
|
3070
|
+
return {
|
|
3071
|
+
total_canonical_entries: total,
|
|
3072
|
+
narrow_with_paths_count: narrowWithPaths,
|
|
3073
|
+
narrow_ratio: narrowRatio,
|
|
3074
|
+
structural_flagged: structuralFlagged,
|
|
3075
|
+
total_edit_fires_in_window: editFires,
|
|
3076
|
+
silence_fires_in_window: silenceFires,
|
|
3077
|
+
silence_rate: silenceRate,
|
|
3078
|
+
telemetry_skipped: telemetrySkipped,
|
|
3079
|
+
telemetry_flagged: telemetryFlagged
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
function readCounterTimestamps(absPath, windowStartMs) {
|
|
3083
|
+
if (!existsSync4(absPath)) return 0;
|
|
3084
|
+
let raw;
|
|
3085
|
+
try {
|
|
3086
|
+
raw = readFileSync(absPath, "utf8");
|
|
3087
|
+
} catch {
|
|
3088
|
+
return 0;
|
|
3089
|
+
}
|
|
3090
|
+
let count = 0;
|
|
3091
|
+
for (const line of raw.split(/\r?\n/u)) {
|
|
3092
|
+
const trimmed = line.trim();
|
|
3093
|
+
if (trimmed.length === 0) continue;
|
|
3094
|
+
const ts = Date.parse(trimmed);
|
|
3095
|
+
if (!Number.isFinite(ts)) continue;
|
|
3096
|
+
if (ts < windowStartMs) continue;
|
|
3097
|
+
count += 1;
|
|
3098
|
+
}
|
|
3099
|
+
return count;
|
|
3100
|
+
}
|
|
3101
|
+
function readUnderseedThresholdFromConfig(projectRoot) {
|
|
3102
|
+
const configPath = join5(projectRoot, ".fabric", "fabric-config.json");
|
|
3103
|
+
if (!existsSync4(configPath)) return DEFAULT_UNDERSEED_NODE_THRESHOLD;
|
|
3104
|
+
try {
|
|
3105
|
+
const raw = readFileSync(configPath, "utf8");
|
|
3106
|
+
const parsed = JSON.parse(raw);
|
|
3107
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3108
|
+
const v = parsed.underseed_node_threshold;
|
|
3109
|
+
if (typeof v === "number" && Number.isFinite(v) && v > 0) {
|
|
3110
|
+
return v;
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
} catch {
|
|
3114
|
+
}
|
|
3115
|
+
return DEFAULT_UNDERSEED_NODE_THRESHOLD;
|
|
3116
|
+
}
|
|
3117
|
+
function createOrphanDemoteCheck(inspection) {
|
|
3118
|
+
if (inspection.candidates.length === 0) {
|
|
3119
|
+
return okCheck(
|
|
3120
|
+
"Knowledge orphan demote",
|
|
3121
|
+
"No canonical knowledge entries exceed their maturity-keyed inactivity threshold."
|
|
3122
|
+
);
|
|
3123
|
+
}
|
|
3124
|
+
const first = inspection.candidates[0];
|
|
3125
|
+
const detail = `${first.stable_id} (${first.maturity}, ${first.age_days}d inactive at ${first.path})`;
|
|
3126
|
+
return issueCheck(
|
|
3127
|
+
"Knowledge orphan demote",
|
|
3128
|
+
"warn",
|
|
3129
|
+
"warning",
|
|
3130
|
+
"knowledge_orphan_demote_required",
|
|
3131
|
+
`${inspection.candidates.length} canonical knowledge entr${inspection.candidates.length === 1 ? "y exceeds" : "ies exceed"} their maturity-keyed inactivity threshold (stable=${ORPHAN_DEMOTE_THRESHOLD_DAYS.stable}d / endorsed=${ORPHAN_DEMOTE_THRESHOLD_DAYS.endorsed}d / draft=${ORPHAN_DEMOTE_THRESHOLD_DAYS.draft}d). First: ${detail}.`,
|
|
3132
|
+
"Run `fab doctor --apply-lint` (rc.4 TASK-003) to demote orphan entries one maturity tier."
|
|
3133
|
+
);
|
|
3134
|
+
}
|
|
3135
|
+
function createStaleArchiveCheck(inspection) {
|
|
3136
|
+
if (inspection.candidates.length === 0) {
|
|
3137
|
+
return okCheck(
|
|
3138
|
+
"Knowledge stale archive",
|
|
3139
|
+
"No draft knowledge entries exceed the additional stale-archive quiet window."
|
|
3140
|
+
);
|
|
3141
|
+
}
|
|
3142
|
+
const first = inspection.candidates[0];
|
|
3143
|
+
const detail = `${first.stable_id} (${first.age_days}d inactive at ${first.path}) \u2192 ${first.archive_path}`;
|
|
3144
|
+
return issueCheck(
|
|
3145
|
+
"Knowledge stale archive",
|
|
3146
|
+
"warn",
|
|
3147
|
+
"warning",
|
|
3148
|
+
"knowledge_stale_archive_required",
|
|
3149
|
+
`${inspection.candidates.length} draft knowledge entr${inspection.candidates.length === 1 ? "y is" : "ies are"} stale beyond the demote+${STALE_ARCHIVE_ADDITIONAL_DAYS}d additional quiet window. First: ${detail}.`,
|
|
3150
|
+
"Run `fab doctor --apply-lint` (rc.4 TASK-003) to move stale entries into `.fabric/.archive/<type>/`."
|
|
3151
|
+
);
|
|
3152
|
+
}
|
|
3153
|
+
function createPendingOverdueCheck(inspection) {
|
|
3154
|
+
if (inspection.candidates.length === 0) {
|
|
3155
|
+
return okCheck(
|
|
3156
|
+
"Knowledge pending overdue",
|
|
3157
|
+
"No pending knowledge entries exceed the 14-day review threshold."
|
|
3158
|
+
);
|
|
3159
|
+
}
|
|
3160
|
+
const first = inspection.candidates[0];
|
|
3161
|
+
const detail = `${first.path} (${first.age_days}d old)`;
|
|
3162
|
+
return issueCheck(
|
|
3163
|
+
"Knowledge pending overdue",
|
|
3164
|
+
"warn",
|
|
3165
|
+
"warning",
|
|
3166
|
+
"knowledge_pending_overdue",
|
|
3167
|
+
`${inspection.candidates.length} pending knowledge entr${inspection.candidates.length === 1 ? "y has" : "ies have"} been awaiting review for more than ${PENDING_OVERDUE_THRESHOLD_DAYS} days. First: ${detail}.`,
|
|
3168
|
+
"Review pending entries via the fabric-review Skill (`/fabric-review`) and approve, reject, defer, or modify."
|
|
3169
|
+
);
|
|
3170
|
+
}
|
|
3171
|
+
function createUnderseededCheck(inspection) {
|
|
3172
|
+
if (!inspection.underseeded) {
|
|
3173
|
+
return okCheck(
|
|
3174
|
+
"Knowledge underseeded",
|
|
3175
|
+
`Knowledge corpus has ${inspection.node_count} canonical entries (>= ${inspection.threshold}).`
|
|
3176
|
+
);
|
|
3177
|
+
}
|
|
3178
|
+
return issueCheck(
|
|
3179
|
+
"Knowledge underseeded",
|
|
3180
|
+
"ok",
|
|
3181
|
+
"info",
|
|
3182
|
+
"knowledge_underseeded",
|
|
3183
|
+
`Knowledge corpus has only ${inspection.node_count} canonical entr${inspection.node_count === 1 ? "y" : "ies"} (< ${inspection.threshold} threshold). The plan_context retrieval surface is below its useful floor.`,
|
|
3184
|
+
"Run the fabric-import Skill (`/fabric-import`) to backfill knowledge from git history and existing docs."
|
|
3185
|
+
);
|
|
3186
|
+
}
|
|
3187
|
+
function createSessionHintsStaleCheck(inspection) {
|
|
3188
|
+
if (inspection.candidates.length === 0) {
|
|
3189
|
+
return okCheck(
|
|
3190
|
+
"Knowledge session-hints stale",
|
|
3191
|
+
`No session-hints cache files older than ${SESSION_HINTS_STALE_DAYS} days under .fabric/.cache/.`
|
|
3192
|
+
);
|
|
3193
|
+
}
|
|
3194
|
+
const first = inspection.candidates[0];
|
|
3195
|
+
const detail = `${first.path} (${first.age_days}d old)`;
|
|
3196
|
+
return issueCheck(
|
|
3197
|
+
"Knowledge session-hints stale",
|
|
3198
|
+
"ok",
|
|
3199
|
+
"info",
|
|
3200
|
+
"knowledge_session_hints_stale",
|
|
3201
|
+
`${inspection.candidates.length} session-hints cache file${inspection.candidates.length === 1 ? "" : "s"} under .fabric/.cache/ ${inspection.candidates.length === 1 ? "is" : "are"} older than ${SESSION_HINTS_STALE_DAYS} days. First: ${detail}.`,
|
|
3202
|
+
"Run `fab doctor --apply-lint` to delete stale session-hints cache files."
|
|
3203
|
+
);
|
|
3204
|
+
}
|
|
3205
|
+
function extractKnowledgeFrontmatterRelevanceScope(source) {
|
|
3206
|
+
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
3207
|
+
const fm = FM_PATTERN.exec(source);
|
|
3208
|
+
if (fm === null) {
|
|
3209
|
+
return "broad";
|
|
3210
|
+
}
|
|
3211
|
+
const match = RELEVANCE_SCOPE_LINE_PATTERN.exec(fm[1]);
|
|
3212
|
+
if (match === null) {
|
|
3213
|
+
return "broad";
|
|
3214
|
+
}
|
|
3215
|
+
return match[2];
|
|
3216
|
+
}
|
|
3217
|
+
function extractKnowledgeFrontmatterRelevancePaths(source) {
|
|
3218
|
+
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
3219
|
+
const fm = FM_PATTERN.exec(source);
|
|
3220
|
+
if (fm === null) {
|
|
3221
|
+
return [];
|
|
3222
|
+
}
|
|
3223
|
+
const match = RELEVANCE_PATHS_LINE_PATTERN.exec(fm[1]);
|
|
3224
|
+
if (match === null) {
|
|
3225
|
+
return [];
|
|
3226
|
+
}
|
|
3227
|
+
const inner = match[1].trim();
|
|
3228
|
+
if (inner.length === 0) {
|
|
3229
|
+
return [];
|
|
3230
|
+
}
|
|
3231
|
+
return inner.split(",").map((token) => token.trim().replace(/^"(.*)"$/u, "$1")).filter((token) => token.length > 0);
|
|
3232
|
+
}
|
|
3233
|
+
function* iterateRelevanceFrontmatter(projectRoot) {
|
|
3234
|
+
for (const visit of iterateCanonicalFilenames(projectRoot)) {
|
|
3235
|
+
const layerRoot = visit.layer === "team" ? join5(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
|
|
3236
|
+
const absPath = join5(layerRoot, visit.type, visit.filename);
|
|
3237
|
+
let source;
|
|
3238
|
+
try {
|
|
3239
|
+
source = readFileSync(absPath, "utf8");
|
|
3240
|
+
} catch {
|
|
3241
|
+
continue;
|
|
3242
|
+
}
|
|
3243
|
+
const scope = extractKnowledgeFrontmatterRelevanceScope(source);
|
|
3244
|
+
const paths = extractKnowledgeFrontmatterRelevancePaths(source);
|
|
3245
|
+
yield { visit, scope, paths, absPath };
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
function inspectNarrowNoPaths(projectRoot) {
|
|
3249
|
+
const candidates = [];
|
|
3250
|
+
for (const { visit, scope, paths } of iterateRelevanceFrontmatter(projectRoot)) {
|
|
3251
|
+
if (scope !== "narrow") {
|
|
3252
|
+
continue;
|
|
3253
|
+
}
|
|
3254
|
+
if (paths.length > 0) {
|
|
3255
|
+
continue;
|
|
3256
|
+
}
|
|
3257
|
+
candidates.push({
|
|
3258
|
+
stable_id: visit.parsed.stable_id,
|
|
3259
|
+
path: visit.displayPath
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
candidates.sort((a, b) => a.path.localeCompare(b.path));
|
|
3263
|
+
return { candidates };
|
|
3264
|
+
}
|
|
3265
|
+
function inspectRelevancePathsDangling(projectRoot) {
|
|
3266
|
+
const entries = [];
|
|
3267
|
+
const workspacePaths = collectWorkspacePathsForGlobMatch(projectRoot);
|
|
3268
|
+
if (workspacePaths.length === 0) {
|
|
3269
|
+
return { entries };
|
|
3270
|
+
}
|
|
3271
|
+
for (const { visit, paths } of iterateRelevanceFrontmatter(projectRoot)) {
|
|
3272
|
+
if (paths.length === 0) {
|
|
3273
|
+
continue;
|
|
3274
|
+
}
|
|
3275
|
+
for (const rawGlob of paths) {
|
|
3276
|
+
const glob = rawGlob.endsWith("/") ? `${rawGlob}**` : rawGlob;
|
|
3277
|
+
let matched = false;
|
|
3278
|
+
for (const target of workspacePaths) {
|
|
3279
|
+
if (minimatch(target, glob, { dot: true, matchBase: false })) {
|
|
3280
|
+
matched = true;
|
|
3281
|
+
break;
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
if (matched) {
|
|
3285
|
+
continue;
|
|
3286
|
+
}
|
|
3287
|
+
entries.push({
|
|
3288
|
+
stable_id: visit.parsed.stable_id,
|
|
3289
|
+
path: visit.displayPath,
|
|
3290
|
+
dangling_glob: rawGlob
|
|
3291
|
+
});
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
entries.sort((a, b) => {
|
|
3295
|
+
const byPath = a.path.localeCompare(b.path);
|
|
3296
|
+
return byPath !== 0 ? byPath : a.dangling_glob.localeCompare(b.dangling_glob);
|
|
3297
|
+
});
|
|
3298
|
+
return { entries };
|
|
3299
|
+
}
|
|
3300
|
+
function collectWorkspacePathsForGlobMatch(projectRoot) {
|
|
3301
|
+
if (!existsSync4(projectRoot)) {
|
|
3302
|
+
return [];
|
|
3303
|
+
}
|
|
3304
|
+
let rootStat;
|
|
3305
|
+
try {
|
|
3306
|
+
rootStat = statSync3(projectRoot);
|
|
3307
|
+
} catch {
|
|
3308
|
+
return [];
|
|
3309
|
+
}
|
|
3310
|
+
if (!rootStat.isDirectory()) {
|
|
3311
|
+
return [];
|
|
3312
|
+
}
|
|
3313
|
+
const paths = [];
|
|
3314
|
+
const stack = [projectRoot];
|
|
3315
|
+
while (stack.length > 0) {
|
|
3316
|
+
const current = stack.pop();
|
|
3317
|
+
if (current === void 0) continue;
|
|
3318
|
+
let entries;
|
|
3319
|
+
try {
|
|
3320
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
3321
|
+
} catch {
|
|
3322
|
+
continue;
|
|
3323
|
+
}
|
|
3324
|
+
for (const entry of entries) {
|
|
3325
|
+
const abs = join5(current, entry.name);
|
|
3326
|
+
const rel = normalizePath(abs.slice(projectRoot.length + 1));
|
|
3327
|
+
if (rel.length === 0) continue;
|
|
3328
|
+
if (entry.isDirectory()) {
|
|
3329
|
+
if (IGNORED_DIRECTORIES.has(entry.name)) {
|
|
3330
|
+
continue;
|
|
3331
|
+
}
|
|
3332
|
+
paths.push(rel);
|
|
3333
|
+
stack.push(abs);
|
|
3334
|
+
} else if (entry.isFile()) {
|
|
3335
|
+
paths.push(rel);
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
return paths;
|
|
3340
|
+
}
|
|
3341
|
+
function inspectRelevancePathsDrift(projectRoot) {
|
|
3342
|
+
let recentPaths = null;
|
|
3343
|
+
try {
|
|
3344
|
+
recentPaths = readRecentGitTouchedPaths(projectRoot, RELEVANCE_PATHS_DRIFT_WINDOW_DAYS);
|
|
3345
|
+
} catch {
|
|
3346
|
+
recentPaths = null;
|
|
3347
|
+
}
|
|
3348
|
+
if (recentPaths === null) {
|
|
3349
|
+
return { candidates: [], git_available: false };
|
|
3350
|
+
}
|
|
3351
|
+
const candidates = [];
|
|
3352
|
+
for (const { visit, scope, paths } of iterateRelevanceFrontmatter(projectRoot)) {
|
|
3353
|
+
if (scope !== "narrow") {
|
|
3354
|
+
continue;
|
|
3355
|
+
}
|
|
3356
|
+
if (paths.length === 0) {
|
|
3357
|
+
continue;
|
|
3358
|
+
}
|
|
3359
|
+
let anyMatch = false;
|
|
3360
|
+
for (const rawGlob of paths) {
|
|
3361
|
+
const glob = rawGlob.endsWith("/") ? `${rawGlob}**` : rawGlob;
|
|
3362
|
+
for (const target of recentPaths) {
|
|
3363
|
+
if (minimatch(target, glob, { dot: true, matchBase: false })) {
|
|
3364
|
+
anyMatch = true;
|
|
3365
|
+
break;
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
if (anyMatch) break;
|
|
3369
|
+
}
|
|
3370
|
+
if (anyMatch) {
|
|
3371
|
+
continue;
|
|
3372
|
+
}
|
|
3373
|
+
candidates.push({
|
|
3374
|
+
stable_id: visit.parsed.stable_id,
|
|
3375
|
+
path: visit.displayPath,
|
|
3376
|
+
globs: paths.slice()
|
|
3377
|
+
});
|
|
3378
|
+
}
|
|
3379
|
+
candidates.sort((a, b) => a.path.localeCompare(b.path));
|
|
3380
|
+
return { candidates, git_available: true };
|
|
3381
|
+
}
|
|
3382
|
+
function readRecentGitTouchedPaths(projectRoot, windowDays) {
|
|
3383
|
+
const since = new Date(Date.now() - windowDays * MS_PER_DAY).toISOString();
|
|
3384
|
+
const stdout = execFileSync(
|
|
3385
|
+
"git",
|
|
3386
|
+
["log", `--since=${since}`, "--name-only", "--pretty=format:"],
|
|
3387
|
+
{
|
|
3388
|
+
cwd: projectRoot,
|
|
3389
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3390
|
+
encoding: "utf8"
|
|
3391
|
+
}
|
|
3392
|
+
);
|
|
3393
|
+
const set = /* @__PURE__ */ new Set();
|
|
3394
|
+
for (const line of stdout.split(/\r?\n/u)) {
|
|
3395
|
+
const trimmed = line.trim();
|
|
3396
|
+
if (trimmed.length === 0) continue;
|
|
3397
|
+
set.add(normalizePath(trimmed));
|
|
3398
|
+
}
|
|
3399
|
+
return Array.from(set);
|
|
3400
|
+
}
|
|
3401
|
+
function createNarrowNoPathsCheck(inspection) {
|
|
3402
|
+
if (inspection.candidates.length === 0) {
|
|
3403
|
+
return okCheck(
|
|
3404
|
+
"Knowledge narrow without paths",
|
|
3405
|
+
"No narrow-scope canonical entries have an empty relevance_paths array."
|
|
3406
|
+
);
|
|
3407
|
+
}
|
|
3408
|
+
const first = inspection.candidates[0];
|
|
3409
|
+
const detail = `${first.stable_id} (${first.path})`;
|
|
3410
|
+
return issueCheck(
|
|
3411
|
+
"Knowledge narrow without paths",
|
|
3412
|
+
"warn",
|
|
3413
|
+
"warning",
|
|
3414
|
+
"knowledge_narrow_no_paths",
|
|
3415
|
+
`${inspection.candidates.length} narrow-scope canonical entr${inspection.candidates.length === 1 ? "y has" : "ies have"} an empty relevance_paths array (silent recall risk \u2014 narrow without anchors can never match a target path). First: ${detail}.`,
|
|
3416
|
+
"Either add path anchors to relevance_paths or widen the entry's relevance_scope to broad."
|
|
3417
|
+
);
|
|
3418
|
+
}
|
|
3419
|
+
function createRelevancePathsDanglingCheck(inspection) {
|
|
3420
|
+
if (inspection.entries.length === 0) {
|
|
3421
|
+
return okCheck(
|
|
3422
|
+
"Knowledge relevance_paths dangling",
|
|
3423
|
+
"All relevance_paths globs resolve to at least one file under the workspace root."
|
|
3424
|
+
);
|
|
3425
|
+
}
|
|
3426
|
+
const first = inspection.entries[0];
|
|
3427
|
+
const detail = `${first.stable_id} at ${first.path} \u2192 \`${first.dangling_glob}\` (0 matches)`;
|
|
3428
|
+
return issueCheck(
|
|
3429
|
+
"Knowledge relevance_paths dangling",
|
|
3430
|
+
"warn",
|
|
3431
|
+
"warning",
|
|
3432
|
+
"knowledge_relevance_paths_dangling",
|
|
3433
|
+
`${inspection.entries.length} relevance_paths glob${inspection.entries.length === 1 ? " resolves" : "s resolve"} to zero files in the current workspace. First: ${detail}.`,
|
|
3434
|
+
"Update the entry's relevance_paths to remove globs that no longer match any files, or use `fab_review.modify` to rewrite the anchor set."
|
|
3435
|
+
);
|
|
3436
|
+
}
|
|
3437
|
+
function createRelevancePathsDriftCheck(inspection) {
|
|
3438
|
+
if (!inspection.git_available) {
|
|
3439
|
+
return okCheck(
|
|
3440
|
+
"Knowledge relevance_paths drift",
|
|
3441
|
+
`Skipped (git history unavailable; cannot evaluate ${RELEVANCE_PATHS_DRIFT_WINDOW_DAYS}d drift window).`
|
|
3442
|
+
);
|
|
3443
|
+
}
|
|
3444
|
+
if (inspection.candidates.length === 0) {
|
|
3445
|
+
return okCheck(
|
|
3446
|
+
"Knowledge relevance_paths drift",
|
|
3447
|
+
`All narrow-scope canonical entries have at least one relevance_path touched in the last ${RELEVANCE_PATHS_DRIFT_WINDOW_DAYS}d.`
|
|
3448
|
+
);
|
|
3449
|
+
}
|
|
3450
|
+
const first = inspection.candidates[0];
|
|
3451
|
+
const detail = `${first.stable_id} at ${first.path} (globs: ${first.globs.join(", ")})`;
|
|
3452
|
+
return issueCheck(
|
|
3453
|
+
"Knowledge relevance_paths drift",
|
|
3454
|
+
"ok",
|
|
3455
|
+
"info",
|
|
3456
|
+
"knowledge_relevance_paths_drift",
|
|
3457
|
+
`${inspection.candidates.length} narrow-scope canonical entr${inspection.candidates.length === 1 ? "y has" : "ies have"} relevance_paths whose globs match no file touched in the last ${RELEVANCE_PATHS_DRIFT_WINDOW_DAYS}d of git history. First: ${detail}.`,
|
|
3458
|
+
"Review whether the entry is still relevant \u2014 use `fab_review.modify` to refresh the anchors or `fab_review.reject` to archive."
|
|
3459
|
+
);
|
|
3460
|
+
}
|
|
3461
|
+
function createNarrowTooFewCheck(inspection) {
|
|
3462
|
+
const { structural_flagged, telemetry_flagged } = inspection;
|
|
3463
|
+
if (!structural_flagged && !telemetry_flagged) {
|
|
3464
|
+
const ratioPct = (inspection.narrow_ratio * 100).toFixed(0);
|
|
3465
|
+
const teleNote = inspection.telemetry_skipped ? "telemetry skipped (no edit-counter fires in window)" : `silence rate ${(inspection.silence_rate * 100).toFixed(0)}% over ${SILENCE_WINDOW_DAYS}d`;
|
|
3466
|
+
return okCheck(
|
|
3467
|
+
"Knowledge narrow too few",
|
|
3468
|
+
`Narrow-with-paths ratio ${ratioPct}% (${inspection.narrow_with_paths_count}/${inspection.total_canonical_entries}); ${teleNote}.`
|
|
3469
|
+
);
|
|
3470
|
+
}
|
|
3471
|
+
const parts = [];
|
|
3472
|
+
if (structural_flagged) {
|
|
3473
|
+
const ratioPct = (inspection.narrow_ratio * 100).toFixed(0);
|
|
3474
|
+
parts.push(
|
|
3475
|
+
`narrow-with-paths share ${ratioPct}% (${inspection.narrow_with_paths_count}/${inspection.total_canonical_entries}) below ${(NARROW_RATIO_THRESHOLD * 100).toFixed(0)}% threshold`
|
|
3476
|
+
);
|
|
3477
|
+
}
|
|
3478
|
+
if (telemetry_flagged) {
|
|
3479
|
+
const silencePct = (inspection.silence_rate * 100).toFixed(0);
|
|
3480
|
+
parts.push(
|
|
3481
|
+
`narrow-hook silence rate ${silencePct}% (${inspection.silence_fires_in_window}/${inspection.total_edit_fires_in_window}) over ${SILENCE_WINDOW_DAYS}d above ${(SILENCE_RATE_THRESHOLD * 100).toFixed(0)}% threshold`
|
|
3482
|
+
);
|
|
3483
|
+
}
|
|
3484
|
+
return issueCheck(
|
|
3485
|
+
"Knowledge narrow too few",
|
|
3486
|
+
"ok",
|
|
3487
|
+
"info",
|
|
3488
|
+
"knowledge_narrow_too_few",
|
|
3489
|
+
`Narrow-scope KB coverage is below the useful floor: ${parts.join("; ")}.`,
|
|
3490
|
+
"Run the fabric-import Skill (`/fabric-import`) to re-seed narrow anchors against the current codebase."
|
|
3491
|
+
);
|
|
3492
|
+
}
|
|
3493
|
+
function resolvePersonalKnowledgeRoot() {
|
|
3494
|
+
const home = process.env.FABRIC_HOME ?? homedir2();
|
|
3495
|
+
return join5(home, ".fabric", "knowledge");
|
|
3496
|
+
}
|
|
3497
|
+
function parseStableIdFromCanonicalFilename(filename) {
|
|
3498
|
+
const match = CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(filename);
|
|
3499
|
+
if (match === null) {
|
|
3500
|
+
return null;
|
|
3501
|
+
}
|
|
3502
|
+
const stableId = match[1];
|
|
3503
|
+
const inner = /^(K[PT])-(MOD|DEC|GLD|PIT|PRO)-(\d{4,})$/u.exec(stableId);
|
|
3504
|
+
if (inner === null) {
|
|
3505
|
+
return null;
|
|
3506
|
+
}
|
|
3507
|
+
return {
|
|
3508
|
+
prefix: inner[1],
|
|
3509
|
+
typeCode: inner[2],
|
|
3510
|
+
counter: Number.parseInt(inner[3], 10),
|
|
3511
|
+
stable_id: stableId
|
|
3512
|
+
};
|
|
3513
|
+
}
|
|
3514
|
+
function* iterateCanonicalFilenames(projectRoot) {
|
|
3515
|
+
const teamRoot = join5(projectRoot, ".fabric", "knowledge");
|
|
3516
|
+
const personalRoot = resolvePersonalKnowledgeRoot();
|
|
3517
|
+
for (const [layer, root, displayPrefix] of [
|
|
3518
|
+
["team", teamRoot, ".fabric/knowledge"],
|
|
3519
|
+
["personal", personalRoot, "~/.fabric/knowledge"]
|
|
3520
|
+
]) {
|
|
3521
|
+
if (!existsSync4(root)) {
|
|
3522
|
+
continue;
|
|
3523
|
+
}
|
|
3524
|
+
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
3525
|
+
const dir = join5(root, typeDir);
|
|
3526
|
+
if (!existsSync4(dir)) {
|
|
3527
|
+
continue;
|
|
3528
|
+
}
|
|
3529
|
+
let entries;
|
|
3530
|
+
try {
|
|
3531
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
3532
|
+
} catch {
|
|
3533
|
+
continue;
|
|
3534
|
+
}
|
|
3535
|
+
for (const entry of entries) {
|
|
3536
|
+
if (!entry.isFile()) {
|
|
3537
|
+
continue;
|
|
3538
|
+
}
|
|
3539
|
+
const parsed = parseStableIdFromCanonicalFilename(entry.name);
|
|
3540
|
+
if (parsed === null) {
|
|
3541
|
+
continue;
|
|
3542
|
+
}
|
|
3543
|
+
const displayPath = posix.join(displayPrefix, typeDir, entry.name);
|
|
3544
|
+
yield {
|
|
3545
|
+
layer,
|
|
3546
|
+
type: typeDir,
|
|
3547
|
+
filename: entry.name,
|
|
3548
|
+
displayPath,
|
|
3549
|
+
parsed
|
|
3550
|
+
};
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
function inspectStableIdDuplicate(projectRoot) {
|
|
3556
|
+
const idToPaths = /* @__PURE__ */ new Map();
|
|
3557
|
+
for (const visit of iterateCanonicalFilenames(projectRoot)) {
|
|
3558
|
+
const existing = idToPaths.get(visit.parsed.stable_id) ?? [];
|
|
3559
|
+
existing.push(visit.displayPath);
|
|
3560
|
+
idToPaths.set(visit.parsed.stable_id, existing);
|
|
3561
|
+
}
|
|
3562
|
+
const duplicates = [];
|
|
3563
|
+
for (const [stable_id, paths] of idToPaths) {
|
|
3564
|
+
if (paths.length > 1) {
|
|
3565
|
+
duplicates.push({ stable_id, paths: paths.slice().sort() });
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
duplicates.sort((a, b) => a.stable_id.localeCompare(b.stable_id));
|
|
3569
|
+
return { duplicates };
|
|
3570
|
+
}
|
|
3571
|
+
function inspectLayerMismatch(projectRoot) {
|
|
3572
|
+
const mismatches = [];
|
|
3573
|
+
for (const visit of iterateCanonicalFilenames(projectRoot)) {
|
|
3574
|
+
const expected_layer = visit.parsed.prefix === "KT" ? "team" : "personal";
|
|
3575
|
+
if (expected_layer === visit.layer) {
|
|
3576
|
+
continue;
|
|
3577
|
+
}
|
|
3578
|
+
mismatches.push({
|
|
3579
|
+
path: visit.displayPath,
|
|
3580
|
+
located_in: visit.layer,
|
|
3581
|
+
expected_layer,
|
|
3582
|
+
stable_id: visit.parsed.stable_id
|
|
3583
|
+
});
|
|
3584
|
+
}
|
|
3585
|
+
mismatches.sort((a, b) => a.path.localeCompare(b.path));
|
|
3586
|
+
return { mismatches };
|
|
3587
|
+
}
|
|
3588
|
+
function inspectIndexDrift(projectRoot, meta) {
|
|
3589
|
+
if (!meta.valid || meta.meta === null) {
|
|
3590
|
+
return { drifts: [] };
|
|
3591
|
+
}
|
|
3592
|
+
const counters = AgentsMetaCountersSchema.parse(meta.meta.counters ?? void 0);
|
|
3593
|
+
const observed = {
|
|
3594
|
+
KP: { MOD: 0, DEC: 0, GLD: 0, PIT: 0, PRO: 0 },
|
|
3595
|
+
KT: { MOD: 0, DEC: 0, GLD: 0, PIT: 0, PRO: 0 }
|
|
3596
|
+
};
|
|
3597
|
+
for (const visit of iterateCanonicalFilenames(projectRoot)) {
|
|
3598
|
+
const { prefix, typeCode, counter } = visit.parsed;
|
|
3599
|
+
if (counter > observed[prefix][typeCode]) {
|
|
3600
|
+
observed[prefix][typeCode] = counter;
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
const drifts = [];
|
|
3604
|
+
for (const layer of ["KP", "KT"]) {
|
|
3605
|
+
for (const code of COUNTER_TYPE_CODES) {
|
|
3606
|
+
const max = observed[layer][code];
|
|
3607
|
+
if (max === 0) {
|
|
3608
|
+
continue;
|
|
3609
|
+
}
|
|
3610
|
+
const current = counters[layer][code];
|
|
3611
|
+
if (current < max) {
|
|
3612
|
+
drifts.push({
|
|
3613
|
+
layer,
|
|
3614
|
+
type: code,
|
|
3615
|
+
counter: current,
|
|
3616
|
+
max_observed: max,
|
|
3617
|
+
proposed_after: max + 1
|
|
3618
|
+
});
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
drifts.sort(
|
|
3623
|
+
(a, b) => a.layer === b.layer ? a.type.localeCompare(b.type) : a.layer.localeCompare(b.layer)
|
|
3624
|
+
);
|
|
3625
|
+
return { drifts };
|
|
3626
|
+
}
|
|
3627
|
+
function createStableIdDuplicateCheck(inspection) {
|
|
3628
|
+
if (inspection.duplicates.length === 0) {
|
|
3629
|
+
return okCheck(
|
|
3630
|
+
"Knowledge stable_id duplicate",
|
|
3631
|
+
"No canonical knowledge files share a stable_id across team / personal trees."
|
|
3632
|
+
);
|
|
3633
|
+
}
|
|
3634
|
+
const first = inspection.duplicates[0];
|
|
3635
|
+
const detail = `${first.stable_id} appears in ${first.paths.length} files: ${first.paths.join(", ")}`;
|
|
3636
|
+
return issueCheck(
|
|
3637
|
+
"Knowledge stable_id duplicate",
|
|
3638
|
+
"error",
|
|
3639
|
+
"manual_error",
|
|
3640
|
+
"knowledge_stable_id_duplicate",
|
|
3641
|
+
`${inspection.duplicates.length} stable_id${inspection.duplicates.length === 1 ? "" : "s"} duplicated across canonical knowledge files (path-decoupled identity invariant). First: ${detail}.`,
|
|
3642
|
+
"Manually rename one of the colliding files to a fresh `<prefix>-<type>-<counter>--<slug>.md` allocated via the canonical id allocator; do not edit by hand."
|
|
3643
|
+
);
|
|
3644
|
+
}
|
|
3645
|
+
function createLayerMismatchCheck(inspection) {
|
|
3646
|
+
if (inspection.mismatches.length === 0) {
|
|
3647
|
+
return okCheck(
|
|
3648
|
+
"Knowledge layer mismatch",
|
|
3649
|
+
"All canonical knowledge files are physically located under the layer their stable_id prefix declares."
|
|
3650
|
+
);
|
|
3651
|
+
}
|
|
3652
|
+
const first = inspection.mismatches[0];
|
|
3653
|
+
const detail = `${first.stable_id} at ${first.path} (located in ${first.located_in}, expected ${first.expected_layer})`;
|
|
3654
|
+
return issueCheck(
|
|
3655
|
+
"Knowledge layer mismatch",
|
|
3656
|
+
"error",
|
|
3657
|
+
"manual_error",
|
|
3658
|
+
"knowledge_layer_mismatch",
|
|
3659
|
+
`${inspection.mismatches.length} canonical knowledge file${inspection.mismatches.length === 1 ? "" : "s"} are physically misaligned with their stable_id layer prefix (KT-* must live under team/, KP-* under personal/). First: ${detail}.`,
|
|
3660
|
+
"Move the file to the correct layer root, or use the fabric-review modify flow to flip its layer (which renames the stable_id prefix accordingly)."
|
|
3661
|
+
);
|
|
3662
|
+
}
|
|
3663
|
+
function createIndexDriftCheck(inspection) {
|
|
3664
|
+
if (inspection.drifts.length === 0) {
|
|
3665
|
+
return okCheck(
|
|
3666
|
+
"Knowledge index drift",
|
|
3667
|
+
"agents.meta.json counters envelope is at or above the highest existing canonical counter for every (layer, type) pair."
|
|
3668
|
+
);
|
|
3669
|
+
}
|
|
3670
|
+
const first = inspection.drifts[0];
|
|
3671
|
+
const detail = `${first.layer}.${first.type} counter=${first.counter} but max_observed=${first.max_observed} (would propose counters.${first.layer}.${first.type}=${first.proposed_after})`;
|
|
3672
|
+
return issueCheck(
|
|
3673
|
+
"Knowledge index drift",
|
|
3674
|
+
"error",
|
|
3675
|
+
"fixable_error",
|
|
3676
|
+
"knowledge_index_drift",
|
|
3677
|
+
`${inspection.drifts.length} (layer, type) counter slot${inspection.drifts.length === 1 ? "" : "s"} have drifted below the observed canonical maximum (next allocate would collide). First: ${detail}.`,
|
|
3678
|
+
"Run `fab doctor --apply-lint` (rc.4 TASK-003) to bump agents.meta.json counters to max_observed + 1."
|
|
3679
|
+
);
|
|
3680
|
+
}
|
|
3681
|
+
async function fixMcpConfigInWrongFile(projectRoot) {
|
|
3682
|
+
const settingsPath = join5(projectRoot, ".claude", "settings.json");
|
|
3683
|
+
if (!existsSync4(settingsPath)) {
|
|
3684
|
+
return;
|
|
3685
|
+
}
|
|
3686
|
+
let settings;
|
|
3687
|
+
try {
|
|
3688
|
+
const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
3689
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3690
|
+
return;
|
|
3691
|
+
}
|
|
3692
|
+
settings = parsed;
|
|
3693
|
+
} catch {
|
|
3694
|
+
return;
|
|
3695
|
+
}
|
|
3696
|
+
const mcpServers = settings.mcpServers;
|
|
3697
|
+
if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
|
|
3698
|
+
return;
|
|
3699
|
+
}
|
|
3700
|
+
const { fabric: _removed, ...remainingServers } = mcpServers;
|
|
3701
|
+
const cleaned = { ...settings };
|
|
3702
|
+
if (Object.keys(remainingServers).length === 0) {
|
|
3703
|
+
delete cleaned.mcpServers;
|
|
3704
|
+
} else {
|
|
3705
|
+
cleaned.mcpServers = remainingServers;
|
|
3706
|
+
}
|
|
3707
|
+
await atomicWriteJson2(settingsPath, cleaned, { indent: 2 });
|
|
3708
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
3709
|
+
event_type: "mcp_config_migrated",
|
|
3710
|
+
source: "doctor_fix",
|
|
3711
|
+
removed_from: ".claude/settings.json"
|
|
3712
|
+
});
|
|
3713
|
+
}
|
|
3714
|
+
async function ensureKnowledgeSubdirs(projectRoot) {
|
|
3715
|
+
for (const sub of KNOWLEDGE_SUBDIRS2) {
|
|
3716
|
+
await mkdir3(join5(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
async function fixCounterDesync(projectRoot) {
|
|
3720
|
+
const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
|
|
3721
|
+
if (!existsSync4(metaPath)) {
|
|
3722
|
+
return;
|
|
3723
|
+
}
|
|
3724
|
+
let meta;
|
|
3725
|
+
try {
|
|
3726
|
+
meta = agentsMetaSchema4.parse(JSON.parse(await readFile5(metaPath, "utf8")));
|
|
3727
|
+
} catch {
|
|
3728
|
+
return;
|
|
3729
|
+
}
|
|
3730
|
+
const synthetic = {
|
|
3731
|
+
present: true,
|
|
3732
|
+
valid: true,
|
|
3733
|
+
meta,
|
|
3734
|
+
revision: meta.revision,
|
|
3735
|
+
computedRevision: null,
|
|
3736
|
+
ruleCount: 0,
|
|
3737
|
+
missingContentRefs: [],
|
|
3738
|
+
invalidContentRefs: [],
|
|
3739
|
+
stale: false,
|
|
3740
|
+
changed: false
|
|
3741
|
+
};
|
|
3742
|
+
const desync = inspectCounterDesync(synthetic);
|
|
3743
|
+
if (desync.desyncs.length === 0 || desync.correctedCounters === null) {
|
|
3744
|
+
return;
|
|
3745
|
+
}
|
|
3746
|
+
const updated = { ...meta, counters: desync.correctedCounters };
|
|
3747
|
+
await atomicWriteJson2(metaPath, updated, { indent: 2 });
|
|
3748
|
+
}
|
|
3749
|
+
async function ensureEventLedger(projectRoot) {
|
|
3750
|
+
const path = getEventLedgerPath(projectRoot);
|
|
3751
|
+
await ensureParentDirectory(path);
|
|
3752
|
+
await writeFile2(path, "", { encoding: "utf8", flag: "a" });
|
|
3753
|
+
}
|
|
3754
|
+
function createFixMessage(fixed, report) {
|
|
3755
|
+
const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
|
|
3756
|
+
const manualText = report.manual_errors.length === 0 ? "No manual errors remain." : `${report.manual_errors.length} manual error${report.manual_errors.length === 1 ? "" : "s"} remain.`;
|
|
3757
|
+
return `${fixedText} ${manualText}`;
|
|
3758
|
+
}
|
|
3759
|
+
function isValidJsonLine(line) {
|
|
2167
3760
|
try {
|
|
2168
3761
|
JSON.parse(line);
|
|
2169
3762
|
return true;
|
|
@@ -2246,141 +3839,35 @@ function isMissingFileError(error) {
|
|
|
2246
3839
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
2247
3840
|
}
|
|
2248
3841
|
|
|
2249
|
-
// src/services/get-
|
|
3842
|
+
// src/services/get-knowledge.ts
|
|
2250
3843
|
import { readFile as readFile6 } from "fs/promises";
|
|
2251
|
-
import { join as
|
|
2252
|
-
import { minimatch } from "minimatch";
|
|
2253
|
-
|
|
2254
|
-
// src/services/audit-log.ts
|
|
2255
|
-
import { open, stat as stat2 } from "fs/promises";
|
|
2256
|
-
import { isAbsolute as isAbsolute3, join as join6, posix as posix2, relative as relative3, resolve as resolve4 } from "path";
|
|
2257
|
-
var AUDIT_LOG_FILE = `${FABRIC_DIR}/audit.jsonl`;
|
|
2258
|
-
var DEFAULT_AUDIT_WINDOW_MS = 5 * 60 * 1e3;
|
|
2259
|
-
async function appendGetRulesAuditEvent(projectRoot, input) {
|
|
2260
|
-
const entry = {
|
|
2261
|
-
kind: "audit-event",
|
|
2262
|
-
event: "get_rules",
|
|
2263
|
-
ts: input.ts ?? Date.now(),
|
|
2264
|
-
path: normalizeAuditPath(projectRoot, input.path),
|
|
2265
|
-
client_hash: input.client_hash
|
|
2266
|
-
};
|
|
2267
|
-
await appendAuditLogEventLedgerEvents(projectRoot, [entry], {
|
|
2268
|
-
rule_context: {
|
|
2269
|
-
required_stable_ids: input.required_stable_ids,
|
|
2270
|
-
ai_selectable_stable_ids: input.ai_selectable_stable_ids,
|
|
2271
|
-
final_stable_ids: input.final_stable_ids
|
|
2272
|
-
},
|
|
2273
|
-
correlation_id: input.correlation_id,
|
|
2274
|
-
session_id: input.session_id
|
|
2275
|
-
});
|
|
2276
|
-
return entry;
|
|
2277
|
-
}
|
|
2278
|
-
async function appendRuleSelectionAuditEvent(projectRoot, input) {
|
|
2279
|
-
const entry = {
|
|
2280
|
-
kind: "audit-event",
|
|
2281
|
-
event: "knowledge_selection",
|
|
2282
|
-
ts: input.ts ?? Date.now(),
|
|
2283
|
-
path: normalizeAuditPath(projectRoot, input.path),
|
|
2284
|
-
selection_token: input.selection_token,
|
|
2285
|
-
target_paths: input.target_paths.map((path) => normalizeAuditPath(projectRoot, path)),
|
|
2286
|
-
required_stable_ids: input.required_stable_ids,
|
|
2287
|
-
ai_selectable_stable_ids: input.ai_selectable_stable_ids,
|
|
2288
|
-
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
2289
|
-
final_stable_ids: input.final_stable_ids,
|
|
2290
|
-
ai_selection_reasons: input.ai_selection_reasons,
|
|
2291
|
-
rejected_stable_ids: input.rejected_stable_ids,
|
|
2292
|
-
ignored_stable_ids: input.ignored_stable_ids
|
|
2293
|
-
};
|
|
2294
|
-
await appendAuditLogEventLedgerEvents(projectRoot, [entry], {
|
|
2295
|
-
correlation_id: input.correlation_id,
|
|
2296
|
-
session_id: input.session_id
|
|
2297
|
-
});
|
|
2298
|
-
return entry;
|
|
2299
|
-
}
|
|
2300
|
-
function normalizeAuditPath(projectRoot, value) {
|
|
2301
|
-
const normalizedProjectRoot = resolve4(projectRoot);
|
|
2302
|
-
const candidate = isAbsolute3(value) ? resolve4(value) : resolve4(normalizedProjectRoot, value);
|
|
2303
|
-
const relativePath = relative3(normalizedProjectRoot, candidate);
|
|
2304
|
-
if (relativePath.length > 0 && relativePath !== "." && !relativePath.startsWith("..") && !isAbsolute3(relativePath)) {
|
|
2305
|
-
return posix2.normalize(relativePath.split("\\").join("/"));
|
|
2306
|
-
}
|
|
2307
|
-
return posix2.normalize(value.replaceAll("\\", "/"));
|
|
2308
|
-
}
|
|
2309
|
-
async function appendAuditLogEventLedgerEvents(projectRoot, entries, metadata = {}) {
|
|
2310
|
-
for (const entry of entries) {
|
|
2311
|
-
if (entry.event === "get_rules") {
|
|
2312
|
-
await appendEventLedgerEvent(projectRoot, {
|
|
2313
|
-
event_type: "knowledge_context_planned",
|
|
2314
|
-
ts: entry.ts,
|
|
2315
|
-
target_paths: [entry.path],
|
|
2316
|
-
required_stable_ids: metadata.rule_context?.required_stable_ids ?? [],
|
|
2317
|
-
ai_selectable_stable_ids: metadata.rule_context?.ai_selectable_stable_ids ?? [],
|
|
2318
|
-
final_stable_ids: metadata.rule_context?.final_stable_ids ?? [],
|
|
2319
|
-
client_hash: entry.client_hash,
|
|
2320
|
-
correlation_id: metadata.correlation_id,
|
|
2321
|
-
session_id: metadata.session_id
|
|
2322
|
-
});
|
|
2323
|
-
continue;
|
|
2324
|
-
}
|
|
2325
|
-
if (entry.event === "knowledge_selection") {
|
|
2326
|
-
await appendEventLedgerEvent(projectRoot, {
|
|
2327
|
-
event_type: "knowledge_selection",
|
|
2328
|
-
ts: entry.ts,
|
|
2329
|
-
selection_token: entry.selection_token,
|
|
2330
|
-
target_paths: entry.target_paths,
|
|
2331
|
-
required_stable_ids: entry.required_stable_ids,
|
|
2332
|
-
ai_selectable_stable_ids: entry.ai_selectable_stable_ids,
|
|
2333
|
-
ai_selected_stable_ids: entry.ai_selected_stable_ids,
|
|
2334
|
-
final_stable_ids: entry.final_stable_ids,
|
|
2335
|
-
ai_selection_reasons: entry.ai_selection_reasons,
|
|
2336
|
-
rejected_stable_ids: entry.rejected_stable_ids,
|
|
2337
|
-
ignored_stable_ids: entry.ignored_stable_ids,
|
|
2338
|
-
correlation_id: metadata.correlation_id,
|
|
2339
|
-
session_id: metadata.session_id
|
|
2340
|
-
});
|
|
2341
|
-
continue;
|
|
2342
|
-
}
|
|
2343
|
-
await appendEventLedgerEvent(projectRoot, {
|
|
2344
|
-
event_type: "edit_intent_checked",
|
|
2345
|
-
ts: entry.ts,
|
|
2346
|
-
path: entry.path,
|
|
2347
|
-
compliant: entry.compliant,
|
|
2348
|
-
intent: entry.intent,
|
|
2349
|
-
ledger_entry_id: entry.ledger_entry_id,
|
|
2350
|
-
ledger_source: "ai",
|
|
2351
|
-
matched_rule_context_ts: entry.matched_get_rules_ts,
|
|
2352
|
-
window_ms: entry.window_ms,
|
|
2353
|
-
correlation_id: metadata.correlation_id,
|
|
2354
|
-
session_id: metadata.session_id
|
|
2355
|
-
});
|
|
2356
|
-
}
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
// src/services/get-rules.ts
|
|
3844
|
+
import { join as join6 } from "path";
|
|
3845
|
+
import { minimatch as minimatch2 } from "minimatch";
|
|
2360
3846
|
var PRIORITY_ORDER = {
|
|
2361
3847
|
high: 0,
|
|
2362
3848
|
medium: 1,
|
|
2363
3849
|
low: 2
|
|
2364
3850
|
};
|
|
2365
|
-
async function
|
|
2366
|
-
const context = await
|
|
3851
|
+
async function getKnowledge(projectRoot, input) {
|
|
3852
|
+
const context = await loadGetKnowledgeContext(projectRoot);
|
|
2367
3853
|
const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
|
|
2368
3854
|
const matchedNodes = matchRuleNodes(context.meta, input.path);
|
|
2369
3855
|
const requiredStableIds = matchedNodes.filter((node) => node.level === "L2").map((node) => node.stable_id);
|
|
2370
3856
|
const aiSelectableStableIds = matchedNodes.filter((node) => node.level === "L1").map((node) => node.stable_id);
|
|
2371
|
-
const rules = await
|
|
3857
|
+
const rules = await resolveKnowledgeForPath(projectRoot, context, input.path);
|
|
2372
3858
|
const result = {
|
|
2373
3859
|
revision_hash: context.meta.revision,
|
|
2374
3860
|
stale,
|
|
2375
3861
|
rules
|
|
2376
3862
|
};
|
|
2377
3863
|
try {
|
|
2378
|
-
await
|
|
2379
|
-
|
|
2380
|
-
|
|
3864
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
3865
|
+
event_type: "knowledge_context_planned",
|
|
3866
|
+
target_paths: [input.path],
|
|
2381
3867
|
required_stable_ids: requiredStableIds,
|
|
2382
3868
|
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
2383
3869
|
final_stable_ids: [...requiredStableIds, ...aiSelectableStableIds],
|
|
3870
|
+
client_hash: input.client_hash,
|
|
2384
3871
|
correlation_id: input.correlation_id,
|
|
2385
3872
|
session_id: input.session_id
|
|
2386
3873
|
});
|
|
@@ -2388,13 +3875,13 @@ async function getRules(projectRoot, input) {
|
|
|
2388
3875
|
}
|
|
2389
3876
|
return result;
|
|
2390
3877
|
}
|
|
2391
|
-
async function
|
|
3878
|
+
async function loadGetKnowledgeContext(projectRoot) {
|
|
2392
3879
|
const cached = contextCache.get("context", projectRoot);
|
|
2393
3880
|
if (cached !== void 0) {
|
|
2394
3881
|
return cached;
|
|
2395
3882
|
}
|
|
2396
3883
|
const meta = await readAgentsMeta(projectRoot);
|
|
2397
|
-
const l0Content = await readFile6(
|
|
3884
|
+
const l0Content = await readFile6(join6(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
|
|
2398
3885
|
const context = {
|
|
2399
3886
|
meta,
|
|
2400
3887
|
l0Content,
|
|
@@ -2403,20 +3890,20 @@ async function loadGetRulesContext(projectRoot) {
|
|
|
2403
3890
|
contextCache.set("context", projectRoot, context);
|
|
2404
3891
|
return context;
|
|
2405
3892
|
}
|
|
2406
|
-
async function
|
|
3893
|
+
async function resolveKnowledgeForPath(projectRoot, context, path, options = {}) {
|
|
2407
3894
|
const matchedNodes = matchRuleNodes(context.meta, path);
|
|
2408
3895
|
const loaded = await loadMatchedRules(projectRoot, matchedNodes);
|
|
2409
|
-
return
|
|
3896
|
+
return buildKnowledgePayload(context, loaded, options);
|
|
2410
3897
|
}
|
|
2411
|
-
function
|
|
3898
|
+
function normalizeKnowledgePath(value) {
|
|
2412
3899
|
return value.replaceAll("\\", "/");
|
|
2413
3900
|
}
|
|
2414
3901
|
function matchRuleNodes(meta, path) {
|
|
2415
|
-
const requestedPath =
|
|
3902
|
+
const requestedPath = normalizeKnowledgePath(path);
|
|
2416
3903
|
return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
|
|
2417
3904
|
const [leftId, leftNode] = left;
|
|
2418
3905
|
const [rightId, rightNode] = right;
|
|
2419
|
-
const priorityDelta = PRIORITY_ORDER[leftNode.priority] - PRIORITY_ORDER[rightNode.priority];
|
|
3906
|
+
const priorityDelta = PRIORITY_ORDER[leftNode.priority ?? "medium"] - PRIORITY_ORDER[rightNode.priority ?? "medium"];
|
|
2420
3907
|
return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
|
|
2421
3908
|
}).map(([nodeId, node]) => ({
|
|
2422
3909
|
node_id: nodeId,
|
|
@@ -2455,7 +3942,7 @@ async function loadMatchedRules(projectRoot, matchedNodes, fileContentCache = /*
|
|
|
2455
3942
|
}
|
|
2456
3943
|
return { rules, stubs };
|
|
2457
3944
|
}
|
|
2458
|
-
function
|
|
3945
|
+
function buildKnowledgePayload(context, loaded, options = {}) {
|
|
2459
3946
|
const { L1, L2 } = partitionRulesByLevel(loaded.rules, options.dedupeByPath ?? false);
|
|
2460
3947
|
return {
|
|
2461
3948
|
L0: context.l0Content,
|
|
@@ -2472,7 +3959,7 @@ function classifyNode(nodeId, node) {
|
|
|
2472
3959
|
if (nodeId.startsWith("L2/")) {
|
|
2473
3960
|
return "L2";
|
|
2474
3961
|
}
|
|
2475
|
-
return node.layer === "L0" ? null : node.layer;
|
|
3962
|
+
return node.layer === "L0" ? null : node.layer ?? null;
|
|
2476
3963
|
}
|
|
2477
3964
|
function partitionRulesByLevel(loadedRules, dedupeByPath) {
|
|
2478
3965
|
const l1 = [];
|
|
@@ -2509,7 +3996,7 @@ function shouldLoadNodeForPath(requestedPath, node) {
|
|
|
2509
3996
|
return true;
|
|
2510
3997
|
case "path":
|
|
2511
3998
|
case void 0:
|
|
2512
|
-
return
|
|
3999
|
+
return minimatch2(requestedPath, normalizeKnowledgePath(node.scope_glob), { dot: true });
|
|
2513
4000
|
}
|
|
2514
4001
|
}
|
|
2515
4002
|
function dedupeDescriptionStubsByPath(stubs) {
|
|
@@ -2533,7 +4020,7 @@ async function readRuleContent(projectRoot, file, fileContentCache) {
|
|
|
2533
4020
|
if (cached !== void 0) {
|
|
2534
4021
|
return await cached;
|
|
2535
4022
|
}
|
|
2536
|
-
const pending = readFile6(
|
|
4023
|
+
const pending = readFile6(join6(projectRoot, file), "utf8");
|
|
2537
4024
|
fileContentCache.set(file, pending);
|
|
2538
4025
|
return await pending;
|
|
2539
4026
|
}
|
|
@@ -2548,25 +4035,27 @@ export {
|
|
|
2548
4035
|
getLedgerPath,
|
|
2549
4036
|
getLegacyLedgerPath,
|
|
2550
4037
|
getEventLedgerPath,
|
|
4038
|
+
ensureParentDirectory,
|
|
2551
4039
|
sha256,
|
|
2552
4040
|
isNodeError,
|
|
4041
|
+
atomicWriteText,
|
|
2553
4042
|
appendEventLedgerEvent,
|
|
2554
4043
|
readEventLedger,
|
|
2555
4044
|
flushAndSyncEventLedger,
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
4045
|
+
buildKnowledgeMeta,
|
|
4046
|
+
writeKnowledgeMeta,
|
|
4047
|
+
computeKnowledgeBasedAgentsMeta,
|
|
4048
|
+
computeKnowledgeTestIndex,
|
|
4049
|
+
deriveKnowledgeMetaLayer,
|
|
4050
|
+
deriveKnowledgeMetaTopologyType,
|
|
4051
|
+
isSameKnowledgeTestIndex,
|
|
2563
4052
|
stableStringify,
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
normalizeRulesPath,
|
|
4053
|
+
invalidateKnowledgeSyncCooldown,
|
|
4054
|
+
ensureKnowledgeFresh,
|
|
4055
|
+
reconcileKnowledge,
|
|
4056
|
+
getKnowledge,
|
|
4057
|
+
normalizeKnowledgePath,
|
|
2570
4058
|
runDoctorReport,
|
|
2571
|
-
runDoctorFix
|
|
4059
|
+
runDoctorFix,
|
|
4060
|
+
runDoctorApplyLint
|
|
2572
4061
|
};
|