@fenglimg/fabric-server 2.0.0-rc.21 → 2.0.0-rc.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-7R6MFA7Y.js → chunk-IRB77C6E.js} +1041 -243
- package/dist/{http-JGWQGUZS.js → http-ZBV6YUHD.js} +1 -1
- package/dist/index.d.ts +37 -5
- package/dist/index.js +223 -215
- package/package.json +2 -2
|
@@ -160,8 +160,8 @@ function getLegacyLedgerPath(projectRoot) {
|
|
|
160
160
|
function getEventLedgerPath(projectRoot) {
|
|
161
161
|
return join2(projectRoot, EVENT_LEDGER_PATH);
|
|
162
162
|
}
|
|
163
|
-
async function ensureParentDirectory(
|
|
164
|
-
await mkdir(dirname(
|
|
163
|
+
async function ensureParentDirectory(path2) {
|
|
164
|
+
await mkdir(dirname(path2), { recursive: true });
|
|
165
165
|
}
|
|
166
166
|
function sha256(content) {
|
|
167
167
|
return `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
@@ -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 = {}) {
|
|
@@ -224,24 +241,24 @@ async function readEventLedger(projectRoot, options = {}) {
|
|
|
224
241
|
const events = lines.map((line) => line.trim()).filter((line) => line.length > 0).map((line, index) => parseEventLedgerLine(line, index)).filter((entry) => entry !== null).filter((entry) => options.event_type === void 0 || entry.event_type === options.event_type).filter((entry) => options.since === void 0 || entry.ts >= options.since).filter((entry) => options.correlation_id === void 0 || entry.correlation_id === options.correlation_id).filter((entry) => options.session_id === void 0 || entry.session_id === options.session_id);
|
|
225
242
|
return { events, warnings };
|
|
226
243
|
}
|
|
227
|
-
async function truncateLedgerToLastNewline(
|
|
228
|
-
const raw = await readFile2(
|
|
244
|
+
async function truncateLedgerToLastNewline(path2) {
|
|
245
|
+
const raw = await readFile2(path2);
|
|
229
246
|
const content = raw.toString("utf8");
|
|
230
247
|
if (content.endsWith("\n") || content.length === 0) {
|
|
231
248
|
return { truncated_bytes: 0, corrupted_path: "" };
|
|
232
249
|
}
|
|
233
250
|
const lastNewlineIndex = content.lastIndexOf("\n");
|
|
234
251
|
if (lastNewlineIndex === -1) {
|
|
235
|
-
const corruptedPath2 = `${
|
|
252
|
+
const corruptedPath2 = `${path2}.corrupted.${Date.now()}`;
|
|
236
253
|
await writeFile(corruptedPath2, raw);
|
|
237
|
-
await truncate(
|
|
254
|
+
await truncate(path2, 0);
|
|
238
255
|
return { truncated_bytes: raw.length, corrupted_path: corruptedPath2 };
|
|
239
256
|
}
|
|
240
257
|
const keepByteLength = Buffer.byteLength(content.slice(0, lastNewlineIndex + 1), "utf8");
|
|
241
258
|
const corruptedBytes = raw.slice(keepByteLength);
|
|
242
|
-
const corruptedPath = `${
|
|
259
|
+
const corruptedPath = `${path2}.corrupted.${Date.now()}`;
|
|
243
260
|
await writeFile(corruptedPath, corruptedBytes);
|
|
244
|
-
await truncate(
|
|
261
|
+
await truncate(path2, keepByteLength);
|
|
245
262
|
return { truncated_bytes: corruptedBytes.length, corrupted_path: corruptedPath };
|
|
246
263
|
}
|
|
247
264
|
function parseEventLedgerLine(line, index) {
|
|
@@ -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()) {
|
|
@@ -739,8 +870,8 @@ function flattenKeys(value, keys = {}) {
|
|
|
739
870
|
}
|
|
740
871
|
return keys;
|
|
741
872
|
}
|
|
742
|
-
function toPosixPath(
|
|
743
|
-
return
|
|
873
|
+
function toPosixPath(path2) {
|
|
874
|
+
return path2.split(sep2).join("/");
|
|
744
875
|
}
|
|
745
876
|
function deriveRuleIdentity(file, source, existing) {
|
|
746
877
|
const declaredKnowledgeId = extractDeclaredKnowledgeId(source);
|
|
@@ -802,27 +933,33 @@ 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) {
|
|
825
|
-
const sections = Array.from(source.matchAll(
|
|
962
|
+
const sections = Array.from(source.matchAll(/^#{2,6}\s+(.+?)\s*$/gmu)).map((match) => match[1].trim()).filter((section, index, all) => section.length > 0 && all.indexOf(section) === index);
|
|
826
963
|
return sections.length > 0 ? sections : void 0;
|
|
827
964
|
}
|
|
828
965
|
function extractDescriptionFromFrontmatter(frontmatter) {
|
|
@@ -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 {
|
|
@@ -985,42 +1147,46 @@ async function readMetaEntries(projectRoot) {
|
|
|
985
1147
|
return map;
|
|
986
1148
|
}
|
|
987
1149
|
for (const node of Object.values(parsed.nodes ?? {})) {
|
|
988
|
-
const
|
|
1150
|
+
const path2 = node.content_ref ?? node.file;
|
|
989
1151
|
const stable_id = node.stable_id;
|
|
990
1152
|
const content_hash = node.hash;
|
|
991
|
-
if (
|
|
992
|
-
map.set(
|
|
1153
|
+
if (path2 !== void 0 && stable_id !== void 0 && content_hash !== void 0) {
|
|
1154
|
+
map.set(path2, { stable_id, path: path2, content_hash });
|
|
993
1155
|
}
|
|
994
1156
|
}
|
|
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,28 @@ 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
|
+
const forceWriteForDescriptionHeal = trigger === "auto-heal-description";
|
|
1411
|
+
if (events.length > 0 || revisionDrift || forceWriteForDescriptionHeal) {
|
|
1235
1412
|
await writeKnowledgeMeta(projectRoot, { source: "sync_meta" });
|
|
1236
|
-
|
|
1413
|
+
if (events.length > 0) {
|
|
1414
|
+
await appendRuleSyncEvents(projectRoot, events);
|
|
1415
|
+
}
|
|
1237
1416
|
contextCache.invalidate("file_watch", projectRoot);
|
|
1417
|
+
contextCache.invalidate("meta_write", projectRoot);
|
|
1238
1418
|
}
|
|
1239
1419
|
const duration_ms = Date.now() - startTime;
|
|
1240
1420
|
const reconciledFiles = events.map((e) => e.path);
|
|
1241
|
-
if (trigger !== void 0 && events.length > 0) {
|
|
1421
|
+
if (trigger !== void 0 && (events.length > 0 || revisionDrift || forceWriteForDescriptionHeal)) {
|
|
1242
1422
|
if (trigger === "startup") {
|
|
1243
1423
|
await appendEventLedgerEvent(projectRoot, {
|
|
1244
1424
|
event_type: "meta_reconciled_on_startup",
|
|
@@ -1252,11 +1432,12 @@ async function reconcileKnowledge(projectRoot, opts) {
|
|
|
1252
1432
|
reconciled_files: reconciledFiles,
|
|
1253
1433
|
duration_ms,
|
|
1254
1434
|
trigger,
|
|
1255
|
-
source: "reconcileKnowledge"
|
|
1435
|
+
source: "reconcileKnowledge",
|
|
1436
|
+
...events.length === 0 && revisionDrift ? { force_write_reason: "revision_drift" } : {}
|
|
1256
1437
|
});
|
|
1257
1438
|
}
|
|
1258
1439
|
}
|
|
1259
|
-
if (events.length === 0 && warnings.length === 0) {
|
|
1440
|
+
if (events.length === 0 && warnings.length === 0 && !revisionDrift) {
|
|
1260
1441
|
return { status: "fresh", events: [], warnings: [] };
|
|
1261
1442
|
}
|
|
1262
1443
|
const status = warnings.length > 0 ? "errors" : "reconciled";
|
|
@@ -1268,13 +1449,106 @@ async function reconcileKnowledge(projectRoot, opts) {
|
|
|
1268
1449
|
};
|
|
1269
1450
|
}
|
|
1270
1451
|
|
|
1452
|
+
// src/services/serve-lock.ts
|
|
1453
|
+
import fs from "fs";
|
|
1454
|
+
import path from "path";
|
|
1455
|
+
import { createTranslator, detectNodeLocale } from "@fenglimg/fabric-shared";
|
|
1456
|
+
import { IOFabricError as IOFabricError2 } from "@fenglimg/fabric-shared/errors";
|
|
1457
|
+
var LOCK_FILENAME = ".serve.lock";
|
|
1458
|
+
var t = createTranslator(detectNodeLocale());
|
|
1459
|
+
var ServeLockHeldError = class extends IOFabricError2 {
|
|
1460
|
+
code = "SERVE_LOCK_HELD";
|
|
1461
|
+
httpStatus = 423;
|
|
1462
|
+
};
|
|
1463
|
+
function lockPath(projectRoot) {
|
|
1464
|
+
return path.join(projectRoot, ".fabric", LOCK_FILENAME);
|
|
1465
|
+
}
|
|
1466
|
+
function isAlive(pid) {
|
|
1467
|
+
try {
|
|
1468
|
+
process.kill(pid, 0);
|
|
1469
|
+
return true;
|
|
1470
|
+
} catch (e) {
|
|
1471
|
+
const err = e;
|
|
1472
|
+
if (err.code === "ESRCH") return false;
|
|
1473
|
+
if (err.code === "EPERM") return true;
|
|
1474
|
+
throw e;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
function acquireLock(projectRoot, opts) {
|
|
1478
|
+
const p = lockPath(projectRoot);
|
|
1479
|
+
if (fs.existsSync(p)) {
|
|
1480
|
+
let state = null;
|
|
1481
|
+
try {
|
|
1482
|
+
state = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1483
|
+
} catch {
|
|
1484
|
+
}
|
|
1485
|
+
if (state && state.pid && state.pid !== process.pid && isAlive(state.pid) && !opts?.force) {
|
|
1486
|
+
throw new ServeLockHeldError(
|
|
1487
|
+
`serve lock held by live PID ${state.pid}`,
|
|
1488
|
+
{
|
|
1489
|
+
actionHint: t("cli.serve.lock-held.action-hint", { pid: String(state.pid) }),
|
|
1490
|
+
details: state
|
|
1491
|
+
}
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
if (state && state.pid && !isAlive(state.pid)) {
|
|
1495
|
+
process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 overwriting
|
|
1496
|
+
`);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
1500
|
+
fs.writeFileSync(
|
|
1501
|
+
p,
|
|
1502
|
+
JSON.stringify({ pid: process.pid, acquiredAt: Date.now(), host: process.env.HOSTNAME })
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
function releaseLock(projectRoot) {
|
|
1506
|
+
const p = lockPath(projectRoot);
|
|
1507
|
+
try {
|
|
1508
|
+
if (fs.existsSync(p)) {
|
|
1509
|
+
const state = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1510
|
+
if (state.pid === process.pid) {
|
|
1511
|
+
fs.unlinkSync(p);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
} catch {
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
function readLockState(projectRoot) {
|
|
1518
|
+
const p = lockPath(projectRoot);
|
|
1519
|
+
if (!fs.existsSync(p)) return null;
|
|
1520
|
+
try {
|
|
1521
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1522
|
+
} catch {
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
function checkLockOrThrow(projectRoot, opts) {
|
|
1527
|
+
const state = readLockState(projectRoot);
|
|
1528
|
+
if (state === null) return;
|
|
1529
|
+
if (state.pid === process.pid) return;
|
|
1530
|
+
if (!isAlive(state.pid)) {
|
|
1531
|
+
process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 ignoring
|
|
1532
|
+
`);
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
if (opts?.force) return;
|
|
1536
|
+
throw new ServeLockHeldError(
|
|
1537
|
+
`serve lock held by live PID ${state.pid}`,
|
|
1538
|
+
{
|
|
1539
|
+
actionHint: t("cli.serve.lock-held.action-hint", { pid: String(state.pid) }),
|
|
1540
|
+
details: state
|
|
1541
|
+
}
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1271
1545
|
// src/services/doctor.ts
|
|
1272
1546
|
import { execFileSync } from "child_process";
|
|
1273
|
-
import { existsSync as existsSync4, readdirSync, readFileSync, statSync as
|
|
1274
|
-
import { access, mkdir as
|
|
1547
|
+
import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync2, statSync as statSync4 } from "fs";
|
|
1548
|
+
import { access, mkdir as mkdir4, readFile as readFile5, rename, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
1275
1549
|
import { constants } from "fs";
|
|
1276
|
-
import { homedir as
|
|
1277
|
-
import { isAbsolute as isAbsolute2, join as
|
|
1550
|
+
import { homedir as homedir3 } from "os";
|
|
1551
|
+
import { isAbsolute as isAbsolute2, join as join6, posix, relative as nodeRelative, resolve as resolve3, sep as sep3 } from "path";
|
|
1278
1552
|
import { minimatch } from "minimatch";
|
|
1279
1553
|
import {
|
|
1280
1554
|
agentsMetaSchema as agentsMetaSchema4,
|
|
@@ -1286,10 +1560,12 @@ import {
|
|
|
1286
1560
|
BOOTSTRAP_CANONICAL,
|
|
1287
1561
|
BOOTSTRAP_MARKER_BEGIN,
|
|
1288
1562
|
BOOTSTRAP_MARKER_END,
|
|
1289
|
-
BOOTSTRAP_REGEX
|
|
1563
|
+
BOOTSTRAP_REGEX,
|
|
1564
|
+
ONBOARD_SLOT_NAMES,
|
|
1565
|
+
ONBOARD_SLOT_TOTAL
|
|
1290
1566
|
} from "@fenglimg/fabric-shared";
|
|
1291
1567
|
import { detectFramework } from "@fenglimg/fabric-shared/node";
|
|
1292
|
-
import { atomicWriteJson as atomicWriteJson2, atomicWriteText as
|
|
1568
|
+
import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText4 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
1293
1569
|
var ORPHAN_DEMOTE_THRESHOLD_DAYS = {
|
|
1294
1570
|
stable: 90,
|
|
1295
1571
|
endorsed: 30,
|
|
@@ -1327,7 +1603,22 @@ var KNOWLEDGE_CANONICAL_TYPE_DIRS = [
|
|
|
1327
1603
|
"processes"
|
|
1328
1604
|
];
|
|
1329
1605
|
var CANONICAL_KNOWLEDGE_FILENAME_PATTERN = /^(K[PT]-(?:MOD|DEC|GLD|PIT|PRO)-\d{4,})--[a-z0-9][a-z0-9-]*\.md$/u;
|
|
1330
|
-
var
|
|
1606
|
+
var KNOWLEDGE_SUBDIRS3 = ["decisions", "pitfalls", "guidelines", "models", "processes", "pending"];
|
|
1607
|
+
var BASELINE_FILENAME_LINT_BASELINE_IDS = /* @__PURE__ */ new Set([
|
|
1608
|
+
"KT-MOD-0001",
|
|
1609
|
+
// tech-stack
|
|
1610
|
+
"KT-MOD-0002",
|
|
1611
|
+
// module-structure
|
|
1612
|
+
"KT-MOD-0003",
|
|
1613
|
+
// readme-first-paragraph
|
|
1614
|
+
"KT-PRO-0001",
|
|
1615
|
+
// build-config
|
|
1616
|
+
"KT-PRO-0002",
|
|
1617
|
+
// ci-config
|
|
1618
|
+
"KT-GLD-0001"
|
|
1619
|
+
// code-style
|
|
1620
|
+
]);
|
|
1621
|
+
var BASELINE_ID_PREFIXED_FILENAME_PATTERN = /^KT-[A-Z]+-\d+--.+\.md$/u;
|
|
1331
1622
|
var COUNTER_TYPE_CODES = ["MOD", "DEC", "GLD", "PIT", "PRO"];
|
|
1332
1623
|
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
1333
1624
|
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
@@ -1386,6 +1677,7 @@ async function runDoctorReport(target) {
|
|
|
1386
1677
|
const metaManuallyDiverged = await inspectMetaManuallyDiverged(projectRoot);
|
|
1387
1678
|
const knowledgeDirUnindexed = inspectKnowledgeDirUnindexed(projectRoot, meta);
|
|
1388
1679
|
const knowledgeDirMissing = inspectKnowledgeDirMissing(projectRoot);
|
|
1680
|
+
const baselineFilenameFormat = inspectBaselineFilenameFormat(projectRoot);
|
|
1389
1681
|
const stableIdCollision = await inspectStableIdCollisions(projectRoot);
|
|
1390
1682
|
const counterDesync = inspectCounterDesync(meta);
|
|
1391
1683
|
const preexistingRootFiles = inspectPreexistingRootFiles(projectRoot);
|
|
@@ -1404,8 +1696,10 @@ async function runDoctorReport(target) {
|
|
|
1404
1696
|
const relevancePathsDrift = inspectRelevancePathsDrift(projectRoot);
|
|
1405
1697
|
const narrowTooFew = inspectNarrowTooFew(projectRoot, lintNow);
|
|
1406
1698
|
const sessionHintsStale = inspectSessionHintsStale(projectRoot, lintNow);
|
|
1699
|
+
const staleServeLock = inspectStaleServeLock(projectRoot, lintNow);
|
|
1407
1700
|
const relevanceFieldsMissing = inspectRelevanceFieldsMissing(projectRoot);
|
|
1408
1701
|
const skillMdYamlInvalid = inspectSkillMdYamlInvalid(projectRoot);
|
|
1702
|
+
const onboardCoverage = inspectOnboardCoverage(projectRoot);
|
|
1409
1703
|
const checks = [
|
|
1410
1704
|
createBootstrapAnchorCheck(bootstrapAnchor),
|
|
1411
1705
|
// v2.0.0-rc.19 TASK-004: bootstrap marker migration check sits adjacent to
|
|
@@ -1418,6 +1712,11 @@ async function runDoctorReport(target) {
|
|
|
1418
1712
|
createL1BootstrapSnapshotDriftCheck(l1BootstrapSnapshotDrift),
|
|
1419
1713
|
createL2ManagedBlockDriftCheck(l2ManagedBlockDrift),
|
|
1420
1714
|
createKnowledgeDirMissingCheck(knowledgeDirMissing),
|
|
1715
|
+
// v2.0.0-rc.22 TASK-006: baseline filename format. Sits adjacent to
|
|
1716
|
+
// knowledge_dir_missing — both are knowledge-layout invariants. manual_error
|
|
1717
|
+
// kind; resolution is manual file deletion (rc.23 TASK-012 (F8a) removed
|
|
1718
|
+
// the baseline-emit pipeline, so no auto-fix exists).
|
|
1719
|
+
createBaselineFilenameFormatCheck(baselineFilenameFormat),
|
|
1421
1720
|
createForensicCheck(forensic, framework.kind, entryPoints.length),
|
|
1422
1721
|
// v2.0: removed `createInitContextCheck` — `.fabric/init-context.json`
|
|
1423
1722
|
// is owned by the AI-side client init skill, not by `fabric install` CLI.
|
|
@@ -1470,6 +1769,10 @@ async function runDoctorReport(target) {
|
|
|
1470
1769
|
createNarrowTooFewCheck(narrowTooFew),
|
|
1471
1770
|
// rc.6 TASK-021 (E3): session-hints cache hygiene (lint #27). Info kind.
|
|
1472
1771
|
createSessionHintsStaleCheck(sessionHintsStale),
|
|
1772
|
+
// rc.23 TASK-010 (e): stale .fabric/.serve.lock advisory. Info kind —
|
|
1773
|
+
// does not bump report status. `--fix` unlinks the corpse and emits
|
|
1774
|
+
// `serve_lock_cleared`.
|
|
1775
|
+
createStaleServeLockCheck(staleServeLock),
|
|
1473
1776
|
// v2.0.0-rc.9 TASK-003 (A3): relevance fields back-fill (lint #28).
|
|
1474
1777
|
// Info kind — applies to pending entries only; canonical entries get
|
|
1475
1778
|
// the fields written verbatim by fab_review.approve/modify.
|
|
@@ -1477,6 +1780,11 @@ async function runDoctorReport(target) {
|
|
|
1477
1780
|
// rc.12 lint #29: skill_md_yaml_invalid. Warning kind — surfaces
|
|
1478
1781
|
// SKILL.md frontmatter that Codex CLI silently drops at load.
|
|
1479
1782
|
createSkillMdYamlInvalidCheck(skillMdYamlInvalid),
|
|
1783
|
+
// v2.0.0-rc.23 TASK-014 (F8c): Onboard coverage advisory. Info kind.
|
|
1784
|
+
// Surfaces uncovered S5 onboard slots and recommends /fabric-archive
|
|
1785
|
+
// first-run phase. Sits adjacent to Skill markdown YAML — both are
|
|
1786
|
+
// Skill-adjacent advisories. --fix never mutates onboard state.
|
|
1787
|
+
createOnboardCoverageCheck(onboardCoverage),
|
|
1480
1788
|
createPreexistingRootFilesCheck(preexistingRootFiles)
|
|
1481
1789
|
// v2.0 / rc.2: `createLegacyClientPathCheck` removed. The schema now
|
|
1482
1790
|
// rejects retired clientPaths keys (windsurf/rooCode/geminiCLI) at Zod
|
|
@@ -1517,7 +1825,7 @@ async function runDoctorReport(target) {
|
|
|
1517
1825
|
warningCount: warnings.length,
|
|
1518
1826
|
infoCount: infos.length,
|
|
1519
1827
|
targetFiles: Object.fromEntries(
|
|
1520
|
-
TARGET_FILE_PATHS.map((
|
|
1828
|
+
TARGET_FILE_PATHS.map((path2) => [path2, existsSync4(join6(projectRoot, path2))])
|
|
1521
1829
|
)
|
|
1522
1830
|
}
|
|
1523
1831
|
};
|
|
@@ -1531,11 +1839,11 @@ async function runDoctorFix(target) {
|
|
|
1531
1839
|
)) {
|
|
1532
1840
|
const migrated = await migrateBootstrapMarkers(projectRoot);
|
|
1533
1841
|
fixed.push(findIssue(before.fixable_errors, "bootstrap_marker_migration_required"));
|
|
1534
|
-
for (const
|
|
1842
|
+
for (const path2 of migrated.paths) {
|
|
1535
1843
|
await appendEventLedgerEvent(projectRoot, {
|
|
1536
1844
|
event_type: "bootstrap_marker_migrated",
|
|
1537
|
-
path,
|
|
1538
|
-
migrated_count: migrated.countPerPath[
|
|
1845
|
+
path: path2,
|
|
1846
|
+
migrated_count: migrated.countPerPath[path2] ?? 1,
|
|
1539
1847
|
legacy_marker: "fabric:knowledge-base",
|
|
1540
1848
|
new_marker: "fabric:bootstrap",
|
|
1541
1849
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -1544,9 +1852,9 @@ async function runDoctorFix(target) {
|
|
|
1544
1852
|
}
|
|
1545
1853
|
}
|
|
1546
1854
|
if (before.fixable_errors.some((issue) => issue.code === "bootstrap_snapshot_drift")) {
|
|
1547
|
-
const snapshotPath =
|
|
1855
|
+
const snapshotPath = join6(projectRoot, ".fabric", "AGENTS.md");
|
|
1548
1856
|
await ensureParentDirectory(snapshotPath);
|
|
1549
|
-
await
|
|
1857
|
+
await atomicWriteText4(snapshotPath, BOOTSTRAP_CANONICAL);
|
|
1550
1858
|
fixed.push(findIssue(before.fixable_errors, "bootstrap_snapshot_drift"));
|
|
1551
1859
|
}
|
|
1552
1860
|
if (before.fixable_errors.some((issue) => issue.code === "managed_block_drift")) {
|
|
@@ -1566,26 +1874,23 @@ async function runDoctorFix(target) {
|
|
|
1566
1874
|
fixed.push(findIssue(before.fixable_errors, "counter_desync"));
|
|
1567
1875
|
contextCache.invalidate("meta_write", projectRoot);
|
|
1568
1876
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
)) {
|
|
1877
|
+
const reconcileCodes = [
|
|
1878
|
+
"agents_meta_missing",
|
|
1879
|
+
"agents_meta_stale",
|
|
1880
|
+
"knowledge_test_index_missing",
|
|
1881
|
+
"knowledge_test_index_stale",
|
|
1882
|
+
"content_ref_missing",
|
|
1883
|
+
"knowledge_dir_unindexed"
|
|
1884
|
+
];
|
|
1885
|
+
if (before.fixable_errors.some((issue) => reconcileCodes.includes(issue.code)) || before.warnings.some((issue) => reconcileCodes.includes(issue.code))) {
|
|
1579
1886
|
await reconcileKnowledge(projectRoot, { trigger: "doctor" });
|
|
1580
1887
|
for (const issue of before.fixable_errors.filter(
|
|
1581
|
-
(candidate) =>
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
"knowledge_dir_unindexed"
|
|
1588
|
-
].includes(candidate.code)
|
|
1888
|
+
(candidate) => reconcileCodes.includes(candidate.code)
|
|
1889
|
+
)) {
|
|
1890
|
+
fixed.push(issue);
|
|
1891
|
+
}
|
|
1892
|
+
for (const issue of before.warnings.filter(
|
|
1893
|
+
(candidate) => reconcileCodes.includes(candidate.code)
|
|
1589
1894
|
)) {
|
|
1590
1895
|
fixed.push(issue);
|
|
1591
1896
|
}
|
|
@@ -1604,10 +1909,44 @@ async function runDoctorFix(target) {
|
|
|
1604
1909
|
});
|
|
1605
1910
|
fixed.push(findIssue(before.fixable_errors, "event_ledger_partial_write"));
|
|
1606
1911
|
}
|
|
1912
|
+
const rotateResult = await rotateEventLedgerIfNeeded(projectRoot);
|
|
1913
|
+
if (rotateResult.rotated && rotateResult.archivedCount > 0) {
|
|
1914
|
+
fixed.push({
|
|
1915
|
+
code: "event_ledger_rotated",
|
|
1916
|
+
name: "Event ledger rotated",
|
|
1917
|
+
message: `Rotated ${rotateResult.archivedCount} event(s) older than retention window to ${rotateResult.archivePath ?? "archive"}`,
|
|
1918
|
+
path: rotateResult.archivePath
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1607
1921
|
if (before.fixable_errors.some((issue) => issue.code === "mcp_config_in_wrong_file")) {
|
|
1608
1922
|
await fixMcpConfigInWrongFile(projectRoot);
|
|
1609
1923
|
fixed.push(findIssue(before.fixable_errors, "mcp_config_in_wrong_file"));
|
|
1610
1924
|
}
|
|
1925
|
+
if (before.infos.some((issue) => issue.code === "stale_serve_lock")) {
|
|
1926
|
+
const lockInspection = inspectStaleServeLock(projectRoot, Date.now());
|
|
1927
|
+
if (lockInspection.present && !lockInspection.pidAlive) {
|
|
1928
|
+
const lockFilePath = join6(projectRoot, ".fabric", ".serve.lock");
|
|
1929
|
+
try {
|
|
1930
|
+
await unlink(lockFilePath);
|
|
1931
|
+
} catch (err) {
|
|
1932
|
+
const errno = err;
|
|
1933
|
+
if (errno.code !== "ENOENT") throw err;
|
|
1934
|
+
}
|
|
1935
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1936
|
+
event_type: "serve_lock_cleared",
|
|
1937
|
+
pid: lockInspection.pid,
|
|
1938
|
+
age_ms: lockInspection.ageMs,
|
|
1939
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1940
|
+
}).catch(() => {
|
|
1941
|
+
});
|
|
1942
|
+
fixed.push({
|
|
1943
|
+
code: "stale_serve_lock",
|
|
1944
|
+
name: "Serve lock",
|
|
1945
|
+
message: `Removed stale .fabric/.serve.lock (dead PID ${lockInspection.pid}).`,
|
|
1946
|
+
path: ".fabric/.serve.lock"
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1611
1950
|
const report = await runDoctorReport(projectRoot);
|
|
1612
1951
|
return {
|
|
1613
1952
|
changed: fixed.length > 0,
|
|
@@ -1743,7 +2082,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
|
|
|
1743
2082
|
};
|
|
1744
2083
|
}
|
|
1745
2084
|
const detail = `${candidate.maturity} -> ${next}`;
|
|
1746
|
-
const absPath =
|
|
2085
|
+
const absPath = join6(projectRoot, candidate.path);
|
|
1747
2086
|
try {
|
|
1748
2087
|
const source = await readFile5(absPath, "utf8");
|
|
1749
2088
|
const rewritten = rewriteFrontmatterMaturity(source, next);
|
|
@@ -1765,7 +2104,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
|
|
|
1765
2104
|
error: "rewrite produced byte-identical output"
|
|
1766
2105
|
};
|
|
1767
2106
|
}
|
|
1768
|
-
await
|
|
2107
|
+
await atomicWriteText4(absPath, rewritten);
|
|
1769
2108
|
try {
|
|
1770
2109
|
await appendEventLedgerEvent(projectRoot, {
|
|
1771
2110
|
event_type: "knowledge_demoted",
|
|
@@ -1775,7 +2114,7 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
|
|
|
1775
2114
|
});
|
|
1776
2115
|
} catch (ledgerError) {
|
|
1777
2116
|
try {
|
|
1778
|
-
await
|
|
2117
|
+
await atomicWriteText4(absPath, source);
|
|
1779
2118
|
} catch (rollbackError) {
|
|
1780
2119
|
return {
|
|
1781
2120
|
kind: "knowledge_orphan_demote_required",
|
|
@@ -1810,19 +2149,19 @@ async function applyOrphanDemote(projectRoot, candidate, now) {
|
|
|
1810
2149
|
}
|
|
1811
2150
|
}
|
|
1812
2151
|
async function applyStaleArchive(projectRoot, candidate, now) {
|
|
1813
|
-
const sourceAbs =
|
|
1814
|
-
const destAbs =
|
|
2152
|
+
const sourceAbs = join6(projectRoot, candidate.path);
|
|
2153
|
+
const destAbs = join6(projectRoot, candidate.archive_path);
|
|
1815
2154
|
const detail = `${candidate.path} -> ${candidate.archive_path}`;
|
|
1816
2155
|
try {
|
|
1817
|
-
await
|
|
2156
|
+
await mkdir4(join6(destAbs, ".."), { recursive: true });
|
|
1818
2157
|
try {
|
|
1819
2158
|
await rename(sourceAbs, destAbs);
|
|
1820
2159
|
} catch (renameError) {
|
|
1821
2160
|
if (renameError instanceof Error && "code" in renameError && renameError.code === "EXDEV") {
|
|
1822
2161
|
const data = await readFile5(sourceAbs);
|
|
1823
2162
|
await writeFile2(destAbs, data);
|
|
1824
|
-
const { unlink } = await import("fs/promises");
|
|
1825
|
-
await
|
|
2163
|
+
const { unlink: unlink2 } = await import("fs/promises");
|
|
2164
|
+
await unlink2(sourceAbs);
|
|
1826
2165
|
} else {
|
|
1827
2166
|
throw renameError;
|
|
1828
2167
|
}
|
|
@@ -1873,7 +2212,7 @@ async function applyStaleArchive(projectRoot, candidate, now) {
|
|
|
1873
2212
|
async function applyPendingAutoArchive(projectRoot, candidate, now) {
|
|
1874
2213
|
const detail = `${candidate.pending_path} -> ${candidate.archived_to}`;
|
|
1875
2214
|
try {
|
|
1876
|
-
await
|
|
2215
|
+
await mkdir4(join6(candidate.archived_to_abs, ".."), { recursive: true });
|
|
1877
2216
|
let moved = false;
|
|
1878
2217
|
if (candidate.layer === "team") {
|
|
1879
2218
|
try {
|
|
@@ -1894,8 +2233,8 @@ async function applyPendingAutoArchive(projectRoot, candidate, now) {
|
|
|
1894
2233
|
if (renameError instanceof Error && "code" in renameError && renameError.code === "EXDEV") {
|
|
1895
2234
|
const data = await readFile5(candidate.pending_path_abs);
|
|
1896
2235
|
await writeFile2(candidate.archived_to_abs, data);
|
|
1897
|
-
const { unlink } = await import("fs/promises");
|
|
1898
|
-
await
|
|
2236
|
+
const { unlink: unlink2 } = await import("fs/promises");
|
|
2237
|
+
await unlink2(candidate.pending_path_abs);
|
|
1899
2238
|
} else {
|
|
1900
2239
|
throw renameError;
|
|
1901
2240
|
}
|
|
@@ -1946,14 +2285,14 @@ async function applyPendingAutoArchive(projectRoot, candidate, now) {
|
|
|
1946
2285
|
}
|
|
1947
2286
|
function relativePosix(projectRoot, absolutePath) {
|
|
1948
2287
|
const rel = nodeRelative(projectRoot, absolutePath);
|
|
1949
|
-
return rel.split(
|
|
2288
|
+
return rel.split(sep3).join("/");
|
|
1950
2289
|
}
|
|
1951
2290
|
async function applySessionHintsStaleCleanup(projectRoot, candidate) {
|
|
1952
2291
|
const detail = `deleted (${candidate.age_days}d old)`;
|
|
1953
|
-
const absPath =
|
|
2292
|
+
const absPath = join6(projectRoot, candidate.path);
|
|
1954
2293
|
try {
|
|
1955
|
-
const { unlink } = await import("fs/promises");
|
|
1956
|
-
await
|
|
2294
|
+
const { unlink: unlink2 } = await import("fs/promises");
|
|
2295
|
+
await unlink2(absPath);
|
|
1957
2296
|
return {
|
|
1958
2297
|
kind: "knowledge_session_hints_stale_cleanup",
|
|
1959
2298
|
path: candidate.path,
|
|
@@ -1971,7 +2310,7 @@ async function applySessionHintsStaleCleanup(projectRoot, candidate) {
|
|
|
1971
2310
|
}
|
|
1972
2311
|
}
|
|
1973
2312
|
async function applyIndexDriftFix(projectRoot, inspection) {
|
|
1974
|
-
const metaPath =
|
|
2313
|
+
const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
|
|
1975
2314
|
const detailParts = [];
|
|
1976
2315
|
try {
|
|
1977
2316
|
const meta = agentsMetaSchema4.parse(JSON.parse(await readFile5(metaPath, "utf8")));
|
|
@@ -2007,9 +2346,9 @@ function truncateErrorMessage(error) {
|
|
|
2007
2346
|
return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
|
|
2008
2347
|
}
|
|
2009
2348
|
async function inspectForensic(projectRoot) {
|
|
2010
|
-
const
|
|
2349
|
+
const path2 = join6(projectRoot, ".fabric", "forensic.json");
|
|
2011
2350
|
try {
|
|
2012
|
-
const parsed = forensicReportSchema.parse(JSON.parse(await readFile5(
|
|
2351
|
+
const parsed = forensicReportSchema.parse(JSON.parse(await readFile5(path2, "utf8")));
|
|
2013
2352
|
return { present: true, valid: true, report: parsed };
|
|
2014
2353
|
} catch (error) {
|
|
2015
2354
|
if (isMissingFileError(error)) {
|
|
@@ -2019,12 +2358,12 @@ async function inspectForensic(projectRoot) {
|
|
|
2019
2358
|
}
|
|
2020
2359
|
}
|
|
2021
2360
|
function inspectMcpConfigInWrongFile(projectRoot) {
|
|
2022
|
-
const settingsPath =
|
|
2361
|
+
const settingsPath = join6(projectRoot, ".claude", "settings.json");
|
|
2023
2362
|
if (!existsSync4(settingsPath)) {
|
|
2024
2363
|
return { hasWrongEntry: false, settingsPath };
|
|
2025
2364
|
}
|
|
2026
2365
|
try {
|
|
2027
|
-
const parsed = JSON.parse(
|
|
2366
|
+
const parsed = JSON.parse(readFileSync2(settingsPath, "utf8"));
|
|
2028
2367
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2029
2368
|
return { hasWrongEntry: false, settingsPath };
|
|
2030
2369
|
}
|
|
@@ -2040,7 +2379,7 @@ function inspectMcpConfigInWrongFile(projectRoot) {
|
|
|
2040
2379
|
}
|
|
2041
2380
|
}
|
|
2042
2381
|
async function inspectMeta(projectRoot) {
|
|
2043
|
-
const metaPath =
|
|
2382
|
+
const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
|
|
2044
2383
|
const built = await tryBuildRuleMeta(projectRoot);
|
|
2045
2384
|
try {
|
|
2046
2385
|
const raw = await readFile5(metaPath, "utf8");
|
|
@@ -2113,22 +2452,22 @@ function inspectContentRefs(projectRoot, meta) {
|
|
|
2113
2452
|
if (isPersonalKnowledge) {
|
|
2114
2453
|
continue;
|
|
2115
2454
|
}
|
|
2116
|
-
if (!existsSync4(
|
|
2455
|
+
if (!existsSync4(join6(projectRoot, contentRef))) {
|
|
2117
2456
|
missing.push(contentRef);
|
|
2118
2457
|
}
|
|
2119
2458
|
}
|
|
2120
2459
|
return { missing, invalid };
|
|
2121
2460
|
}
|
|
2122
2461
|
async function inspectEventLedger(projectRoot) {
|
|
2123
|
-
const
|
|
2124
|
-
const exists = existsSync4(
|
|
2462
|
+
const path2 = getEventLedgerPath(projectRoot);
|
|
2463
|
+
const exists = existsSync4(path2);
|
|
2125
2464
|
if (!exists) {
|
|
2126
|
-
return { exists: false, writable: false, parseable: false, hasPartialWrite: false, partialWriteByteOffset: 0, partialWriteByteLength: 0, path };
|
|
2465
|
+
return { exists: false, writable: false, parseable: false, hasPartialWrite: false, partialWriteByteOffset: 0, partialWriteByteLength: 0, path: path2 };
|
|
2127
2466
|
}
|
|
2128
2467
|
try {
|
|
2129
|
-
await access(
|
|
2468
|
+
await access(path2, constants.W_OK);
|
|
2130
2469
|
const { warnings } = await readEventLedger(projectRoot);
|
|
2131
|
-
const raw = await readFile5(
|
|
2470
|
+
const raw = await readFile5(path2, "utf8");
|
|
2132
2471
|
const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
|
|
2133
2472
|
const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
|
|
2134
2473
|
return {
|
|
@@ -2138,7 +2477,7 @@ async function inspectEventLedger(projectRoot) {
|
|
|
2138
2477
|
hasPartialWrite: partialWarning !== void 0,
|
|
2139
2478
|
partialWriteByteOffset: partialWarning?.byte_offset ?? 0,
|
|
2140
2479
|
partialWriteByteLength: partialWarning?.byte_length ?? 0,
|
|
2141
|
-
path,
|
|
2480
|
+
path: path2,
|
|
2142
2481
|
error: invalidLine === void 0 ? void 0 : "events.jsonl contains an invalid JSON line."
|
|
2143
2482
|
};
|
|
2144
2483
|
} catch (error) {
|
|
@@ -2149,16 +2488,16 @@ async function inspectEventLedger(projectRoot) {
|
|
|
2149
2488
|
hasPartialWrite: false,
|
|
2150
2489
|
partialWriteByteOffset: 0,
|
|
2151
2490
|
partialWriteByteLength: 0,
|
|
2152
|
-
path,
|
|
2491
|
+
path: path2,
|
|
2153
2492
|
error: error instanceof Error ? error.message : String(error)
|
|
2154
2493
|
};
|
|
2155
2494
|
}
|
|
2156
2495
|
}
|
|
2157
2496
|
async function inspectKnowledgeTestIndex(projectRoot) {
|
|
2158
|
-
const
|
|
2497
|
+
const path2 = join6(projectRoot, ".fabric", ".cache", "knowledge-test.index.json");
|
|
2159
2498
|
const built = await tryBuildRuleMeta(projectRoot);
|
|
2160
2499
|
try {
|
|
2161
|
-
const index = knowledgeTestIndexSchema2.parse(JSON.parse(await readFile5(
|
|
2500
|
+
const index = knowledgeTestIndexSchema2.parse(JSON.parse(await readFile5(path2, "utf8")));
|
|
2162
2501
|
return {
|
|
2163
2502
|
present: true,
|
|
2164
2503
|
valid: true,
|
|
@@ -2179,8 +2518,8 @@ async function inspectKnowledgeTestIndex(projectRoot) {
|
|
|
2179
2518
|
}
|
|
2180
2519
|
function inspectBootstrapAnchor(projectRoot) {
|
|
2181
2520
|
return {
|
|
2182
|
-
hasAgentsMd: existsSync4(
|
|
2183
|
-
hasClaudeMd: existsSync4(
|
|
2521
|
+
hasAgentsMd: existsSync4(join6(projectRoot, "AGENTS.md")),
|
|
2522
|
+
hasClaudeMd: existsSync4(join6(projectRoot, "CLAUDE.md"))
|
|
2184
2523
|
};
|
|
2185
2524
|
}
|
|
2186
2525
|
var BOOTSTRAP_MARKER_MIGRATION_TARGETS = [
|
|
@@ -2192,7 +2531,7 @@ var BOOTSTRAP_MARKER_MIGRATION_TARGETS = [
|
|
|
2192
2531
|
async function inspectBootstrapMarkerMigration(target) {
|
|
2193
2532
|
const filesNeedingMigration = [];
|
|
2194
2533
|
for (const rel of BOOTSTRAP_MARKER_MIGRATION_TARGETS) {
|
|
2195
|
-
const abs =
|
|
2534
|
+
const abs = join6(target, rel);
|
|
2196
2535
|
if (!existsSync4(abs)) {
|
|
2197
2536
|
continue;
|
|
2198
2537
|
}
|
|
@@ -2226,7 +2565,7 @@ function createBootstrapMarkerMigrationCheck(inspection) {
|
|
|
2226
2565
|
);
|
|
2227
2566
|
}
|
|
2228
2567
|
async function inspectL1BootstrapSnapshotDrift(target) {
|
|
2229
|
-
const abs =
|
|
2568
|
+
const abs = join6(target, ".fabric", "AGENTS.md");
|
|
2230
2569
|
if (!existsSync4(abs)) {
|
|
2231
2570
|
return { status: "missing", canonical: BOOTSTRAP_CANONICAL, onDisk: null };
|
|
2232
2571
|
}
|
|
@@ -2258,7 +2597,7 @@ function createL1BootstrapSnapshotDriftCheck(inspection) {
|
|
|
2258
2597
|
);
|
|
2259
2598
|
}
|
|
2260
2599
|
async function inspectL2ManagedBlockDrift(target) {
|
|
2261
|
-
const snapshotPath =
|
|
2600
|
+
const snapshotPath = join6(target, ".fabric", "AGENTS.md");
|
|
2262
2601
|
if (!existsSync4(snapshotPath)) {
|
|
2263
2602
|
return { status: "ok", drifted: [] };
|
|
2264
2603
|
}
|
|
@@ -2268,7 +2607,7 @@ async function inspectL2ManagedBlockDrift(target) {
|
|
|
2268
2607
|
} catch {
|
|
2269
2608
|
return { status: "ok", drifted: [] };
|
|
2270
2609
|
}
|
|
2271
|
-
const projectRulesPath =
|
|
2610
|
+
const projectRulesPath = join6(target, ".fabric", "project-rules.md");
|
|
2272
2611
|
let expectedBody = snapshot;
|
|
2273
2612
|
if (existsSync4(projectRulesPath)) {
|
|
2274
2613
|
try {
|
|
@@ -2282,8 +2621,8 @@ ${projectRules}`;
|
|
|
2282
2621
|
const drifted = [];
|
|
2283
2622
|
let anyManagedBlockFound = false;
|
|
2284
2623
|
const blockTargets = [
|
|
2285
|
-
|
|
2286
|
-
|
|
2624
|
+
join6(target, "AGENTS.md"),
|
|
2625
|
+
join6(target, ".cursor", "rules", "fabric-bootstrap.mdc")
|
|
2287
2626
|
];
|
|
2288
2627
|
for (const abs of blockTargets) {
|
|
2289
2628
|
if (!existsSync4(abs)) {
|
|
@@ -2317,7 +2656,7 @@ ${projectRules}`;
|
|
|
2317
2656
|
drifted.push({ path: abs, expected: expectedBody, actual: body });
|
|
2318
2657
|
}
|
|
2319
2658
|
}
|
|
2320
|
-
const claudeMdPath =
|
|
2659
|
+
const claudeMdPath = join6(target, "CLAUDE.md");
|
|
2321
2660
|
if (existsSync4(claudeMdPath)) {
|
|
2322
2661
|
let claudeContent;
|
|
2323
2662
|
try {
|
|
@@ -2381,16 +2720,82 @@ function createBootstrapAnchorCheck(inspection) {
|
|
|
2381
2720
|
return okCheck("Bootstrap anchor", `Bootstrap anchor present at repo root: ${present}.`);
|
|
2382
2721
|
}
|
|
2383
2722
|
function inspectKnowledgeDirMissing(projectRoot) {
|
|
2384
|
-
const knowledgeRoot =
|
|
2723
|
+
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
2385
2724
|
const missingSubdirs = [];
|
|
2386
|
-
for (const sub of
|
|
2387
|
-
const
|
|
2388
|
-
if (!existsSync4(
|
|
2725
|
+
for (const sub of KNOWLEDGE_SUBDIRS3) {
|
|
2726
|
+
const path2 = join6(knowledgeRoot, sub);
|
|
2727
|
+
if (!existsSync4(path2)) {
|
|
2389
2728
|
missingSubdirs.push(`.fabric/knowledge/${sub}`);
|
|
2390
2729
|
}
|
|
2391
2730
|
}
|
|
2392
2731
|
return { missingSubdirs };
|
|
2393
2732
|
}
|
|
2733
|
+
function inspectBaselineFilenameFormat(projectRoot) {
|
|
2734
|
+
const offenders = [];
|
|
2735
|
+
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
2736
|
+
if (!existsSync4(knowledgeRoot)) {
|
|
2737
|
+
return { offenders };
|
|
2738
|
+
}
|
|
2739
|
+
for (const sub of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
2740
|
+
const dir = join6(knowledgeRoot, sub);
|
|
2741
|
+
if (!existsSync4(dir)) {
|
|
2742
|
+
continue;
|
|
2743
|
+
}
|
|
2744
|
+
let entries;
|
|
2745
|
+
try {
|
|
2746
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
2747
|
+
} catch {
|
|
2748
|
+
continue;
|
|
2749
|
+
}
|
|
2750
|
+
for (const entry of entries) {
|
|
2751
|
+
const entryName = entry.name;
|
|
2752
|
+
if (!entry.isFile() || !entryName.endsWith(".md")) {
|
|
2753
|
+
continue;
|
|
2754
|
+
}
|
|
2755
|
+
if (BASELINE_ID_PREFIXED_FILENAME_PATTERN.test(entryName)) {
|
|
2756
|
+
continue;
|
|
2757
|
+
}
|
|
2758
|
+
const abs = join6(dir, entryName);
|
|
2759
|
+
let source;
|
|
2760
|
+
try {
|
|
2761
|
+
source = readFileSync2(abs, "utf8");
|
|
2762
|
+
} catch {
|
|
2763
|
+
continue;
|
|
2764
|
+
}
|
|
2765
|
+
const id = extractKnowledgeFrontmatterId(source);
|
|
2766
|
+
if (id === null) {
|
|
2767
|
+
continue;
|
|
2768
|
+
}
|
|
2769
|
+
if (!BASELINE_FILENAME_LINT_BASELINE_IDS.has(id)) {
|
|
2770
|
+
continue;
|
|
2771
|
+
}
|
|
2772
|
+
offenders.push({
|
|
2773
|
+
path: posix.join(".fabric/knowledge", sub, entryName),
|
|
2774
|
+
stable_id: id
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
offenders.sort((a, b) => a.path.localeCompare(b.path));
|
|
2779
|
+
return { offenders };
|
|
2780
|
+
}
|
|
2781
|
+
function createBaselineFilenameFormatCheck(inspection) {
|
|
2782
|
+
if (inspection.offenders.length === 0) {
|
|
2783
|
+
return okCheck(
|
|
2784
|
+
"Baseline filename format",
|
|
2785
|
+
"All baseline knowledge files use the canonical `${id}--${slug}.md` filename format."
|
|
2786
|
+
);
|
|
2787
|
+
}
|
|
2788
|
+
const first = inspection.offenders[0];
|
|
2789
|
+
const detail = `${first.stable_id} at ${first.path}`;
|
|
2790
|
+
return issueCheck(
|
|
2791
|
+
"Baseline filename format",
|
|
2792
|
+
"error",
|
|
2793
|
+
"manual_error",
|
|
2794
|
+
"lint-baseline-filename-format",
|
|
2795
|
+
`${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}.`,
|
|
2796
|
+
"Delete the legacy bare-slug baseline file(s) manually \u2014 the baseline pipeline was removed in rc.23 and is no longer an auto-fix path."
|
|
2797
|
+
);
|
|
2798
|
+
}
|
|
2394
2799
|
function createKnowledgeDirMissingCheck(inspection) {
|
|
2395
2800
|
if (inspection.missingSubdirs.length > 0) {
|
|
2396
2801
|
const list = inspection.missingSubdirs.join(", ");
|
|
@@ -2405,7 +2810,7 @@ function createKnowledgeDirMissingCheck(inspection) {
|
|
|
2405
2810
|
}
|
|
2406
2811
|
return okCheck(
|
|
2407
2812
|
"Knowledge layout",
|
|
2408
|
-
`All ${
|
|
2813
|
+
`All ${KNOWLEDGE_SUBDIRS3.length} required .fabric/knowledge/* subdirectories exist.`
|
|
2409
2814
|
);
|
|
2410
2815
|
}
|
|
2411
2816
|
function createForensicCheck(forensic, frameworkKind, entryPointCount) {
|
|
@@ -2434,11 +2839,11 @@ function createMetaCheck(meta) {
|
|
|
2434
2839
|
if (meta.stale) {
|
|
2435
2840
|
return issueCheck(
|
|
2436
2841
|
"Agents metadata",
|
|
2437
|
-
"
|
|
2438
|
-
"
|
|
2842
|
+
"warn",
|
|
2843
|
+
"warning",
|
|
2439
2844
|
"agents_meta_stale",
|
|
2440
2845
|
`.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/knowledge derived revision ${meta.computedRevision ?? "<unknown>"}.`,
|
|
2441
|
-
"Run `fab doctor --fix`
|
|
2846
|
+
"Benign \u2014 engine auto-heals on next plan-context/get-sections call. Run `fab doctor --fix` for explicit reconciliation."
|
|
2442
2847
|
);
|
|
2443
2848
|
}
|
|
2444
2849
|
return okCheck("Agents metadata", `.fabric/agents.meta.json revision ${meta.revision} is aligned with .fabric/knowledge.`);
|
|
@@ -2551,7 +2956,7 @@ function findIssue(issues, code) {
|
|
|
2551
2956
|
};
|
|
2552
2957
|
}
|
|
2553
2958
|
async function inspectMetaManuallyDiverged(projectRoot) {
|
|
2554
|
-
const metaPath =
|
|
2959
|
+
const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
|
|
2555
2960
|
if (!existsSync4(metaPath)) {
|
|
2556
2961
|
return { extraMetaEntries: [], hashMismatchEntries: [], readable: false };
|
|
2557
2962
|
}
|
|
@@ -2571,13 +2976,13 @@ async function inspectMetaManuallyDiverged(projectRoot) {
|
|
|
2571
2976
|
const hashMismatchEntries = [];
|
|
2572
2977
|
for (const node of Object.values(meta.nodes)) {
|
|
2573
2978
|
const contentRef = node.content_ref ?? node.file;
|
|
2574
|
-
const absPath =
|
|
2979
|
+
const absPath = join6(projectRoot, contentRef);
|
|
2575
2980
|
if (!existsSync4(absPath)) {
|
|
2576
2981
|
extraMetaEntries.push(contentRef);
|
|
2577
2982
|
continue;
|
|
2578
2983
|
}
|
|
2579
2984
|
try {
|
|
2580
|
-
const content =
|
|
2985
|
+
const content = readFileSync2(absPath, "utf8");
|
|
2581
2986
|
const diskHash = sha256(content);
|
|
2582
2987
|
if (node.hash !== "" && node.hash !== diskHash) {
|
|
2583
2988
|
hashMismatchEntries.push(contentRef);
|
|
@@ -2590,7 +2995,7 @@ async function inspectMetaManuallyDiverged(projectRoot) {
|
|
|
2590
2995
|
}
|
|
2591
2996
|
function inspectKnowledgeDirUnindexed(projectRoot, meta) {
|
|
2592
2997
|
const physicalMdFiles = /* @__PURE__ */ new Set();
|
|
2593
|
-
collectMdFilesUnder(physicalMdFiles, projectRoot,
|
|
2998
|
+
collectMdFilesUnder(physicalMdFiles, projectRoot, join6(projectRoot, ".fabric", "knowledge"), ".fabric/knowledge");
|
|
2594
2999
|
if (physicalMdFiles.size === 0) {
|
|
2595
3000
|
return { unindexedFiles: [] };
|
|
2596
3001
|
}
|
|
@@ -2615,9 +3020,11 @@ function collectMdFilesUnder(out, projectRoot, rootDir, relPrefix) {
|
|
|
2615
3020
|
continue;
|
|
2616
3021
|
}
|
|
2617
3022
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2618
|
-
const abs =
|
|
3023
|
+
const abs = join6(dir, entry.name);
|
|
2619
3024
|
if (entry.isDirectory()) {
|
|
2620
|
-
|
|
3025
|
+
if (entry.name !== "pending" && entry.name !== "archive") {
|
|
3026
|
+
stack.push(abs);
|
|
3027
|
+
}
|
|
2621
3028
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2622
3029
|
const rel = posix.join(relPrefix, abs.slice(rootDir.length + 1).replace(/\\/gu, "/"));
|
|
2623
3030
|
out.add(rel);
|
|
@@ -2640,7 +3047,7 @@ function createKnowledgeDirUnindexedCheck(inspection) {
|
|
|
2640
3047
|
}
|
|
2641
3048
|
async function inspectStableIdCollisions(projectRoot) {
|
|
2642
3049
|
const found = [];
|
|
2643
|
-
const knowledgeDir =
|
|
3050
|
+
const knowledgeDir = join6(projectRoot, ".fabric", "knowledge");
|
|
2644
3051
|
if (existsSync4(knowledgeDir)) {
|
|
2645
3052
|
const stack = [knowledgeDir];
|
|
2646
3053
|
while (stack.length > 0) {
|
|
@@ -2649,7 +3056,7 @@ async function inspectStableIdCollisions(projectRoot) {
|
|
|
2649
3056
|
continue;
|
|
2650
3057
|
}
|
|
2651
3058
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2652
|
-
const abs =
|
|
3059
|
+
const abs = join6(dir, entry.name);
|
|
2653
3060
|
if (entry.isDirectory()) {
|
|
2654
3061
|
stack.push(abs);
|
|
2655
3062
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
@@ -2719,7 +3126,7 @@ function inspectCounterDesync(meta) {
|
|
|
2719
3126
|
["guideline", "GLD"],
|
|
2720
3127
|
["pitfall", "PIT"],
|
|
2721
3128
|
["process", "PRO"]
|
|
2722
|
-
].find(([
|
|
3129
|
+
].find(([t2]) => t2 === parsed.type)?.[1];
|
|
2723
3130
|
if (typeCode === void 0) {
|
|
2724
3131
|
continue;
|
|
2725
3132
|
}
|
|
@@ -2805,17 +3212,17 @@ function createMetaManuallyDivergedCheck(inspection) {
|
|
|
2805
3212
|
}
|
|
2806
3213
|
function inspectPreexistingRootFiles(projectRoot) {
|
|
2807
3214
|
const candidates = ["CLAUDE.md", "AGENTS.md"];
|
|
2808
|
-
const detected = candidates.filter((name) => existsSync4(
|
|
3215
|
+
const detected = candidates.filter((name) => existsSync4(join6(projectRoot, name)));
|
|
2809
3216
|
return { detected };
|
|
2810
3217
|
}
|
|
2811
3218
|
async function inspectFilesystemEditFallback(projectRoot) {
|
|
2812
|
-
const knowledgeRoot =
|
|
3219
|
+
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
2813
3220
|
if (!existsSync4(knowledgeRoot)) {
|
|
2814
3221
|
return { synthesized: 0, synthesizedStableIds: [] };
|
|
2815
3222
|
}
|
|
2816
3223
|
const canonicalIds = /* @__PURE__ */ new Set();
|
|
2817
3224
|
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
2818
|
-
const dir =
|
|
3225
|
+
const dir = join6(knowledgeRoot, typeDir);
|
|
2819
3226
|
if (!existsSync4(dir)) {
|
|
2820
3227
|
continue;
|
|
2821
3228
|
}
|
|
@@ -3017,12 +3424,12 @@ function extractKnowledgeFrontmatterCreatedAt(source) {
|
|
|
3017
3424
|
return Number.isFinite(parsed) ? parsed : null;
|
|
3018
3425
|
}
|
|
3019
3426
|
function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
|
|
3020
|
-
const knowledgeRoot =
|
|
3427
|
+
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
3021
3428
|
if (!existsSync4(knowledgeRoot)) {
|
|
3022
3429
|
return;
|
|
3023
3430
|
}
|
|
3024
3431
|
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
3025
|
-
const dir =
|
|
3432
|
+
const dir = join6(knowledgeRoot, typeDir);
|
|
3026
3433
|
if (!existsSync4(dir)) {
|
|
3027
3434
|
continue;
|
|
3028
3435
|
}
|
|
@@ -3041,10 +3448,10 @@ function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
|
|
|
3041
3448
|
continue;
|
|
3042
3449
|
}
|
|
3043
3450
|
const stableId = match[1];
|
|
3044
|
-
const absPath =
|
|
3451
|
+
const absPath = join6(dir, entry.name);
|
|
3045
3452
|
let source;
|
|
3046
3453
|
try {
|
|
3047
|
-
source =
|
|
3454
|
+
source = readFileSync2(absPath, "utf8");
|
|
3048
3455
|
} catch {
|
|
3049
3456
|
continue;
|
|
3050
3457
|
}
|
|
@@ -3057,7 +3464,7 @@ function* iterateCanonicalEntries(projectRoot, lastActiveIndex) {
|
|
|
3057
3464
|
let lastReferenceMs = Math.max(createdAt ?? 0, eventTs);
|
|
3058
3465
|
if (lastReferenceMs === 0) {
|
|
3059
3466
|
try {
|
|
3060
|
-
lastReferenceMs =
|
|
3467
|
+
lastReferenceMs = statSync4(absPath).mtimeMs;
|
|
3061
3468
|
} catch {
|
|
3062
3469
|
lastReferenceMs = 0;
|
|
3063
3470
|
}
|
|
@@ -3117,8 +3524,8 @@ async function inspectStaleArchive(projectRoot, now) {
|
|
|
3117
3524
|
return { candidates };
|
|
3118
3525
|
}
|
|
3119
3526
|
function* iteratePendingFiles(projectRoot, now) {
|
|
3120
|
-
const teamRoot =
|
|
3121
|
-
const personalRoot =
|
|
3527
|
+
const teamRoot = join6(projectRoot, ".fabric", "knowledge", "pending");
|
|
3528
|
+
const personalRoot = join6(resolvePersonalRootForPending(), ".fabric", "knowledge", "pending");
|
|
3122
3529
|
for (const [layer, root, displayPrefix] of [
|
|
3123
3530
|
["team", teamRoot, ".fabric/knowledge/pending"],
|
|
3124
3531
|
["personal", personalRoot, "~/.fabric/knowledge/pending"]
|
|
@@ -3133,7 +3540,7 @@ function* iteratePendingFiles(projectRoot, now) {
|
|
|
3133
3540
|
continue;
|
|
3134
3541
|
}
|
|
3135
3542
|
for (const typeDir of typeDirs) {
|
|
3136
|
-
const dir =
|
|
3543
|
+
const dir = join6(root, typeDir);
|
|
3137
3544
|
let entries;
|
|
3138
3545
|
try {
|
|
3139
3546
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -3144,17 +3551,17 @@ function* iteratePendingFiles(projectRoot, now) {
|
|
|
3144
3551
|
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
3145
3552
|
continue;
|
|
3146
3553
|
}
|
|
3147
|
-
const absPath =
|
|
3554
|
+
const absPath = join6(dir, entry.name);
|
|
3148
3555
|
let source = "";
|
|
3149
3556
|
try {
|
|
3150
|
-
source =
|
|
3557
|
+
source = readFileSync2(absPath, "utf8");
|
|
3151
3558
|
} catch {
|
|
3152
3559
|
continue;
|
|
3153
3560
|
}
|
|
3154
3561
|
const createdAt = extractKnowledgeFrontmatterCreatedAt(source);
|
|
3155
3562
|
let mtimeMs = 0;
|
|
3156
3563
|
try {
|
|
3157
|
-
mtimeMs =
|
|
3564
|
+
mtimeMs = statSync4(absPath).mtimeMs;
|
|
3158
3565
|
} catch {
|
|
3159
3566
|
mtimeMs = 0;
|
|
3160
3567
|
}
|
|
@@ -3188,7 +3595,7 @@ function* iteratePendingFiles(projectRoot, now) {
|
|
|
3188
3595
|
}
|
|
3189
3596
|
}
|
|
3190
3597
|
function resolvePersonalRootForPending() {
|
|
3191
|
-
return process.env.FABRIC_HOME ??
|
|
3598
|
+
return process.env.FABRIC_HOME ?? homedir3();
|
|
3192
3599
|
}
|
|
3193
3600
|
function inspectPendingOverdue(projectRoot, now) {
|
|
3194
3601
|
const candidates = [];
|
|
@@ -3219,7 +3626,7 @@ function inspectPendingAutoArchive(projectRoot, now) {
|
|
|
3219
3626
|
pending_path: visit.pending_path,
|
|
3220
3627
|
pending_path_abs: visit.pending_path_abs,
|
|
3221
3628
|
archived_to: archivedToRel,
|
|
3222
|
-
archived_to_abs:
|
|
3629
|
+
archived_to_abs: join6(projectRoot, archivedToRel),
|
|
3223
3630
|
age_days: visit.age_days
|
|
3224
3631
|
});
|
|
3225
3632
|
} else {
|
|
@@ -3228,7 +3635,7 @@ function inspectPendingAutoArchive(projectRoot, now) {
|
|
|
3228
3635
|
visit.type,
|
|
3229
3636
|
visit.filename
|
|
3230
3637
|
);
|
|
3231
|
-
const archivedToAbs =
|
|
3638
|
+
const archivedToAbs = join6(
|
|
3232
3639
|
resolvePersonalRootForPending(),
|
|
3233
3640
|
".fabric",
|
|
3234
3641
|
".archive",
|
|
@@ -3252,11 +3659,11 @@ function inspectPendingAutoArchive(projectRoot, now) {
|
|
|
3252
3659
|
}
|
|
3253
3660
|
function inspectUnderseeded(projectRoot) {
|
|
3254
3661
|
const threshold = readUnderseedThresholdFromConfig(projectRoot);
|
|
3255
|
-
const knowledgeRoot =
|
|
3662
|
+
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
3256
3663
|
let nodeCount = 0;
|
|
3257
3664
|
if (existsSync4(knowledgeRoot)) {
|
|
3258
3665
|
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
3259
|
-
const dir =
|
|
3666
|
+
const dir = join6(knowledgeRoot, typeDir);
|
|
3260
3667
|
if (!existsSync4(dir)) continue;
|
|
3261
3668
|
let entries;
|
|
3262
3669
|
try {
|
|
@@ -3278,7 +3685,7 @@ function inspectUnderseeded(projectRoot) {
|
|
|
3278
3685
|
};
|
|
3279
3686
|
}
|
|
3280
3687
|
function inspectSessionHintsStale(projectRoot, now) {
|
|
3281
|
-
const cacheDir =
|
|
3688
|
+
const cacheDir = join6(projectRoot, ".fabric", ".cache");
|
|
3282
3689
|
if (!existsSync4(cacheDir)) {
|
|
3283
3690
|
return { candidates: [] };
|
|
3284
3691
|
}
|
|
@@ -3293,10 +3700,10 @@ function inspectSessionHintsStale(projectRoot, now) {
|
|
|
3293
3700
|
if (!entry.isFile()) continue;
|
|
3294
3701
|
if (!entry.name.startsWith(SESSION_HINTS_FILE_PREFIX)) continue;
|
|
3295
3702
|
if (!entry.name.endsWith(SESSION_HINTS_FILE_SUFFIX)) continue;
|
|
3296
|
-
const absPath =
|
|
3703
|
+
const absPath = join6(cacheDir, entry.name);
|
|
3297
3704
|
let mtimeMs = 0;
|
|
3298
3705
|
try {
|
|
3299
|
-
mtimeMs =
|
|
3706
|
+
mtimeMs = statSync4(absPath).mtimeMs;
|
|
3300
3707
|
} catch {
|
|
3301
3708
|
continue;
|
|
3302
3709
|
}
|
|
@@ -3310,6 +3717,20 @@ function inspectSessionHintsStale(projectRoot, now) {
|
|
|
3310
3717
|
candidates.sort((a, b) => a.path.localeCompare(b.path));
|
|
3311
3718
|
return { candidates };
|
|
3312
3719
|
}
|
|
3720
|
+
function inspectStaleServeLock(projectRoot, now) {
|
|
3721
|
+
const state = readLockState(projectRoot);
|
|
3722
|
+
if (state === null) {
|
|
3723
|
+
return { present: false };
|
|
3724
|
+
}
|
|
3725
|
+
const ageMs = Math.max(0, now - state.acquiredAt);
|
|
3726
|
+
return {
|
|
3727
|
+
present: true,
|
|
3728
|
+
pid: state.pid,
|
|
3729
|
+
acquiredAt: state.acquiredAt,
|
|
3730
|
+
ageMs,
|
|
3731
|
+
pidAlive: isAlive(state.pid)
|
|
3732
|
+
};
|
|
3733
|
+
}
|
|
3313
3734
|
function inspectNarrowTooFew(projectRoot, now) {
|
|
3314
3735
|
let total = 0;
|
|
3315
3736
|
let narrowWithPaths = 0;
|
|
@@ -3323,11 +3744,11 @@ function inspectNarrowTooFew(projectRoot, now) {
|
|
|
3323
3744
|
const structuralFlagged = total >= NARROW_MIN_TOTAL && narrowRatio < NARROW_RATIO_THRESHOLD;
|
|
3324
3745
|
const windowStartMs = now - SILENCE_WINDOW_DAYS * MS_PER_DAY;
|
|
3325
3746
|
const editFires = readCounterTimestamps(
|
|
3326
|
-
|
|
3747
|
+
join6(projectRoot, EDIT_COUNTER_FILE_REL),
|
|
3327
3748
|
windowStartMs
|
|
3328
3749
|
);
|
|
3329
3750
|
const silenceFires = readCounterTimestamps(
|
|
3330
|
-
|
|
3751
|
+
join6(projectRoot, HINT_SILENCE_COUNTER_FILE_REL),
|
|
3331
3752
|
windowStartMs
|
|
3332
3753
|
);
|
|
3333
3754
|
const telemetrySkipped = editFires === 0;
|
|
@@ -3349,7 +3770,7 @@ function readCounterTimestamps(absPath, windowStartMs) {
|
|
|
3349
3770
|
if (!existsSync4(absPath)) return 0;
|
|
3350
3771
|
let raw;
|
|
3351
3772
|
try {
|
|
3352
|
-
raw =
|
|
3773
|
+
raw = readFileSync2(absPath, "utf8");
|
|
3353
3774
|
} catch {
|
|
3354
3775
|
return 0;
|
|
3355
3776
|
}
|
|
@@ -3365,10 +3786,10 @@ function readCounterTimestamps(absPath, windowStartMs) {
|
|
|
3365
3786
|
return count;
|
|
3366
3787
|
}
|
|
3367
3788
|
function readUnderseedThresholdFromConfig(projectRoot) {
|
|
3368
|
-
const configPath =
|
|
3789
|
+
const configPath = join6(projectRoot, ".fabric", "fabric-config.json");
|
|
3369
3790
|
if (!existsSync4(configPath)) return DEFAULT_UNDERSEED_NODE_THRESHOLD;
|
|
3370
3791
|
try {
|
|
3371
|
-
const raw =
|
|
3792
|
+
const raw = readFileSync2(configPath, "utf8");
|
|
3372
3793
|
const parsed = JSON.parse(raw);
|
|
3373
3794
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3374
3795
|
const v = parsed.underseed_node_threshold;
|
|
@@ -3468,6 +3889,28 @@ function createSessionHintsStaleCheck(inspection) {
|
|
|
3468
3889
|
"Run `fab doctor --apply-lint` to delete stale session-hints cache files."
|
|
3469
3890
|
);
|
|
3470
3891
|
}
|
|
3892
|
+
function createStaleServeLockCheck(inspection) {
|
|
3893
|
+
if (!inspection.present) {
|
|
3894
|
+
return okCheck("Serve lock", "No .fabric/.serve.lock present.");
|
|
3895
|
+
}
|
|
3896
|
+
if (inspection.pidAlive) {
|
|
3897
|
+
return okCheck(
|
|
3898
|
+
"Serve lock",
|
|
3899
|
+
`.fabric/.serve.lock held by live PID ${inspection.pid}.`
|
|
3900
|
+
);
|
|
3901
|
+
}
|
|
3902
|
+
const days = Math.floor(inspection.ageMs / MS_PER_DAY);
|
|
3903
|
+
const hours = Math.floor(inspection.ageMs / (60 * 60 * 1e3));
|
|
3904
|
+
const acquiredAgo = days >= 1 ? `${days} day${days === 1 ? "" : "s"} ago` : `${hours} hour${hours === 1 ? "" : "s"} ago`;
|
|
3905
|
+
return issueCheck(
|
|
3906
|
+
"Serve lock",
|
|
3907
|
+
"ok",
|
|
3908
|
+
"info",
|
|
3909
|
+
"stale_serve_lock",
|
|
3910
|
+
`[advisory] .fabric/.serve.lock holds dead PID ${inspection.pid} (acquired ${acquiredAgo}). Run \`fab doctor --fix\` to remove.`,
|
|
3911
|
+
"Run `fab doctor --fix` to remove the stale .fabric/.serve.lock."
|
|
3912
|
+
);
|
|
3913
|
+
}
|
|
3471
3914
|
function extractKnowledgeFrontmatterRelevanceScope(source) {
|
|
3472
3915
|
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
3473
3916
|
const fm = FM_PATTERN.exec(source);
|
|
@@ -3498,11 +3941,11 @@ function extractKnowledgeFrontmatterRelevancePaths(source) {
|
|
|
3498
3941
|
}
|
|
3499
3942
|
function* iterateRelevanceFrontmatter(projectRoot) {
|
|
3500
3943
|
for (const visit of iterateCanonicalFilenames(projectRoot)) {
|
|
3501
|
-
const layerRoot = visit.layer === "team" ?
|
|
3502
|
-
const absPath =
|
|
3944
|
+
const layerRoot = visit.layer === "team" ? join6(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
|
|
3945
|
+
const absPath = join6(layerRoot, visit.type, visit.filename);
|
|
3503
3946
|
let source;
|
|
3504
3947
|
try {
|
|
3505
|
-
source =
|
|
3948
|
+
source = readFileSync2(absPath, "utf8");
|
|
3506
3949
|
} catch {
|
|
3507
3950
|
continue;
|
|
3508
3951
|
}
|
|
@@ -3569,7 +4012,7 @@ function collectWorkspacePathsForGlobMatch(projectRoot) {
|
|
|
3569
4012
|
}
|
|
3570
4013
|
let rootStat;
|
|
3571
4014
|
try {
|
|
3572
|
-
rootStat =
|
|
4015
|
+
rootStat = statSync4(projectRoot);
|
|
3573
4016
|
} catch {
|
|
3574
4017
|
return [];
|
|
3575
4018
|
}
|
|
@@ -3588,7 +4031,7 @@ function collectWorkspacePathsForGlobMatch(projectRoot) {
|
|
|
3588
4031
|
continue;
|
|
3589
4032
|
}
|
|
3590
4033
|
for (const entry of entries) {
|
|
3591
|
-
const abs =
|
|
4034
|
+
const abs = join6(current, entry.name);
|
|
3592
4035
|
const rel = normalizePath(abs.slice(projectRoot.length + 1));
|
|
3593
4036
|
if (rel.length === 0) continue;
|
|
3594
4037
|
if (entry.isDirectory()) {
|
|
@@ -3728,8 +4171,8 @@ function inspectRelevanceFieldsMissing(projectRoot) {
|
|
|
3728
4171
|
const candidates = [];
|
|
3729
4172
|
let scannedCount = 0;
|
|
3730
4173
|
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
3731
|
-
const teamRoot =
|
|
3732
|
-
const personalRoot =
|
|
4174
|
+
const teamRoot = join6(projectRoot, ".fabric", "knowledge", "pending");
|
|
4175
|
+
const personalRoot = join6(
|
|
3733
4176
|
resolvePersonalRootForPending(),
|
|
3734
4177
|
".fabric",
|
|
3735
4178
|
"knowledge",
|
|
@@ -3749,7 +4192,7 @@ function inspectRelevanceFieldsMissing(projectRoot) {
|
|
|
3749
4192
|
continue;
|
|
3750
4193
|
}
|
|
3751
4194
|
for (const typeDir of typeDirs) {
|
|
3752
|
-
const dir =
|
|
4195
|
+
const dir = join6(root, typeDir);
|
|
3753
4196
|
let entries;
|
|
3754
4197
|
try {
|
|
3755
4198
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -3760,10 +4203,10 @@ function inspectRelevanceFieldsMissing(projectRoot) {
|
|
|
3760
4203
|
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
3761
4204
|
continue;
|
|
3762
4205
|
}
|
|
3763
|
-
const absPath =
|
|
4206
|
+
const absPath = join6(dir, entry.name);
|
|
3764
4207
|
let source;
|
|
3765
4208
|
try {
|
|
3766
|
-
source =
|
|
4209
|
+
source = readFileSync2(absPath, "utf8");
|
|
3767
4210
|
} catch {
|
|
3768
4211
|
continue;
|
|
3769
4212
|
}
|
|
@@ -3847,7 +4290,7 @@ async function applyRelevanceFieldsMissing(candidate) {
|
|
|
3847
4290
|
error: "fields already present at write time (no diff)"
|
|
3848
4291
|
};
|
|
3849
4292
|
}
|
|
3850
|
-
await
|
|
4293
|
+
await atomicWriteText4(candidate.pending_path_abs, rewritten);
|
|
3851
4294
|
return {
|
|
3852
4295
|
kind: "knowledge_relevance_fields_missing",
|
|
3853
4296
|
path: candidate.pending_path,
|
|
@@ -3891,7 +4334,7 @@ var SKILL_QUOTED_VALUE_LEADS = /* @__PURE__ */ new Set(['"', "'", "[", "{", ">",
|
|
|
3891
4334
|
function inspectSkillMdYamlInvalid(projectRoot) {
|
|
3892
4335
|
const candidates = [];
|
|
3893
4336
|
for (const rootRel of SKILL_MD_FRONTMATTER_ROOTS) {
|
|
3894
|
-
const rootAbs =
|
|
4337
|
+
const rootAbs = join6(projectRoot, rootRel);
|
|
3895
4338
|
if (!existsSync4(rootAbs)) continue;
|
|
3896
4339
|
let dirEntries;
|
|
3897
4340
|
try {
|
|
@@ -3901,11 +4344,11 @@ function inspectSkillMdYamlInvalid(projectRoot) {
|
|
|
3901
4344
|
}
|
|
3902
4345
|
for (const dirEntry of dirEntries) {
|
|
3903
4346
|
if (!dirEntry.isDirectory()) continue;
|
|
3904
|
-
const skillFile =
|
|
4347
|
+
const skillFile = join6(rootAbs, dirEntry.name, "SKILL.md");
|
|
3905
4348
|
if (!existsSync4(skillFile)) continue;
|
|
3906
4349
|
let raw;
|
|
3907
4350
|
try {
|
|
3908
|
-
raw =
|
|
4351
|
+
raw = readFileSync2(skillFile, "utf8");
|
|
3909
4352
|
} catch {
|
|
3910
4353
|
continue;
|
|
3911
4354
|
}
|
|
@@ -3972,6 +4415,117 @@ function createSkillMdYamlInvalidCheck(inspection) {
|
|
|
3972
4415
|
'Quote the value with double quotes (`description: "\u2026"`) or rewrite the inner `key: value` token to `key=value`.'
|
|
3973
4416
|
);
|
|
3974
4417
|
}
|
|
4418
|
+
var KNOWLEDGE_CANONICAL_TYPE_DIRS_FOR_ONBOARD = [
|
|
4419
|
+
"decisions",
|
|
4420
|
+
"pitfalls",
|
|
4421
|
+
"guidelines",
|
|
4422
|
+
"models",
|
|
4423
|
+
"processes"
|
|
4424
|
+
];
|
|
4425
|
+
function inspectOnboardCoverage(projectRoot) {
|
|
4426
|
+
const filled = {};
|
|
4427
|
+
for (const slot of ONBOARD_SLOT_NAMES) {
|
|
4428
|
+
filled[slot] = [];
|
|
4429
|
+
}
|
|
4430
|
+
const knowledgeRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
4431
|
+
if (existsSync4(knowledgeRoot)) {
|
|
4432
|
+
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS_FOR_ONBOARD) {
|
|
4433
|
+
const dir = join6(knowledgeRoot, typeDir);
|
|
4434
|
+
if (!existsSync4(dir)) continue;
|
|
4435
|
+
let entries;
|
|
4436
|
+
try {
|
|
4437
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
4438
|
+
} catch {
|
|
4439
|
+
continue;
|
|
4440
|
+
}
|
|
4441
|
+
for (const entry of entries) {
|
|
4442
|
+
if (!entry.isFile()) continue;
|
|
4443
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
4444
|
+
const filePath = join6(dir, entry.name);
|
|
4445
|
+
let content;
|
|
4446
|
+
try {
|
|
4447
|
+
content = readFileSync2(filePath, "utf8");
|
|
4448
|
+
} catch {
|
|
4449
|
+
continue;
|
|
4450
|
+
}
|
|
4451
|
+
const slot = readFrontmatterScalar(content, "onboard_slot");
|
|
4452
|
+
if (slot === void 0) continue;
|
|
4453
|
+
if (!ONBOARD_SLOT_NAMES.includes(slot)) continue;
|
|
4454
|
+
const stableId = readFrontmatterScalar(content, "id") ?? entry.name.replace(/\.md$/u, "");
|
|
4455
|
+
filled[slot].push(stableId);
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
for (const slot of ONBOARD_SLOT_NAMES) {
|
|
4460
|
+
filled[slot].sort();
|
|
4461
|
+
}
|
|
4462
|
+
const optedOut = readOnboardOptedOut(projectRoot);
|
|
4463
|
+
const missing = ONBOARD_SLOT_NAMES.filter((slot) => {
|
|
4464
|
+
if (filled[slot].length > 0) return false;
|
|
4465
|
+
if (optedOut.includes(slot)) return false;
|
|
4466
|
+
return true;
|
|
4467
|
+
});
|
|
4468
|
+
return { filled, missing, opted_out: optedOut };
|
|
4469
|
+
}
|
|
4470
|
+
function readOnboardOptedOut(projectRoot) {
|
|
4471
|
+
const path2 = join6(projectRoot, ".fabric", "fabric-config.json");
|
|
4472
|
+
if (!existsSync4(path2)) return [];
|
|
4473
|
+
let raw;
|
|
4474
|
+
try {
|
|
4475
|
+
raw = readFileSync2(path2, "utf8");
|
|
4476
|
+
} catch {
|
|
4477
|
+
return [];
|
|
4478
|
+
}
|
|
4479
|
+
let parsed;
|
|
4480
|
+
try {
|
|
4481
|
+
parsed = JSON.parse(raw);
|
|
4482
|
+
} catch {
|
|
4483
|
+
return [];
|
|
4484
|
+
}
|
|
4485
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4486
|
+
return [];
|
|
4487
|
+
}
|
|
4488
|
+
const list = parsed.onboard_slots_opted_out;
|
|
4489
|
+
if (!Array.isArray(list)) return [];
|
|
4490
|
+
return list.filter((v) => typeof v === "string");
|
|
4491
|
+
}
|
|
4492
|
+
function readFrontmatterScalar(content, key) {
|
|
4493
|
+
const match = /^---\n([\s\S]*?)\n---/u.exec(content);
|
|
4494
|
+
if (match === null) return void 0;
|
|
4495
|
+
const block = match[1];
|
|
4496
|
+
if (block === void 0) return void 0;
|
|
4497
|
+
for (const rawLine of block.split(/\r?\n/u)) {
|
|
4498
|
+
const line = rawLine.trim();
|
|
4499
|
+
const sep4 = line.indexOf(":");
|
|
4500
|
+
if (sep4 === -1) continue;
|
|
4501
|
+
if (line.slice(0, sep4).trim() !== key) continue;
|
|
4502
|
+
let value = line.slice(sep4 + 1).trim();
|
|
4503
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
4504
|
+
value = value.slice(1, -1);
|
|
4505
|
+
}
|
|
4506
|
+
return value;
|
|
4507
|
+
}
|
|
4508
|
+
return void 0;
|
|
4509
|
+
}
|
|
4510
|
+
function createOnboardCoverageCheck(inspection) {
|
|
4511
|
+
const filledCount = ONBOARD_SLOT_NAMES.filter(
|
|
4512
|
+
(slot) => inspection.filled[slot].length > 0
|
|
4513
|
+
).length;
|
|
4514
|
+
if (inspection.missing.length === 0) {
|
|
4515
|
+
return okCheck(
|
|
4516
|
+
"Onboard coverage",
|
|
4517
|
+
`Onboard coverage: ${filledCount}/${ONBOARD_SLOT_TOTAL} \u2713 (opted-out: ${inspection.opted_out.length}).`
|
|
4518
|
+
);
|
|
4519
|
+
}
|
|
4520
|
+
return issueCheck(
|
|
4521
|
+
"Onboard coverage",
|
|
4522
|
+
"ok",
|
|
4523
|
+
"info",
|
|
4524
|
+
"onboard_coverage_incomplete",
|
|
4525
|
+
`Onboard slots not yet covered: [${inspection.missing.join(", ")}]. ${filledCount}/${ONBOARD_SLOT_TOTAL} filled; ${inspection.opted_out.length} opted-out.`,
|
|
4526
|
+
"Run /fabric-archive to onboard \u2014 the Skill's first-run phase will tour the project and propose pending entries for each unclaimed slot."
|
|
4527
|
+
);
|
|
4528
|
+
}
|
|
3975
4529
|
function createNarrowTooFewCheck(inspection) {
|
|
3976
4530
|
const { structural_flagged, telemetry_flagged } = inspection;
|
|
3977
4531
|
if (!structural_flagged && !telemetry_flagged) {
|
|
@@ -4005,8 +4559,8 @@ function createNarrowTooFewCheck(inspection) {
|
|
|
4005
4559
|
);
|
|
4006
4560
|
}
|
|
4007
4561
|
function resolvePersonalKnowledgeRoot() {
|
|
4008
|
-
const home = process.env.FABRIC_HOME ??
|
|
4009
|
-
return
|
|
4562
|
+
const home = process.env.FABRIC_HOME ?? homedir3();
|
|
4563
|
+
return join6(home, ".fabric", "knowledge");
|
|
4010
4564
|
}
|
|
4011
4565
|
function parseStableIdFromCanonicalFilename(filename) {
|
|
4012
4566
|
const match = CANONICAL_KNOWLEDGE_FILENAME_PATTERN.exec(filename);
|
|
@@ -4026,7 +4580,7 @@ function parseStableIdFromCanonicalFilename(filename) {
|
|
|
4026
4580
|
};
|
|
4027
4581
|
}
|
|
4028
4582
|
function* iterateCanonicalFilenames(projectRoot) {
|
|
4029
|
-
const teamRoot =
|
|
4583
|
+
const teamRoot = join6(projectRoot, ".fabric", "knowledge");
|
|
4030
4584
|
const personalRoot = resolvePersonalKnowledgeRoot();
|
|
4031
4585
|
for (const [layer, root, displayPrefix] of [
|
|
4032
4586
|
["team", teamRoot, ".fabric/knowledge"],
|
|
@@ -4036,7 +4590,7 @@ function* iterateCanonicalFilenames(projectRoot) {
|
|
|
4036
4590
|
continue;
|
|
4037
4591
|
}
|
|
4038
4592
|
for (const typeDir of KNOWLEDGE_CANONICAL_TYPE_DIRS) {
|
|
4039
|
-
const dir =
|
|
4593
|
+
const dir = join6(root, typeDir);
|
|
4040
4594
|
if (!existsSync4(dir)) {
|
|
4041
4595
|
continue;
|
|
4042
4596
|
}
|
|
@@ -4196,7 +4750,7 @@ async function migrateBootstrapMarkers(projectRoot) {
|
|
|
4196
4750
|
const paths = [];
|
|
4197
4751
|
const countPerPath = {};
|
|
4198
4752
|
for (const rel of BOOTSTRAP_MARKER_MIGRATION_TARGETS) {
|
|
4199
|
-
const abs =
|
|
4753
|
+
const abs = join6(projectRoot, rel);
|
|
4200
4754
|
if (!existsSync4(abs)) {
|
|
4201
4755
|
continue;
|
|
4202
4756
|
}
|
|
@@ -4216,14 +4770,14 @@ async function migrateBootstrapMarkers(projectRoot) {
|
|
|
4216
4770
|
if (rewritten === original) {
|
|
4217
4771
|
continue;
|
|
4218
4772
|
}
|
|
4219
|
-
await
|
|
4773
|
+
await atomicWriteText4(abs, rewritten);
|
|
4220
4774
|
paths.push(abs);
|
|
4221
4775
|
countPerPath[abs] = replacedCount;
|
|
4222
4776
|
}
|
|
4223
4777
|
return { paths, countPerPath };
|
|
4224
4778
|
}
|
|
4225
4779
|
async function rewriteThreeEndManagedBlocks(projectRoot) {
|
|
4226
|
-
const snapshotPath =
|
|
4780
|
+
const snapshotPath = join6(projectRoot, ".fabric", "AGENTS.md");
|
|
4227
4781
|
if (!existsSync4(snapshotPath)) {
|
|
4228
4782
|
return;
|
|
4229
4783
|
}
|
|
@@ -4233,7 +4787,7 @@ async function rewriteThreeEndManagedBlocks(projectRoot) {
|
|
|
4233
4787
|
} catch {
|
|
4234
4788
|
return;
|
|
4235
4789
|
}
|
|
4236
|
-
const projectRulesPath =
|
|
4790
|
+
const projectRulesPath = join6(projectRoot, ".fabric", "project-rules.md");
|
|
4237
4791
|
const hasProjectRules = existsSync4(projectRulesPath);
|
|
4238
4792
|
let expectedBody = snapshot;
|
|
4239
4793
|
if (hasProjectRules) {
|
|
@@ -4249,8 +4803,8 @@ ${projectRules}`;
|
|
|
4249
4803
|
${expectedBody}
|
|
4250
4804
|
${BOOTSTRAP_MARKER_END}`;
|
|
4251
4805
|
const blockTargets = [
|
|
4252
|
-
|
|
4253
|
-
|
|
4806
|
+
join6(projectRoot, "AGENTS.md"),
|
|
4807
|
+
join6(projectRoot, ".cursor", "rules", "fabric-bootstrap.mdc")
|
|
4254
4808
|
];
|
|
4255
4809
|
for (const abs of blockTargets) {
|
|
4256
4810
|
if (!existsSync4(abs)) {
|
|
@@ -4281,9 +4835,9 @@ ${managedBlock}
|
|
|
4281
4835
|
if (next === existing) {
|
|
4282
4836
|
continue;
|
|
4283
4837
|
}
|
|
4284
|
-
await
|
|
4838
|
+
await atomicWriteText4(abs, next);
|
|
4285
4839
|
}
|
|
4286
|
-
const claudeMdPath =
|
|
4840
|
+
const claudeMdPath = join6(projectRoot, "CLAUDE.md");
|
|
4287
4841
|
if (existsSync4(claudeMdPath)) {
|
|
4288
4842
|
let claudeContent;
|
|
4289
4843
|
try {
|
|
@@ -4307,18 +4861,18 @@ ${managedBlock}
|
|
|
4307
4861
|
ensureLine("@.fabric/project-rules.md");
|
|
4308
4862
|
}
|
|
4309
4863
|
if (updated !== claudeContent) {
|
|
4310
|
-
await
|
|
4864
|
+
await atomicWriteText4(claudeMdPath, updated);
|
|
4311
4865
|
}
|
|
4312
4866
|
}
|
|
4313
4867
|
}
|
|
4314
4868
|
async function fixMcpConfigInWrongFile(projectRoot) {
|
|
4315
|
-
const settingsPath =
|
|
4869
|
+
const settingsPath = join6(projectRoot, ".claude", "settings.json");
|
|
4316
4870
|
if (!existsSync4(settingsPath)) {
|
|
4317
4871
|
return;
|
|
4318
4872
|
}
|
|
4319
4873
|
let settings;
|
|
4320
4874
|
try {
|
|
4321
|
-
const parsed = JSON.parse(
|
|
4875
|
+
const parsed = JSON.parse(readFileSync2(settingsPath, "utf8"));
|
|
4322
4876
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4323
4877
|
return;
|
|
4324
4878
|
}
|
|
@@ -4345,12 +4899,12 @@ async function fixMcpConfigInWrongFile(projectRoot) {
|
|
|
4345
4899
|
});
|
|
4346
4900
|
}
|
|
4347
4901
|
async function ensureKnowledgeSubdirs(projectRoot) {
|
|
4348
|
-
for (const sub of
|
|
4349
|
-
await
|
|
4902
|
+
for (const sub of KNOWLEDGE_SUBDIRS3) {
|
|
4903
|
+
await mkdir4(join6(projectRoot, ".fabric", "knowledge", sub), { recursive: true });
|
|
4350
4904
|
}
|
|
4351
4905
|
}
|
|
4352
4906
|
async function fixCounterDesync(projectRoot) {
|
|
4353
|
-
const metaPath =
|
|
4907
|
+
const metaPath = join6(projectRoot, ".fabric", "agents.meta.json");
|
|
4354
4908
|
if (!existsSync4(metaPath)) {
|
|
4355
4909
|
return;
|
|
4356
4910
|
}
|
|
@@ -4380,9 +4934,9 @@ async function fixCounterDesync(projectRoot) {
|
|
|
4380
4934
|
await atomicWriteJson2(metaPath, updated, { indent: 2 });
|
|
4381
4935
|
}
|
|
4382
4936
|
async function ensureEventLedger(projectRoot) {
|
|
4383
|
-
const
|
|
4384
|
-
await ensureParentDirectory(
|
|
4385
|
-
await writeFile2(
|
|
4937
|
+
const path2 = getEventLedgerPath(projectRoot);
|
|
4938
|
+
await ensureParentDirectory(path2);
|
|
4939
|
+
await writeFile2(path2, "", { encoding: "utf8", flag: "a" });
|
|
4386
4940
|
}
|
|
4387
4941
|
var CITE_POLICY_VERSION = "2.0.0-rc.20";
|
|
4388
4942
|
async function ensureCitePolicyActivatedMarker(projectRoot) {
|
|
@@ -4409,6 +4963,14 @@ async function ensureCitePolicyActivatedMarker(projectRoot) {
|
|
|
4409
4963
|
return { marker_ts: 0, emitted_now: false };
|
|
4410
4964
|
}
|
|
4411
4965
|
}
|
|
4966
|
+
function parseNoneSentinel(kbLineRaw) {
|
|
4967
|
+
if (typeof kbLineRaw !== "string" || kbLineRaw.length === 0) return "unspecified";
|
|
4968
|
+
const m = kbLineRaw.match(/^KB:\s*none\b\s*(?:\[([^\]]*)\])?\s*$/i);
|
|
4969
|
+
if (m === null) return "unspecified";
|
|
4970
|
+
const inner = (m[1] ?? "").trim().toLowerCase();
|
|
4971
|
+
if (inner === "no-relevant" || inner === "not-applicable") return inner;
|
|
4972
|
+
return "unspecified";
|
|
4973
|
+
}
|
|
4412
4974
|
function categorizeCiteTag(tag) {
|
|
4413
4975
|
if (tag === "planned" || tag === "recalled" || tag === "chained-from" || tag === "none") {
|
|
4414
4976
|
return { category: tag };
|
|
@@ -4492,7 +5054,7 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
4492
5054
|
break;
|
|
4493
5055
|
}
|
|
4494
5056
|
}
|
|
4495
|
-
const filteredTurns = options.client === "all" ? assistantTurns : assistantTurns.filter((
|
|
5057
|
+
const filteredTurns = options.client === "all" ? assistantTurns : assistantTurns.filter((t2) => t2.client === options.client);
|
|
4496
5058
|
let clientSessionIds = null;
|
|
4497
5059
|
if (options.client !== "all") {
|
|
4498
5060
|
clientSessionIds = /* @__PURE__ */ new Set();
|
|
@@ -4547,6 +5109,7 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
4547
5109
|
return false;
|
|
4548
5110
|
};
|
|
4549
5111
|
const dismissedHistogram = {};
|
|
5112
|
+
const noneHistogram = {};
|
|
4550
5113
|
const perClientAccum = /* @__PURE__ */ new Map();
|
|
4551
5114
|
const emptyMetrics = () => ({
|
|
4552
5115
|
edits_touched: 0,
|
|
@@ -4596,7 +5159,11 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
4596
5159
|
dismissedHistogram[key] = (dismissedHistogram[key] ?? 0) + 1;
|
|
4597
5160
|
break;
|
|
4598
5161
|
}
|
|
4599
|
-
case "none":
|
|
5162
|
+
case "none": {
|
|
5163
|
+
const sentinel = parseNoneSentinel(turn.kb_line_raw);
|
|
5164
|
+
noneHistogram[sentinel] = (noneHistogram[sentinel] ?? 0) + 1;
|
|
5165
|
+
break;
|
|
5166
|
+
}
|
|
4600
5167
|
default:
|
|
4601
5168
|
break;
|
|
4602
5169
|
}
|
|
@@ -4650,6 +5217,7 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
4650
5217
|
metrics,
|
|
4651
5218
|
...perClient !== void 0 ? { per_client: perClient } : {},
|
|
4652
5219
|
...Object.keys(dismissedHistogram).length > 0 ? { dismissed_reason_histogram: dismissedHistogram } : {},
|
|
5220
|
+
...Object.keys(noneHistogram).length > 0 ? { none_reason_histogram: noneHistogram } : {},
|
|
4653
5221
|
generated_at: generatedAt
|
|
4654
5222
|
};
|
|
4655
5223
|
}
|
|
@@ -4669,11 +5237,11 @@ function isValidJsonLine(line) {
|
|
|
4669
5237
|
function normalizeTarget(targetInput) {
|
|
4670
5238
|
return isAbsolute2(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
|
|
4671
5239
|
}
|
|
4672
|
-
function normalizePath(
|
|
4673
|
-
return posix.normalize(
|
|
5240
|
+
function normalizePath(path2) {
|
|
5241
|
+
return posix.normalize(path2.split("\\").join("/"));
|
|
4674
5242
|
}
|
|
4675
5243
|
function collectEntryPoints(root) {
|
|
4676
|
-
if (!existsSync4(root) || !
|
|
5244
|
+
if (!existsSync4(root) || !statSync4(root).isDirectory()) {
|
|
4677
5245
|
return [];
|
|
4678
5246
|
}
|
|
4679
5247
|
const entries = [];
|
|
@@ -4684,7 +5252,7 @@ function collectEntryPoints(root) {
|
|
|
4684
5252
|
continue;
|
|
4685
5253
|
}
|
|
4686
5254
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
4687
|
-
const absolutePath =
|
|
5255
|
+
const absolutePath = join6(current, entry.name);
|
|
4688
5256
|
const relativePath = normalizePath(absolutePath.slice(root.length + 1));
|
|
4689
5257
|
if (relativePath.length === 0) {
|
|
4690
5258
|
continue;
|
|
@@ -4740,10 +5308,227 @@ function reduceStatus(statuses) {
|
|
|
4740
5308
|
function isMissingFileError(error) {
|
|
4741
5309
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
4742
5310
|
}
|
|
5311
|
+
var ENRICH_DESC_FIELDS = ["intent_clues", "tech_stack", "impact", "must_read_if"];
|
|
5312
|
+
var ENRICH_DESC_FIELD_PATTERNS = {
|
|
5313
|
+
intent_clues: /^intent_clues\s*:/mu,
|
|
5314
|
+
tech_stack: /^tech_stack\s*:/mu,
|
|
5315
|
+
impact: /^impact\s*:/mu,
|
|
5316
|
+
must_read_if: /^must_read_if\s*:/mu
|
|
5317
|
+
};
|
|
5318
|
+
async function enrichDescriptions(projectRoot, opts = {}) {
|
|
5319
|
+
const auto = opts.auto === true;
|
|
5320
|
+
const dryRun = opts.dryRun === true;
|
|
5321
|
+
const mode = auto ? "auto" : "interactive";
|
|
5322
|
+
const candidates = [];
|
|
5323
|
+
let scanned = 0;
|
|
5324
|
+
let modified = 0;
|
|
5325
|
+
let skipped = 0;
|
|
5326
|
+
for (const visit of iterateCanonicalFilenames(projectRoot)) {
|
|
5327
|
+
const layerRoot = visit.layer === "team" ? join6(projectRoot, ".fabric", "knowledge") : resolvePersonalKnowledgeRoot();
|
|
5328
|
+
const absPath = join6(layerRoot, visit.type, visit.filename);
|
|
5329
|
+
scanned += 1;
|
|
5330
|
+
let source;
|
|
5331
|
+
try {
|
|
5332
|
+
source = await readFile5(absPath, "utf8");
|
|
5333
|
+
} catch {
|
|
5334
|
+
continue;
|
|
5335
|
+
}
|
|
5336
|
+
const fmMatch = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u.exec(source);
|
|
5337
|
+
if (fmMatch === null) {
|
|
5338
|
+
candidates.push({
|
|
5339
|
+
path: visit.displayPath,
|
|
5340
|
+
missing: [...ENRICH_DESC_FIELDS],
|
|
5341
|
+
modified: false,
|
|
5342
|
+
added_fields: [],
|
|
5343
|
+
error: "frontmatter not parseable"
|
|
5344
|
+
});
|
|
5345
|
+
continue;
|
|
5346
|
+
}
|
|
5347
|
+
const block = fmMatch[1];
|
|
5348
|
+
const missing = ENRICH_DESC_FIELDS.filter(
|
|
5349
|
+
(field) => !ENRICH_DESC_FIELD_PATTERNS[field].test(block)
|
|
5350
|
+
);
|
|
5351
|
+
if (missing.length === 0) {
|
|
5352
|
+
skipped += 1;
|
|
5353
|
+
continue;
|
|
5354
|
+
}
|
|
5355
|
+
if (!auto || dryRun) {
|
|
5356
|
+
candidates.push({
|
|
5357
|
+
path: visit.displayPath,
|
|
5358
|
+
missing,
|
|
5359
|
+
modified: false,
|
|
5360
|
+
added_fields: []
|
|
5361
|
+
});
|
|
5362
|
+
continue;
|
|
5363
|
+
}
|
|
5364
|
+
const mustReadIf = synthesizeMustReadIfStub(source, visit.filename);
|
|
5365
|
+
const additions = [];
|
|
5366
|
+
for (const field of missing) {
|
|
5367
|
+
if (field === "must_read_if") {
|
|
5368
|
+
additions.push({ field, line: `must_read_if: ${yamlQuoteIfNeeded(mustReadIf)}` });
|
|
5369
|
+
} else {
|
|
5370
|
+
additions.push({ field, line: `${field}: []` });
|
|
5371
|
+
}
|
|
5372
|
+
}
|
|
5373
|
+
const trailing = block.endsWith("\n") ? "" : "\n";
|
|
5374
|
+
const replacedBlock = `${block}${trailing}${additions.map((a) => a.line).join("\n")}`;
|
|
5375
|
+
const blockStart = source.indexOf(block);
|
|
5376
|
+
if (blockStart < 0) {
|
|
5377
|
+
candidates.push({
|
|
5378
|
+
path: visit.displayPath,
|
|
5379
|
+
missing,
|
|
5380
|
+
modified: false,
|
|
5381
|
+
added_fields: [],
|
|
5382
|
+
error: "frontmatter block not located after match"
|
|
5383
|
+
});
|
|
5384
|
+
continue;
|
|
5385
|
+
}
|
|
5386
|
+
const rewritten = source.slice(0, blockStart) + replacedBlock + source.slice(blockStart + block.length);
|
|
5387
|
+
await atomicWriteText4(absPath, rewritten);
|
|
5388
|
+
modified += 1;
|
|
5389
|
+
candidates.push({
|
|
5390
|
+
path: visit.displayPath,
|
|
5391
|
+
missing,
|
|
5392
|
+
modified: true,
|
|
5393
|
+
added_fields: additions.map((a) => a.field)
|
|
5394
|
+
});
|
|
5395
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
5396
|
+
event_type: "knowledge_enriched",
|
|
5397
|
+
path: visit.displayPath,
|
|
5398
|
+
added_fields: additions.map((a) => a.field),
|
|
5399
|
+
mode,
|
|
5400
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5401
|
+
}).catch(() => {
|
|
5402
|
+
});
|
|
5403
|
+
}
|
|
5404
|
+
candidates.sort((a, b) => a.path.localeCompare(b.path));
|
|
5405
|
+
return { mode, dryRun, scanned, modified, skipped, candidates };
|
|
5406
|
+
}
|
|
5407
|
+
function synthesizeMustReadIfStub(source, filename) {
|
|
5408
|
+
const h1Match = /^#\s+(.+?)\s*$/mu.exec(source);
|
|
5409
|
+
let raw = h1Match !== null ? h1Match[1] : filename.replace(/^K[PT]-[A-Z]+-\d+--/, "").replace(/\.md$/u, "").replace(/-/g, " ");
|
|
5410
|
+
raw = raw.trim();
|
|
5411
|
+
if (raw.length === 0) {
|
|
5412
|
+
raw = "describes a knowledge invariant for this project";
|
|
5413
|
+
}
|
|
5414
|
+
if (raw.length > 120) {
|
|
5415
|
+
raw = `${raw.slice(0, 117)}...`;
|
|
5416
|
+
}
|
|
5417
|
+
return raw;
|
|
5418
|
+
}
|
|
5419
|
+
function yamlQuoteIfNeeded(value) {
|
|
5420
|
+
if (value.length === 0) {
|
|
5421
|
+
return '""';
|
|
5422
|
+
}
|
|
5423
|
+
if (/[:#"'\\[\]{},&*!|>%@`]/.test(value) || /^[\s-?]/.test(value) || /\s$/.test(value)) {
|
|
5424
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
5425
|
+
}
|
|
5426
|
+
return value;
|
|
5427
|
+
}
|
|
5428
|
+
|
|
5429
|
+
// src/services/load-active-meta.ts
|
|
5430
|
+
async function loadActiveMeta(projectRoot, opts = {}) {
|
|
5431
|
+
const onDisk = await readAgentsMeta(projectRoot);
|
|
5432
|
+
const previousRevisionHash = onDisk.revision;
|
|
5433
|
+
const derived = await buildKnowledgeMeta(projectRoot);
|
|
5434
|
+
if (derived.meta.revision === previousRevisionHash) {
|
|
5435
|
+
return {
|
|
5436
|
+
meta: onDisk,
|
|
5437
|
+
auto_healed: false,
|
|
5438
|
+
previous_revision_hash: previousRevisionHash,
|
|
5439
|
+
revision_hash: previousRevisionHash
|
|
5440
|
+
};
|
|
5441
|
+
}
|
|
5442
|
+
const written = await writeKnowledgeMeta(projectRoot, { source: "doctor_fix" });
|
|
5443
|
+
contextCache.invalidate("meta_write", projectRoot);
|
|
5444
|
+
await emitAutoHealEventBestEffort(projectRoot, {
|
|
5445
|
+
previous_revision_hash: previousRevisionHash,
|
|
5446
|
+
revision_hash: written.meta.revision,
|
|
5447
|
+
caller: opts.caller
|
|
5448
|
+
});
|
|
5449
|
+
return {
|
|
5450
|
+
meta: written.meta,
|
|
5451
|
+
auto_healed: true,
|
|
5452
|
+
previous_revision_hash: previousRevisionHash,
|
|
5453
|
+
revision_hash: written.meta.revision
|
|
5454
|
+
};
|
|
5455
|
+
}
|
|
5456
|
+
async function loadActiveMetaOrStale(projectRoot, opts = {}) {
|
|
5457
|
+
let onDisk;
|
|
5458
|
+
try {
|
|
5459
|
+
onDisk = await readAgentsMeta(projectRoot);
|
|
5460
|
+
} catch (error) {
|
|
5461
|
+
if (error instanceof AgentsMetaFileMissingError || error instanceof AgentsMetaInvalidError) {
|
|
5462
|
+
throw error;
|
|
5463
|
+
}
|
|
5464
|
+
throw error;
|
|
5465
|
+
}
|
|
5466
|
+
const previousRevisionHash = onDisk.revision;
|
|
5467
|
+
let derived;
|
|
5468
|
+
try {
|
|
5469
|
+
derived = await buildKnowledgeMeta(projectRoot);
|
|
5470
|
+
} catch (error) {
|
|
5471
|
+
return {
|
|
5472
|
+
meta: onDisk,
|
|
5473
|
+
auto_healed: false,
|
|
5474
|
+
previous_revision_hash: previousRevisionHash,
|
|
5475
|
+
revision_hash: previousRevisionHash,
|
|
5476
|
+
degraded: true,
|
|
5477
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5478
|
+
};
|
|
5479
|
+
}
|
|
5480
|
+
if (derived.meta.revision === previousRevisionHash) {
|
|
5481
|
+
return {
|
|
5482
|
+
meta: onDisk,
|
|
5483
|
+
auto_healed: false,
|
|
5484
|
+
previous_revision_hash: previousRevisionHash,
|
|
5485
|
+
revision_hash: previousRevisionHash,
|
|
5486
|
+
degraded: false
|
|
5487
|
+
};
|
|
5488
|
+
}
|
|
5489
|
+
let written;
|
|
5490
|
+
try {
|
|
5491
|
+
written = await writeKnowledgeMeta(projectRoot, { source: "doctor_fix" });
|
|
5492
|
+
} catch (error) {
|
|
5493
|
+
return {
|
|
5494
|
+
meta: onDisk,
|
|
5495
|
+
auto_healed: false,
|
|
5496
|
+
previous_revision_hash: previousRevisionHash,
|
|
5497
|
+
revision_hash: previousRevisionHash,
|
|
5498
|
+
degraded: true,
|
|
5499
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5500
|
+
};
|
|
5501
|
+
}
|
|
5502
|
+
contextCache.invalidate("meta_write", projectRoot);
|
|
5503
|
+
await emitAutoHealEventBestEffort(projectRoot, {
|
|
5504
|
+
previous_revision_hash: previousRevisionHash,
|
|
5505
|
+
revision_hash: written.meta.revision,
|
|
5506
|
+
caller: opts.caller
|
|
5507
|
+
});
|
|
5508
|
+
return {
|
|
5509
|
+
meta: written.meta,
|
|
5510
|
+
auto_healed: true,
|
|
5511
|
+
previous_revision_hash: previousRevisionHash,
|
|
5512
|
+
revision_hash: written.meta.revision,
|
|
5513
|
+
degraded: false
|
|
5514
|
+
};
|
|
5515
|
+
}
|
|
5516
|
+
async function emitAutoHealEventBestEffort(projectRoot, payload) {
|
|
5517
|
+
try {
|
|
5518
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
5519
|
+
event_type: "knowledge_meta_auto_healed",
|
|
5520
|
+
previous_revision_hash: payload.previous_revision_hash,
|
|
5521
|
+
revision_hash: payload.revision_hash,
|
|
5522
|
+
trigger: "read",
|
|
5523
|
+
...payload.caller !== void 0 ? { caller: payload.caller } : {}
|
|
5524
|
+
});
|
|
5525
|
+
} catch {
|
|
5526
|
+
}
|
|
5527
|
+
}
|
|
4743
5528
|
|
|
4744
5529
|
// src/services/get-knowledge.ts
|
|
4745
5530
|
import { readFile as readFile6 } from "fs/promises";
|
|
4746
|
-
import { join as
|
|
5531
|
+
import { join as join7 } from "path";
|
|
4747
5532
|
import { minimatch as minimatch2 } from "minimatch";
|
|
4748
5533
|
var PRIORITY_ORDER = {
|
|
4749
5534
|
high: 0,
|
|
@@ -4751,6 +5536,10 @@ var PRIORITY_ORDER = {
|
|
|
4751
5536
|
low: 2
|
|
4752
5537
|
};
|
|
4753
5538
|
async function getKnowledge(projectRoot, input) {
|
|
5539
|
+
const metaResult = await loadActiveMeta(projectRoot, { caller: "getKnowledge" });
|
|
5540
|
+
if (metaResult.auto_healed) {
|
|
5541
|
+
contextCache.invalidate("file_watch", projectRoot);
|
|
5542
|
+
}
|
|
4754
5543
|
const context = await loadGetKnowledgeContext(projectRoot);
|
|
4755
5544
|
const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
|
|
4756
5545
|
const matchedNodes = matchRuleNodes(context.meta, input.path);
|
|
@@ -4783,7 +5572,7 @@ async function loadGetKnowledgeContext(projectRoot) {
|
|
|
4783
5572
|
return cached;
|
|
4784
5573
|
}
|
|
4785
5574
|
const meta = await readAgentsMeta(projectRoot);
|
|
4786
|
-
const l0Content = await readFile6(
|
|
5575
|
+
const l0Content = await readFile6(join7(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
|
|
4787
5576
|
const context = {
|
|
4788
5577
|
meta,
|
|
4789
5578
|
l0Content,
|
|
@@ -4792,16 +5581,16 @@ async function loadGetKnowledgeContext(projectRoot) {
|
|
|
4792
5581
|
contextCache.set("context", projectRoot, context);
|
|
4793
5582
|
return context;
|
|
4794
5583
|
}
|
|
4795
|
-
async function resolveKnowledgeForPath(projectRoot, context,
|
|
4796
|
-
const matchedNodes = matchRuleNodes(context.meta,
|
|
5584
|
+
async function resolveKnowledgeForPath(projectRoot, context, path2, options = {}) {
|
|
5585
|
+
const matchedNodes = matchRuleNodes(context.meta, path2);
|
|
4797
5586
|
const loaded = await loadMatchedRules(projectRoot, matchedNodes);
|
|
4798
5587
|
return buildKnowledgePayload(context, loaded, options);
|
|
4799
5588
|
}
|
|
4800
5589
|
function normalizeKnowledgePath(value) {
|
|
4801
5590
|
return value.replaceAll("\\", "/");
|
|
4802
5591
|
}
|
|
4803
|
-
function matchRuleNodes(meta,
|
|
4804
|
-
const requestedPath = normalizeKnowledgePath(
|
|
5592
|
+
function matchRuleNodes(meta, path2) {
|
|
5593
|
+
const requestedPath = normalizeKnowledgePath(path2);
|
|
4805
5594
|
return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
|
|
4806
5595
|
const [leftId, leftNode] = left;
|
|
4807
5596
|
const [rightId, rightNode] = right;
|
|
@@ -4922,13 +5711,14 @@ async function readRuleContent(projectRoot, file, fileContentCache) {
|
|
|
4922
5711
|
if (cached !== void 0) {
|
|
4923
5712
|
return await cached;
|
|
4924
5713
|
}
|
|
4925
|
-
const pending = readFile6(
|
|
5714
|
+
const pending = readFile6(join7(projectRoot, file), "utf8");
|
|
4926
5715
|
fileContentCache.set(file, pending);
|
|
4927
5716
|
return await pending;
|
|
4928
5717
|
}
|
|
4929
5718
|
|
|
4930
5719
|
export {
|
|
4931
5720
|
contextCache,
|
|
5721
|
+
AgentsMetaFileMissingError,
|
|
4932
5722
|
resolveProjectRoot,
|
|
4933
5723
|
readAgentsMeta,
|
|
4934
5724
|
LEDGER_PATH,
|
|
@@ -4955,10 +5745,18 @@ export {
|
|
|
4955
5745
|
invalidateKnowledgeSyncCooldown,
|
|
4956
5746
|
ensureKnowledgeFresh,
|
|
4957
5747
|
reconcileKnowledge,
|
|
5748
|
+
loadActiveMeta,
|
|
5749
|
+
loadActiveMetaOrStale,
|
|
4958
5750
|
getKnowledge,
|
|
4959
5751
|
normalizeKnowledgePath,
|
|
5752
|
+
ServeLockHeldError,
|
|
5753
|
+
acquireLock,
|
|
5754
|
+
releaseLock,
|
|
5755
|
+
readLockState,
|
|
5756
|
+
checkLockOrThrow,
|
|
4960
5757
|
runDoctorReport,
|
|
4961
5758
|
runDoctorFix,
|
|
4962
5759
|
runDoctorApplyLint,
|
|
4963
|
-
runDoctorCiteCoverage
|
|
5760
|
+
runDoctorCiteCoverage,
|
|
5761
|
+
enrichDescriptions
|
|
4964
5762
|
};
|