@fenglimg/fabric-server 2.0.0-rc.15 → 2.0.0-rc.22
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.
|
@@ -172,13 +172,18 @@ function isNodeError(error) {
|
|
|
172
172
|
|
|
173
173
|
// src/services/event-ledger.ts
|
|
174
174
|
import { randomUUID } from "crypto";
|
|
175
|
-
import { existsSync, fsyncSync, openSync, closeSync } from "fs";
|
|
176
|
-
import { readFile as readFile2, truncate, writeFile } from "fs/promises";
|
|
175
|
+
import { existsSync, fsyncSync, openSync, closeSync, readFileSync, statSync } from "fs";
|
|
176
|
+
import { appendFile, mkdir as mkdir2, readFile as readFile2, truncate, writeFile } from "fs/promises";
|
|
177
|
+
import { join as join3 } from "path";
|
|
177
178
|
import {
|
|
178
179
|
eventLedgerEventSchema
|
|
179
180
|
} from "@fenglimg/fabric-shared";
|
|
180
|
-
import { createLedgerWriteQueue } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
181
|
+
import { atomicWriteText as atomicWriteText2, createLedgerWriteQueue } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
181
182
|
var ledgerQueue = createLedgerWriteQueue();
|
|
183
|
+
var EVENT_LEDGER_DEFAULT_RETENTION_DAYS = 30;
|
|
184
|
+
var EVENT_LEDGER_SIZE_WARN_BYTES = 50 * 1024 * 1024;
|
|
185
|
+
var EVENT_LEDGER_ARCHIVE_DIR = ".fabric/events.archive";
|
|
186
|
+
var warnedOversize = false;
|
|
182
187
|
async function appendEventLedgerEvent(projectRoot, event) {
|
|
183
188
|
const eventPath = getEventLedgerPath(projectRoot);
|
|
184
189
|
const nextEvent = eventLedgerEventSchema.parse({
|
|
@@ -190,6 +195,18 @@ async function appendEventLedgerEvent(projectRoot, event) {
|
|
|
190
195
|
});
|
|
191
196
|
await ensureParentDirectory(eventPath);
|
|
192
197
|
await ledgerQueue.append(eventPath, JSON.stringify(nextEvent));
|
|
198
|
+
if (!warnedOversize) {
|
|
199
|
+
try {
|
|
200
|
+
const size = statSync(eventPath).size;
|
|
201
|
+
if (size > EVENT_LEDGER_SIZE_WARN_BYTES) {
|
|
202
|
+
warnedOversize = true;
|
|
203
|
+
process.stderr.write(
|
|
204
|
+
'fabric: events.jsonl > 50MB, run "fab doctor --fix" to rotate\n'
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
}
|
|
209
|
+
}
|
|
193
210
|
return nextEvent;
|
|
194
211
|
}
|
|
195
212
|
async function readEventLedger(projectRoot, options = {}) {
|
|
@@ -265,6 +282,120 @@ function createDerivedId(index, line) {
|
|
|
265
282
|
function isNodeError2(error) {
|
|
266
283
|
return error instanceof Error;
|
|
267
284
|
}
|
|
285
|
+
async function rotateEventLedgerIfNeeded(projectRoot, opts = {}) {
|
|
286
|
+
const eventPath = getEventLedgerPath(projectRoot);
|
|
287
|
+
return ledgerQueue.runExclusive(eventPath, async () => {
|
|
288
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
289
|
+
const retentionDays = resolveRetentionDays(projectRoot, opts.retentionDays);
|
|
290
|
+
const cutoffMs = now.getTime() - retentionDays * 864e5;
|
|
291
|
+
let raw;
|
|
292
|
+
try {
|
|
293
|
+
raw = await readFile2(eventPath, "utf8");
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
296
|
+
return { rotated: false, archivedCount: 0, keptCount: 0 };
|
|
297
|
+
}
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
if (raw.length === 0) {
|
|
301
|
+
return { rotated: false, archivedCount: 0, keptCount: 0 };
|
|
302
|
+
}
|
|
303
|
+
const hasTrailingNewline = raw.endsWith("\n");
|
|
304
|
+
const segments = raw.split(/\r?\n/);
|
|
305
|
+
let keptTail = "";
|
|
306
|
+
if (!hasTrailingNewline && segments.length > 0) {
|
|
307
|
+
keptTail = segments.pop() ?? "";
|
|
308
|
+
}
|
|
309
|
+
const archived = [];
|
|
310
|
+
const kept = [];
|
|
311
|
+
for (const line of segments) {
|
|
312
|
+
const trimmed = line.trim();
|
|
313
|
+
if (trimmed.length === 0) continue;
|
|
314
|
+
let ts;
|
|
315
|
+
try {
|
|
316
|
+
const parsed = JSON.parse(trimmed);
|
|
317
|
+
const candidate = parsed["ts"];
|
|
318
|
+
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
319
|
+
ts = candidate;
|
|
320
|
+
}
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
if (ts !== void 0 && ts < cutoffMs) {
|
|
324
|
+
archived.push(trimmed);
|
|
325
|
+
} else {
|
|
326
|
+
kept.push(trimmed);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (archived.length === 0) {
|
|
330
|
+
return {
|
|
331
|
+
rotated: false,
|
|
332
|
+
archivedCount: 0,
|
|
333
|
+
keptCount: kept.length
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const yyyymmdd = formatUtcDate(now);
|
|
337
|
+
const archiveDirAbsolute = join3(projectRoot, EVENT_LEDGER_ARCHIVE_DIR);
|
|
338
|
+
const archiveFilename = `events-rotated-${yyyymmdd}.jsonl`;
|
|
339
|
+
const archiveAbsolutePath = join3(archiveDirAbsolute, archiveFilename);
|
|
340
|
+
const archiveRelativePath = `${EVENT_LEDGER_ARCHIVE_DIR}/${archiveFilename}`;
|
|
341
|
+
await mkdir2(archiveDirAbsolute, { recursive: true });
|
|
342
|
+
await appendFile(
|
|
343
|
+
archiveAbsolutePath,
|
|
344
|
+
archived.map((line) => `${line}
|
|
345
|
+
`).join(""),
|
|
346
|
+
"utf8"
|
|
347
|
+
);
|
|
348
|
+
const auditEvent = eventLedgerEventSchema.parse({
|
|
349
|
+
kind: "fabric-event",
|
|
350
|
+
id: `event:${randomUUID()}`,
|
|
351
|
+
ts: now.getTime(),
|
|
352
|
+
schema_version: 1,
|
|
353
|
+
event_type: "events_rotated",
|
|
354
|
+
cutoff_ts: new Date(cutoffMs).toISOString(),
|
|
355
|
+
archived_count: archived.length,
|
|
356
|
+
kept_count: kept.length,
|
|
357
|
+
archive_path: archiveRelativePath
|
|
358
|
+
});
|
|
359
|
+
const newMainLines = [JSON.stringify(auditEvent), ...kept];
|
|
360
|
+
let newMainContent = newMainLines.join("\n") + "\n";
|
|
361
|
+
if (keptTail.length > 0) {
|
|
362
|
+
newMainContent += keptTail;
|
|
363
|
+
}
|
|
364
|
+
await atomicWriteText2(eventPath, newMainContent);
|
|
365
|
+
return {
|
|
366
|
+
rotated: true,
|
|
367
|
+
archivedCount: archived.length,
|
|
368
|
+
keptCount: kept.length,
|
|
369
|
+
archivePath: archiveRelativePath
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
function resolveRetentionDays(projectRoot, override) {
|
|
374
|
+
if (typeof override === "number" && Number.isFinite(override) && override >= 0) {
|
|
375
|
+
return override;
|
|
376
|
+
}
|
|
377
|
+
const configPath = join3(projectRoot, ".fabric", "fabric-config.json");
|
|
378
|
+
try {
|
|
379
|
+
if (existsSync(configPath)) {
|
|
380
|
+
const raw = readFileSync(configPath, "utf8");
|
|
381
|
+
const parsed = JSON.parse(raw);
|
|
382
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
383
|
+
const v = parsed.fabric_event_retention_days;
|
|
384
|
+
if (v === 7 || v === 30 || v === 90) {
|
|
385
|
+
return v;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
return EVENT_LEDGER_DEFAULT_RETENTION_DAYS;
|
|
392
|
+
}
|
|
393
|
+
function formatUtcDate(date) {
|
|
394
|
+
const yyyy = date.getUTCFullYear().toString().padStart(4, "0");
|
|
395
|
+
const mm = (date.getUTCMonth() + 1).toString().padStart(2, "0");
|
|
396
|
+
const dd = date.getUTCDate().toString().padStart(2, "0");
|
|
397
|
+
return `${yyyy}-${mm}-${dd}`;
|
|
398
|
+
}
|
|
268
399
|
function flushAndSyncEventLedger(projectRoot) {
|
|
269
400
|
const ledgerPath = getEventLedgerPath(projectRoot);
|
|
270
401
|
if (!existsSync(ledgerPath)) return;
|
|
@@ -277,10 +408,10 @@ function flushAndSyncEventLedger(projectRoot) {
|
|
|
277
408
|
}
|
|
278
409
|
|
|
279
410
|
// src/services/knowledge-meta-builder.ts
|
|
280
|
-
import { mkdir as
|
|
281
|
-
import { existsSync as existsSync2, statSync } from "fs";
|
|
411
|
+
import { mkdir as mkdir3, readdir, readFile as readFile3 } from "fs/promises";
|
|
412
|
+
import { existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
282
413
|
import { homedir } from "os";
|
|
283
|
-
import { isAbsolute, join as
|
|
414
|
+
import { isAbsolute, join as join4, relative, resolve as resolve2, sep as sep2 } from "path";
|
|
284
415
|
import {
|
|
285
416
|
KNOWLEDGE_TEST_INDEX_SCHEMA_VERSION,
|
|
286
417
|
agentsMetaSchema as agentsMetaSchema3,
|
|
@@ -296,12 +427,12 @@ import {
|
|
|
296
427
|
StableIdSchema,
|
|
297
428
|
parseKnowledgeId
|
|
298
429
|
} from "@fenglimg/fabric-shared";
|
|
299
|
-
import { atomicWriteText as
|
|
430
|
+
import { atomicWriteText as atomicWriteText3 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
300
431
|
async function buildKnowledgeMeta(projectRootInput) {
|
|
301
432
|
const projectRoot = normalizeProjectRoot(projectRootInput);
|
|
302
433
|
assertExistingDirectory(projectRoot);
|
|
303
|
-
const metaPath =
|
|
304
|
-
const knowledgeTestIndexPath =
|
|
434
|
+
const metaPath = join4(projectRoot, ".fabric", "agents.meta.json");
|
|
435
|
+
const knowledgeTestIndexPath = join4(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
|
|
305
436
|
const existingMeta = await readExistingMeta(metaPath);
|
|
306
437
|
const existingKnowledgeTestIndex = await readExistingKnowledgeTestIndex(knowledgeTestIndexPath);
|
|
307
438
|
const meta = await computeKnowledgeBasedAgentsMeta(projectRoot, existingMeta);
|
|
@@ -314,18 +445,18 @@ async function buildKnowledgeMeta(projectRootInput) {
|
|
|
314
445
|
}
|
|
315
446
|
async function writeKnowledgeMeta(projectRootInput, options) {
|
|
316
447
|
const projectRoot = normalizeProjectRoot(projectRootInput);
|
|
317
|
-
const metaPath =
|
|
318
|
-
const knowledgeTestIndexPath =
|
|
448
|
+
const metaPath = join4(projectRoot, ".fabric", "agents.meta.json");
|
|
449
|
+
const knowledgeTestIndexPath = join4(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
|
|
319
450
|
const existingMeta = await readExistingMeta(metaPath);
|
|
320
451
|
const result = await buildKnowledgeMeta(projectRoot);
|
|
321
452
|
if (!result.changed) {
|
|
322
453
|
return result;
|
|
323
454
|
}
|
|
324
455
|
await ensureParentDirectory(metaPath);
|
|
325
|
-
await
|
|
456
|
+
await atomicWriteText3(metaPath, `${JSON.stringify(result.meta, null, 2)}
|
|
326
457
|
`);
|
|
327
458
|
await ensureParentDirectory(knowledgeTestIndexPath);
|
|
328
|
-
await
|
|
459
|
+
await atomicWriteText3(knowledgeTestIndexPath, `${JSON.stringify(result.knowledgeTestIndex, null, 2)}
|
|
329
460
|
`);
|
|
330
461
|
if (existingMeta === void 0 || stableStringify(existingMeta) !== stableStringify(result.meta)) {
|
|
331
462
|
await recordBaselineSynced(projectRoot, {
|
|
@@ -342,7 +473,7 @@ async function writeKnowledgeMeta(projectRootInput, options) {
|
|
|
342
473
|
async function computeKnowledgeBasedAgentsMeta(projectRootInput, existingMeta) {
|
|
343
474
|
const projectRoot = normalizeProjectRoot(projectRootInput);
|
|
344
475
|
assertExistingDirectory(projectRoot);
|
|
345
|
-
const previousMeta = existingMeta ?? await readExistingMeta(
|
|
476
|
+
const previousMeta = existingMeta ?? await readExistingMeta(join4(projectRoot, ".fabric", "agents.meta.json"));
|
|
346
477
|
const existingByContentRef = indexExistingNodesByContentRef(previousMeta);
|
|
347
478
|
const ruleFiles = await findKnowledgeFiles(projectRoot);
|
|
348
479
|
const nodes = {};
|
|
@@ -433,7 +564,7 @@ function normalizeProjectRoot(projectRoot) {
|
|
|
433
564
|
return isAbsolute(projectRoot) ? projectRoot : resolve2(process.cwd(), projectRoot);
|
|
434
565
|
}
|
|
435
566
|
function assertExistingDirectory(projectRoot) {
|
|
436
|
-
if (!existsSync2(projectRoot) || !
|
|
567
|
+
if (!existsSync2(projectRoot) || !statSync2(projectRoot).isDirectory()) {
|
|
437
568
|
throw new Error(`Target directory does not exist: ${projectRoot}`);
|
|
438
569
|
}
|
|
439
570
|
}
|
|
@@ -477,17 +608,17 @@ function resolvePersonalRoot() {
|
|
|
477
608
|
}
|
|
478
609
|
function resolveContentRefPath(projectRoot, contentRef) {
|
|
479
610
|
if (contentRef.startsWith(PERSONAL_CONTENT_REF_PREFIX)) {
|
|
480
|
-
return
|
|
611
|
+
return join4(resolvePersonalRoot(), ".fabric", "knowledge", contentRef.slice(PERSONAL_CONTENT_REF_PREFIX.length));
|
|
481
612
|
}
|
|
482
|
-
return
|
|
613
|
+
return join4(projectRoot, contentRef);
|
|
483
614
|
}
|
|
484
615
|
async function findKnowledgeFiles(projectRoot) {
|
|
485
|
-
const teamRoot =
|
|
486
|
-
const personalRoot =
|
|
616
|
+
const teamRoot = join4(projectRoot, ".fabric", "knowledge");
|
|
617
|
+
const personalRoot = join4(resolvePersonalRoot(), ".fabric", "knowledge");
|
|
487
618
|
try {
|
|
488
|
-
await
|
|
619
|
+
await mkdir3(personalRoot, { recursive: true });
|
|
489
620
|
for (const sub of KNOWLEDGE_SUBDIRS) {
|
|
490
|
-
await
|
|
621
|
+
await mkdir3(join4(personalRoot, sub), { recursive: true });
|
|
491
622
|
}
|
|
492
623
|
} catch {
|
|
493
624
|
}
|
|
@@ -496,11 +627,11 @@ async function findKnowledgeFiles(projectRoot) {
|
|
|
496
627
|
[teamRoot, TEAM_CONTENT_REF_PREFIX],
|
|
497
628
|
[personalRoot, PERSONAL_CONTENT_REF_PREFIX]
|
|
498
629
|
]) {
|
|
499
|
-
if (!existsSync2(root) || !
|
|
630
|
+
if (!existsSync2(root) || !statSync2(root).isDirectory()) {
|
|
500
631
|
continue;
|
|
501
632
|
}
|
|
502
633
|
for (const subdir of KNOWLEDGE_SUBDIRS) {
|
|
503
|
-
const dir =
|
|
634
|
+
const dir = join4(root, subdir);
|
|
504
635
|
let entries;
|
|
505
636
|
try {
|
|
506
637
|
entries = await readdir(dir, { withFileTypes: true });
|
|
@@ -524,7 +655,7 @@ async function findFabricVerifyAnnotations(projectRoot) {
|
|
|
524
655
|
const annotations = [];
|
|
525
656
|
const annotationPattern = /^\s*\/\/\s*@fabric-verify\s+([A-Za-z0-9][A-Za-z0-9/_-]*)\s*$/u;
|
|
526
657
|
for (const testFile of files) {
|
|
527
|
-
const source = await readFile3(
|
|
658
|
+
const source = await readFile3(join4(projectRoot, testFile), "utf8");
|
|
528
659
|
const testHash = sha256(source);
|
|
529
660
|
const lines = source.split(/\r?\n/u);
|
|
530
661
|
for (const [index, line] of lines.entries()) {
|
|
@@ -552,7 +683,7 @@ async function findTestFiles(projectRoot) {
|
|
|
552
683
|
continue;
|
|
553
684
|
}
|
|
554
685
|
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
555
|
-
const absolutePath =
|
|
686
|
+
const absolutePath = join4(current, entry.name);
|
|
556
687
|
const relativePath = toPosixPath(relative(projectRoot, absolutePath));
|
|
557
688
|
const [rootSegment] = relativePath.split("/");
|
|
558
689
|
if (entry.isDirectory()) {
|
|
@@ -802,23 +933,29 @@ function extractRuleDescription(source) {
|
|
|
802
933
|
if (summary === void 0 || summary.length === 0) {
|
|
803
934
|
return void 0;
|
|
804
935
|
}
|
|
936
|
+
const knowledge = frontmatter !== null ? extractKnowledgeFieldsFromFrontmatter(frontmatter[1]) : void 0;
|
|
805
937
|
return {
|
|
806
938
|
summary,
|
|
807
939
|
intent_clues: [],
|
|
808
940
|
tech_stack: [],
|
|
809
941
|
impact: [],
|
|
810
942
|
must_read_if: summary,
|
|
811
|
-
// v2.0
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
943
|
+
// v2.0-rc.22: when frontmatter is present, merge its knowledge fields;
|
|
944
|
+
// when fully absent (no `---` block), all knowledge fields stay
|
|
945
|
+
// undefined, matching the original heading-only fallback contract.
|
|
946
|
+
id: knowledge?.id,
|
|
947
|
+
knowledge_type: knowledge?.knowledge_type,
|
|
948
|
+
maturity: knowledge?.maturity,
|
|
949
|
+
knowledge_layer: knowledge?.knowledge_layer,
|
|
950
|
+
layer_reason: knowledge?.layer_reason,
|
|
951
|
+
created_at: knowledge?.created_at,
|
|
952
|
+
tags: knowledge?.tags,
|
|
953
|
+
// v2.0-rc.5 (C1): default-safe values when there is no frontmatter at all;
|
|
954
|
+
// when frontmatter exists, honor its declared values (extractKnowledge
|
|
955
|
+
// FieldsFromFrontmatter already applies the broad-default for missing
|
|
956
|
+
// or malformed scopes).
|
|
957
|
+
relevance_scope: knowledge?.relevance_scope ?? "broad",
|
|
958
|
+
relevance_paths: knowledge?.relevance_paths ?? []
|
|
822
959
|
};
|
|
823
960
|
}
|
|
824
961
|
function extractRuleSections(source) {
|
|
@@ -960,9 +1097,34 @@ function isNodeError3(error) {
|
|
|
960
1097
|
|
|
961
1098
|
// src/services/knowledge-sync.ts
|
|
962
1099
|
import { readdir as readdir2, readFile as readFile4, stat } from "fs/promises";
|
|
963
|
-
import { existsSync as existsSync3, statSync as
|
|
964
|
-
import {
|
|
1100
|
+
import { existsSync as existsSync3, statSync as statSync3 } from "fs";
|
|
1101
|
+
import { homedir as homedir2 } from "os";
|
|
1102
|
+
import { join as join5 } from "path";
|
|
965
1103
|
import { RuleValidationError } from "@fenglimg/fabric-shared/errors";
|
|
1104
|
+
var PERSONAL_CONTENT_REF_PREFIX2 = "~/.fabric/knowledge/";
|
|
1105
|
+
var TEAM_CONTENT_REF_PREFIX2 = ".fabric/knowledge/";
|
|
1106
|
+
var KNOWLEDGE_SUBDIRS2 = [
|
|
1107
|
+
"decisions",
|
|
1108
|
+
"pitfalls",
|
|
1109
|
+
"guidelines",
|
|
1110
|
+
"models",
|
|
1111
|
+
"processes",
|
|
1112
|
+
"pending"
|
|
1113
|
+
];
|
|
1114
|
+
function resolvePersonalRoot2() {
|
|
1115
|
+
return process.env.FABRIC_HOME ?? homedir2();
|
|
1116
|
+
}
|
|
1117
|
+
function resolveContentRefPath2(projectRoot, relPath) {
|
|
1118
|
+
if (relPath.startsWith(PERSONAL_CONTENT_REF_PREFIX2)) {
|
|
1119
|
+
return join5(
|
|
1120
|
+
resolvePersonalRoot2(),
|
|
1121
|
+
".fabric",
|
|
1122
|
+
"knowledge",
|
|
1123
|
+
relPath.slice(PERSONAL_CONTENT_REF_PREFIX2.length)
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
return join5(projectRoot, relPath);
|
|
1127
|
+
}
|
|
966
1128
|
var lastSyncState = /* @__PURE__ */ new Map();
|
|
967
1129
|
var freshSyncCooldown = /* @__PURE__ */ new Map();
|
|
968
1130
|
var SYNC_COOLDOWN_MS = 500;
|
|
@@ -970,7 +1132,7 @@ function invalidateKnowledgeSyncCooldown(projectRoot) {
|
|
|
970
1132
|
freshSyncCooldown.delete(projectRoot);
|
|
971
1133
|
}
|
|
972
1134
|
async function readMetaEntries(projectRoot) {
|
|
973
|
-
const metaPath =
|
|
1135
|
+
const metaPath = join5(projectRoot, ".fabric", "agents.meta.json");
|
|
974
1136
|
const map = /* @__PURE__ */ new Map();
|
|
975
1137
|
let raw;
|
|
976
1138
|
try {
|
|
@@ -995,32 +1157,36 @@ async function readMetaEntries(projectRoot) {
|
|
|
995
1157
|
return map;
|
|
996
1158
|
}
|
|
997
1159
|
async function findRuleFiles(projectRoot) {
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
return [];
|
|
1001
|
-
}
|
|
1160
|
+
const teamRoot = join5(projectRoot, ".fabric", "knowledge");
|
|
1161
|
+
const personalRoot = join5(resolvePersonalRoot2(), ".fabric", "knowledge");
|
|
1002
1162
|
const files = [];
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1163
|
+
for (const [root, prefix] of [
|
|
1164
|
+
[teamRoot, TEAM_CONTENT_REF_PREFIX2],
|
|
1165
|
+
[personalRoot, PERSONAL_CONTENT_REF_PREFIX2]
|
|
1166
|
+
]) {
|
|
1167
|
+
if (!existsSync3(root) || !statSync3(root).isDirectory()) {
|
|
1007
1168
|
continue;
|
|
1008
1169
|
}
|
|
1009
|
-
for (const
|
|
1010
|
-
const
|
|
1011
|
-
if (
|
|
1012
|
-
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1170
|
+
for (const subdir of KNOWLEDGE_SUBDIRS2) {
|
|
1171
|
+
const dir = join5(root, subdir);
|
|
1172
|
+
if (!existsSync3(dir) || !statSync3(dir).isDirectory()) {
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
let entries;
|
|
1176
|
+
try {
|
|
1177
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
1178
|
+
} catch {
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
for (const entry of entries) {
|
|
1182
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1183
|
+
files.push(`${prefix}${subdir}/${entry.name}`);
|
|
1184
|
+
}
|
|
1016
1185
|
}
|
|
1017
1186
|
}
|
|
1018
1187
|
}
|
|
1019
1188
|
return files.sort();
|
|
1020
1189
|
}
|
|
1021
|
-
function toPosixPath2(p) {
|
|
1022
|
-
return p.split(sep3).join("/");
|
|
1023
|
-
}
|
|
1024
1190
|
function validateFrontmatter(source, filePath, throwOnInvalid) {
|
|
1025
1191
|
if (!source.startsWith("---")) {
|
|
1026
1192
|
return null;
|
|
@@ -1066,7 +1232,7 @@ function validateFrontmatter(source, filePath, throwOnInvalid) {
|
|
|
1066
1232
|
return null;
|
|
1067
1233
|
}
|
|
1068
1234
|
async function processSingleFile(projectRoot, relPath, metaEntry, source, throwOnInvalidFrontmatter) {
|
|
1069
|
-
const absPath =
|
|
1235
|
+
const absPath = resolveContentRefPath2(projectRoot, relPath);
|
|
1070
1236
|
try {
|
|
1071
1237
|
await stat(absPath);
|
|
1072
1238
|
} catch {
|
|
@@ -1165,7 +1331,7 @@ async function ensureKnowledgeFresh(projectRoot, opts) {
|
|
|
1165
1331
|
}
|
|
1166
1332
|
for (const [relPath, entry] of metaEntries) {
|
|
1167
1333
|
if (!ruleFiles.includes(relPath)) {
|
|
1168
|
-
const absPath =
|
|
1334
|
+
const absPath = resolveContentRefPath2(projectRoot, relPath);
|
|
1169
1335
|
if (!existsSync3(absPath)) {
|
|
1170
1336
|
events.push({
|
|
1171
1337
|
type: "rule_removed",
|
|
@@ -1217,7 +1383,7 @@ async function reconcileKnowledge(projectRoot, opts) {
|
|
|
1217
1383
|
}
|
|
1218
1384
|
for (const [relPath, entry] of metaEntries) {
|
|
1219
1385
|
if (!ruleFiles.includes(relPath)) {
|
|
1220
|
-
const absPath =
|
|
1386
|
+
const absPath = resolveContentRefPath2(projectRoot, relPath);
|
|
1221
1387
|
if (!existsSync3(absPath)) {
|
|
1222
1388
|
events.push({
|
|
1223
1389
|
type: "rule_removed",
|
|
@@ -1231,14 +1397,27 @@ async function reconcileKnowledge(projectRoot, opts) {
|
|
|
1231
1397
|
}
|
|
1232
1398
|
}
|
|
1233
1399
|
}
|
|
1234
|
-
|
|
1400
|
+
let revisionDrift = false;
|
|
1401
|
+
try {
|
|
1402
|
+
const derived = await buildKnowledgeMeta(projectRoot);
|
|
1403
|
+
const onDisk = await readAgentsMeta(projectRoot);
|
|
1404
|
+
revisionDrift = onDisk.revision !== derived.meta.revision;
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
if (!(error instanceof AgentsMetaFileMissingError) && !(error instanceof AgentsMetaInvalidError)) {
|
|
1407
|
+
throw error;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
if (events.length > 0 || revisionDrift) {
|
|
1235
1411
|
await writeKnowledgeMeta(projectRoot, { source: "sync_meta" });
|
|
1236
|
-
|
|
1412
|
+
if (events.length > 0) {
|
|
1413
|
+
await appendRuleSyncEvents(projectRoot, events);
|
|
1414
|
+
}
|
|
1237
1415
|
contextCache.invalidate("file_watch", projectRoot);
|
|
1416
|
+
contextCache.invalidate("meta_write", projectRoot);
|
|
1238
1417
|
}
|
|
1239
1418
|
const duration_ms = Date.now() - startTime;
|
|
1240
1419
|
const reconciledFiles = events.map((e) => e.path);
|
|
1241
|
-
if (trigger !== void 0 && events.length > 0) {
|
|
1420
|
+
if (trigger !== void 0 && (events.length > 0 || revisionDrift)) {
|
|
1242
1421
|
if (trigger === "startup") {
|
|
1243
1422
|
await appendEventLedgerEvent(projectRoot, {
|
|
1244
1423
|
event_type: "meta_reconciled_on_startup",
|
|
@@ -1252,11 +1431,12 @@ async function reconcileKnowledge(projectRoot, opts) {
|
|
|
1252
1431
|
reconciled_files: reconciledFiles,
|
|
1253
1432
|
duration_ms,
|
|
1254
1433
|
trigger,
|
|
1255
|
-
source: "reconcileKnowledge"
|
|
1434
|
+
source: "reconcileKnowledge",
|
|
1435
|
+
...events.length === 0 && revisionDrift ? { force_write_reason: "revision_drift" } : {}
|
|
1256
1436
|
});
|
|
1257
1437
|
}
|
|
1258
1438
|
}
|
|
1259
|
-
if (events.length === 0 && warnings.length === 0) {
|
|
1439
|
+
if (events.length === 0 && warnings.length === 0 && !revisionDrift) {
|
|
1260
1440
|
return { status: "fresh", events: [], warnings: [] };
|
|
1261
1441
|
}
|
|
1262
1442
|
const status = warnings.length > 0 ? "errors" : "reconciled";
|
|
@@ -1270,21 +1450,26 @@ async function reconcileKnowledge(projectRoot, opts) {
|
|
|
1270
1450
|
|
|
1271
1451
|
// src/services/doctor.ts
|
|
1272
1452
|
import { execFileSync } from "child_process";
|
|
1273
|
-
import { existsSync as existsSync4, readdirSync, readFileSync, statSync as
|
|
1274
|
-
import { access, mkdir as
|
|
1453
|
+
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync2, statSync as statSync4 } from "fs";
|
|
1454
|
+
import { access, mkdir as mkdir4, readFile as readFile5, rename, writeFile as writeFile2 } from "fs/promises";
|
|
1275
1455
|
import { constants } from "fs";
|
|
1276
|
-
import { homedir as
|
|
1277
|
-
import { isAbsolute as isAbsolute2, join as
|
|
1456
|
+
import { homedir as homedir3 } from "os";
|
|
1457
|
+
import { isAbsolute as isAbsolute2, join as join6, posix, relative as nodeRelative, resolve as resolve3, sep as sep3 } from "path";
|
|
1278
1458
|
import { minimatch } from "minimatch";
|
|
1279
1459
|
import {
|
|
1280
1460
|
agentsMetaSchema as agentsMetaSchema4,
|
|
1281
1461
|
AgentsMetaCountersSchema,
|
|
1282
1462
|
forensicReportSchema,
|
|
1283
1463
|
parseKnowledgeId as parseKnowledgeId2,
|
|
1284
|
-
knowledgeTestIndexSchema as knowledgeTestIndexSchema2
|
|
1464
|
+
knowledgeTestIndexSchema as knowledgeTestIndexSchema2,
|
|
1465
|
+
LEGACY_KB_REGEX,
|
|
1466
|
+
BOOTSTRAP_CANONICAL,
|
|
1467
|
+
BOOTSTRAP_MARKER_BEGIN,
|
|
1468
|
+
BOOTSTRAP_MARKER_END,
|
|
1469
|
+
BOOTSTRAP_REGEX
|
|
1285
1470
|
} from "@fenglimg/fabric-shared";
|
|
1286
1471
|
import { detectFramework } from "@fenglimg/fabric-shared/node";
|
|
1287
|
-
import { atomicWriteJson as atomicWriteJson2, atomicWriteText as
|
|
1472
|
+
import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText4 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
1288
1473
|
var ORPHAN_DEMOTE_THRESHOLD_DAYS = {
|
|
1289
1474
|
stable: 90,
|
|
1290
1475
|
endorsed: 30,
|
|
@@ -1322,7 +1507,22 @@ var KNOWLEDGE_CANONICAL_TYPE_DIRS = [
|
|
|
1322
1507
|
"processes"
|
|
1323
1508
|
];
|
|
1324
1509
|
var CANONICAL_KNOWLEDGE_FILENAME_PATTERN = /^(K[PT]-(?:MOD|DEC|GLD|PIT|PRO)-\d{4,})--[a-z0-9][a-z0-9-]*\.md$/u;
|
|
1325
|
-
var
|
|
1510
|
+
var KNOWLEDGE_SUBDIRS3 = ["decisions", "pitfalls", "guidelines", "models", "processes", "pending"];
|
|
1511
|
+
var BASELINE_FILENAME_LINT_BASELINE_IDS = /* @__PURE__ */ new Set([
|
|
1512
|
+
"KT-MOD-0001",
|
|
1513
|
+
// tech-stack
|
|
1514
|
+
"KT-MOD-0002",
|
|
1515
|
+
// module-structure
|
|
1516
|
+
"KT-MOD-0003",
|
|
1517
|
+
// readme-first-paragraph
|
|
1518
|
+
"KT-PRO-0001",
|
|
1519
|
+
// build-config
|
|
1520
|
+
"KT-PRO-0002",
|
|
1521
|
+
// ci-config
|
|
1522
|
+
"KT-GLD-0001"
|
|
1523
|
+
// code-style
|
|
1524
|
+
]);
|
|
1525
|
+
var BASELINE_ID_PREFIXED_FILENAME_PATTERN = /^KT-[A-Z]+-\d+--.+\.md$/u;
|
|
1326
1526
|
var COUNTER_TYPE_CODES = ["MOD", "DEC", "GLD", "PIT", "PRO"];
|
|
1327
1527
|
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
1328
1528
|
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
@@ -1342,7 +1542,13 @@ var TARGET_FILE_PATHS = [
|
|
|
1342
1542
|
".fabric/agents.meta.json",
|
|
1343
1543
|
".fabric/.cache/knowledge-test.index.json",
|
|
1344
1544
|
".fabric/events.jsonl",
|
|
1345
|
-
".fabric/knowledge"
|
|
1545
|
+
".fabric/knowledge",
|
|
1546
|
+
// v2.0.0-rc.19 bootstrap-consolidation TASK-005: L1 canonical snapshot
|
|
1547
|
+
// (.fabric/AGENTS.md) and optional project-rules concat source
|
|
1548
|
+
// (.fabric/project-rules.md). Surfaced in summary.targetFiles so --json
|
|
1549
|
+
// consumers can confirm L1 presence at a glance.
|
|
1550
|
+
".fabric/AGENTS.md",
|
|
1551
|
+
".fabric/project-rules.md"
|
|
1346
1552
|
];
|
|
1347
1553
|
async function runDoctorReport(target) {
|
|
1348
1554
|
const projectRoot = normalizeTarget(target);
|
|
@@ -1352,17 +1558,30 @@ async function runDoctorReport(target) {
|
|
|
1352
1558
|
forensic,
|
|
1353
1559
|
meta,
|
|
1354
1560
|
eventLedger,
|
|
1355
|
-
knowledgeTestIndex
|
|
1561
|
+
knowledgeTestIndex,
|
|
1562
|
+
bootstrapMarkerMigration,
|
|
1563
|
+
l1BootstrapSnapshotDrift,
|
|
1564
|
+
l2ManagedBlockDrift
|
|
1356
1565
|
] = await Promise.all([
|
|
1357
1566
|
inspectForensic(projectRoot),
|
|
1358
1567
|
inspectMeta(projectRoot),
|
|
1359
1568
|
inspectEventLedger(projectRoot),
|
|
1360
|
-
inspectKnowledgeTestIndex(projectRoot)
|
|
1569
|
+
inspectKnowledgeTestIndex(projectRoot),
|
|
1570
|
+
// v2.0.0-rc.19 TASK-004: one-time fabric:knowledge-base → fabric:bootstrap
|
|
1571
|
+
// marker migration scan. Inspect runs in this Promise.all block to keep
|
|
1572
|
+
// performance parity with the other I/O-bound inspections.
|
|
1573
|
+
inspectBootstrapMarkerMigration(projectRoot),
|
|
1574
|
+
// v2.0.0-rc.19 TASK-005: L1 + L2 byte-level drift detection. Both are
|
|
1575
|
+
// I/O-bound (small file reads + buffer compare) so they live in the same
|
|
1576
|
+
// Promise.all block as the other bootstrap inspections.
|
|
1577
|
+
inspectL1BootstrapSnapshotDrift(projectRoot),
|
|
1578
|
+
inspectL2ManagedBlockDrift(projectRoot)
|
|
1361
1579
|
]);
|
|
1362
1580
|
const mcpConfigInWrongFile = inspectMcpConfigInWrongFile(projectRoot);
|
|
1363
1581
|
const metaManuallyDiverged = await inspectMetaManuallyDiverged(projectRoot);
|
|
1364
1582
|
const knowledgeDirUnindexed = inspectKnowledgeDirUnindexed(projectRoot, meta);
|
|
1365
1583
|
const knowledgeDirMissing = inspectKnowledgeDirMissing(projectRoot);
|
|
1584
|
+
const baselineFilenameFormat = inspectBaselineFilenameFormat(projectRoot);
|
|
1366
1585
|
const stableIdCollision = await inspectStableIdCollisions(projectRoot);
|
|
1367
1586
|
const counterDesync = inspectCounterDesync(meta);
|
|
1368
1587
|
const preexistingRootFiles = inspectPreexistingRootFiles(projectRoot);
|
|
@@ -1385,7 +1604,20 @@ async function runDoctorReport(target) {
|
|
|
1385
1604
|
const skillMdYamlInvalid = inspectSkillMdYamlInvalid(projectRoot);
|
|
1386
1605
|
const checks = [
|
|
1387
1606
|
createBootstrapAnchorCheck(bootstrapAnchor),
|
|
1607
|
+
// v2.0.0-rc.19 TASK-004: bootstrap marker migration check sits adjacent to
|
|
1608
|
+
// the anchor check — both are bootstrap-file invariants. fixable_error
|
|
1609
|
+
// when any of the four target paths still carries the legacy marker.
|
|
1610
|
+
createBootstrapMarkerMigrationCheck(bootstrapMarkerMigration),
|
|
1611
|
+
// v2.0.0-rc.19 TASK-005: L1 + L2 byte-level drift detection sit immediately
|
|
1612
|
+
// after the marker migration check. Order: anchor existence → migration →
|
|
1613
|
+
// L1 (canonical ↔ snapshot) → L2 (snapshot+rules ↔ three-end blocks).
|
|
1614
|
+
createL1BootstrapSnapshotDriftCheck(l1BootstrapSnapshotDrift),
|
|
1615
|
+
createL2ManagedBlockDriftCheck(l2ManagedBlockDrift),
|
|
1388
1616
|
createKnowledgeDirMissingCheck(knowledgeDirMissing),
|
|
1617
|
+
// v2.0.0-rc.22 TASK-006: baseline filename format. Sits adjacent to
|
|
1618
|
+
// knowledge_dir_missing — both are knowledge-layout invariants. manual_error
|
|
1619
|
+
// kind; resolution delegates to `fab scan` (no --fix path).
|
|
1620
|
+
createBaselineFilenameFormatCheck(baselineFilenameFormat),
|
|
1389
1621
|
createForensicCheck(forensic, framework.kind, entryPoints.length),
|
|
1390
1622
|
// v2.0: removed `createInitContextCheck` — `.fabric/init-context.json`
|
|
1391
1623
|
// is owned by the AI-side client init skill, not by `fabric install` CLI.
|
|
@@ -1485,7 +1717,7 @@ async function runDoctorReport(target) {
|
|
|
1485
1717
|
warningCount: warnings.length,
|
|
1486
1718
|
infoCount: infos.length,
|
|
1487
1719
|
targetFiles: Object.fromEntries(
|
|
1488
|
-
TARGET_FILE_PATHS.map((path) => [path, existsSync4(
|
|
1720
|
+
TARGET_FILE_PATHS.map((path) => [path, existsSync4(join6(projectRoot, path))])
|
|
1489
1721
|
)
|
|
1490
1722
|
}
|
|
1491
1723
|
};
|
|
@@ -1494,6 +1726,33 @@ async function runDoctorFix(target) {
|
|
|
1494
1726
|
const projectRoot = normalizeTarget(target);
|
|
1495
1727
|
const before = await runDoctorReport(projectRoot);
|
|
1496
1728
|
const fixed = [];
|
|
1729
|
+
if (before.fixable_errors.some(
|
|
1730
|
+
(issue) => issue.code === "bootstrap_marker_migration_required"
|
|
1731
|
+
)) {
|
|
1732
|
+
const migrated = await migrateBootstrapMarkers(projectRoot);
|
|
1733
|
+
fixed.push(findIssue(before.fixable_errors, "bootstrap_marker_migration_required"));
|
|
1734
|
+
for (const path of migrated.paths) {
|
|
1735
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1736
|
+
event_type: "bootstrap_marker_migrated",
|
|
1737
|
+
path,
|
|
1738
|
+
migrated_count: migrated.countPerPath[path] ?? 1,
|
|
1739
|
+
legacy_marker: "fabric:knowledge-base",
|
|
1740
|
+
new_marker: "fabric:bootstrap",
|
|
1741
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1742
|
+
}).catch(() => {
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
|
|
1747
|
+
const snapshotPath = join6(projectRoot, ".fabric", "AGENTS.md");
|
|
1748
|
+
await ensureParentDirectory(snapshotPath);
|
|
1749
|
+
await atomicWriteText4(snapshotPath, BOOTSTRAP_CANONICAL);
|
|
1750
|
+
fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
|
|
1751
|
+
}
|
|
1752
|
+
if (before.fixable_errors.some((issue) => issue.code === "managed_block_drift")) {
|
|
1753
|
+
await rewriteThreeEndManagedBlocks(projectRoot);
|
|
1754
|
+
fixed.push(findIssue(before.fixable_errors, "managed_block_drift"));
|
|
1755
|
+
}
|
|
1497
1756
|
if (before.fixable_errors.some((issue) => issue.code === "knowledge_dir_missing")) {
|
|
1498
1757
|
await ensureKnowledgeSubdirs(projectRoot);
|
|
1499
1758
|
fixed.push(findIssue(before.fixable_errors, "knowledge_dir_missing"));
|
|
@@ -1507,26 +1766,23 @@ async function runDoctorFix(target) {
|
|
|
1507
1766
|
fixed.push(findIssue(before.fixable_errors, "counter_desync"));
|
|
1508
1767
|
contextCache.invalidate("meta_write", projectRoot);
|
|
1509
1768
|
}
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
)) {
|
|
1769
|
+
const reconcileCodes = [
|
|
1770
|
+
"agents_meta_missing",
|
|
1771
|
+
"agents_meta_stale",
|
|
1772
|
+
"knowledge_test_index_missing",
|
|
1773
|
+
"knowledge_test_index_stale",
|
|
1774
|
+
"content_ref_missing",
|
|
1775
|
+
"knowledge_dir_unindexed"
|
|
1776
|
+
];
|
|
1777
|
+
if (before.fixable_errors.some((issue) => reconcileCodes.includes(issue.code)) || before.warnings.some((issue) => reconcileCodes.includes(issue.code))) {
|
|
1520
1778
|
await reconcileKnowledge(projectRoot, { trigger: "doctor" });
|
|
1521
1779
|
for (const issue of before.fixable_errors.filter(
|
|
1522
|
-
(candidate) =>
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
"knowledge_dir_unindexed"
|
|
1529
|
-
].includes(candidate.code)
|
|
1780
|
+
(candidate) => reconcileCodes.includes(candidate.code)
|
|
1781
|
+
)) {
|
|
1782
|
+
fixed.push(issue);
|
|
1783
|
+
}
|
|
1784
|
+
for (const issue of before.warnings.filter(
|
|
1785
|
+
(candidate) => reconcileCodes.includes(candidate.code)
|
|
1530
1786
|
)) {
|
|
1531
1787
|
fixed.push(issue);
|
|
1532
1788
|
}
|
|
@@ -1545,6 +1801,15 @@ async function runDoctorFix(target) {
|
|
|
1545
1801
|
});
|
|
1546
1802
|
fixed.push(findIssue(before.fixable_errors, "event_ledger_partial_write"));
|
|
1547
1803
|
}
|
|
1804
|
+
const rotateResult = await rotateEventLedgerIfNeeded(projectRoot);
|
|
1805
|
+
if (rotateResult.rotated && rotateResult.archivedCount > 0) {
|
|
1806
|
+
fixed.push({
|
|
1807
|
+
code: "event_ledger_rotated",
|
|
1808
|
+
name: "Event ledger rotated",
|
|
1809
|
+
message: `Rotated ${rotateResult.archivedCount} event(s) older than retention window to ${rotateResult.archivePath ?? "archive"}`,
|
|
1810
|
+
path: rotateResult.archivePath
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1548
1813
|
if (before.fixable_errors.some((issue) => issue.code === "mcp_config_in_wrong_file")) {
|
|
1549
1814
|
await fixMcpConfigInWrongFile(projectRoot);
|
|
1550
1815
|
fixed.push(findIssue(before.fixable_errors, "mcp_config_in_wrong_file"));
|
|
@@ -1684,7 +1949,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
|
|
|
1684
1949
|
};
|
|
1685
1950
|
}
|
|
1686
1951
|
const detail = `${candidate.maturity} -> ${next}`;
|
|
1687
|
-
const absPath =
|
|
1952
|
+
const absPath = join6(projectRoot, candidate.path);
|
|
1688
1953
|
try {
|
|
1689
1954
|
const source = await readFile5(absPath, "utf8");
|
|
1690
1955
|
const rewritten = rewriteFrontmatterMaturity(source, next);
|
|
@@ -1706,7 +1971,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
|
|
|
1706
1971
|
error: "rewrite produced byte-identical output"
|
|
1707
1972
|
};
|
|
1708
1973
|
}
|
|
1709
|
-
await
|
|
1974
|
+
await atomicWriteText4(absPath, rewritten);
|
|
1710
1975
|
try {
|
|
1711
1976
|
await appendEventLedgerEvent(projectRoot, {
|
|
1712
1977
|
event_type: "knowledge_demoted",
|
|
@@ -1716,7 +1981,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
|
|
|
1716
1981
|
});
|
|
1717
1982
|
} catch (ledgerError) {
|
|
1718
1983
|
try {
|
|
1719
|
-
await
|
|
1984
|
+
await atomicWriteText4(absPath, source);
|
|
1720
1985
|
} catch (rollbackError) {
|
|
1721
1986
|
return {
|
|
1722
1987
|
kind: "knowledge_orphan_demote_required",
|
|
@@ -1751,11 +2016,11 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
|
|
|
1751
2016
|
}
|
|
1752
2017
|
}
|
|
1753
2018
|
async function applyStaleArchive(projectRoot, candidate, now) {
|
|
1754
|
-
const sourceAbs =
|
|
1755
|
-
const destAbs =
|
|
2019
|
+
const sourceAbs = join6(projectRoot, candidate.path);
|
|
2020
|
+
const destAbs = join6(projectRoot, candidate.archive_path);
|
|
1756
2021
|
const detail = `${candidate.path} -> ${candidate.archive_path}`;
|
|
1757
2022
|
try {
|
|
1758
|
-
await
|
|
2023
|
+
await mkdir4(join6(destAbs, ".."), { recursive: true });
|
|
1759
2024
|
try {
|
|
1760
2025
|
await rename(sourceAbs, destAbs);
|
|
1761
2026
|
} catch (renameError) {
|
|
@@ -1814,7 +2079,7 @@ async function applyStaleArchive(projectRoot, candidate, now) {
|
|
|
1814
2079
|
async function applyPendingAutoArchive(projectRoot, candidate, now) {
|
|
1815
2080
|
const detail = `${candidate.pending_path} -> ${candidate.archived_to}`;
|
|
1816
2081
|
try {
|
|
1817
|
-
await
|
|
2082
|
+
await mkdir4(join6(candidate.archived_to_abs, ".."), { recursive: true });
|
|
1818
2083
|
let moved = false;
|
|
1819
2084
|
if (candidate.layer === "team") {
|
|
1820
2085
|
try {
|
|
@@ -1887,11 +2152,11 @@ async function applyPendingAutoArchive(projectRoot, candidate, now) {
|
|
|
1887
2152
|
}
|
|
1888
2153
|
function relativePosix(projectRoot, absolutePath) {
|
|
1889
2154
|
const rel = nodeRelative(projectRoot, absolutePath);
|
|
1890
|
-
return rel.split(
|
|
2155
|
+
return rel.split(sep3).join("/");
|
|
1891
2156
|
}
|
|
1892
2157
|
async function applySessionHintsStaleCleanup(projectRoot, candidate) {
|
|
1893
2158
|
const detail = `deleted (${candidate.age_days}d old)`;
|
|
1894
|
-
const absPath =
|
|
2159
|
+
const absPath = join6(projectRoot, candidate.path);
|
|
1895
2160
|
try {
|
|
1896
2161
|
const { unlink } = await import("fs/promises");
|
|
1897
2162
|
await unlink(absPath);
|
|
@@ -1912,7 +2177,7 @@ async function applySessionHintsStaleCleanup(projectRoot, candidate) {
|
|
|
1912
2177
|
}
|
|
1913
2178
|
}
|
|
1914
2179
|
async function applyIndexDriftFix(projectRoot, inspection) {
|
|
1915
|
-
const metaPath =
|
|
2180
|
+
const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
|
|
1916
2181
|
const detailParts = [];
|
|
1917
2182
|
try {
|
|
1918
2183
|
const meta = agentsMetaSchema4.parse(JSON.parse(await readFile5(metaPath, "utf8")));
|
|
@@ -1948,7 +2213,7 @@ function truncateErrorMessage(error) {
|
|
|
1948
2213
|
return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
|
|
1949
2214
|
}
|
|
1950
2215
|
async function inspectForensic(projectRoot) {
|
|
1951
|
-
const path =
|
|
2216
|
+
const path = join6(projectRoot, ".fabric", "forensic.json");
|
|
1952
2217
|
try {
|
|
1953
2218
|
const parsed = forensicReportSchema.parse(JSON.parse(await readFile5(path, "utf8")));
|
|
1954
2219
|
return { present: true, valid: true, report: parsed };
|
|
@@ -1960,12 +2225,12 @@ async function inspectForensic(projectRoot) {
|
|
|
1960
2225
|
}
|
|
1961
2226
|
}
|
|
1962
2227
|
function inspectMcpConfigInWrongFile(projectRoot) {
|
|
1963
|
-
const settingsPath =
|
|
2228
|
+
const settingsPath = join6(projectRoot, ".claude", "settings.json");
|
|
1964
2229
|
if (!existsSync4(settingsPath)) {
|
|
1965
2230
|
return { hasWrongEntry: false, settingsPath };
|
|
1966
2231
|
}
|
|
1967
2232
|
try {
|
|
1968
|
-
const parsed = JSON.parse(
|
|
2233
|
+
const parsed = JSON.parse(readFileSync2(settingsPath, "utf8"));
|
|
1969
2234
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1970
2235
|
return { hasWrongEntry: false, settingsPath };
|
|
1971
2236
|
}
|
|
@@ -1981,7 +2246,7 @@ function inspectMcpConfigInWrongFile(projectRoot) {
|
|
|
1981
2246
|
}
|
|
1982
2247
|
}
|
|
1983
2248
|
async function inspectMeta(projectRoot) {
|
|
1984
|
-
const metaPath =
|
|
2249
|
+
const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
|
|
1985
2250
|
const built = await tryBuildRuleMeta(projectRoot);
|
|
1986
2251
|
try {
|
|
1987
2252
|
const raw = await readFile5(metaPath, "utf8");
|
|
@@ -2054,7 +2319,7 @@ function inspectContentRefs(projectRoot, meta) {
|
|
|
2054
2319
|
if (isPersonalKnowledge) {
|
|
2055
2320
|
continue;
|
|
2056
2321
|
}
|
|
2057
|
-
if (!existsSync4(
|
|
2322
|
+
if (!existsSync4(join6(projectRoot, contentRef))) {
|
|
2058
2323
|
missing.push(contentRef);
|
|
2059
2324
|
}
|
|
2060
2325
|
}
|
|
@@ -2096,7 +2361,7 @@ async function inspectEventLedger(projectRoot) {
|
|
|
2096
2361
|
}
|
|
2097
2362
|
}
|
|
2098
2363
|
async function inspectKnowledgeTestIndex(projectRoot) {
|
|
2099
|
-
const path =
|
|
2364
|
+
const path = join6(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
|
|
2100
2365
|
const built = await tryBuildRuleMeta(projectRoot);
|
|
2101
2366
|
try {
|
|
2102
2367
|
const index = knowledgeTestIndexSchema2.parse(JSON.parse(await readFile5(path, "utf8")));
|
|
@@ -2120,10 +2385,190 @@ async function inspectKnowledgeTestIndex(projectRoot) {
|
|
|
2120
2385
|
}
|
|
2121
2386
|
function inspectBootstrapAnchor(projectRoot) {
|
|
2122
2387
|
return {
|
|
2123
|
-
hasAgentsMd: existsSync4(
|
|
2124
|
-
hasClaudeMd: existsSync4(
|
|
2388
|
+
hasAgentsMd: existsSync4(join6(projectRoot, "AGENTS.md")),
|
|
2389
|
+
hasClaudeMd: existsSync4(join6(projectRoot, "CLAUDE.md"))
|
|
2125
2390
|
};
|
|
2126
2391
|
}
|
|
2392
|
+
var BOOTSTRAP_MARKER_MIGRATION_TARGETS = [
|
|
2393
|
+
"CLAUDE.md",
|
|
2394
|
+
"AGENTS.md",
|
|
2395
|
+
".cursor/rules",
|
|
2396
|
+
".cursor/rules/fabric-bootstrap.mdc"
|
|
2397
|
+
];
|
|
2398
|
+
async function inspectBootstrapMarkerMigration(target) {
|
|
2399
|
+
const filesNeedingMigration = [];
|
|
2400
|
+
for (const rel of BOOTSTRAP_MARKER_MIGRATION_TARGETS) {
|
|
2401
|
+
const abs = join6(target, rel);
|
|
2402
|
+
if (!existsSync4(abs)) {
|
|
2403
|
+
continue;
|
|
2404
|
+
}
|
|
2405
|
+
let content;
|
|
2406
|
+
try {
|
|
2407
|
+
content = await readFile5(abs, "utf8");
|
|
2408
|
+
} catch {
|
|
2409
|
+
continue;
|
|
2410
|
+
}
|
|
2411
|
+
if (LEGACY_KB_REGEX.test(content)) {
|
|
2412
|
+
filesNeedingMigration.push(abs);
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
return { filesNeedingMigration };
|
|
2416
|
+
}
|
|
2417
|
+
function createBootstrapMarkerMigrationCheck(inspection) {
|
|
2418
|
+
if (inspection.filesNeedingMigration.length === 0) {
|
|
2419
|
+
return okCheck(
|
|
2420
|
+
"Bootstrap marker migration",
|
|
2421
|
+
"No legacy fabric:knowledge-base markers detected in bootstrap target files."
|
|
2422
|
+
);
|
|
2423
|
+
}
|
|
2424
|
+
const list = inspection.filesNeedingMigration.join(", ");
|
|
2425
|
+
return issueCheck(
|
|
2426
|
+
"Bootstrap marker migration",
|
|
2427
|
+
"error",
|
|
2428
|
+
"fixable_error",
|
|
2429
|
+
"bootstrap_marker_migration_required",
|
|
2430
|
+
`${inspection.filesNeedingMigration.length} file${inspection.filesNeedingMigration.length === 1 ? "" : "s"} still carry the legacy fabric:knowledge-base bootstrap marker: ${list}.`,
|
|
2431
|
+
"Run `fab doctor --fix` to migrate to fabric:bootstrap marker"
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
async function inspectL1BootstrapSnapshotDrift(target) {
|
|
2435
|
+
const abs = join6(target, ".fabric", "AGENTS.md");
|
|
2436
|
+
if (!existsSync4(abs)) {
|
|
2437
|
+
return { status: "missing", canonical: BOOTSTRAP_CANONICAL, onDisk: null };
|
|
2438
|
+
}
|
|
2439
|
+
let onDisk;
|
|
2440
|
+
try {
|
|
2441
|
+
onDisk = await readFile5(abs, "utf8");
|
|
2442
|
+
} catch {
|
|
2443
|
+
return { status: "missing", canonical: BOOTSTRAP_CANONICAL, onDisk: null };
|
|
2444
|
+
}
|
|
2445
|
+
if (onDisk === BOOTSTRAP_CANONICAL) {
|
|
2446
|
+
return { status: "ok", canonical: BOOTSTRAP_CANONICAL, onDisk };
|
|
2447
|
+
}
|
|
2448
|
+
return { status: "drift", canonical: BOOTSTRAP_CANONICAL, onDisk };
|
|
2449
|
+
}
|
|
2450
|
+
function createL1BootstrapSnapshotDriftCheck(inspection) {
|
|
2451
|
+
if (inspection.status === "drift") {
|
|
2452
|
+
return issueCheck(
|
|
2453
|
+
"Bootstrap snapshot drift",
|
|
2454
|
+
"error",
|
|
2455
|
+
"fixable_error",
|
|
2456
|
+
"bootstrap_snapshot_drift",
|
|
2457
|
+
".fabric/AGENTS.md content diverges byte-for-byte from BOOTSTRAP_CANONICAL.",
|
|
2458
|
+
"Run `fab doctor --fix` to restore canonical bootstrap snapshot"
|
|
2459
|
+
);
|
|
2460
|
+
}
|
|
2461
|
+
return okCheck(
|
|
2462
|
+
"Bootstrap snapshot drift",
|
|
2463
|
+
inspection.status === "ok" ? ".fabric/AGENTS.md byte-equals BOOTSTRAP_CANONICAL." : ".fabric/AGENTS.md absent \u2014 delegated to bootstrap_anchor_missing."
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
async function inspectL2ManagedBlockDrift(target) {
|
|
2467
|
+
const snapshotPath = join6(target, ".fabric", "AGENTS.md");
|
|
2468
|
+
if (!existsSync4(snapshotPath)) {
|
|
2469
|
+
return { status: "ok", drifted: [] };
|
|
2470
|
+
}
|
|
2471
|
+
let snapshot;
|
|
2472
|
+
try {
|
|
2473
|
+
snapshot = await readFile5(snapshotPath, "utf8");
|
|
2474
|
+
} catch {
|
|
2475
|
+
return { status: "ok", drifted: [] };
|
|
2476
|
+
}
|
|
2477
|
+
const projectRulesPath = join6(target, ".fabric", "project-rules.md");
|
|
2478
|
+
let expectedBody = snapshot;
|
|
2479
|
+
if (existsSync4(projectRulesPath)) {
|
|
2480
|
+
try {
|
|
2481
|
+
const projectRules = await readFile5(projectRulesPath, "utf8");
|
|
2482
|
+
expectedBody = `${snapshot}
|
|
2483
|
+
---
|
|
2484
|
+
${projectRules}`;
|
|
2485
|
+
} catch {
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
const drifted = [];
|
|
2489
|
+
let anyManagedBlockFound = false;
|
|
2490
|
+
const blockTargets = [
|
|
2491
|
+
join6(target, "AGENTS.md"),
|
|
2492
|
+
join6(target, ".cursor", "rules", "fabric-bootstrap.mdc")
|
|
2493
|
+
];
|
|
2494
|
+
for (const abs of blockTargets) {
|
|
2495
|
+
if (!existsSync4(abs)) {
|
|
2496
|
+
continue;
|
|
2497
|
+
}
|
|
2498
|
+
let content;
|
|
2499
|
+
try {
|
|
2500
|
+
content = await readFile5(abs, "utf8");
|
|
2501
|
+
} catch {
|
|
2502
|
+
continue;
|
|
2503
|
+
}
|
|
2504
|
+
if (!BOOTSTRAP_REGEX.test(content) && LEGACY_KB_REGEX.test(content)) {
|
|
2505
|
+
continue;
|
|
2506
|
+
}
|
|
2507
|
+
const match = content.match(BOOTSTRAP_REGEX);
|
|
2508
|
+
if (match === null) {
|
|
2509
|
+
continue;
|
|
2510
|
+
}
|
|
2511
|
+
anyManagedBlockFound = true;
|
|
2512
|
+
const region = match[0];
|
|
2513
|
+
const beginIdx = region.indexOf(BOOTSTRAP_MARKER_BEGIN);
|
|
2514
|
+
const bodyStart = beginIdx + BOOTSTRAP_MARKER_BEGIN.length;
|
|
2515
|
+
const endIdx = region.indexOf(BOOTSTRAP_MARKER_END, bodyStart);
|
|
2516
|
+
if (bodyStart < 0 || endIdx < 0) {
|
|
2517
|
+
continue;
|
|
2518
|
+
}
|
|
2519
|
+
let body = region.slice(bodyStart, endIdx);
|
|
2520
|
+
if (body.startsWith("\n")) body = body.slice(1);
|
|
2521
|
+
if (body.endsWith("\n")) body = body.slice(0, -1);
|
|
2522
|
+
if (body !== expectedBody) {
|
|
2523
|
+
drifted.push({ path: abs, expected: expectedBody, actual: body });
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
const claudeMdPath = join6(target, "CLAUDE.md");
|
|
2527
|
+
if (existsSync4(claudeMdPath)) {
|
|
2528
|
+
let claudeContent;
|
|
2529
|
+
try {
|
|
2530
|
+
claudeContent = await readFile5(claudeMdPath, "utf8");
|
|
2531
|
+
if (!BOOTSTRAP_REGEX.test(claudeContent) && LEGACY_KB_REGEX.test(claudeContent)) {
|
|
2532
|
+
} else {
|
|
2533
|
+
anyManagedBlockFound = true;
|
|
2534
|
+
const lines = claudeContent.split(/\r?\n/u);
|
|
2535
|
+
const hasAtImport = lines.some((line) => line.trim() === "@.fabric/AGENTS.md");
|
|
2536
|
+
if (!hasAtImport) {
|
|
2537
|
+
drifted.push({
|
|
2538
|
+
path: claudeMdPath,
|
|
2539
|
+
expected: "@.fabric/AGENTS.md",
|
|
2540
|
+
actual: "(line missing)"
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
} catch {
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
if (!anyManagedBlockFound) {
|
|
2548
|
+
return { status: "no-managed-block", drifted: [] };
|
|
2549
|
+
}
|
|
2550
|
+
if (drifted.length === 0) {
|
|
2551
|
+
return { status: "ok", drifted: [] };
|
|
2552
|
+
}
|
|
2553
|
+
return { status: "drift", drifted };
|
|
2554
|
+
}
|
|
2555
|
+
function createL2ManagedBlockDriftCheck(inspection) {
|
|
2556
|
+
if (inspection.status === "drift") {
|
|
2557
|
+
const list = inspection.drifted.map((d) => d.path).join(", ");
|
|
2558
|
+
return issueCheck(
|
|
2559
|
+
"Managed block drift",
|
|
2560
|
+
"error",
|
|
2561
|
+
"fixable_error",
|
|
2562
|
+
"managed_block_drift",
|
|
2563
|
+
`${inspection.drifted.length} three-end managed block${inspection.drifted.length === 1 ? "" : "s"} diverge from expected body (snapshot + optional project-rules concat): ${list}.`,
|
|
2564
|
+
"Run `fab doctor --fix` to restore three-end managed blocks from canonical"
|
|
2565
|
+
);
|
|
2566
|
+
}
|
|
2567
|
+
return okCheck(
|
|
2568
|
+
"Managed block drift",
|
|
2569
|
+
inspection.status === "ok" ? "Three-end managed blocks byte-equal expectedBody." : "No three-end managed blocks detected \u2014 propagation pending or legacy-marker state."
|
|
2570
|
+
);
|
|
2571
|
+
}
|
|
2127
2572
|
function createBootstrapAnchorCheck(inspection) {
|
|
2128
2573
|
if (!inspection.hasAgentsMd && !inspection.hasClaudeMd) {
|
|
2129
2574
|
return issueCheck(
|
|
@@ -2142,16 +2587,82 @@ function createBootstrapAnchorCheck(inspection) {
|
|
|
2142
2587
|
return okCheck("Bootstrap anchor", `Bootstrap anchor present at repo root: ${present}.`);
|
|
2143
2588
|
}
|
|
2144
2589
|
function inspectKnowledgeDirMissing(projectRoot) {
|
|
2145
|
-
const knowledgeRoot =
|
|
2590
|
+
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
2146
2591
|
const missingSubdirs = [];
|
|
2147
|
-
for (const sub of
|
|
2148
|
-
const path =
|
|
2592
|
+
for (const sub of KNOWLEDGE_SUBDIRS3) {
|
|
2593
|
+
const path = join6(knowledgeRoot, sub);
|
|
2149
2594
|
if (!existsSync4(path)) {
|
|
2150
2595
|
missingSubdirs.push(`.fabric/knowledge/${sub}`);
|
|
2151
2596
|
}
|
|
2152
2597
|
}
|
|
2153
2598
|
return { missingSubdirs };
|
|
2154
2599
|
}
|
|
2600
|
+
function inspectBaselineFilenameFormat(projectRoot) {
|
|
2601
|
+
const offenders = [];
|
|
2602
|
+
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
2603
|
+
if (!existsSync4(knowledgeRoot)) {
|
|
2604
|
+
return { offenders };
|
|
2605
|
+
}
|
|
2606
|
+
for (const sub of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
2607
|
+
const dir = join6(knowledgeRoot, sub);
|
|
2608
|
+
if (!existsSync4(dir)) {
|
|
2609
|
+
continue;
|
|
2610
|
+
}
|
|
2611
|
+
let entries;
|
|
2612
|
+
try {
|
|
2613
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
2614
|
+
} catch {
|
|
2615
|
+
continue;
|
|
2616
|
+
}
|
|
2617
|
+
for (const entry of entries) {
|
|
2618
|
+
const entryName = entry.name;
|
|
2619
|
+
if (!entry.isFile() || !entryName.endsWith(".md")) {
|
|
2620
|
+
continue;
|
|
2621
|
+
}
|
|
2622
|
+
if (BASELINE_ID_PREFIXED_FILENAME_PATTERN.test(entryName)) {
|
|
2623
|
+
continue;
|
|
2624
|
+
}
|
|
2625
|
+
const abs = join6(dir, entryName);
|
|
2626
|
+
let source;
|
|
2627
|
+
try {
|
|
2628
|
+
source = readFileSync2(abs, "utf8");
|
|
2629
|
+
} catch {
|
|
2630
|
+
continue;
|
|
2631
|
+
}
|
|
2632
|
+
const id = extractKnowledgeFrontmatterId(source);
|
|
2633
|
+
if (id === null) {
|
|
2634
|
+
continue;
|
|
2635
|
+
}
|
|
2636
|
+
if (!BASELINE_FILENAME_LINT_BASELINE_IDS.has(id)) {
|
|
2637
|
+
continue;
|
|
2638
|
+
}
|
|
2639
|
+
offenders.push({
|
|
2640
|
+
path: posix.join(".fabric/knowledge", sub, entryName),
|
|
2641
|
+
stable_id: id
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
offenders.sort((a, b) => a.path.localeCompare(b.path));
|
|
2646
|
+
return { offenders };
|
|
2647
|
+
}
|
|
2648
|
+
function createBaselineFilenameFormatCheck(inspection) {
|
|
2649
|
+
if (inspection.offenders.length === 0) {
|
|
2650
|
+
return okCheck(
|
|
2651
|
+
"Baseline filename format",
|
|
2652
|
+
"All baseline knowledge files use the canonical `${id}--${slug}.md` filename format."
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
const first = inspection.offenders[0];
|
|
2656
|
+
const detail = `${first.stable_id} at ${first.path}`;
|
|
2657
|
+
return issueCheck(
|
|
2658
|
+
"Baseline filename format",
|
|
2659
|
+
"error",
|
|
2660
|
+
"manual_error",
|
|
2661
|
+
"lint-baseline-filename-format",
|
|
2662
|
+
`${inspection.offenders.length} baseline knowledge file${inspection.offenders.length === 1 ? "" : "s"} use${inspection.offenders.length === 1 ? "s" : ""} the deprecated bare-slug filename format and must be migrated to \`\${id}--\${slug}.md\`. First: ${detail}.`,
|
|
2663
|
+
"Run `fab scan` to auto-migrate baseline filenames to the canonical `${id}--${slug}.md` format."
|
|
2664
|
+
);
|
|
2665
|
+
}
|
|
2155
2666
|
function createKnowledgeDirMissingCheck(inspection) {
|
|
2156
2667
|
if (inspection.missingSubdirs.length > 0) {
|
|
2157
2668
|
const list = inspection.missingSubdirs.join(", ");
|
|
@@ -2166,7 +2677,7 @@ function createKnowledgeDirMissingCheck(inspection) {
|
|
|
2166
2677
|
}
|
|
2167
2678
|
return okCheck(
|
|
2168
2679
|
"Knowledge layout",
|
|
2169
|
-
`All ${
|
|
2680
|
+
`All ${KNOWLEDGE_SUBDIRS3.length} required .fabric/knowledge/* subdirectories exist.`
|
|
2170
2681
|
);
|
|
2171
2682
|
}
|
|
2172
2683
|
function createForensicCheck(forensic, frameworkKind, entryPointCount) {
|
|
@@ -2195,11 +2706,11 @@ function createMetaCheck(meta) {
|
|
|
2195
2706
|
if (meta.stale) {
|
|
2196
2707
|
return issueCheck(
|
|
2197
2708
|
"Agents metadata",
|
|
2198
|
-
"
|
|
2199
|
-
"
|
|
2709
|
+
"warn",
|
|
2710
|
+
"warning",
|
|
2200
2711
|
"agents_meta_stale",
|
|
2201
2712
|
`.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/knowledge derived revision ${meta.computedRevision ?? "<unknown>"}.`,
|
|
2202
|
-
"Run `fab doctor --fix`
|
|
2713
|
+
"Benign \u2014 engine auto-heals on next plan-context/get-sections call. Run `fab doctor --fix` for explicit reconciliation."
|
|
2203
2714
|
);
|
|
2204
2715
|
}
|
|
2205
2716
|
return okCheck("Agents metadata", `.fabric/agents.meta.json revision ${meta.revision} is aligned with .fabric/knowledge.`);
|
|
@@ -2312,7 +2823,7 @@ function findIssue(issues, code) {
|
|
|
2312
2823
|
};
|
|
2313
2824
|
}
|
|
2314
2825
|
async function inspectMetaManuallyDiverged(projectRoot) {
|
|
2315
|
-
const metaPath =
|
|
2826
|
+
const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
|
|
2316
2827
|
if (!existsSync4(metaPath)) {
|
|
2317
2828
|
return { extraMetaEntries: [], hashMismatchEntries: [], readable: false };
|
|
2318
2829
|
}
|
|
@@ -2332,13 +2843,13 @@ async function inspectMetaManuallyDiverged(projectRoot) {
|
|
|
2332
2843
|
const hashMismatchEntries = [];
|
|
2333
2844
|
for (const node of Object.values(meta.nodes)) {
|
|
2334
2845
|
const contentRef = node.content_ref ?? node.file;
|
|
2335
|
-
const absPath =
|
|
2846
|
+
const absPath = join6(projectRoot, contentRef);
|
|
2336
2847
|
if (!existsSync4(absPath)) {
|
|
2337
2848
|
extraMetaEntries.push(contentRef);
|
|
2338
2849
|
continue;
|
|
2339
2850
|
}
|
|
2340
2851
|
try {
|
|
2341
|
-
const content =
|
|
2852
|
+
const content = readFileSync2(absPath, "utf8");
|
|
2342
2853
|
const diskHash = sha256(content);
|
|
2343
2854
|
if (node.hash !== "" && node.hash !== diskHash) {
|
|
2344
2855
|
hashMismatchEntries.push(contentRef);
|
|
@@ -2351,7 +2862,7 @@ async function inspectMetaManuallyDiverged(projectRoot) {
|
|
|
2351
2862
|
}
|
|
2352
2863
|
function inspectKnowledgeDirUnindexed(projectRoot, meta) {
|
|
2353
2864
|
const physicalMdFiles = /* @__PURE__ */ new Set();
|
|
2354
|
-
collectMdFilesUnder(physicalMdFiles, projectRoot,
|
|
2865
|
+
collectMdFilesUnder(physicalMdFiles, projectRoot, join6(projectRoot, ".fabric", "knowledge"), ".fabric/knowledge");
|
|
2355
2866
|
if (physicalMdFiles.size === 0) {
|
|
2356
2867
|
return { unindexedFiles: [] };
|
|
2357
2868
|
}
|
|
@@ -2376,7 +2887,7 @@ function collectMdFilesUnder(out, projectRoot, rootDir, relPrefix) {
|
|
|
2376
2887
|
continue;
|
|
2377
2888
|
}
|
|
2378
2889
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2379
|
-
const abs =
|
|
2890
|
+
const abs = join6(dir, entry.name);
|
|
2380
2891
|
if (entry.isDirectory()) {
|
|
2381
2892
|
stack.push(abs);
|
|
2382
2893
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -2401,7 +2912,7 @@ function createKnowledgeDirUnindexedCheck(inspection) {
|
|
|
2401
2912
|
}
|
|
2402
2913
|
async function inspectStableIdCollisions(projectRoot) {
|
|
2403
2914
|
const found = [];
|
|
2404
|
-
const knowledgeDir =
|
|
2915
|
+
const knowledgeDir = join6(projectRoot, ".fabric", "knowledge");
|
|
2405
2916
|
if (existsSync4(knowledgeDir)) {
|
|
2406
2917
|
const stack = [knowledgeDir];
|
|
2407
2918
|
while (stack.length > 0) {
|
|
@@ -2410,7 +2921,7 @@ async function inspectStableIdCollisions(projectRoot) {
|
|
|
2410
2921
|
continue;
|
|
2411
2922
|
}
|
|
2412
2923
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2413
|
-
const abs =
|
|
2924
|
+
const abs = join6(dir, entry.name);
|
|
2414
2925
|
if (entry.isDirectory()) {
|
|
2415
2926
|
stack.push(abs);
|
|
2416
2927
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -2566,17 +3077,17 @@ function createMetaManuallyDivergedCheck(inspection) {
|
|
|
2566
3077
|
}
|
|
2567
3078
|
function inspectPreexistingRootFiles(projectRoot) {
|
|
2568
3079
|
const candidates = ["CLAUDE.md", "AGENTS.md"];
|
|
2569
|
-
const detected = candidates.filter((name) => existsSync4(
|
|
3080
|
+
const detected = candidates.filter((name) => existsSync4(join6(projectRoot, name)));
|
|
2570
3081
|
return { detected };
|
|
2571
3082
|
}
|
|
2572
3083
|
async function inspectFilesystemEditFallback(projectRoot) {
|
|
2573
|
-
const knowledgeRoot =
|
|
3084
|
+
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
2574
3085
|
if (!existsSync4(knowledgeRoot)) {
|
|
2575
3086
|
return { synthesized: 0, synthesizedStableIds: [] };
|
|
2576
3087
|
}
|
|
2577
3088
|
const canonicalIds = /* @__PURE__ */ new Set();
|
|
2578
3089
|
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
2579
|
-
const dir =
|
|
3090
|
+
const dir = join6(knowledgeRoot, typeDir);
|
|
2580
3091
|
if (!existsSync4(dir)) {
|
|
2581
3092
|
continue;
|
|
2582
3093
|
}
|
|
@@ -2778,12 +3289,12 @@ function extractKnowledgeFrontmatterCreatedAt(source) {
|
|
|
2778
3289
|
return Number.isFinite(parsed) ? parsed : null;
|
|
2779
3290
|
}
|
|
2780
3291
|
function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
|
|
2781
|
-
const knowledgeRoot =
|
|
3292
|
+
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
2782
3293
|
if (!existsSync4(knowledgeRoot)) {
|
|
2783
3294
|
return;
|
|
2784
3295
|
}
|
|
2785
3296
|
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
2786
|
-
const dir =
|
|
3297
|
+
const dir = join6(knowledgeRoot, typeDir);
|
|
2787
3298
|
if (!existsSync4(dir)) {
|
|
2788
3299
|
continue;
|
|
2789
3300
|
}
|
|
@@ -2802,10 +3313,10 @@ function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
|
|
|
2802
3313
|
continue;
|
|
2803
3314
|
}
|
|
2804
3315
|
const stableId = match[1];
|
|
2805
|
-
const absPath =
|
|
3316
|
+
const absPath = join6(dir, entry.name);
|
|
2806
3317
|
let source;
|
|
2807
3318
|
try {
|
|
2808
|
-
source =
|
|
3319
|
+
source = readFileSync2(absPath, "utf8");
|
|
2809
3320
|
} catch {
|
|
2810
3321
|
continue;
|
|
2811
3322
|
}
|
|
@@ -2818,7 +3329,7 @@ function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
|
|
|
2818
3329
|
let lastReferenceMs = Math.max(createdAt ?? 0, eventTs);
|
|
2819
3330
|
if (lastReferenceMs === 0) {
|
|
2820
3331
|
try {
|
|
2821
|
-
lastReferenceMs =
|
|
3332
|
+
lastReferenceMs = statSync4(absPath).mtimeMs;
|
|
2822
3333
|
} catch {
|
|
2823
3334
|
lastReferenceMs = 0;
|
|
2824
3335
|
}
|
|
@@ -2878,8 +3389,8 @@ async function inspectStaleArchive(projectRoot, now) {
|
|
|
2878
3389
|
return { candidates };
|
|
2879
3390
|
}
|
|
2880
3391
|
function* iteratePendingFiles(projectRoot, now) {
|
|
2881
|
-
const teamRoot =
|
|
2882
|
-
const personalRoot =
|
|
3392
|
+
const teamRoot = join6(projectRoot, ".fabric", "knowledge", "pending");
|
|
3393
|
+
const personalRoot = join6(resolvePersonalRootForPending(), ".fabric", "knowledge", "pending");
|
|
2883
3394
|
for (const [layer, root, displayPrefix] of [
|
|
2884
3395
|
["team", teamRoot, ".fabric/knowledge/pending"],
|
|
2885
3396
|
["personal", personalRoot, "~/.fabric/knowledge/pending"]
|
|
@@ -2894,7 +3405,7 @@ function* iteratePendingFiles(projectRoot, now) {
|
|
|
2894
3405
|
continue;
|
|
2895
3406
|
}
|
|
2896
3407
|
for (const typeDir of typeDirs) {
|
|
2897
|
-
const dir =
|
|
3408
|
+
const dir = join6(root, typeDir);
|
|
2898
3409
|
let entries;
|
|
2899
3410
|
try {
|
|
2900
3411
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -2905,17 +3416,17 @@ function* iteratePendingFiles(projectRoot, now) {
|
|
|
2905
3416
|
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
2906
3417
|
continue;
|
|
2907
3418
|
}
|
|
2908
|
-
const absPath =
|
|
3419
|
+
const absPath = join6(dir, entry.name);
|
|
2909
3420
|
let source = "";
|
|
2910
3421
|
try {
|
|
2911
|
-
source =
|
|
3422
|
+
source = readFileSync2(absPath, "utf8");
|
|
2912
3423
|
} catch {
|
|
2913
3424
|
continue;
|
|
2914
3425
|
}
|
|
2915
3426
|
const createdAt = extractKnowledgeFrontmatterCreatedAt(source);
|
|
2916
3427
|
let mtimeMs = 0;
|
|
2917
3428
|
try {
|
|
2918
|
-
mtimeMs =
|
|
3429
|
+
mtimeMs = statSync4(absPath).mtimeMs;
|
|
2919
3430
|
} catch {
|
|
2920
3431
|
mtimeMs = 0;
|
|
2921
3432
|
}
|
|
@@ -2949,7 +3460,7 @@ function* iteratePendingFiles(projectRoot, now) {
|
|
|
2949
3460
|
}
|
|
2950
3461
|
}
|
|
2951
3462
|
function resolvePersonalRootForPending() {
|
|
2952
|
-
return process.env.FABRIC_HOME ??
|
|
3463
|
+
return process.env.FABRIC_HOME ?? homedir3();
|
|
2953
3464
|
}
|
|
2954
3465
|
function inspectPendingOverdue(projectRoot, now) {
|
|
2955
3466
|
const candidates = [];
|
|
@@ -2980,7 +3491,7 @@ function inspectPendingAutoArchive(projectRoot, now) {
|
|
|
2980
3491
|
pending_path: visit.pending_path,
|
|
2981
3492
|
pending_path_abs: visit.pending_path_abs,
|
|
2982
3493
|
archived_to: archivedToRel,
|
|
2983
|
-
archived_to_abs:
|
|
3494
|
+
archived_to_abs: join6(projectRoot, archivedToRel),
|
|
2984
3495
|
age_days: visit.age_days
|
|
2985
3496
|
});
|
|
2986
3497
|
} else {
|
|
@@ -2989,7 +3500,7 @@ function inspectPendingAutoArchive(projectRoot, now) {
|
|
|
2989
3500
|
visit.type,
|
|
2990
3501
|
visit.filename
|
|
2991
3502
|
);
|
|
2992
|
-
const archivedToAbs =
|
|
3503
|
+
const archivedToAbs = join6(
|
|
2993
3504
|
resolvePersonalRootForPending(),
|
|
2994
3505
|
".fabric",
|
|
2995
3506
|
".archive",
|
|
@@ -3013,11 +3524,11 @@ function inspectPendingAutoArchive(projectRoot, now) {
|
|
|
3013
3524
|
}
|
|
3014
3525
|
function inspectUnderseeded(projectRoot) {
|
|
3015
3526
|
const threshold = readUnderseedThresholdFromConfig(projectRoot);
|
|
3016
|
-
const knowledgeRoot =
|
|
3527
|
+
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
3017
3528
|
let nodeCount = 0;
|
|
3018
3529
|
if (existsSync4(knowledgeRoot)) {
|
|
3019
3530
|
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
3020
|
-
const dir =
|
|
3531
|
+
const dir = join6(knowledgeRoot, typeDir);
|
|
3021
3532
|
if (!existsSync4(dir)) continue;
|
|
3022
3533
|
let entries;
|
|
3023
3534
|
try {
|
|
@@ -3039,7 +3550,7 @@ function inspectUnderseeded(projectRoot) {
|
|
|
3039
3550
|
};
|
|
3040
3551
|
}
|
|
3041
3552
|
function inspectSessionHintsStale(projectRoot, now) {
|
|
3042
|
-
const cacheDir =
|
|
3553
|
+
const cacheDir = join6(projectRoot, ".fabric", ".cache");
|
|
3043
3554
|
if (!existsSync4(cacheDir)) {
|
|
3044
3555
|
return { candidates: [] };
|
|
3045
3556
|
}
|
|
@@ -3054,10 +3565,10 @@ function inspectSessionHintsStale(projectRoot, now) {
|
|
|
3054
3565
|
if (!entry.isFile()) continue;
|
|
3055
3566
|
if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
|
|
3056
3567
|
if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
|
|
3057
|
-
const absPath =
|
|
3568
|
+
const absPath = join6(cacheDir, entry.name);
|
|
3058
3569
|
let mtimeMs = 0;
|
|
3059
3570
|
try {
|
|
3060
|
-
mtimeMs =
|
|
3571
|
+
mtimeMs = statSync4(absPath).mtimeMs;
|
|
3061
3572
|
} catch {
|
|
3062
3573
|
continue;
|
|
3063
3574
|
}
|
|
@@ -3084,11 +3595,11 @@ function inspectNarrowTooFew(projectRoot, now) {
|
|
|
3084
3595
|
const structuralFlagged = total >= NARROW_MIN_TOTAL && narrowRatio < NARROW_RATIO_THRESHOLD;
|
|
3085
3596
|
const windowStartMs = now - SILENCE_WINDOW_DAYS * MS_PER_DAY;
|
|
3086
3597
|
const editFires = readCounterTimestamps(
|
|
3087
|
-
|
|
3598
|
+
join6(projectRoot, EDIT_COUNTER_FILE_REL),
|
|
3088
3599
|
windowStartMs
|
|
3089
3600
|
);
|
|
3090
3601
|
const silenceFires = readCounterTimestamps(
|
|
3091
|
-
|
|
3602
|
+
join6(projectRoot, HINT_SILENCE_COUNTER_FILE_REL),
|
|
3092
3603
|
windowStartMs
|
|
3093
3604
|
);
|
|
3094
3605
|
const telemetrySkipped = editFires === 0;
|
|
@@ -3110,7 +3621,7 @@ function readCounterTimestamps(absPath, windowStartMs) {
|
|
|
3110
3621
|
if (!existsSync4(absPath)) return 0;
|
|
3111
3622
|
let raw;
|
|
3112
3623
|
try {
|
|
3113
|
-
raw =
|
|
3624
|
+
raw = readFileSync2(absPath, "utf8");
|
|
3114
3625
|
} catch {
|
|
3115
3626
|
return 0;
|
|
3116
3627
|
}
|
|
@@ -3126,10 +3637,10 @@ function readCounterTimestamps(absPath, windowStartMs) {
|
|
|
3126
3637
|
return count;
|
|
3127
3638
|
}
|
|
3128
3639
|
function readUnderseedThresholdFromConfig(projectRoot) {
|
|
3129
|
-
const configPath =
|
|
3640
|
+
const configPath = join6(projectRoot, ".fabric", "fabric-config.json");
|
|
3130
3641
|
if (!existsSync4(configPath)) return DEFAULT_UNDERSEED_NODE_THRESHOLD;
|
|
3131
3642
|
try {
|
|
3132
|
-
const raw =
|
|
3643
|
+
const raw = readFileSync2(configPath, "utf8");
|
|
3133
3644
|
const parsed = JSON.parse(raw);
|
|
3134
3645
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3135
3646
|
const v = parsed.underseed_node_threshold;
|
|
@@ -3259,11 +3770,11 @@ function extractKnowledgeFrontmatterRelevancePaths(source) {
|
|
|
3259
3770
|
}
|
|
3260
3771
|
function* iterateRelevanceFrontmatter(projectRoot) {
|
|
3261
3772
|
for (const visit of iterateCanonicalFilenames(projectRoot)) {
|
|
3262
|
-
const layerRoot = visit.layer === "team" ?
|
|
3263
|
-
const absPath =
|
|
3773
|
+
const layerRoot = visit.layer === "team" ? join6(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
|
|
3774
|
+
const absPath = join6(layerRoot, visit.type, visit.filename);
|
|
3264
3775
|
let source;
|
|
3265
3776
|
try {
|
|
3266
|
-
source =
|
|
3777
|
+
source = readFileSync2(absPath, "utf8");
|
|
3267
3778
|
} catch {
|
|
3268
3779
|
continue;
|
|
3269
3780
|
}
|
|
@@ -3330,7 +3841,7 @@ function collectWorkspacePathsForGlobMatch(projectRoot) {
|
|
|
3330
3841
|
}
|
|
3331
3842
|
let rootStat;
|
|
3332
3843
|
try {
|
|
3333
|
-
rootStat =
|
|
3844
|
+
rootStat = statSync4(projectRoot);
|
|
3334
3845
|
} catch {
|
|
3335
3846
|
return [];
|
|
3336
3847
|
}
|
|
@@ -3349,7 +3860,7 @@ function collectWorkspacePathsForGlobMatch(projectRoot) {
|
|
|
3349
3860
|
continue;
|
|
3350
3861
|
}
|
|
3351
3862
|
for (const entry of entries) {
|
|
3352
|
-
const abs =
|
|
3863
|
+
const abs = join6(current, entry.name);
|
|
3353
3864
|
const rel = normalizePath(abs.slice(projectRoot.length + 1));
|
|
3354
3865
|
if (rel.length === 0) continue;
|
|
3355
3866
|
if (entry.isDirectory()) {
|
|
@@ -3489,8 +4000,8 @@ function inspectRelevanceFieldsMissing(projectRoot) {
|
|
|
3489
4000
|
const candidates = [];
|
|
3490
4001
|
let scannedCount = 0;
|
|
3491
4002
|
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
3492
|
-
const teamRoot =
|
|
3493
|
-
const personalRoot =
|
|
4003
|
+
const teamRoot = join6(projectRoot, ".fabric", "knowledge", "pending");
|
|
4004
|
+
const personalRoot = join6(
|
|
3494
4005
|
resolvePersonalRootForPending(),
|
|
3495
4006
|
".fabric",
|
|
3496
4007
|
"knowledge",
|
|
@@ -3510,7 +4021,7 @@ function inspectRelevanceFieldsMissing(projectRoot) {
|
|
|
3510
4021
|
continue;
|
|
3511
4022
|
}
|
|
3512
4023
|
for (const typeDir of typeDirs) {
|
|
3513
|
-
const dir =
|
|
4024
|
+
const dir = join6(root, typeDir);
|
|
3514
4025
|
let entries;
|
|
3515
4026
|
try {
|
|
3516
4027
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -3521,10 +4032,10 @@ function inspectRelevanceFieldsMissing(projectRoot) {
|
|
|
3521
4032
|
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
3522
4033
|
continue;
|
|
3523
4034
|
}
|
|
3524
|
-
const absPath =
|
|
4035
|
+
const absPath = join6(dir, entry.name);
|
|
3525
4036
|
let source;
|
|
3526
4037
|
try {
|
|
3527
|
-
source =
|
|
4038
|
+
source = readFileSync2(absPath, "utf8");
|
|
3528
4039
|
} catch {
|
|
3529
4040
|
continue;
|
|
3530
4041
|
}
|
|
@@ -3608,7 +4119,7 @@ async function applyRelevanceFieldsMissing(candidate) {
|
|
|
3608
4119
|
error: "fields already present at write time (no diff)"
|
|
3609
4120
|
};
|
|
3610
4121
|
}
|
|
3611
|
-
await
|
|
4122
|
+
await atomicWriteText4(candidate.pending_path_abs, rewritten);
|
|
3612
4123
|
return {
|
|
3613
4124
|
kind: "knowledge_relevance_fields_missing",
|
|
3614
4125
|
path: candidate.pending_path,
|
|
@@ -3652,7 +4163,7 @@ var SKILL_QUOTED_VALUE_LEADS = /* @__PURE__ */ new Set(['"', "'", "[", "{", ">",
|
|
|
3652
4163
|
function inspectSkillMdYamlInvalid(projectRoot) {
|
|
3653
4164
|
const candidates = [];
|
|
3654
4165
|
for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
|
|
3655
|
-
const rootAbs =
|
|
4166
|
+
const rootAbs = join6(projectRoot, rootRel);
|
|
3656
4167
|
if (!existsSync4(rootAbs)) continue;
|
|
3657
4168
|
let dirEntries;
|
|
3658
4169
|
try {
|
|
@@ -3662,11 +4173,11 @@ function inspectSkillMdYamlInvalid(projectRoot) {
|
|
|
3662
4173
|
}
|
|
3663
4174
|
for (const dirEntry of dirEntries) {
|
|
3664
4175
|
if (!dirEntry.isDirectory()) continue;
|
|
3665
|
-
const skillFile =
|
|
4176
|
+
const skillFile = join6(rootAbs, dirEntry.name, "SKILL.md");
|
|
3666
4177
|
if (!existsSync4(skillFile)) continue;
|
|
3667
4178
|
let raw;
|
|
3668
4179
|
try {
|
|
3669
|
-
raw =
|
|
4180
|
+
raw = readFileSync2(skillFile, "utf8");
|
|
3670
4181
|
} catch {
|
|
3671
4182
|
continue;
|
|
3672
4183
|
}
|
|
@@ -3766,8 +4277,8 @@ function createNarrowTooFewCheck(inspection) {
|
|
|
3766
4277
|
);
|
|
3767
4278
|
}
|
|
3768
4279
|
function resolvePersonalKnowledgeRoot() {
|
|
3769
|
-
const home = process.env.FABRIC_HOME ??
|
|
3770
|
-
return
|
|
4280
|
+
const home = process.env.FABRIC_HOME ?? homedir3();
|
|
4281
|
+
return join6(home, ".fabric", "knowledge");
|
|
3771
4282
|
}
|
|
3772
4283
|
function parseStableIdFromCanonicalFilename(filename) {
|
|
3773
4284
|
const match = CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(filename);
|
|
@@ -3787,7 +4298,7 @@ function parseStableIdFromCanonicalFilename(filename) {
|
|
|
3787
4298
|
};
|
|
3788
4299
|
}
|
|
3789
4300
|
function* iterateCanonicalFilenames(projectRoot) {
|
|
3790
|
-
const teamRoot =
|
|
4301
|
+
const teamRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
3791
4302
|
const personalRoot = resolvePersonalKnowledgeRoot();
|
|
3792
4303
|
for (const [layer, root, displayPrefix] of [
|
|
3793
4304
|
["team", teamRoot, ".fabric/knowledge"],
|
|
@@ -3797,7 +4308,7 @@ function* iterateCanonicalFilenames(projectRoot) {
|
|
|
3797
4308
|
continue;
|
|
3798
4309
|
}
|
|
3799
4310
|
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
3800
|
-
const dir =
|
|
4311
|
+
const dir = join6(root, typeDir);
|
|
3801
4312
|
if (!existsSync4(dir)) {
|
|
3802
4313
|
continue;
|
|
3803
4314
|
}
|
|
@@ -3953,14 +4464,133 @@ function createIndexDriftCheck(inspection) {
|
|
|
3953
4464
|
"Run `fab doctor --apply-lint` (rc.4 TASK-003) to bump agents.meta.json counters to max_observed + 1."
|
|
3954
4465
|
);
|
|
3955
4466
|
}
|
|
4467
|
+
async function migrateBootstrapMarkers(projectRoot) {
|
|
4468
|
+
const paths = [];
|
|
4469
|
+
const countPerPath = {};
|
|
4470
|
+
for (const rel of BOOTSTRAP_MARKER_MIGRATION_TARGETS) {
|
|
4471
|
+
const abs = join6(projectRoot, rel);
|
|
4472
|
+
if (!existsSync4(abs)) {
|
|
4473
|
+
continue;
|
|
4474
|
+
}
|
|
4475
|
+
let original;
|
|
4476
|
+
try {
|
|
4477
|
+
original = await readFile5(abs, "utf8");
|
|
4478
|
+
} catch {
|
|
4479
|
+
continue;
|
|
4480
|
+
}
|
|
4481
|
+
const beginMatches = original.match(/<!-- fabric:knowledge-base:begin -->/g);
|
|
4482
|
+
const endMatches = original.match(/<!-- fabric:knowledge-base:end -->/g);
|
|
4483
|
+
const replacedCount = (beginMatches?.length ?? 0) + (endMatches?.length ?? 0);
|
|
4484
|
+
if (replacedCount === 0) {
|
|
4485
|
+
continue;
|
|
4486
|
+
}
|
|
4487
|
+
const rewritten = original.replace(/<!-- fabric:knowledge-base:begin -->/g, BOOTSTRAP_MARKER_BEGIN).replace(/<!-- fabric:knowledge-base:end -->/g, BOOTSTRAP_MARKER_END);
|
|
4488
|
+
if (rewritten === original) {
|
|
4489
|
+
continue;
|
|
4490
|
+
}
|
|
4491
|
+
await atomicWriteText4(abs, rewritten);
|
|
4492
|
+
paths.push(abs);
|
|
4493
|
+
countPerPath[abs] = replacedCount;
|
|
4494
|
+
}
|
|
4495
|
+
return { paths, countPerPath };
|
|
4496
|
+
}
|
|
4497
|
+
async function rewriteThreeEndManagedBlocks(projectRoot) {
|
|
4498
|
+
const snapshotPath = join6(projectRoot, ".fabric", "AGENTS.md");
|
|
4499
|
+
if (!existsSync4(snapshotPath)) {
|
|
4500
|
+
return;
|
|
4501
|
+
}
|
|
4502
|
+
let snapshot;
|
|
4503
|
+
try {
|
|
4504
|
+
snapshot = await readFile5(snapshotPath, "utf8");
|
|
4505
|
+
} catch {
|
|
4506
|
+
return;
|
|
4507
|
+
}
|
|
4508
|
+
const projectRulesPath = join6(projectRoot, ".fabric", "project-rules.md");
|
|
4509
|
+
const hasProjectRules = existsSync4(projectRulesPath);
|
|
4510
|
+
let expectedBody = snapshot;
|
|
4511
|
+
if (hasProjectRules) {
|
|
4512
|
+
try {
|
|
4513
|
+
const projectRules = await readFile5(projectRulesPath, "utf8");
|
|
4514
|
+
expectedBody = `${snapshot}
|
|
4515
|
+
---
|
|
4516
|
+
${projectRules}`;
|
|
4517
|
+
} catch {
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
const managedBlock = `${BOOTSTRAP_MARKER_BEGIN}
|
|
4521
|
+
${expectedBody}
|
|
4522
|
+
${BOOTSTRAP_MARKER_END}`;
|
|
4523
|
+
const blockTargets = [
|
|
4524
|
+
join6(projectRoot, "AGENTS.md"),
|
|
4525
|
+
join6(projectRoot, ".cursor", "rules", "fabric-bootstrap.mdc")
|
|
4526
|
+
];
|
|
4527
|
+
for (const abs of blockTargets) {
|
|
4528
|
+
if (!existsSync4(abs)) {
|
|
4529
|
+
continue;
|
|
4530
|
+
}
|
|
4531
|
+
let existing;
|
|
4532
|
+
try {
|
|
4533
|
+
existing = await readFile5(abs, "utf8");
|
|
4534
|
+
} catch {
|
|
4535
|
+
continue;
|
|
4536
|
+
}
|
|
4537
|
+
let next;
|
|
4538
|
+
const match = existing.match(BOOTSTRAP_REGEX);
|
|
4539
|
+
if (match !== null) {
|
|
4540
|
+
const before = existing.slice(0, match.index ?? 0);
|
|
4541
|
+
const after = existing.slice((match.index ?? 0) + match[0].length);
|
|
4542
|
+
const stripped = `${before}${after.replace(/^\r?\n/, "")}`;
|
|
4543
|
+
const trailingNewline = stripped.length === 0 || stripped.endsWith("\n") ? "" : "\n";
|
|
4544
|
+
next = `${stripped}${trailingNewline}
|
|
4545
|
+
${managedBlock}
|
|
4546
|
+
`;
|
|
4547
|
+
} else {
|
|
4548
|
+
const trailingNewline = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
|
|
4549
|
+
next = `${existing}${trailingNewline}
|
|
4550
|
+
${managedBlock}
|
|
4551
|
+
`;
|
|
4552
|
+
}
|
|
4553
|
+
if (next === existing) {
|
|
4554
|
+
continue;
|
|
4555
|
+
}
|
|
4556
|
+
await atomicWriteText4(abs, next);
|
|
4557
|
+
}
|
|
4558
|
+
const claudeMdPath = join6(projectRoot, "CLAUDE.md");
|
|
4559
|
+
if (existsSync4(claudeMdPath)) {
|
|
4560
|
+
let claudeContent;
|
|
4561
|
+
try {
|
|
4562
|
+
claudeContent = await readFile5(claudeMdPath, "utf8");
|
|
4563
|
+
} catch {
|
|
4564
|
+
return;
|
|
4565
|
+
}
|
|
4566
|
+
const lines = claudeContent.split(/\r?\n/u);
|
|
4567
|
+
let updated = claudeContent;
|
|
4568
|
+
const ensureLine = (line) => {
|
|
4569
|
+
if (lines.some((existingLine) => existingLine.trim() === line)) {
|
|
4570
|
+
return;
|
|
4571
|
+
}
|
|
4572
|
+
const trailingNewline = updated.length === 0 || updated.endsWith("\n") ? "" : "\n";
|
|
4573
|
+
updated = `${updated}${trailingNewline}${line}
|
|
4574
|
+
`;
|
|
4575
|
+
lines.push(line);
|
|
4576
|
+
};
|
|
4577
|
+
ensureLine("@.fabric/AGENTS.md");
|
|
4578
|
+
if (hasProjectRules) {
|
|
4579
|
+
ensureLine("@.fabric/project-rules.md");
|
|
4580
|
+
}
|
|
4581
|
+
if (updated !== claudeContent) {
|
|
4582
|
+
await atomicWriteText4(claudeMdPath, updated);
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
3956
4586
|
async function fixMcpConfigInWrongFile(projectRoot) {
|
|
3957
|
-
const settingsPath =
|
|
4587
|
+
const settingsPath = join6(projectRoot, ".claude", "settings.json");
|
|
3958
4588
|
if (!existsSync4(settingsPath)) {
|
|
3959
4589
|
return;
|
|
3960
4590
|
}
|
|
3961
4591
|
let settings;
|
|
3962
4592
|
try {
|
|
3963
|
-
const parsed = JSON.parse(
|
|
4593
|
+
const parsed = JSON.parse(readFileSync2(settingsPath, "utf8"));
|
|
3964
4594
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3965
4595
|
return;
|
|
3966
4596
|
}
|
|
@@ -3987,12 +4617,12 @@ async function fixMcpConfigInWrongFile(projectRoot) {
|
|
|
3987
4617
|
});
|
|
3988
4618
|
}
|
|
3989
4619
|
async function ensureKnowledgeSubdirs(projectRoot) {
|
|
3990
|
-
for (const sub of
|
|
3991
|
-
await
|
|
4620
|
+
for (const sub of KNOWLEDGE_SUBDIRS3) {
|
|
4621
|
+
await mkdir4(join6(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
|
|
3992
4622
|
}
|
|
3993
4623
|
}
|
|
3994
4624
|
async function fixCounterDesync(projectRoot) {
|
|
3995
|
-
const metaPath =
|
|
4625
|
+
const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
|
|
3996
4626
|
if (!existsSync4(metaPath)) {
|
|
3997
4627
|
return;
|
|
3998
4628
|
}
|
|
@@ -4026,6 +4656,275 @@ async function ensureEventLedger(projectRoot) {
|
|
|
4026
4656
|
await ensureParentDirectory(path);
|
|
4027
4657
|
await writeFile2(path, "", { encoding: "utf8", flag: "a" });
|
|
4028
4658
|
}
|
|
4659
|
+
var CITE_POLICY_VERSION = "2.0.0-rc.20";
|
|
4660
|
+
async function ensureCitePolicyActivatedMarker(projectRoot) {
|
|
4661
|
+
let existing;
|
|
4662
|
+
try {
|
|
4663
|
+
const { events } = await readEventLedger(projectRoot, { event_type: "cite_policy_activated" });
|
|
4664
|
+
if (events.length > 0) {
|
|
4665
|
+
existing = events[0];
|
|
4666
|
+
}
|
|
4667
|
+
} catch {
|
|
4668
|
+
return { marker_ts: 0, emitted_now: false };
|
|
4669
|
+
}
|
|
4670
|
+
if (existing !== void 0) {
|
|
4671
|
+
return { marker_ts: existing.ts, emitted_now: false };
|
|
4672
|
+
}
|
|
4673
|
+
try {
|
|
4674
|
+
const stored = await appendEventLedgerEvent(projectRoot, {
|
|
4675
|
+
event_type: "cite_policy_activated",
|
|
4676
|
+
policy_version: CITE_POLICY_VERSION,
|
|
4677
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4678
|
+
});
|
|
4679
|
+
return { marker_ts: stored.ts, emitted_now: true };
|
|
4680
|
+
} catch {
|
|
4681
|
+
return { marker_ts: 0, emitted_now: false };
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
function categorizeCiteTag(tag) {
|
|
4685
|
+
if (tag === "planned" || tag === "recalled" || tag === "chained-from" || tag === "none") {
|
|
4686
|
+
return { category: tag };
|
|
4687
|
+
}
|
|
4688
|
+
if (tag === "dismissed") {
|
|
4689
|
+
return { category: "dismissed", reason: "unspecified" };
|
|
4690
|
+
}
|
|
4691
|
+
if (tag.startsWith("dismissed:")) {
|
|
4692
|
+
const remainder = tag.slice("dismissed:".length);
|
|
4693
|
+
if (remainder.startsWith("other:")) {
|
|
4694
|
+
return { category: "dismissed", reason: remainder.slice("other:".length) || "other" };
|
|
4695
|
+
}
|
|
4696
|
+
return { category: "dismissed", reason: remainder || "unspecified" };
|
|
4697
|
+
}
|
|
4698
|
+
return { category: "none" };
|
|
4699
|
+
}
|
|
4700
|
+
function matchesRelevancePath(editPath, relevancePaths) {
|
|
4701
|
+
if (relevancePaths.length === 0) {
|
|
4702
|
+
return false;
|
|
4703
|
+
}
|
|
4704
|
+
const normalized = normalizePath(editPath);
|
|
4705
|
+
for (const glob of relevancePaths) {
|
|
4706
|
+
if (minimatch(normalized, glob, { dot: true, matchBase: false })) {
|
|
4707
|
+
return true;
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
return false;
|
|
4711
|
+
}
|
|
4712
|
+
async function runDoctorCiteCoverage(projectRoot, options) {
|
|
4713
|
+
const marker = await ensureCitePolicyActivatedMarker(projectRoot);
|
|
4714
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4715
|
+
const zeroMetrics = {
|
|
4716
|
+
edits_touched: 0,
|
|
4717
|
+
qualifying_cites: 0,
|
|
4718
|
+
recalled_unverified: 0,
|
|
4719
|
+
expected_but_missed: 0,
|
|
4720
|
+
total_turns: 0
|
|
4721
|
+
};
|
|
4722
|
+
if (marker.marker_ts === 0) {
|
|
4723
|
+
return {
|
|
4724
|
+
status: "skipped",
|
|
4725
|
+
marker_ts: 0,
|
|
4726
|
+
marker_emitted_now: false,
|
|
4727
|
+
since_ts: options.since,
|
|
4728
|
+
client_filter: options.client,
|
|
4729
|
+
metrics: zeroMetrics,
|
|
4730
|
+
generated_at: generatedAt
|
|
4731
|
+
};
|
|
4732
|
+
}
|
|
4733
|
+
const effectiveSince = Math.max(marker.marker_ts, options.since);
|
|
4734
|
+
let ledgerEvents = [];
|
|
4735
|
+
try {
|
|
4736
|
+
const result = await readEventLedger(projectRoot, { since: effectiveSince });
|
|
4737
|
+
ledgerEvents = result.events;
|
|
4738
|
+
} catch {
|
|
4739
|
+
return {
|
|
4740
|
+
status: "ok",
|
|
4741
|
+
marker_ts: marker.marker_ts,
|
|
4742
|
+
marker_emitted_now: marker.emitted_now,
|
|
4743
|
+
since_ts: effectiveSince,
|
|
4744
|
+
client_filter: options.client,
|
|
4745
|
+
metrics: zeroMetrics,
|
|
4746
|
+
generated_at: generatedAt
|
|
4747
|
+
};
|
|
4748
|
+
}
|
|
4749
|
+
const assistantTurns = [];
|
|
4750
|
+
const editEvents = [];
|
|
4751
|
+
const fetchEvents = [];
|
|
4752
|
+
for (const event of ledgerEvents) {
|
|
4753
|
+
switch (event.event_type) {
|
|
4754
|
+
case "assistant_turn_observed":
|
|
4755
|
+
assistantTurns.push(event);
|
|
4756
|
+
break;
|
|
4757
|
+
case "edit_intent_checked":
|
|
4758
|
+
editEvents.push(event);
|
|
4759
|
+
break;
|
|
4760
|
+
case "knowledge_sections_fetched":
|
|
4761
|
+
fetchEvents.push(event);
|
|
4762
|
+
break;
|
|
4763
|
+
default:
|
|
4764
|
+
break;
|
|
4765
|
+
}
|
|
4766
|
+
}
|
|
4767
|
+
const filteredTurns = options.client === "all" ? assistantTurns : assistantTurns.filter((t) => t.client === options.client);
|
|
4768
|
+
let clientSessionIds = null;
|
|
4769
|
+
if (options.client !== "all") {
|
|
4770
|
+
clientSessionIds = /* @__PURE__ */ new Set();
|
|
4771
|
+
for (const turn of assistantTurns) {
|
|
4772
|
+
if (turn.client === options.client) {
|
|
4773
|
+
const sid = turn.session_id;
|
|
4774
|
+
if (typeof sid === "string" && sid.length > 0) {
|
|
4775
|
+
clientSessionIds.add(sid);
|
|
4776
|
+
}
|
|
4777
|
+
}
|
|
4778
|
+
}
|
|
4779
|
+
}
|
|
4780
|
+
const kbIndex = /* @__PURE__ */ new Map();
|
|
4781
|
+
try {
|
|
4782
|
+
const meta = await readAgentsMeta(projectRoot);
|
|
4783
|
+
for (const node of Object.values(meta.nodes)) {
|
|
4784
|
+
const stableId = node.stable_id;
|
|
4785
|
+
if (typeof stableId !== "string" || stableId.length === 0) continue;
|
|
4786
|
+
const description = node.description;
|
|
4787
|
+
if (description === void 0) continue;
|
|
4788
|
+
const paths = description.relevance_paths ?? [];
|
|
4789
|
+
const scope = description.relevance_scope ?? "broad";
|
|
4790
|
+
kbIndex.set(stableId, {
|
|
4791
|
+
relevance_paths: paths,
|
|
4792
|
+
// A broad entry with no paths is the safe default. A narrow entry must
|
|
4793
|
+
// carry at least one path; an empty-paths narrow is treated as broad.
|
|
4794
|
+
relevance_scope: scope === "narrow" && paths.length > 0 ? "narrow" : "broad"
|
|
4795
|
+
});
|
|
4796
|
+
}
|
|
4797
|
+
} catch {
|
|
4798
|
+
}
|
|
4799
|
+
const fetchesBySession = /* @__PURE__ */ new Map();
|
|
4800
|
+
for (const fetch of fetchEvents) {
|
|
4801
|
+
const sid = fetch.session_id;
|
|
4802
|
+
if (typeof sid !== "string" || sid.length === 0) continue;
|
|
4803
|
+
const list = fetchesBySession.get(sid) ?? [];
|
|
4804
|
+
list.push(fetch.ts);
|
|
4805
|
+
fetchesBySession.set(sid, list);
|
|
4806
|
+
}
|
|
4807
|
+
for (const list of fetchesBySession.values()) {
|
|
4808
|
+
list.sort((a, b) => a - b);
|
|
4809
|
+
}
|
|
4810
|
+
const RECALL_WINDOW_MS = 6e4;
|
|
4811
|
+
const isRecallVerified = (turn) => {
|
|
4812
|
+
const sid = turn.session_id;
|
|
4813
|
+
if (typeof sid !== "string" || sid.length === 0) return false;
|
|
4814
|
+
const fetches = fetchesBySession.get(sid);
|
|
4815
|
+
if (fetches === void 0 || fetches.length === 0) return false;
|
|
4816
|
+
for (const ft of fetches) {
|
|
4817
|
+
if (Math.abs(ft - turn.ts) <= RECALL_WINDOW_MS) return true;
|
|
4818
|
+
}
|
|
4819
|
+
return false;
|
|
4820
|
+
};
|
|
4821
|
+
const dismissedHistogram = {};
|
|
4822
|
+
const perClientAccum = /* @__PURE__ */ new Map();
|
|
4823
|
+
const emptyMetrics = () => ({
|
|
4824
|
+
edits_touched: 0,
|
|
4825
|
+
qualifying_cites: 0,
|
|
4826
|
+
recalled_unverified: 0,
|
|
4827
|
+
expected_but_missed: 0,
|
|
4828
|
+
total_turns: 0
|
|
4829
|
+
});
|
|
4830
|
+
const bumpClient = (client, mut) => {
|
|
4831
|
+
if (typeof client !== "string" || client.length === 0) return;
|
|
4832
|
+
const existing = perClientAccum.get(client) ?? emptyMetrics();
|
|
4833
|
+
mut(existing);
|
|
4834
|
+
perClientAccum.set(client, existing);
|
|
4835
|
+
};
|
|
4836
|
+
const sessionCitedKbs = /* @__PURE__ */ new Map();
|
|
4837
|
+
let totalTurns = 0;
|
|
4838
|
+
let qualifyingCites = 0;
|
|
4839
|
+
let recalledUnverified = 0;
|
|
4840
|
+
for (const turn of filteredTurns) {
|
|
4841
|
+
totalTurns += 1;
|
|
4842
|
+
bumpClient(turn.client, (m) => {
|
|
4843
|
+
m.total_turns += 1;
|
|
4844
|
+
});
|
|
4845
|
+
const sid = turn.session_id;
|
|
4846
|
+
if (typeof sid === "string" && sid.length > 0) {
|
|
4847
|
+
const set = sessionCitedKbs.get(sid) ?? /* @__PURE__ */ new Set();
|
|
4848
|
+
for (const id of turn.cite_ids) {
|
|
4849
|
+
set.add(id);
|
|
4850
|
+
}
|
|
4851
|
+
sessionCitedKbs.set(sid, set);
|
|
4852
|
+
}
|
|
4853
|
+
let turnHadRecalled = false;
|
|
4854
|
+
for (const tag of turn.cite_tags) {
|
|
4855
|
+
const { category, reason } = categorizeCiteTag(tag);
|
|
4856
|
+
switch (category) {
|
|
4857
|
+
case "planned":
|
|
4858
|
+
case "recalled":
|
|
4859
|
+
case "chained-from":
|
|
4860
|
+
qualifyingCites += 1;
|
|
4861
|
+
bumpClient(turn.client, (m) => {
|
|
4862
|
+
m.qualifying_cites += 1;
|
|
4863
|
+
});
|
|
4864
|
+
if (category === "recalled") turnHadRecalled = true;
|
|
4865
|
+
break;
|
|
4866
|
+
case "dismissed": {
|
|
4867
|
+
const key = reason ?? "unspecified";
|
|
4868
|
+
dismissedHistogram[key] = (dismissedHistogram[key] ?? 0) + 1;
|
|
4869
|
+
break;
|
|
4870
|
+
}
|
|
4871
|
+
case "none":
|
|
4872
|
+
default:
|
|
4873
|
+
break;
|
|
4874
|
+
}
|
|
4875
|
+
}
|
|
4876
|
+
if (turnHadRecalled && !isRecallVerified(turn)) {
|
|
4877
|
+
recalledUnverified += 1;
|
|
4878
|
+
bumpClient(turn.client, (m) => {
|
|
4879
|
+
m.recalled_unverified += 1;
|
|
4880
|
+
});
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
let editsTouched = 0;
|
|
4884
|
+
let expectedButMissed = 0;
|
|
4885
|
+
for (const edit of editEvents) {
|
|
4886
|
+
const sid = edit.session_id;
|
|
4887
|
+
if (clientSessionIds !== null) {
|
|
4888
|
+
if (typeof sid !== "string" || sid.length === 0) continue;
|
|
4889
|
+
if (!clientSessionIds.has(sid)) continue;
|
|
4890
|
+
}
|
|
4891
|
+
editsTouched += 1;
|
|
4892
|
+
if (typeof sid !== "string" || sid.length === 0) continue;
|
|
4893
|
+
const citedSet = sessionCitedKbs.get(sid) ?? /* @__PURE__ */ new Set();
|
|
4894
|
+
for (const [kbId, kb] of kbIndex) {
|
|
4895
|
+
if (kb.relevance_scope !== "narrow") continue;
|
|
4896
|
+
if (!matchesRelevancePath(edit.path, kb.relevance_paths)) continue;
|
|
4897
|
+
if (!citedSet.has(kbId)) {
|
|
4898
|
+
expectedButMissed += 1;
|
|
4899
|
+
}
|
|
4900
|
+
}
|
|
4901
|
+
}
|
|
4902
|
+
const metrics = {
|
|
4903
|
+
edits_touched: editsTouched,
|
|
4904
|
+
qualifying_cites: qualifyingCites,
|
|
4905
|
+
recalled_unverified: recalledUnverified,
|
|
4906
|
+
expected_but_missed: expectedButMissed,
|
|
4907
|
+
total_turns: totalTurns
|
|
4908
|
+
};
|
|
4909
|
+
let perClient;
|
|
4910
|
+
if (options.client === "all" && perClientAccum.size > 0) {
|
|
4911
|
+
perClient = {};
|
|
4912
|
+
for (const [client, m] of perClientAccum) {
|
|
4913
|
+
perClient[client] = m;
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
return {
|
|
4917
|
+
status: "ok",
|
|
4918
|
+
marker_ts: marker.marker_ts,
|
|
4919
|
+
marker_emitted_now: marker.emitted_now,
|
|
4920
|
+
since_ts: effectiveSince,
|
|
4921
|
+
client_filter: options.client,
|
|
4922
|
+
metrics,
|
|
4923
|
+
...perClient !== void 0 ? { per_client: perClient } : {},
|
|
4924
|
+
...Object.keys(dismissedHistogram).length > 0 ? { dismissed_reason_histogram: dismissedHistogram } : {},
|
|
4925
|
+
generated_at: generatedAt
|
|
4926
|
+
};
|
|
4927
|
+
}
|
|
4029
4928
|
function createFixMessage(fixed, report) {
|
|
4030
4929
|
const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
|
|
4031
4930
|
const manualText = report.manual_errors.length === 0 ? "No manual errors remain." : `${report.manual_errors.length} manual error${report.manual_errors.length === 1 ? "" : "s"} remain.`;
|
|
@@ -4046,7 +4945,7 @@ function normalizePath(path) {
|
|
|
4046
4945
|
return posix.normalize(path.split("\\").join("/"));
|
|
4047
4946
|
}
|
|
4048
4947
|
function collectEntryPoints(root) {
|
|
4049
|
-
if (!existsSync4(root) || !
|
|
4948
|
+
if (!existsSync4(root) || !statSync4(root).isDirectory()) {
|
|
4050
4949
|
return [];
|
|
4051
4950
|
}
|
|
4052
4951
|
const entries = [];
|
|
@@ -4057,7 +4956,7 @@ function collectEntryPoints(root) {
|
|
|
4057
4956
|
continue;
|
|
4058
4957
|
}
|
|
4059
4958
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
4060
|
-
const absolutePath =
|
|
4959
|
+
const absolutePath = join6(current, entry.name);
|
|
4061
4960
|
const relativePath = normalizePath(absolutePath.slice(root.length + 1));
|
|
4062
4961
|
if (relativePath.length === 0) {
|
|
4063
4962
|
continue;
|
|
@@ -4114,9 +5013,109 @@ function isMissingFileError(error) {
|
|
|
4114
5013
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
4115
5014
|
}
|
|
4116
5015
|
|
|
5016
|
+
// src/services/load-active-meta.ts
|
|
5017
|
+
async function loadActiveMeta(projectRoot, opts = {}) {
|
|
5018
|
+
const onDisk = await readAgentsMeta(projectRoot);
|
|
5019
|
+
const previousRevisionHash = onDisk.revision;
|
|
5020
|
+
const derived = await buildKnowledgeMeta(projectRoot);
|
|
5021
|
+
if (derived.meta.revision === previousRevisionHash) {
|
|
5022
|
+
return {
|
|
5023
|
+
meta: onDisk,
|
|
5024
|
+
auto_healed: false,
|
|
5025
|
+
previous_revision_hash: previousRevisionHash,
|
|
5026
|
+
revision_hash: previousRevisionHash
|
|
5027
|
+
};
|
|
5028
|
+
}
|
|
5029
|
+
const written = await writeKnowledgeMeta(projectRoot, { source: "doctor_fix" });
|
|
5030
|
+
contextCache.invalidate("meta_write", projectRoot);
|
|
5031
|
+
await emitAutoHealEventBestEffort(projectRoot, {
|
|
5032
|
+
previous_revision_hash: previousRevisionHash,
|
|
5033
|
+
revision_hash: written.meta.revision,
|
|
5034
|
+
caller: opts.caller
|
|
5035
|
+
});
|
|
5036
|
+
return {
|
|
5037
|
+
meta: written.meta,
|
|
5038
|
+
auto_healed: true,
|
|
5039
|
+
previous_revision_hash: previousRevisionHash,
|
|
5040
|
+
revision_hash: written.meta.revision
|
|
5041
|
+
};
|
|
5042
|
+
}
|
|
5043
|
+
async function loadActiveMetaOrStale(projectRoot, opts = {}) {
|
|
5044
|
+
let onDisk;
|
|
5045
|
+
try {
|
|
5046
|
+
onDisk = await readAgentsMeta(projectRoot);
|
|
5047
|
+
} catch (error) {
|
|
5048
|
+
if (error instanceof AgentsMetaFileMissingError || error instanceof AgentsMetaInvalidError) {
|
|
5049
|
+
throw error;
|
|
5050
|
+
}
|
|
5051
|
+
throw error;
|
|
5052
|
+
}
|
|
5053
|
+
const previousRevisionHash = onDisk.revision;
|
|
5054
|
+
let derived;
|
|
5055
|
+
try {
|
|
5056
|
+
derived = await buildKnowledgeMeta(projectRoot);
|
|
5057
|
+
} catch (error) {
|
|
5058
|
+
return {
|
|
5059
|
+
meta: onDisk,
|
|
5060
|
+
auto_healed: false,
|
|
5061
|
+
previous_revision_hash: previousRevisionHash,
|
|
5062
|
+
revision_hash: previousRevisionHash,
|
|
5063
|
+
degraded: true,
|
|
5064
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5065
|
+
};
|
|
5066
|
+
}
|
|
5067
|
+
if (derived.meta.revision === previousRevisionHash) {
|
|
5068
|
+
return {
|
|
5069
|
+
meta: onDisk,
|
|
5070
|
+
auto_healed: false,
|
|
5071
|
+
previous_revision_hash: previousRevisionHash,
|
|
5072
|
+
revision_hash: previousRevisionHash,
|
|
5073
|
+
degraded: false
|
|
5074
|
+
};
|
|
5075
|
+
}
|
|
5076
|
+
let written;
|
|
5077
|
+
try {
|
|
5078
|
+
written = await writeKnowledgeMeta(projectRoot, { source: "doctor_fix" });
|
|
5079
|
+
} catch (error) {
|
|
5080
|
+
return {
|
|
5081
|
+
meta: onDisk,
|
|
5082
|
+
auto_healed: false,
|
|
5083
|
+
previous_revision_hash: previousRevisionHash,
|
|
5084
|
+
revision_hash: previousRevisionHash,
|
|
5085
|
+
degraded: true,
|
|
5086
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5087
|
+
};
|
|
5088
|
+
}
|
|
5089
|
+
contextCache.invalidate("meta_write", projectRoot);
|
|
5090
|
+
await emitAutoHealEventBestEffort(projectRoot, {
|
|
5091
|
+
previous_revision_hash: previousRevisionHash,
|
|
5092
|
+
revision_hash: written.meta.revision,
|
|
5093
|
+
caller: opts.caller
|
|
5094
|
+
});
|
|
5095
|
+
return {
|
|
5096
|
+
meta: written.meta,
|
|
5097
|
+
auto_healed: true,
|
|
5098
|
+
previous_revision_hash: previousRevisionHash,
|
|
5099
|
+
revision_hash: written.meta.revision,
|
|
5100
|
+
degraded: false
|
|
5101
|
+
};
|
|
5102
|
+
}
|
|
5103
|
+
async function emitAutoHealEventBestEffort(projectRoot, payload) {
|
|
5104
|
+
try {
|
|
5105
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
5106
|
+
event_type: "knowledge_meta_auto_healed",
|
|
5107
|
+
previous_revision_hash: payload.previous_revision_hash,
|
|
5108
|
+
revision_hash: payload.revision_hash,
|
|
5109
|
+
trigger: "read",
|
|
5110
|
+
...payload.caller !== void 0 ? { caller: payload.caller } : {}
|
|
5111
|
+
});
|
|
5112
|
+
} catch {
|
|
5113
|
+
}
|
|
5114
|
+
}
|
|
5115
|
+
|
|
4117
5116
|
// src/services/get-knowledge.ts
|
|
4118
5117
|
import { readFile as readFile6 } from "fs/promises";
|
|
4119
|
-
import { join as
|
|
5118
|
+
import { join as join7 } from "path";
|
|
4120
5119
|
import { minimatch as minimatch2 } from "minimatch";
|
|
4121
5120
|
var PRIORITY_ORDER = {
|
|
4122
5121
|
high: 0,
|
|
@@ -4124,6 +5123,10 @@ var PRIORITY_ORDER = {
|
|
|
4124
5123
|
low: 2
|
|
4125
5124
|
};
|
|
4126
5125
|
async function getKnowledge(projectRoot, input) {
|
|
5126
|
+
const metaResult = await loadActiveMeta(projectRoot, { caller: "getKnowledge" });
|
|
5127
|
+
if (metaResult.auto_healed) {
|
|
5128
|
+
contextCache.invalidate("file_watch", projectRoot);
|
|
5129
|
+
}
|
|
4127
5130
|
const context = await loadGetKnowledgeContext(projectRoot);
|
|
4128
5131
|
const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
|
|
4129
5132
|
const matchedNodes = matchRuleNodes(context.meta, input.path);
|
|
@@ -4156,7 +5159,7 @@ async function loadGetKnowledgeContext(projectRoot) {
|
|
|
4156
5159
|
return cached;
|
|
4157
5160
|
}
|
|
4158
5161
|
const meta = await readAgentsMeta(projectRoot);
|
|
4159
|
-
const l0Content = await readFile6(
|
|
5162
|
+
const l0Content = await readFile6(join7(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
|
|
4160
5163
|
const context = {
|
|
4161
5164
|
meta,
|
|
4162
5165
|
l0Content,
|
|
@@ -4295,13 +5298,14 @@ async function readRuleContent(projectRoot, file, fileContentCache) {
|
|
|
4295
5298
|
if (cached !== void 0) {
|
|
4296
5299
|
return await cached;
|
|
4297
5300
|
}
|
|
4298
|
-
const pending = readFile6(
|
|
5301
|
+
const pending = readFile6(join7(projectRoot, file), "utf8");
|
|
4299
5302
|
fileContentCache.set(file, pending);
|
|
4300
5303
|
return await pending;
|
|
4301
5304
|
}
|
|
4302
5305
|
|
|
4303
5306
|
export {
|
|
4304
5307
|
contextCache,
|
|
5308
|
+
AgentsMetaFileMissingError,
|
|
4305
5309
|
resolveProjectRoot,
|
|
4306
5310
|
readAgentsMeta,
|
|
4307
5311
|
LEDGER_PATH,
|
|
@@ -4328,9 +5332,12 @@ export {
|
|
|
4328
5332
|
invalidateKnowledgeSyncCooldown,
|
|
4329
5333
|
ensureKnowledgeFresh,
|
|
4330
5334
|
reconcileKnowledge,
|
|
5335
|
+
loadActiveMeta,
|
|
5336
|
+
loadActiveMetaOrStale,
|
|
4331
5337
|
getKnowledge,
|
|
4332
5338
|
normalizeKnowledgePath,
|
|
4333
5339
|
runDoctorReport,
|
|
4334
5340
|
runDoctorFix,
|
|
4335
|
-
runDoctorApplyLint
|
|
5341
|
+
runDoctorApplyLint,
|
|
5342
|
+
runDoctorCiteCoverage
|
|
4336
5343
|
};
|