@fenglimg/fabric-server 2.0.0-rc.21 → 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,11 +1450,11 @@ 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,
|
|
@@ -1289,7 +1469,7 @@ import {
|
|
|
1289
1469
|
BOOTSTRAP_REGEX
|
|
1290
1470
|
} from "@fenglimg/fabric-shared";
|
|
1291
1471
|
import { detectFramework } from "@fenglimg/fabric-shared/node";
|
|
1292
|
-
import { atomicWriteJson as atomicWriteJson2, atomicWriteText as
|
|
1472
|
+
import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText4 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
1293
1473
|
var ORPHAN_DEMOTE_THRESHOLD_DAYS = {
|
|
1294
1474
|
stable: 90,
|
|
1295
1475
|
endorsed: 30,
|
|
@@ -1327,7 +1507,22 @@ var KNOWLEDGE_CANONICAL_TYPE_DIRS = [
|
|
|
1327
1507
|
"processes"
|
|
1328
1508
|
];
|
|
1329
1509
|
var CANONICAL_KNOWLEDGE_FILENAME_PATTERN = /^(K[PT]-(?:MOD|DEC|GLD|PIT|PRO)-\d{4,})--[a-z0-9][a-z0-9-]*\.md$/u;
|
|
1330
|
-
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;
|
|
1331
1526
|
var COUNTER_TYPE_CODES = ["MOD", "DEC", "GLD", "PIT", "PRO"];
|
|
1332
1527
|
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
1333
1528
|
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
@@ -1386,6 +1581,7 @@ async function runDoctorReport(target) {
|
|
|
1386
1581
|
const metaManuallyDiverged = await inspectMetaManuallyDiverged(projectRoot);
|
|
1387
1582
|
const knowledgeDirUnindexed = inspectKnowledgeDirUnindexed(projectRoot, meta);
|
|
1388
1583
|
const knowledgeDirMissing = inspectKnowledgeDirMissing(projectRoot);
|
|
1584
|
+
const baselineFilenameFormat = inspectBaselineFilenameFormat(projectRoot);
|
|
1389
1585
|
const stableIdCollision = await inspectStableIdCollisions(projectRoot);
|
|
1390
1586
|
const counterDesync = inspectCounterDesync(meta);
|
|
1391
1587
|
const preexistingRootFiles = inspectPreexistingRootFiles(projectRoot);
|
|
@@ -1418,6 +1614,10 @@ async function runDoctorReport(target) {
|
|
|
1418
1614
|
createL1BootstrapSnapshotDriftCheck(l1BootstrapSnapshotDrift),
|
|
1419
1615
|
createL2ManagedBlockDriftCheck(l2ManagedBlockDrift),
|
|
1420
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),
|
|
1421
1621
|
createForensicCheck(forensic, framework.kind, entryPoints.length),
|
|
1422
1622
|
// v2.0: removed `createInitContextCheck` — `.fabric/init-context.json`
|
|
1423
1623
|
// is owned by the AI-side client init skill, not by `fabric install` CLI.
|
|
@@ -1517,7 +1717,7 @@ async function runDoctorReport(target) {
|
|
|
1517
1717
|
warningCount: warnings.length,
|
|
1518
1718
|
infoCount: infos.length,
|
|
1519
1719
|
targetFiles: Object.fromEntries(
|
|
1520
|
-
TARGET_FILE_PATHS.map((path) => [path, existsSync4(
|
|
1720
|
+
TARGET_FILE_PATHS.map((path) => [path, existsSync4(join6(projectRoot, path))])
|
|
1521
1721
|
)
|
|
1522
1722
|
}
|
|
1523
1723
|
};
|
|
@@ -1544,9 +1744,9 @@ async function runDoctorFix(target) {
|
|
|
1544
1744
|
}
|
|
1545
1745
|
}
|
|
1546
1746
|
if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
|
|
1547
|
-
const snapshotPath =
|
|
1747
|
+
const snapshotPath = join6(projectRoot, ".fabric", "AGENTS.md");
|
|
1548
1748
|
await ensureParentDirectory(snapshotPath);
|
|
1549
|
-
await
|
|
1749
|
+
await atomicWriteText4(snapshotPath, BOOTSTRAP_CANONICAL);
|
|
1550
1750
|
fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
|
|
1551
1751
|
}
|
|
1552
1752
|
if (before.fixable_errors.some((issue) => issue.code === "managed_block_drift")) {
|
|
@@ -1566,26 +1766,23 @@ async function runDoctorFix(target) {
|
|
|
1566
1766
|
fixed.push(findIssue(before.fixable_errors, "counter_desync"));
|
|
1567
1767
|
contextCache.invalidate("meta_write", projectRoot);
|
|
1568
1768
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
)) {
|
|
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))) {
|
|
1579
1778
|
await reconcileKnowledge(projectRoot, { trigger: "doctor" });
|
|
1580
1779
|
for (const issue of before.fixable_errors.filter(
|
|
1581
|
-
(candidate) =>
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
"knowledge_dir_unindexed"
|
|
1588
|
-
].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)
|
|
1589
1786
|
)) {
|
|
1590
1787
|
fixed.push(issue);
|
|
1591
1788
|
}
|
|
@@ -1604,6 +1801,15 @@ async function runDoctorFix(target) {
|
|
|
1604
1801
|
});
|
|
1605
1802
|
fixed.push(findIssue(before.fixable_errors, "event_ledger_partial_write"));
|
|
1606
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
|
+
}
|
|
1607
1813
|
if (before.fixable_errors.some((issue) => issue.code === "mcp_config_in_wrong_file")) {
|
|
1608
1814
|
await fixMcpConfigInWrongFile(projectRoot);
|
|
1609
1815
|
fixed.push(findIssue(before.fixable_errors, "mcp_config_in_wrong_file"));
|
|
@@ -1743,7 +1949,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
|
|
|
1743
1949
|
};
|
|
1744
1950
|
}
|
|
1745
1951
|
const detail = `${candidate.maturity} -> ${next}`;
|
|
1746
|
-
const absPath =
|
|
1952
|
+
const absPath = join6(projectRoot, candidate.path);
|
|
1747
1953
|
try {
|
|
1748
1954
|
const source = await readFile5(absPath, "utf8");
|
|
1749
1955
|
const rewritten = rewriteFrontmatterMaturity(source, next);
|
|
@@ -1765,7 +1971,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
|
|
|
1765
1971
|
error: "rewrite produced byte-identical output"
|
|
1766
1972
|
};
|
|
1767
1973
|
}
|
|
1768
|
-
await
|
|
1974
|
+
await atomicWriteText4(absPath, rewritten);
|
|
1769
1975
|
try {
|
|
1770
1976
|
await appendEventLedgerEvent(projectRoot, {
|
|
1771
1977
|
event_type: "knowledge_demoted",
|
|
@@ -1775,7 +1981,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
|
|
|
1775
1981
|
});
|
|
1776
1982
|
} catch (ledgerError) {
|
|
1777
1983
|
try {
|
|
1778
|
-
await
|
|
1984
|
+
await atomicWriteText4(absPath, source);
|
|
1779
1985
|
} catch (rollbackError) {
|
|
1780
1986
|
return {
|
|
1781
1987
|
kind: "knowledge_orphan_demote_required",
|
|
@@ -1810,11 +2016,11 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
|
|
|
1810
2016
|
}
|
|
1811
2017
|
}
|
|
1812
2018
|
async function applyStaleArchive(projectRoot, candidate, now) {
|
|
1813
|
-
const sourceAbs =
|
|
1814
|
-
const destAbs =
|
|
2019
|
+
const sourceAbs = join6(projectRoot, candidate.path);
|
|
2020
|
+
const destAbs = join6(projectRoot, candidate.archive_path);
|
|
1815
2021
|
const detail = `${candidate.path} -> ${candidate.archive_path}`;
|
|
1816
2022
|
try {
|
|
1817
|
-
await
|
|
2023
|
+
await mkdir4(join6(destAbs, ".."), { recursive: true });
|
|
1818
2024
|
try {
|
|
1819
2025
|
await rename(sourceAbs, destAbs);
|
|
1820
2026
|
} catch (renameError) {
|
|
@@ -1873,7 +2079,7 @@ async function applyStaleArchive(projectRoot, candidate, now) {
|
|
|
1873
2079
|
async function applyPendingAutoArchive(projectRoot, candidate, now) {
|
|
1874
2080
|
const detail = `${candidate.pending_path} -> ${candidate.archived_to}`;
|
|
1875
2081
|
try {
|
|
1876
|
-
await
|
|
2082
|
+
await mkdir4(join6(candidate.archived_to_abs, ".."), { recursive: true });
|
|
1877
2083
|
let moved = false;
|
|
1878
2084
|
if (candidate.layer === "team") {
|
|
1879
2085
|
try {
|
|
@@ -1946,11 +2152,11 @@ async function applyPendingAutoArchive(projectRoot, candidate, now) {
|
|
|
1946
2152
|
}
|
|
1947
2153
|
function relativePosix(projectRoot, absolutePath) {
|
|
1948
2154
|
const rel = nodeRelative(projectRoot, absolutePath);
|
|
1949
|
-
return rel.split(
|
|
2155
|
+
return rel.split(sep3).join("/");
|
|
1950
2156
|
}
|
|
1951
2157
|
async function applySessionHintsStaleCleanup(projectRoot, candidate) {
|
|
1952
2158
|
const detail = `deleted (${candidate.age_days}d old)`;
|
|
1953
|
-
const absPath =
|
|
2159
|
+
const absPath = join6(projectRoot, candidate.path);
|
|
1954
2160
|
try {
|
|
1955
2161
|
const { unlink } = await import("fs/promises");
|
|
1956
2162
|
await unlink(absPath);
|
|
@@ -1971,7 +2177,7 @@ async function applySessionHintsStaleCleanup(projectRoot, candidate) {
|
|
|
1971
2177
|
}
|
|
1972
2178
|
}
|
|
1973
2179
|
async function applyIndexDriftFix(projectRoot, inspection) {
|
|
1974
|
-
const metaPath =
|
|
2180
|
+
const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
|
|
1975
2181
|
const detailParts = [];
|
|
1976
2182
|
try {
|
|
1977
2183
|
const meta = agentsMetaSchema4.parse(JSON.parse(await readFile5(metaPath, "utf8")));
|
|
@@ -2007,7 +2213,7 @@ function truncateErrorMessage(error) {
|
|
|
2007
2213
|
return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
|
|
2008
2214
|
}
|
|
2009
2215
|
async function inspectForensic(projectRoot) {
|
|
2010
|
-
const path =
|
|
2216
|
+
const path = join6(projectRoot, ".fabric", "forensic.json");
|
|
2011
2217
|
try {
|
|
2012
2218
|
const parsed = forensicReportSchema.parse(JSON.parse(await readFile5(path, "utf8")));
|
|
2013
2219
|
return { present: true, valid: true, report: parsed };
|
|
@@ -2019,12 +2225,12 @@ async function inspectForensic(projectRoot) {
|
|
|
2019
2225
|
}
|
|
2020
2226
|
}
|
|
2021
2227
|
function inspectMcpConfigInWrongFile(projectRoot) {
|
|
2022
|
-
const settingsPath =
|
|
2228
|
+
const settingsPath = join6(projectRoot, ".claude", "settings.json");
|
|
2023
2229
|
if (!existsSync4(settingsPath)) {
|
|
2024
2230
|
return { hasWrongEntry: false, settingsPath };
|
|
2025
2231
|
}
|
|
2026
2232
|
try {
|
|
2027
|
-
const parsed = JSON.parse(
|
|
2233
|
+
const parsed = JSON.parse(readFileSync2(settingsPath, "utf8"));
|
|
2028
2234
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2029
2235
|
return { hasWrongEntry: false, settingsPath };
|
|
2030
2236
|
}
|
|
@@ -2040,7 +2246,7 @@ function inspectMcpConfigInWrongFile(projectRoot) {
|
|
|
2040
2246
|
}
|
|
2041
2247
|
}
|
|
2042
2248
|
async function inspectMeta(projectRoot) {
|
|
2043
|
-
const metaPath =
|
|
2249
|
+
const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
|
|
2044
2250
|
const built = await tryBuildRuleMeta(projectRoot);
|
|
2045
2251
|
try {
|
|
2046
2252
|
const raw = await readFile5(metaPath, "utf8");
|
|
@@ -2113,7 +2319,7 @@ function inspectContentRefs(projectRoot, meta) {
|
|
|
2113
2319
|
if (isPersonalKnowledge) {
|
|
2114
2320
|
continue;
|
|
2115
2321
|
}
|
|
2116
|
-
if (!existsSync4(
|
|
2322
|
+
if (!existsSync4(join6(projectRoot, contentRef))) {
|
|
2117
2323
|
missing.push(contentRef);
|
|
2118
2324
|
}
|
|
2119
2325
|
}
|
|
@@ -2155,7 +2361,7 @@ async function inspectEventLedger(projectRoot) {
|
|
|
2155
2361
|
}
|
|
2156
2362
|
}
|
|
2157
2363
|
async function inspectKnowledgeTestIndex(projectRoot) {
|
|
2158
|
-
const path =
|
|
2364
|
+
const path = join6(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
|
|
2159
2365
|
const built = await tryBuildRuleMeta(projectRoot);
|
|
2160
2366
|
try {
|
|
2161
2367
|
const index = knowledgeTestIndexSchema2.parse(JSON.parse(await readFile5(path, "utf8")));
|
|
@@ -2179,8 +2385,8 @@ async function inspectKnowledgeTestIndex(projectRoot) {
|
|
|
2179
2385
|
}
|
|
2180
2386
|
function inspectBootstrapAnchor(projectRoot) {
|
|
2181
2387
|
return {
|
|
2182
|
-
hasAgentsMd: existsSync4(
|
|
2183
|
-
hasClaudeMd: existsSync4(
|
|
2388
|
+
hasAgentsMd: existsSync4(join6(projectRoot, "AGENTS.md")),
|
|
2389
|
+
hasClaudeMd: existsSync4(join6(projectRoot, "CLAUDE.md"))
|
|
2184
2390
|
};
|
|
2185
2391
|
}
|
|
2186
2392
|
var BOOTSTRAP_MARKER_MIGRATION_TARGETS = [
|
|
@@ -2192,7 +2398,7 @@ var BOOTSTRAP_MARKER_MIGRATION_TARGETS = [
|
|
|
2192
2398
|
async function inspectBootstrapMarkerMigration(target) {
|
|
2193
2399
|
const filesNeedingMigration = [];
|
|
2194
2400
|
for (const rel of BOOTSTRAP_MARKER_MIGRATION_TARGETS) {
|
|
2195
|
-
const abs =
|
|
2401
|
+
const abs = join6(target, rel);
|
|
2196
2402
|
if (!existsSync4(abs)) {
|
|
2197
2403
|
continue;
|
|
2198
2404
|
}
|
|
@@ -2226,7 +2432,7 @@ function createBootstrapMarkerMigrationCheck(inspection) {
|
|
|
2226
2432
|
);
|
|
2227
2433
|
}
|
|
2228
2434
|
async function inspectL1BootstrapSnapshotDrift(target) {
|
|
2229
|
-
const abs =
|
|
2435
|
+
const abs = join6(target, ".fabric", "AGENTS.md");
|
|
2230
2436
|
if (!existsSync4(abs)) {
|
|
2231
2437
|
return { status: "missing", canonical: BOOTSTRAP_CANONICAL, onDisk: null };
|
|
2232
2438
|
}
|
|
@@ -2258,7 +2464,7 @@ function createL1BootstrapSnapshotDriftCheck(inspection) {
|
|
|
2258
2464
|
);
|
|
2259
2465
|
}
|
|
2260
2466
|
async function inspectL2ManagedBlockDrift(target) {
|
|
2261
|
-
const snapshotPath =
|
|
2467
|
+
const snapshotPath = join6(target, ".fabric", "AGENTS.md");
|
|
2262
2468
|
if (!existsSync4(snapshotPath)) {
|
|
2263
2469
|
return { status: "ok", drifted: [] };
|
|
2264
2470
|
}
|
|
@@ -2268,7 +2474,7 @@ async function inspectL2ManagedBlockDrift(target) {
|
|
|
2268
2474
|
} catch {
|
|
2269
2475
|
return { status: "ok", drifted: [] };
|
|
2270
2476
|
}
|
|
2271
|
-
const projectRulesPath =
|
|
2477
|
+
const projectRulesPath = join6(target, ".fabric", "project-rules.md");
|
|
2272
2478
|
let expectedBody = snapshot;
|
|
2273
2479
|
if (existsSync4(projectRulesPath)) {
|
|
2274
2480
|
try {
|
|
@@ -2282,8 +2488,8 @@ ${projectRules}`;
|
|
|
2282
2488
|
const drifted = [];
|
|
2283
2489
|
let anyManagedBlockFound = false;
|
|
2284
2490
|
const blockTargets = [
|
|
2285
|
-
|
|
2286
|
-
|
|
2491
|
+
join6(target, "AGENTS.md"),
|
|
2492
|
+
join6(target, ".cursor", "rules", "fabric-bootstrap.mdc")
|
|
2287
2493
|
];
|
|
2288
2494
|
for (const abs of blockTargets) {
|
|
2289
2495
|
if (!existsSync4(abs)) {
|
|
@@ -2317,7 +2523,7 @@ ${projectRules}`;
|
|
|
2317
2523
|
drifted.push({ path: abs, expected: expectedBody, actual: body });
|
|
2318
2524
|
}
|
|
2319
2525
|
}
|
|
2320
|
-
const claudeMdPath =
|
|
2526
|
+
const claudeMdPath = join6(target, "CLAUDE.md");
|
|
2321
2527
|
if (existsSync4(claudeMdPath)) {
|
|
2322
2528
|
let claudeContent;
|
|
2323
2529
|
try {
|
|
@@ -2381,16 +2587,82 @@ function createBootstrapAnchorCheck(inspection) {
|
|
|
2381
2587
|
return okCheck("Bootstrap anchor", `Bootstrap anchor present at repo root: ${present}.`);
|
|
2382
2588
|
}
|
|
2383
2589
|
function inspectKnowledgeDirMissing(projectRoot) {
|
|
2384
|
-
const knowledgeRoot =
|
|
2590
|
+
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
2385
2591
|
const missingSubdirs = [];
|
|
2386
|
-
for (const sub of
|
|
2387
|
-
const path =
|
|
2592
|
+
for (const sub of KNOWLEDGE_SUBDIRS3) {
|
|
2593
|
+
const path = join6(knowledgeRoot, sub);
|
|
2388
2594
|
if (!existsSync4(path)) {
|
|
2389
2595
|
missingSubdirs.push(`.fabric/knowledge/${sub}`);
|
|
2390
2596
|
}
|
|
2391
2597
|
}
|
|
2392
2598
|
return { missingSubdirs };
|
|
2393
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
|
+
}
|
|
2394
2666
|
function createKnowledgeDirMissingCheck(inspection) {
|
|
2395
2667
|
if (inspection.missingSubdirs.length > 0) {
|
|
2396
2668
|
const list = inspection.missingSubdirs.join(", ");
|
|
@@ -2405,7 +2677,7 @@ function createKnowledgeDirMissingCheck(inspection) {
|
|
|
2405
2677
|
}
|
|
2406
2678
|
return okCheck(
|
|
2407
2679
|
"Knowledge layout",
|
|
2408
|
-
`All ${
|
|
2680
|
+
`All ${KNOWLEDGE_SUBDIRS3.length} required .fabric/knowledge/* subdirectories exist.`
|
|
2409
2681
|
);
|
|
2410
2682
|
}
|
|
2411
2683
|
function createForensicCheck(forensic, frameworkKind, entryPointCount) {
|
|
@@ -2434,11 +2706,11 @@ function createMetaCheck(meta) {
|
|
|
2434
2706
|
if (meta.stale) {
|
|
2435
2707
|
return issueCheck(
|
|
2436
2708
|
"Agents metadata",
|
|
2437
|
-
"
|
|
2438
|
-
"
|
|
2709
|
+
"warn",
|
|
2710
|
+
"warning",
|
|
2439
2711
|
"agents_meta_stale",
|
|
2440
2712
|
`.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/knowledge derived revision ${meta.computedRevision ?? "<unknown>"}.`,
|
|
2441
|
-
"Run `fab doctor --fix`
|
|
2713
|
+
"Benign \u2014 engine auto-heals on next plan-context/get-sections call. Run `fab doctor --fix` for explicit reconciliation."
|
|
2442
2714
|
);
|
|
2443
2715
|
}
|
|
2444
2716
|
return okCheck("Agents metadata", `.fabric/agents.meta.json revision ${meta.revision} is aligned with .fabric/knowledge.`);
|
|
@@ -2551,7 +2823,7 @@ function findIssue(issues, code) {
|
|
|
2551
2823
|
};
|
|
2552
2824
|
}
|
|
2553
2825
|
async function inspectMetaManuallyDiverged(projectRoot) {
|
|
2554
|
-
const metaPath =
|
|
2826
|
+
const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
|
|
2555
2827
|
if (!existsSync4(metaPath)) {
|
|
2556
2828
|
return { extraMetaEntries: [], hashMismatchEntries: [], readable: false };
|
|
2557
2829
|
}
|
|
@@ -2571,13 +2843,13 @@ async function inspectMetaManuallyDiverged(projectRoot) {
|
|
|
2571
2843
|
const hashMismatchEntries = [];
|
|
2572
2844
|
for (const node of Object.values(meta.nodes)) {
|
|
2573
2845
|
const contentRef = node.content_ref ?? node.file;
|
|
2574
|
-
const absPath =
|
|
2846
|
+
const absPath = join6(projectRoot, contentRef);
|
|
2575
2847
|
if (!existsSync4(absPath)) {
|
|
2576
2848
|
extraMetaEntries.push(contentRef);
|
|
2577
2849
|
continue;
|
|
2578
2850
|
}
|
|
2579
2851
|
try {
|
|
2580
|
-
const content =
|
|
2852
|
+
const content = readFileSync2(absPath, "utf8");
|
|
2581
2853
|
const diskHash = sha256(content);
|
|
2582
2854
|
if (node.hash !== "" && node.hash !== diskHash) {
|
|
2583
2855
|
hashMismatchEntries.push(contentRef);
|
|
@@ -2590,7 +2862,7 @@ async function inspectMetaManuallyDiverged(projectRoot) {
|
|
|
2590
2862
|
}
|
|
2591
2863
|
function inspectKnowledgeDirUnindexed(projectRoot, meta) {
|
|
2592
2864
|
const physicalMdFiles = /* @__PURE__ */ new Set();
|
|
2593
|
-
collectMdFilesUnder(physicalMdFiles, projectRoot,
|
|
2865
|
+
collectMdFilesUnder(physicalMdFiles, projectRoot, join6(projectRoot, ".fabric", "knowledge"), ".fabric/knowledge");
|
|
2594
2866
|
if (physicalMdFiles.size === 0) {
|
|
2595
2867
|
return { unindexedFiles: [] };
|
|
2596
2868
|
}
|
|
@@ -2615,7 +2887,7 @@ function collectMdFilesUnder(out, projectRoot, rootDir, relPrefix) {
|
|
|
2615
2887
|
continue;
|
|
2616
2888
|
}
|
|
2617
2889
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2618
|
-
const abs =
|
|
2890
|
+
const abs = join6(dir, entry.name);
|
|
2619
2891
|
if (entry.isDirectory()) {
|
|
2620
2892
|
stack.push(abs);
|
|
2621
2893
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -2640,7 +2912,7 @@ function createKnowledgeDirUnindexedCheck(inspection) {
|
|
|
2640
2912
|
}
|
|
2641
2913
|
async function inspectStableIdCollisions(projectRoot) {
|
|
2642
2914
|
const found = [];
|
|
2643
|
-
const knowledgeDir =
|
|
2915
|
+
const knowledgeDir = join6(projectRoot, ".fabric", "knowledge");
|
|
2644
2916
|
if (existsSync4(knowledgeDir)) {
|
|
2645
2917
|
const stack = [knowledgeDir];
|
|
2646
2918
|
while (stack.length > 0) {
|
|
@@ -2649,7 +2921,7 @@ async function inspectStableIdCollisions(projectRoot) {
|
|
|
2649
2921
|
continue;
|
|
2650
2922
|
}
|
|
2651
2923
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2652
|
-
const abs =
|
|
2924
|
+
const abs = join6(dir, entry.name);
|
|
2653
2925
|
if (entry.isDirectory()) {
|
|
2654
2926
|
stack.push(abs);
|
|
2655
2927
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -2805,17 +3077,17 @@ function createMetaManuallyDivergedCheck(inspection) {
|
|
|
2805
3077
|
}
|
|
2806
3078
|
function inspectPreexistingRootFiles(projectRoot) {
|
|
2807
3079
|
const candidates = ["CLAUDE.md", "AGENTS.md"];
|
|
2808
|
-
const detected = candidates.filter((name) => existsSync4(
|
|
3080
|
+
const detected = candidates.filter((name) => existsSync4(join6(projectRoot, name)));
|
|
2809
3081
|
return { detected };
|
|
2810
3082
|
}
|
|
2811
3083
|
async function inspectFilesystemEditFallback(projectRoot) {
|
|
2812
|
-
const knowledgeRoot =
|
|
3084
|
+
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
2813
3085
|
if (!existsSync4(knowledgeRoot)) {
|
|
2814
3086
|
return { synthesized: 0, synthesizedStableIds: [] };
|
|
2815
3087
|
}
|
|
2816
3088
|
const canonicalIds = /* @__PURE__ */ new Set();
|
|
2817
3089
|
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
2818
|
-
const dir =
|
|
3090
|
+
const dir = join6(knowledgeRoot, typeDir);
|
|
2819
3091
|
if (!existsSync4(dir)) {
|
|
2820
3092
|
continue;
|
|
2821
3093
|
}
|
|
@@ -3017,12 +3289,12 @@ function extractKnowledgeFrontmatterCreatedAt(source) {
|
|
|
3017
3289
|
return Number.isFinite(parsed) ? parsed : null;
|
|
3018
3290
|
}
|
|
3019
3291
|
function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
|
|
3020
|
-
const knowledgeRoot =
|
|
3292
|
+
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
3021
3293
|
if (!existsSync4(knowledgeRoot)) {
|
|
3022
3294
|
return;
|
|
3023
3295
|
}
|
|
3024
3296
|
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
3025
|
-
const dir =
|
|
3297
|
+
const dir = join6(knowledgeRoot, typeDir);
|
|
3026
3298
|
if (!existsSync4(dir)) {
|
|
3027
3299
|
continue;
|
|
3028
3300
|
}
|
|
@@ -3041,10 +3313,10 @@ function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
|
|
|
3041
3313
|
continue;
|
|
3042
3314
|
}
|
|
3043
3315
|
const stableId = match[1];
|
|
3044
|
-
const absPath =
|
|
3316
|
+
const absPath = join6(dir, entry.name);
|
|
3045
3317
|
let source;
|
|
3046
3318
|
try {
|
|
3047
|
-
source =
|
|
3319
|
+
source = readFileSync2(absPath, "utf8");
|
|
3048
3320
|
} catch {
|
|
3049
3321
|
continue;
|
|
3050
3322
|
}
|
|
@@ -3057,7 +3329,7 @@ function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
|
|
|
3057
3329
|
let lastReferenceMs = Math.max(createdAt ?? 0, eventTs);
|
|
3058
3330
|
if (lastReferenceMs === 0) {
|
|
3059
3331
|
try {
|
|
3060
|
-
lastReferenceMs =
|
|
3332
|
+
lastReferenceMs = statSync4(absPath).mtimeMs;
|
|
3061
3333
|
} catch {
|
|
3062
3334
|
lastReferenceMs = 0;
|
|
3063
3335
|
}
|
|
@@ -3117,8 +3389,8 @@ async function inspectStaleArchive(projectRoot, now) {
|
|
|
3117
3389
|
return { candidates };
|
|
3118
3390
|
}
|
|
3119
3391
|
function* iteratePendingFiles(projectRoot, now) {
|
|
3120
|
-
const teamRoot =
|
|
3121
|
-
const personalRoot =
|
|
3392
|
+
const teamRoot = join6(projectRoot, ".fabric", "knowledge", "pending");
|
|
3393
|
+
const personalRoot = join6(resolvePersonalRootForPending(), ".fabric", "knowledge", "pending");
|
|
3122
3394
|
for (const [layer, root, displayPrefix] of [
|
|
3123
3395
|
["team", teamRoot, ".fabric/knowledge/pending"],
|
|
3124
3396
|
["personal", personalRoot, "~/.fabric/knowledge/pending"]
|
|
@@ -3133,7 +3405,7 @@ function* iteratePendingFiles(projectRoot, now) {
|
|
|
3133
3405
|
continue;
|
|
3134
3406
|
}
|
|
3135
3407
|
for (const typeDir of typeDirs) {
|
|
3136
|
-
const dir =
|
|
3408
|
+
const dir = join6(root, typeDir);
|
|
3137
3409
|
let entries;
|
|
3138
3410
|
try {
|
|
3139
3411
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -3144,17 +3416,17 @@ function* iteratePendingFiles(projectRoot, now) {
|
|
|
3144
3416
|
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
3145
3417
|
continue;
|
|
3146
3418
|
}
|
|
3147
|
-
const absPath =
|
|
3419
|
+
const absPath = join6(dir, entry.name);
|
|
3148
3420
|
let source = "";
|
|
3149
3421
|
try {
|
|
3150
|
-
source =
|
|
3422
|
+
source = readFileSync2(absPath, "utf8");
|
|
3151
3423
|
} catch {
|
|
3152
3424
|
continue;
|
|
3153
3425
|
}
|
|
3154
3426
|
const createdAt = extractKnowledgeFrontmatterCreatedAt(source);
|
|
3155
3427
|
let mtimeMs = 0;
|
|
3156
3428
|
try {
|
|
3157
|
-
mtimeMs =
|
|
3429
|
+
mtimeMs = statSync4(absPath).mtimeMs;
|
|
3158
3430
|
} catch {
|
|
3159
3431
|
mtimeMs = 0;
|
|
3160
3432
|
}
|
|
@@ -3188,7 +3460,7 @@ function* iteratePendingFiles(projectRoot, now) {
|
|
|
3188
3460
|
}
|
|
3189
3461
|
}
|
|
3190
3462
|
function resolvePersonalRootForPending() {
|
|
3191
|
-
return process.env.FABRIC_HOME ??
|
|
3463
|
+
return process.env.FABRIC_HOME ?? homedir3();
|
|
3192
3464
|
}
|
|
3193
3465
|
function inspectPendingOverdue(projectRoot, now) {
|
|
3194
3466
|
const candidates = [];
|
|
@@ -3219,7 +3491,7 @@ function inspectPendingAutoArchive(projectRoot, now) {
|
|
|
3219
3491
|
pending_path: visit.pending_path,
|
|
3220
3492
|
pending_path_abs: visit.pending_path_abs,
|
|
3221
3493
|
archived_to: archivedToRel,
|
|
3222
|
-
archived_to_abs:
|
|
3494
|
+
archived_to_abs: join6(projectRoot, archivedToRel),
|
|
3223
3495
|
age_days: visit.age_days
|
|
3224
3496
|
});
|
|
3225
3497
|
} else {
|
|
@@ -3228,7 +3500,7 @@ function inspectPendingAutoArchive(projectRoot, now) {
|
|
|
3228
3500
|
visit.type,
|
|
3229
3501
|
visit.filename
|
|
3230
3502
|
);
|
|
3231
|
-
const archivedToAbs =
|
|
3503
|
+
const archivedToAbs = join6(
|
|
3232
3504
|
resolvePersonalRootForPending(),
|
|
3233
3505
|
".fabric",
|
|
3234
3506
|
".archive",
|
|
@@ -3252,11 +3524,11 @@ function inspectPendingAutoArchive(projectRoot, now) {
|
|
|
3252
3524
|
}
|
|
3253
3525
|
function inspectUnderseeded(projectRoot) {
|
|
3254
3526
|
const threshold = readUnderseedThresholdFromConfig(projectRoot);
|
|
3255
|
-
const knowledgeRoot =
|
|
3527
|
+
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
3256
3528
|
let nodeCount = 0;
|
|
3257
3529
|
if (existsSync4(knowledgeRoot)) {
|
|
3258
3530
|
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
3259
|
-
const dir =
|
|
3531
|
+
const dir = join6(knowledgeRoot, typeDir);
|
|
3260
3532
|
if (!existsSync4(dir)) continue;
|
|
3261
3533
|
let entries;
|
|
3262
3534
|
try {
|
|
@@ -3278,7 +3550,7 @@ function inspectUnderseeded(projectRoot) {
|
|
|
3278
3550
|
};
|
|
3279
3551
|
}
|
|
3280
3552
|
function inspectSessionHintsStale(projectRoot, now) {
|
|
3281
|
-
const cacheDir =
|
|
3553
|
+
const cacheDir = join6(projectRoot, ".fabric", ".cache");
|
|
3282
3554
|
if (!existsSync4(cacheDir)) {
|
|
3283
3555
|
return { candidates: [] };
|
|
3284
3556
|
}
|
|
@@ -3293,10 +3565,10 @@ function inspectSessionHintsStale(projectRoot, now) {
|
|
|
3293
3565
|
if (!entry.isFile()) continue;
|
|
3294
3566
|
if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
|
|
3295
3567
|
if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
|
|
3296
|
-
const absPath =
|
|
3568
|
+
const absPath = join6(cacheDir, entry.name);
|
|
3297
3569
|
let mtimeMs = 0;
|
|
3298
3570
|
try {
|
|
3299
|
-
mtimeMs =
|
|
3571
|
+
mtimeMs = statSync4(absPath).mtimeMs;
|
|
3300
3572
|
} catch {
|
|
3301
3573
|
continue;
|
|
3302
3574
|
}
|
|
@@ -3323,11 +3595,11 @@ function inspectNarrowTooFew(projectRoot, now) {
|
|
|
3323
3595
|
const structuralFlagged = total >= NARROW_MIN_TOTAL && narrowRatio < NARROW_RATIO_THRESHOLD;
|
|
3324
3596
|
const windowStartMs = now - SILENCE_WINDOW_DAYS * MS_PER_DAY;
|
|
3325
3597
|
const editFires = readCounterTimestamps(
|
|
3326
|
-
|
|
3598
|
+
join6(projectRoot, EDIT_COUNTER_FILE_REL),
|
|
3327
3599
|
windowStartMs
|
|
3328
3600
|
);
|
|
3329
3601
|
const silenceFires = readCounterTimestamps(
|
|
3330
|
-
|
|
3602
|
+
join6(projectRoot, HINT_SILENCE_COUNTER_FILE_REL),
|
|
3331
3603
|
windowStartMs
|
|
3332
3604
|
);
|
|
3333
3605
|
const telemetrySkipped = editFires === 0;
|
|
@@ -3349,7 +3621,7 @@ function readCounterTimestamps(absPath, windowStartMs) {
|
|
|
3349
3621
|
if (!existsSync4(absPath)) return 0;
|
|
3350
3622
|
let raw;
|
|
3351
3623
|
try {
|
|
3352
|
-
raw =
|
|
3624
|
+
raw = readFileSync2(absPath, "utf8");
|
|
3353
3625
|
} catch {
|
|
3354
3626
|
return 0;
|
|
3355
3627
|
}
|
|
@@ -3365,10 +3637,10 @@ function readCounterTimestamps(absPath, windowStartMs) {
|
|
|
3365
3637
|
return count;
|
|
3366
3638
|
}
|
|
3367
3639
|
function readUnderseedThresholdFromConfig(projectRoot) {
|
|
3368
|
-
const configPath =
|
|
3640
|
+
const configPath = join6(projectRoot, ".fabric", "fabric-config.json");
|
|
3369
3641
|
if (!existsSync4(configPath)) return DEFAULT_UNDERSEED_NODE_THRESHOLD;
|
|
3370
3642
|
try {
|
|
3371
|
-
const raw =
|
|
3643
|
+
const raw = readFileSync2(configPath, "utf8");
|
|
3372
3644
|
const parsed = JSON.parse(raw);
|
|
3373
3645
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3374
3646
|
const v = parsed.underseed_node_threshold;
|
|
@@ -3498,11 +3770,11 @@ function extractKnowledgeFrontmatterRelevancePaths(source) {
|
|
|
3498
3770
|
}
|
|
3499
3771
|
function* iterateRelevanceFrontmatter(projectRoot) {
|
|
3500
3772
|
for (const visit of iterateCanonicalFilenames(projectRoot)) {
|
|
3501
|
-
const layerRoot = visit.layer === "team" ?
|
|
3502
|
-
const absPath =
|
|
3773
|
+
const layerRoot = visit.layer === "team" ? join6(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
|
|
3774
|
+
const absPath = join6(layerRoot, visit.type, visit.filename);
|
|
3503
3775
|
let source;
|
|
3504
3776
|
try {
|
|
3505
|
-
source =
|
|
3777
|
+
source = readFileSync2(absPath, "utf8");
|
|
3506
3778
|
} catch {
|
|
3507
3779
|
continue;
|
|
3508
3780
|
}
|
|
@@ -3569,7 +3841,7 @@ function collectWorkspacePathsForGlobMatch(projectRoot) {
|
|
|
3569
3841
|
}
|
|
3570
3842
|
let rootStat;
|
|
3571
3843
|
try {
|
|
3572
|
-
rootStat =
|
|
3844
|
+
rootStat = statSync4(projectRoot);
|
|
3573
3845
|
} catch {
|
|
3574
3846
|
return [];
|
|
3575
3847
|
}
|
|
@@ -3588,7 +3860,7 @@ function collectWorkspacePathsForGlobMatch(projectRoot) {
|
|
|
3588
3860
|
continue;
|
|
3589
3861
|
}
|
|
3590
3862
|
for (const entry of entries) {
|
|
3591
|
-
const abs =
|
|
3863
|
+
const abs = join6(current, entry.name);
|
|
3592
3864
|
const rel = normalizePath(abs.slice(projectRoot.length + 1));
|
|
3593
3865
|
if (rel.length === 0) continue;
|
|
3594
3866
|
if (entry.isDirectory()) {
|
|
@@ -3728,8 +4000,8 @@ function inspectRelevanceFieldsMissing(projectRoot) {
|
|
|
3728
4000
|
const candidates = [];
|
|
3729
4001
|
let scannedCount = 0;
|
|
3730
4002
|
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
3731
|
-
const teamRoot =
|
|
3732
|
-
const personalRoot =
|
|
4003
|
+
const teamRoot = join6(projectRoot, ".fabric", "knowledge", "pending");
|
|
4004
|
+
const personalRoot = join6(
|
|
3733
4005
|
resolvePersonalRootForPending(),
|
|
3734
4006
|
".fabric",
|
|
3735
4007
|
"knowledge",
|
|
@@ -3749,7 +4021,7 @@ function inspectRelevanceFieldsMissing(projectRoot) {
|
|
|
3749
4021
|
continue;
|
|
3750
4022
|
}
|
|
3751
4023
|
for (const typeDir of typeDirs) {
|
|
3752
|
-
const dir =
|
|
4024
|
+
const dir = join6(root, typeDir);
|
|
3753
4025
|
let entries;
|
|
3754
4026
|
try {
|
|
3755
4027
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -3760,10 +4032,10 @@ function inspectRelevanceFieldsMissing(projectRoot) {
|
|
|
3760
4032
|
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
3761
4033
|
continue;
|
|
3762
4034
|
}
|
|
3763
|
-
const absPath =
|
|
4035
|
+
const absPath = join6(dir, entry.name);
|
|
3764
4036
|
let source;
|
|
3765
4037
|
try {
|
|
3766
|
-
source =
|
|
4038
|
+
source = readFileSync2(absPath, "utf8");
|
|
3767
4039
|
} catch {
|
|
3768
4040
|
continue;
|
|
3769
4041
|
}
|
|
@@ -3847,7 +4119,7 @@ async function applyRelevanceFieldsMissing(candidate) {
|
|
|
3847
4119
|
error: "fields already present at write time (no diff)"
|
|
3848
4120
|
};
|
|
3849
4121
|
}
|
|
3850
|
-
await
|
|
4122
|
+
await atomicWriteText4(candidate.pending_path_abs, rewritten);
|
|
3851
4123
|
return {
|
|
3852
4124
|
kind: "knowledge_relevance_fields_missing",
|
|
3853
4125
|
path: candidate.pending_path,
|
|
@@ -3891,7 +4163,7 @@ var SKILL_QUOTED_VALUE_LEADS = /* @__PURE__ */ new Set(['"', "'", "[", "{", ">",
|
|
|
3891
4163
|
function inspectSkillMdYamlInvalid(projectRoot) {
|
|
3892
4164
|
const candidates = [];
|
|
3893
4165
|
for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
|
|
3894
|
-
const rootAbs =
|
|
4166
|
+
const rootAbs = join6(projectRoot, rootRel);
|
|
3895
4167
|
if (!existsSync4(rootAbs)) continue;
|
|
3896
4168
|
let dirEntries;
|
|
3897
4169
|
try {
|
|
@@ -3901,11 +4173,11 @@ function inspectSkillMdYamlInvalid(projectRoot) {
|
|
|
3901
4173
|
}
|
|
3902
4174
|
for (const dirEntry of dirEntries) {
|
|
3903
4175
|
if (!dirEntry.isDirectory()) continue;
|
|
3904
|
-
const skillFile =
|
|
4176
|
+
const skillFile = join6(rootAbs, dirEntry.name, "SKILL.md");
|
|
3905
4177
|
if (!existsSync4(skillFile)) continue;
|
|
3906
4178
|
let raw;
|
|
3907
4179
|
try {
|
|
3908
|
-
raw =
|
|
4180
|
+
raw = readFileSync2(skillFile, "utf8");
|
|
3909
4181
|
} catch {
|
|
3910
4182
|
continue;
|
|
3911
4183
|
}
|
|
@@ -4005,8 +4277,8 @@ function createNarrowTooFewCheck(inspection) {
|
|
|
4005
4277
|
);
|
|
4006
4278
|
}
|
|
4007
4279
|
function resolvePersonalKnowledgeRoot() {
|
|
4008
|
-
const home = process.env.FABRIC_HOME ??
|
|
4009
|
-
return
|
|
4280
|
+
const home = process.env.FABRIC_HOME ?? homedir3();
|
|
4281
|
+
return join6(home, ".fabric", "knowledge");
|
|
4010
4282
|
}
|
|
4011
4283
|
function parseStableIdFromCanonicalFilename(filename) {
|
|
4012
4284
|
const match = CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(filename);
|
|
@@ -4026,7 +4298,7 @@ function parseStableIdFromCanonicalFilename(filename) {
|
|
|
4026
4298
|
};
|
|
4027
4299
|
}
|
|
4028
4300
|
function* iterateCanonicalFilenames(projectRoot) {
|
|
4029
|
-
const teamRoot =
|
|
4301
|
+
const teamRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
4030
4302
|
const personalRoot = resolvePersonalKnowledgeRoot();
|
|
4031
4303
|
for (const [layer, root, displayPrefix] of [
|
|
4032
4304
|
["team", teamRoot, ".fabric/knowledge"],
|
|
@@ -4036,7 +4308,7 @@ function* iterateCanonicalFilenames(projectRoot) {
|
|
|
4036
4308
|
continue;
|
|
4037
4309
|
}
|
|
4038
4310
|
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
4039
|
-
const dir =
|
|
4311
|
+
const dir = join6(root, typeDir);
|
|
4040
4312
|
if (!existsSync4(dir)) {
|
|
4041
4313
|
continue;
|
|
4042
4314
|
}
|
|
@@ -4196,7 +4468,7 @@ async function migrateBootstrapMarkers(projectRoot) {
|
|
|
4196
4468
|
const paths = [];
|
|
4197
4469
|
const countPerPath = {};
|
|
4198
4470
|
for (const rel of BOOTSTRAP_MARKER_MIGRATION_TARGETS) {
|
|
4199
|
-
const abs =
|
|
4471
|
+
const abs = join6(projectRoot, rel);
|
|
4200
4472
|
if (!existsSync4(abs)) {
|
|
4201
4473
|
continue;
|
|
4202
4474
|
}
|
|
@@ -4216,14 +4488,14 @@ async function migrateBootstrapMarkers(projectRoot) {
|
|
|
4216
4488
|
if (rewritten === original) {
|
|
4217
4489
|
continue;
|
|
4218
4490
|
}
|
|
4219
|
-
await
|
|
4491
|
+
await atomicWriteText4(abs, rewritten);
|
|
4220
4492
|
paths.push(abs);
|
|
4221
4493
|
countPerPath[abs] = replacedCount;
|
|
4222
4494
|
}
|
|
4223
4495
|
return { paths, countPerPath };
|
|
4224
4496
|
}
|
|
4225
4497
|
async function rewriteThreeEndManagedBlocks(projectRoot) {
|
|
4226
|
-
const snapshotPath =
|
|
4498
|
+
const snapshotPath = join6(projectRoot, ".fabric", "AGENTS.md");
|
|
4227
4499
|
if (!existsSync4(snapshotPath)) {
|
|
4228
4500
|
return;
|
|
4229
4501
|
}
|
|
@@ -4233,7 +4505,7 @@ async function rewriteThreeEndManagedBlocks(projectRoot) {
|
|
|
4233
4505
|
} catch {
|
|
4234
4506
|
return;
|
|
4235
4507
|
}
|
|
4236
|
-
const projectRulesPath =
|
|
4508
|
+
const projectRulesPath = join6(projectRoot, ".fabric", "project-rules.md");
|
|
4237
4509
|
const hasProjectRules = existsSync4(projectRulesPath);
|
|
4238
4510
|
let expectedBody = snapshot;
|
|
4239
4511
|
if (hasProjectRules) {
|
|
@@ -4249,8 +4521,8 @@ ${projectRules}`;
|
|
|
4249
4521
|
${expectedBody}
|
|
4250
4522
|
${BOOTSTRAP_MARKER_END}`;
|
|
4251
4523
|
const blockTargets = [
|
|
4252
|
-
|
|
4253
|
-
|
|
4524
|
+
join6(projectRoot, "AGENTS.md"),
|
|
4525
|
+
join6(projectRoot, ".cursor", "rules", "fabric-bootstrap.mdc")
|
|
4254
4526
|
];
|
|
4255
4527
|
for (const abs of blockTargets) {
|
|
4256
4528
|
if (!existsSync4(abs)) {
|
|
@@ -4281,9 +4553,9 @@ ${managedBlock}
|
|
|
4281
4553
|
if (next === existing) {
|
|
4282
4554
|
continue;
|
|
4283
4555
|
}
|
|
4284
|
-
await
|
|
4556
|
+
await atomicWriteText4(abs, next);
|
|
4285
4557
|
}
|
|
4286
|
-
const claudeMdPath =
|
|
4558
|
+
const claudeMdPath = join6(projectRoot, "CLAUDE.md");
|
|
4287
4559
|
if (existsSync4(claudeMdPath)) {
|
|
4288
4560
|
let claudeContent;
|
|
4289
4561
|
try {
|
|
@@ -4307,18 +4579,18 @@ ${managedBlock}
|
|
|
4307
4579
|
ensureLine("@.fabric/project-rules.md");
|
|
4308
4580
|
}
|
|
4309
4581
|
if (updated !== claudeContent) {
|
|
4310
|
-
await
|
|
4582
|
+
await atomicWriteText4(claudeMdPath, updated);
|
|
4311
4583
|
}
|
|
4312
4584
|
}
|
|
4313
4585
|
}
|
|
4314
4586
|
async function fixMcpConfigInWrongFile(projectRoot) {
|
|
4315
|
-
const settingsPath =
|
|
4587
|
+
const settingsPath = join6(projectRoot, ".claude", "settings.json");
|
|
4316
4588
|
if (!existsSync4(settingsPath)) {
|
|
4317
4589
|
return;
|
|
4318
4590
|
}
|
|
4319
4591
|
let settings;
|
|
4320
4592
|
try {
|
|
4321
|
-
const parsed = JSON.parse(
|
|
4593
|
+
const parsed = JSON.parse(readFileSync2(settingsPath, "utf8"));
|
|
4322
4594
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4323
4595
|
return;
|
|
4324
4596
|
}
|
|
@@ -4345,12 +4617,12 @@ async function fixMcpConfigInWrongFile(projectRoot) {
|
|
|
4345
4617
|
});
|
|
4346
4618
|
}
|
|
4347
4619
|
async function ensureKnowledgeSubdirs(projectRoot) {
|
|
4348
|
-
for (const sub of
|
|
4349
|
-
await
|
|
4620
|
+
for (const sub of KNOWLEDGE_SUBDIRS3) {
|
|
4621
|
+
await mkdir4(join6(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
|
|
4350
4622
|
}
|
|
4351
4623
|
}
|
|
4352
4624
|
async function fixCounterDesync(projectRoot) {
|
|
4353
|
-
const metaPath =
|
|
4625
|
+
const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
|
|
4354
4626
|
if (!existsSync4(metaPath)) {
|
|
4355
4627
|
return;
|
|
4356
4628
|
}
|
|
@@ -4673,7 +4945,7 @@ function normalizePath(path) {
|
|
|
4673
4945
|
return posix.normalize(path.split("\\").join("/"));
|
|
4674
4946
|
}
|
|
4675
4947
|
function collectEntryPoints(root) {
|
|
4676
|
-
if (!existsSync4(root) || !
|
|
4948
|
+
if (!existsSync4(root) || !statSync4(root).isDirectory()) {
|
|
4677
4949
|
return [];
|
|
4678
4950
|
}
|
|
4679
4951
|
const entries = [];
|
|
@@ -4684,7 +4956,7 @@ function collectEntryPoints(root) {
|
|
|
4684
4956
|
continue;
|
|
4685
4957
|
}
|
|
4686
4958
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
4687
|
-
const absolutePath =
|
|
4959
|
+
const absolutePath = join6(current, entry.name);
|
|
4688
4960
|
const relativePath = normalizePath(absolutePath.slice(root.length + 1));
|
|
4689
4961
|
if (relativePath.length === 0) {
|
|
4690
4962
|
continue;
|
|
@@ -4741,9 +5013,109 @@ function isMissingFileError(error) {
|
|
|
4741
5013
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
4742
5014
|
}
|
|
4743
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
|
+
|
|
4744
5116
|
// src/services/get-knowledge.ts
|
|
4745
5117
|
import { readFile as readFile6 } from "fs/promises";
|
|
4746
|
-
import { join as
|
|
5118
|
+
import { join as join7 } from "path";
|
|
4747
5119
|
import { minimatch as minimatch2 } from "minimatch";
|
|
4748
5120
|
var PRIORITY_ORDER = {
|
|
4749
5121
|
high: 0,
|
|
@@ -4751,6 +5123,10 @@ var PRIORITY_ORDER = {
|
|
|
4751
5123
|
low: 2
|
|
4752
5124
|
};
|
|
4753
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
|
+
}
|
|
4754
5130
|
const context = await loadGetKnowledgeContext(projectRoot);
|
|
4755
5131
|
const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
|
|
4756
5132
|
const matchedNodes = matchRuleNodes(context.meta, input.path);
|
|
@@ -4783,7 +5159,7 @@ async function loadGetKnowledgeContext(projectRoot) {
|
|
|
4783
5159
|
return cached;
|
|
4784
5160
|
}
|
|
4785
5161
|
const meta = await readAgentsMeta(projectRoot);
|
|
4786
|
-
const l0Content = await readFile6(
|
|
5162
|
+
const l0Content = await readFile6(join7(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
|
|
4787
5163
|
const context = {
|
|
4788
5164
|
meta,
|
|
4789
5165
|
l0Content,
|
|
@@ -4922,13 +5298,14 @@ async function readRuleContent(projectRoot, file, fileContentCache) {
|
|
|
4922
5298
|
if (cached !== void 0) {
|
|
4923
5299
|
return await cached;
|
|
4924
5300
|
}
|
|
4925
|
-
const pending = readFile6(
|
|
5301
|
+
const pending = readFile6(join7(projectRoot, file), "utf8");
|
|
4926
5302
|
fileContentCache.set(file, pending);
|
|
4927
5303
|
return await pending;
|
|
4928
5304
|
}
|
|
4929
5305
|
|
|
4930
5306
|
export {
|
|
4931
5307
|
contextCache,
|
|
5308
|
+
AgentsMetaFileMissingError,
|
|
4932
5309
|
resolveProjectRoot,
|
|
4933
5310
|
readAgentsMeta,
|
|
4934
5311
|
LEDGER_PATH,
|
|
@@ -4955,6 +5332,8 @@ export {
|
|
|
4955
5332
|
invalidateKnowledgeSyncCooldown,
|
|
4956
5333
|
ensureKnowledgeFresh,
|
|
4957
5334
|
reconcileKnowledge,
|
|
5335
|
+
loadActiveMeta,
|
|
5336
|
+
loadActiveMetaOrStale,
|
|
4958
5337
|
getKnowledge,
|
|
4959
5338
|
normalizeKnowledgePath,
|
|
4960
5339
|
runDoctorReport,
|