@fenglimg/fabric-server 1.7.0 → 1.8.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-PTFSYO4Y.js → chunk-E3BHIUIW.js} +986 -166
- package/dist/{http-6LFZLHCN.js → http-MEFXOG3L.js} +82 -83
- package/dist/index.d.ts +147 -3
- package/dist/index.js +329 -140
- package/dist/static/assets/index-C-ba4ih0.js +10 -0
- package/dist/static/index.html +1 -1
- package/package.json +10 -4
- package/dist/static/assets/index-DEc8gkD_.js +0 -10
|
@@ -82,29 +82,86 @@ var ContextCache = class {
|
|
|
82
82
|
};
|
|
83
83
|
var contextCache = new ContextCache(5e3);
|
|
84
84
|
|
|
85
|
+
// src/meta-reader.ts
|
|
86
|
+
import { readFile } from "fs/promises";
|
|
87
|
+
import { join } from "path";
|
|
88
|
+
import { agentsMetaSchema } from "@fenglimg/fabric-shared";
|
|
89
|
+
import { IOFabricError } from "@fenglimg/fabric-shared/errors";
|
|
90
|
+
import { agentsMetaNodeSchema, agentsMetaSchema as agentsMetaSchema2 } from "@fenglimg/fabric-shared";
|
|
91
|
+
var AgentsMetaFileMissingError = class extends IOFabricError {
|
|
92
|
+
constructor(metaPath, opts) {
|
|
93
|
+
super(`Fabric agents metadata file is missing: ${metaPath}`, {
|
|
94
|
+
actionHint: opts?.actionHint ?? "Run `fab init` to scaffold the .fabric/agents.meta.json file"
|
|
95
|
+
});
|
|
96
|
+
this.metaPath = metaPath;
|
|
97
|
+
}
|
|
98
|
+
metaPath;
|
|
99
|
+
code = "FABRIC_META_MISSING";
|
|
100
|
+
httpStatus = 404;
|
|
101
|
+
};
|
|
102
|
+
var AgentsMetaInvalidError = class extends IOFabricError {
|
|
103
|
+
constructor(metaPath, cause, opts) {
|
|
104
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
105
|
+
super(`Fabric agents metadata file is invalid: ${metaPath}. ${detail}`, {
|
|
106
|
+
actionHint: opts?.actionHint ?? "Check the agents.meta.json file for schema errors and regenerate if needed"
|
|
107
|
+
});
|
|
108
|
+
this.metaPath = metaPath;
|
|
109
|
+
}
|
|
110
|
+
metaPath;
|
|
111
|
+
code = "FABRIC_META_INVALID";
|
|
112
|
+
httpStatus = 500;
|
|
113
|
+
};
|
|
114
|
+
function getAgentsMetaPath(projectRoot) {
|
|
115
|
+
return join(projectRoot, ".fabric", "agents.meta.json");
|
|
116
|
+
}
|
|
117
|
+
function resolveProjectRoot() {
|
|
118
|
+
return process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
119
|
+
}
|
|
120
|
+
async function readAgentsMeta(projectRoot) {
|
|
121
|
+
const cached = contextCache.get("meta", projectRoot);
|
|
122
|
+
if (cached !== void 0) {
|
|
123
|
+
return cached;
|
|
124
|
+
}
|
|
125
|
+
const metaPath = getAgentsMetaPath(projectRoot);
|
|
126
|
+
let raw;
|
|
127
|
+
try {
|
|
128
|
+
raw = await readFile(metaPath, "utf8");
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
131
|
+
throw new AgentsMetaFileMissingError(metaPath);
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
let parsed;
|
|
136
|
+
try {
|
|
137
|
+
parsed = agentsMetaSchema.parse(JSON.parse(raw));
|
|
138
|
+
} catch (error) {
|
|
139
|
+
throw new AgentsMetaInvalidError(metaPath, error);
|
|
140
|
+
}
|
|
141
|
+
contextCache.set("meta", projectRoot, parsed);
|
|
142
|
+
return parsed;
|
|
143
|
+
}
|
|
144
|
+
|
|
85
145
|
// src/services/_shared.ts
|
|
86
|
-
import { dirname, join, resolve, sep } from "path";
|
|
146
|
+
import { dirname, join as join2, resolve, sep } from "path";
|
|
87
147
|
import { createHash } from "crypto";
|
|
88
|
-
import { mkdir
|
|
148
|
+
import { mkdir } from "fs/promises";
|
|
149
|
+
import { PathEscapeError } from "@fenglimg/fabric-shared/errors";
|
|
150
|
+
import { atomicWriteText, atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
89
151
|
var FABRIC_DIR = ".fabric";
|
|
90
152
|
var LEDGER_FILE = ".intent-ledger.jsonl";
|
|
91
153
|
var LEDGER_PATH = `${FABRIC_DIR}/${LEDGER_FILE}`;
|
|
92
154
|
var LEGACY_LEDGER_PATH = LEDGER_FILE;
|
|
93
155
|
var EVENT_LEDGER_FILE = "events.jsonl";
|
|
94
156
|
var EVENT_LEDGER_PATH = `${FABRIC_DIR}/${EVENT_LEDGER_FILE}`;
|
|
95
|
-
async function atomicWriteText(path, content) {
|
|
96
|
-
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
97
|
-
await writeFile(tempPath, content, "utf8");
|
|
98
|
-
await rename(tempPath, path);
|
|
99
|
-
}
|
|
100
157
|
function getLedgerPath(projectRoot) {
|
|
101
|
-
return
|
|
158
|
+
return join2(projectRoot, LEDGER_PATH);
|
|
102
159
|
}
|
|
103
160
|
function getLegacyLedgerPath(projectRoot) {
|
|
104
|
-
return
|
|
161
|
+
return join2(projectRoot, LEGACY_LEDGER_PATH);
|
|
105
162
|
}
|
|
106
163
|
function getEventLedgerPath(projectRoot) {
|
|
107
|
-
return
|
|
164
|
+
return join2(projectRoot, EVENT_LEDGER_PATH);
|
|
108
165
|
}
|
|
109
166
|
async function ensureParentDirectory(path) {
|
|
110
167
|
await mkdir(dirname(path), { recursive: true });
|
|
@@ -118,10 +175,13 @@ function isNodeError(error) {
|
|
|
118
175
|
|
|
119
176
|
// src/services/event-ledger.ts
|
|
120
177
|
import { randomUUID } from "crypto";
|
|
121
|
-
import {
|
|
178
|
+
import { existsSync, fsyncSync, openSync, closeSync } from "fs";
|
|
179
|
+
import { readFile as readFile2, truncate, writeFile } from "fs/promises";
|
|
122
180
|
import {
|
|
123
181
|
eventLedgerEventSchema
|
|
124
182
|
} from "@fenglimg/fabric-shared";
|
|
183
|
+
import { createLedgerWriteQueue } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
184
|
+
var ledgerQueue = createLedgerWriteQueue();
|
|
125
185
|
async function appendEventLedgerEvent(projectRoot, event) {
|
|
126
186
|
const eventPath = getEventLedgerPath(projectRoot);
|
|
127
187
|
const nextEvent = eventLedgerEventSchema.parse({
|
|
@@ -132,22 +192,60 @@ async function appendEventLedgerEvent(projectRoot, event) {
|
|
|
132
192
|
schema_version: 1
|
|
133
193
|
});
|
|
134
194
|
await ensureParentDirectory(eventPath);
|
|
135
|
-
await
|
|
136
|
-
`, "utf8");
|
|
195
|
+
await ledgerQueue.append(eventPath, JSON.stringify(nextEvent));
|
|
137
196
|
return nextEvent;
|
|
138
197
|
}
|
|
139
198
|
async function readEventLedger(projectRoot, options = {}) {
|
|
140
199
|
const eventPath = getEventLedgerPath(projectRoot);
|
|
141
200
|
let raw;
|
|
142
201
|
try {
|
|
143
|
-
raw = await
|
|
202
|
+
raw = await readFile2(eventPath, "utf8");
|
|
144
203
|
} catch (error) {
|
|
145
204
|
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
146
|
-
return [];
|
|
205
|
+
return { events: [], warnings: [] };
|
|
147
206
|
}
|
|
148
207
|
throw error;
|
|
149
208
|
}
|
|
150
|
-
|
|
209
|
+
const warnings = [];
|
|
210
|
+
const lines = raw.split(/\r?\n/);
|
|
211
|
+
const hasTrailingNewline = raw.endsWith("\n");
|
|
212
|
+
let partialLine;
|
|
213
|
+
if (!hasTrailingNewline && lines.length > 0) {
|
|
214
|
+
partialLine = lines.pop();
|
|
215
|
+
}
|
|
216
|
+
if (partialLine !== void 0 && partialLine.trim().length > 0) {
|
|
217
|
+
const fullContentBeforePartial = raw.slice(0, raw.length - partialLine.length);
|
|
218
|
+
const byteOffset = Buffer.byteLength(fullContentBeforePartial, "utf8");
|
|
219
|
+
const byteLength = Buffer.byteLength(partialLine, "utf8");
|
|
220
|
+
warnings.push({
|
|
221
|
+
kind: "partial_write_at_tail",
|
|
222
|
+
byte_offset: byteOffset,
|
|
223
|
+
byte_length: byteLength,
|
|
224
|
+
snippet_first_120: partialLine.slice(0, 120)
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
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);
|
|
228
|
+
return { events, warnings };
|
|
229
|
+
}
|
|
230
|
+
async function truncateLedgerToLastNewline(path) {
|
|
231
|
+
const raw = await readFile2(path);
|
|
232
|
+
const content = raw.toString("utf8");
|
|
233
|
+
if (content.endsWith("\n") || content.length === 0) {
|
|
234
|
+
return { truncated_bytes: 0, corrupted_path: "" };
|
|
235
|
+
}
|
|
236
|
+
const lastNewlineIndex = content.lastIndexOf("\n");
|
|
237
|
+
if (lastNewlineIndex === -1) {
|
|
238
|
+
const corruptedPath2 = `${path}.corrupted.${Date.now()}`;
|
|
239
|
+
await writeFile(corruptedPath2, raw);
|
|
240
|
+
await truncate(path, 0);
|
|
241
|
+
return { truncated_bytes: raw.length, corrupted_path: corruptedPath2 };
|
|
242
|
+
}
|
|
243
|
+
const keepByteLength = Buffer.byteLength(content.slice(0, lastNewlineIndex + 1), "utf8");
|
|
244
|
+
const corruptedBytes = raw.slice(keepByteLength);
|
|
245
|
+
const corruptedPath = `${path}.corrupted.${Date.now()}`;
|
|
246
|
+
await writeFile(corruptedPath, corruptedBytes);
|
|
247
|
+
await truncate(path, keepByteLength);
|
|
248
|
+
return { truncated_bytes: corruptedBytes.length, corrupted_path: corruptedPath };
|
|
151
249
|
}
|
|
152
250
|
function parseEventLedgerLine(line, index) {
|
|
153
251
|
try {
|
|
@@ -170,24 +268,35 @@ function createDerivedId(index, line) {
|
|
|
170
268
|
function isNodeError2(error) {
|
|
171
269
|
return error instanceof Error;
|
|
172
270
|
}
|
|
271
|
+
function flushAndSyncEventLedger(projectRoot) {
|
|
272
|
+
const ledgerPath = getEventLedgerPath(projectRoot);
|
|
273
|
+
if (!existsSync(ledgerPath)) return;
|
|
274
|
+
const fd = openSync(ledgerPath, "r+");
|
|
275
|
+
try {
|
|
276
|
+
fsyncSync(fd);
|
|
277
|
+
} finally {
|
|
278
|
+
closeSync(fd);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
173
281
|
|
|
174
282
|
// src/services/rule-meta-builder.ts
|
|
175
|
-
import { readdir, readFile as
|
|
176
|
-
import { existsSync, statSync } from "fs";
|
|
177
|
-
import { isAbsolute, join as
|
|
283
|
+
import { readdir, readFile as readFile3 } from "fs/promises";
|
|
284
|
+
import { existsSync as existsSync2, statSync } from "fs";
|
|
285
|
+
import { isAbsolute, join as join3, relative, resolve as resolve2, sep as sep2 } from "path";
|
|
178
286
|
import {
|
|
179
287
|
RULE_TEST_INDEX_SCHEMA_VERSION,
|
|
180
|
-
agentsMetaSchema,
|
|
288
|
+
agentsMetaSchema as agentsMetaSchema3,
|
|
181
289
|
deriveAgentsMetaLayer,
|
|
182
290
|
deriveAgentsMetaStableId,
|
|
183
291
|
deriveAgentsMetaTopologyType,
|
|
184
292
|
ruleTestIndexSchema
|
|
185
293
|
} from "@fenglimg/fabric-shared";
|
|
294
|
+
import { atomicWriteText as atomicWriteText2 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
186
295
|
async function buildRuleMeta(projectRootInput) {
|
|
187
296
|
const projectRoot = normalizeProjectRoot(projectRootInput);
|
|
188
297
|
assertExistingDirectory(projectRoot);
|
|
189
|
-
const metaPath =
|
|
190
|
-
const ruleTestIndexPath =
|
|
298
|
+
const metaPath = join3(projectRoot, ".fabric", "agents.meta.json");
|
|
299
|
+
const ruleTestIndexPath = join3(projectRoot, ".fabric", "rule-test.index.json");
|
|
191
300
|
const existingMeta = await readExistingMeta(metaPath);
|
|
192
301
|
const existingRuleTestIndex = await readExistingRuleTestIndex(ruleTestIndexPath);
|
|
193
302
|
const meta = await computeRulesBasedAgentsMeta(projectRoot, existingMeta);
|
|
@@ -200,17 +309,17 @@ async function buildRuleMeta(projectRootInput) {
|
|
|
200
309
|
}
|
|
201
310
|
async function writeRuleMeta(projectRootInput, options) {
|
|
202
311
|
const projectRoot = normalizeProjectRoot(projectRootInput);
|
|
203
|
-
const metaPath =
|
|
204
|
-
const ruleTestIndexPath =
|
|
312
|
+
const metaPath = join3(projectRoot, ".fabric", "agents.meta.json");
|
|
313
|
+
const ruleTestIndexPath = join3(projectRoot, ".fabric", "rule-test.index.json");
|
|
205
314
|
const existingMeta = await readExistingMeta(metaPath);
|
|
206
315
|
const result = await buildRuleMeta(projectRoot);
|
|
207
316
|
if (!result.changed) {
|
|
208
317
|
return result;
|
|
209
318
|
}
|
|
210
319
|
await ensureParentDirectory(metaPath);
|
|
211
|
-
await
|
|
320
|
+
await atomicWriteText2(metaPath, `${JSON.stringify(result.meta, null, 2)}
|
|
212
321
|
`);
|
|
213
|
-
await
|
|
322
|
+
await atomicWriteText2(ruleTestIndexPath, `${JSON.stringify(result.ruleTestIndex, null, 2)}
|
|
214
323
|
`);
|
|
215
324
|
if (existingMeta === void 0 || stableStringify(existingMeta) !== stableStringify(result.meta)) {
|
|
216
325
|
await recordBaselineSynced(projectRoot, {
|
|
@@ -227,7 +336,7 @@ async function writeRuleMeta(projectRootInput, options) {
|
|
|
227
336
|
async function computeRulesBasedAgentsMeta(projectRootInput, existingMeta) {
|
|
228
337
|
const projectRoot = normalizeProjectRoot(projectRootInput);
|
|
229
338
|
assertExistingDirectory(projectRoot);
|
|
230
|
-
const previousMeta = existingMeta ?? await readExistingMeta(
|
|
339
|
+
const previousMeta = existingMeta ?? await readExistingMeta(join3(projectRoot, ".fabric", "agents.meta.json"));
|
|
231
340
|
const existingByContentRef = indexExistingNodesByContentRef(previousMeta);
|
|
232
341
|
const ruleFiles = await findFabricRuleFiles(projectRoot);
|
|
233
342
|
const nodes = {};
|
|
@@ -236,7 +345,7 @@ async function computeRulesBasedAgentsMeta(projectRootInput, existingMeta) {
|
|
|
236
345
|
nodes.L0 = bootstrapNode;
|
|
237
346
|
}
|
|
238
347
|
for (const contentRef of ruleFiles) {
|
|
239
|
-
const source = await
|
|
348
|
+
const source = await readFile3(join3(projectRoot, contentRef), "utf8");
|
|
240
349
|
const existing = existingByContentRef.get(contentRef);
|
|
241
350
|
const id = deriveNodeId(contentRef);
|
|
242
351
|
const hash = sha256(source);
|
|
@@ -320,14 +429,14 @@ function normalizeProjectRoot(projectRoot) {
|
|
|
320
429
|
return isAbsolute(projectRoot) ? projectRoot : resolve2(process.cwd(), projectRoot);
|
|
321
430
|
}
|
|
322
431
|
function assertExistingDirectory(projectRoot) {
|
|
323
|
-
if (!
|
|
432
|
+
if (!existsSync2(projectRoot) || !statSync(projectRoot).isDirectory()) {
|
|
324
433
|
throw new Error(`Target directory does not exist: ${projectRoot}`);
|
|
325
434
|
}
|
|
326
435
|
}
|
|
327
436
|
async function readExistingMeta(metaPath) {
|
|
328
437
|
let raw;
|
|
329
438
|
try {
|
|
330
|
-
raw = await
|
|
439
|
+
raw = await readFile3(metaPath, "utf8");
|
|
331
440
|
} catch (error) {
|
|
332
441
|
if (isNodeError3(error) && error.code === "ENOENT") {
|
|
333
442
|
return void 0;
|
|
@@ -335,7 +444,7 @@ async function readExistingMeta(metaPath) {
|
|
|
335
444
|
throw error;
|
|
336
445
|
}
|
|
337
446
|
try {
|
|
338
|
-
return
|
|
447
|
+
return agentsMetaSchema3.parse(JSON.parse(raw));
|
|
339
448
|
} catch {
|
|
340
449
|
return void 0;
|
|
341
450
|
}
|
|
@@ -343,7 +452,7 @@ async function readExistingMeta(metaPath) {
|
|
|
343
452
|
async function readExistingRuleTestIndex(indexPath) {
|
|
344
453
|
let raw;
|
|
345
454
|
try {
|
|
346
|
-
raw = await
|
|
455
|
+
raw = await readFile3(indexPath, "utf8");
|
|
347
456
|
} catch (error) {
|
|
348
457
|
if (isNodeError3(error) && error.code === "ENOENT") {
|
|
349
458
|
return void 0;
|
|
@@ -357,8 +466,8 @@ async function readExistingRuleTestIndex(indexPath) {
|
|
|
357
466
|
}
|
|
358
467
|
}
|
|
359
468
|
async function findFabricRuleFiles(projectRoot) {
|
|
360
|
-
const rulesRoot =
|
|
361
|
-
if (!
|
|
469
|
+
const rulesRoot = join3(projectRoot, ".fabric", "rules");
|
|
470
|
+
if (!existsSync2(rulesRoot) || !statSync(rulesRoot).isDirectory()) {
|
|
362
471
|
return [];
|
|
363
472
|
}
|
|
364
473
|
const files = [];
|
|
@@ -369,7 +478,7 @@ async function findFabricRuleFiles(projectRoot) {
|
|
|
369
478
|
continue;
|
|
370
479
|
}
|
|
371
480
|
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
372
|
-
const absolutePath =
|
|
481
|
+
const absolutePath = join3(current, entry.name);
|
|
373
482
|
const relativePath = toPosixPath(relative(projectRoot, absolutePath));
|
|
374
483
|
if (entry.isDirectory()) {
|
|
375
484
|
stack.push(absolutePath);
|
|
@@ -385,7 +494,7 @@ async function findFabricVerifyAnnotations(projectRoot) {
|
|
|
385
494
|
const annotations = [];
|
|
386
495
|
const annotationPattern = /^\s*\/\/\s*@fabric-verify\s+([A-Za-z0-9][A-Za-z0-9/_-]*)\s*$/u;
|
|
387
496
|
for (const testFile of files) {
|
|
388
|
-
const source = await
|
|
497
|
+
const source = await readFile3(join3(projectRoot, testFile), "utf8");
|
|
389
498
|
const testHash = sha256(source);
|
|
390
499
|
const lines = source.split(/\r?\n/u);
|
|
391
500
|
for (const [index, line] of lines.entries()) {
|
|
@@ -413,7 +522,7 @@ async function findTestFiles(projectRoot) {
|
|
|
413
522
|
continue;
|
|
414
523
|
}
|
|
415
524
|
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
416
|
-
const absolutePath =
|
|
525
|
+
const absolutePath = join3(current, entry.name);
|
|
417
526
|
const relativePath = toPosixPath(relative(projectRoot, absolutePath));
|
|
418
527
|
const [rootSegment] = relativePath.split("/");
|
|
419
528
|
if (entry.isDirectory()) {
|
|
@@ -513,11 +622,11 @@ function createDefaultNodeMeta(contentRef) {
|
|
|
513
622
|
}
|
|
514
623
|
async function createBootstrapNode(projectRoot, existing) {
|
|
515
624
|
const contentRef = ".fabric/bootstrap/README.md";
|
|
516
|
-
const bootstrapPath =
|
|
517
|
-
if (!
|
|
625
|
+
const bootstrapPath = join3(projectRoot, contentRef);
|
|
626
|
+
if (!existsSync2(bootstrapPath)) {
|
|
518
627
|
return void 0;
|
|
519
628
|
}
|
|
520
|
-
const hash = sha256(await
|
|
629
|
+
const hash = sha256(await readFile3(bootstrapPath, "utf8"));
|
|
521
630
|
const identity = {
|
|
522
631
|
stableId: existing?.stable_id ?? deriveAgentsMetaStableId(contentRef),
|
|
523
632
|
identitySource: existing?.identity_source ?? "derived"
|
|
@@ -722,80 +831,335 @@ function isNodeError3(error) {
|
|
|
722
831
|
return error instanceof Error;
|
|
723
832
|
}
|
|
724
833
|
|
|
725
|
-
// src/services/
|
|
726
|
-
import {
|
|
727
|
-
import {
|
|
728
|
-
import {
|
|
729
|
-
import {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
import { agentsMetaSchema as agentsMetaSchema2 } from "@fenglimg/fabric-shared";
|
|
745
|
-
import { agentsMetaNodeSchema, agentsMetaSchema as agentsMetaSchema3 } from "@fenglimg/fabric-shared";
|
|
746
|
-
var AgentsMetaFileMissingError = class extends Error {
|
|
747
|
-
constructor(metaPath) {
|
|
748
|
-
super(`Fabric agents metadata file is missing: ${metaPath}`);
|
|
749
|
-
this.metaPath = metaPath;
|
|
750
|
-
this.name = "AgentsMetaFileMissingError";
|
|
834
|
+
// src/services/rule-sync.ts
|
|
835
|
+
import { readdir as readdir2, readFile as readFile4, stat } from "fs/promises";
|
|
836
|
+
import { existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
837
|
+
import { join as join4, relative as relative2, sep as sep3 } from "path";
|
|
838
|
+
import { RuleValidationError } from "@fenglimg/fabric-shared/errors";
|
|
839
|
+
var lastSyncState = /* @__PURE__ */ new Map();
|
|
840
|
+
var freshSyncCooldown = /* @__PURE__ */ new Map();
|
|
841
|
+
var SYNC_COOLDOWN_MS = 500;
|
|
842
|
+
function invalidateRuleSyncCooldown(projectRoot) {
|
|
843
|
+
freshSyncCooldown.delete(projectRoot);
|
|
844
|
+
}
|
|
845
|
+
async function readMetaEntries(projectRoot) {
|
|
846
|
+
const metaPath = join4(projectRoot, ".fabric", "agents.meta.json");
|
|
847
|
+
const map = /* @__PURE__ */ new Map();
|
|
848
|
+
let raw;
|
|
849
|
+
try {
|
|
850
|
+
raw = await readFile4(metaPath, "utf8");
|
|
851
|
+
} catch {
|
|
852
|
+
return map;
|
|
751
853
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
854
|
+
let parsed;
|
|
855
|
+
try {
|
|
856
|
+
parsed = JSON.parse(raw);
|
|
857
|
+
} catch {
|
|
858
|
+
return map;
|
|
859
|
+
}
|
|
860
|
+
for (const node of Object.values(parsed.nodes ?? {})) {
|
|
861
|
+
const path = node.content_ref ?? node.file;
|
|
862
|
+
const stable_id = node.stable_id;
|
|
863
|
+
const content_hash = node.hash;
|
|
864
|
+
if (path !== void 0 && stable_id !== void 0 && content_hash !== void 0) {
|
|
865
|
+
map.set(path, { stable_id, path, content_hash });
|
|
866
|
+
}
|
|
761
867
|
}
|
|
762
|
-
|
|
763
|
-
code = "FABRIC_META_INVALID";
|
|
764
|
-
};
|
|
765
|
-
function getAgentsMetaPath(projectRoot) {
|
|
766
|
-
return join3(projectRoot, ".fabric", "agents.meta.json");
|
|
868
|
+
return map;
|
|
767
869
|
}
|
|
768
|
-
function
|
|
769
|
-
|
|
870
|
+
async function findRuleFiles(projectRoot) {
|
|
871
|
+
const rulesRoot = join4(projectRoot, ".fabric", "rules");
|
|
872
|
+
if (!existsSync3(rulesRoot) || !statSync2(rulesRoot).isDirectory()) {
|
|
873
|
+
return [];
|
|
874
|
+
}
|
|
875
|
+
const files = [];
|
|
876
|
+
const stack = [rulesRoot];
|
|
877
|
+
while (stack.length > 0) {
|
|
878
|
+
const current = stack.pop();
|
|
879
|
+
if (current === void 0) {
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
for (const entry of await readdir2(current, { withFileTypes: true })) {
|
|
883
|
+
const absolutePath = join4(current, entry.name);
|
|
884
|
+
if (entry.isDirectory()) {
|
|
885
|
+
stack.push(absolutePath);
|
|
886
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
887
|
+
const rel = toPosixPath2(relative2(projectRoot, absolutePath));
|
|
888
|
+
files.push(rel);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return files.sort();
|
|
770
893
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
894
|
+
function toPosixPath2(p) {
|
|
895
|
+
return p.split(sep3).join("/");
|
|
896
|
+
}
|
|
897
|
+
function validateFrontmatter(source, filePath, throwOnInvalid) {
|
|
898
|
+
if (!source.startsWith("---")) {
|
|
899
|
+
return null;
|
|
775
900
|
}
|
|
776
|
-
const
|
|
777
|
-
|
|
901
|
+
const endIdx = source.indexOf("\n---", 3);
|
|
902
|
+
if (endIdx === -1) {
|
|
903
|
+
const msg = `Unterminated YAML frontmatter in ${filePath}`;
|
|
904
|
+
if (throwOnInvalid) {
|
|
905
|
+
throw new RuleValidationError(msg, {
|
|
906
|
+
actionHint: "Run `fab doctor --fix` to repair frontmatter",
|
|
907
|
+
fixable: true,
|
|
908
|
+
details: { file: filePath }
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
code: "rule_frontmatter_invalid",
|
|
913
|
+
file: filePath,
|
|
914
|
+
action_hint: "Run `fab doctor --fix` to repair frontmatter"
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
const frontmatter = source.slice(3, endIdx).trim();
|
|
918
|
+
for (const line of frontmatter.split("\n")) {
|
|
919
|
+
const trimmed = line.trim();
|
|
920
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) {
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
if (!trimmed.includes(":") && !trimmed.startsWith("-")) {
|
|
924
|
+
const msg = `Invalid YAML frontmatter line "${trimmed}" in ${filePath}`;
|
|
925
|
+
if (throwOnInvalid) {
|
|
926
|
+
throw new RuleValidationError(msg, {
|
|
927
|
+
actionHint: "Run `fab doctor --fix` to repair frontmatter",
|
|
928
|
+
fixable: true,
|
|
929
|
+
details: { file: filePath, line: trimmed }
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
return {
|
|
933
|
+
code: "rule_frontmatter_invalid",
|
|
934
|
+
file: filePath,
|
|
935
|
+
action_hint: "Run `fab doctor --fix` to repair frontmatter"
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
async function processSingleFile(projectRoot, relPath, metaEntry, source, throwOnInvalidFrontmatter) {
|
|
942
|
+
const absPath = join4(projectRoot, relPath);
|
|
778
943
|
try {
|
|
779
|
-
|
|
780
|
-
} catch
|
|
781
|
-
if (
|
|
782
|
-
|
|
944
|
+
await stat(absPath);
|
|
945
|
+
} catch {
|
|
946
|
+
if (metaEntry !== void 0) {
|
|
947
|
+
return {
|
|
948
|
+
event: {
|
|
949
|
+
type: "rule_removed",
|
|
950
|
+
stable_id: metaEntry.stable_id,
|
|
951
|
+
path: relPath,
|
|
952
|
+
prev_hash: metaEntry.content_hash,
|
|
953
|
+
new_hash: null,
|
|
954
|
+
changed_fields: ["content"],
|
|
955
|
+
source
|
|
956
|
+
},
|
|
957
|
+
warning: null
|
|
958
|
+
};
|
|
783
959
|
}
|
|
784
|
-
|
|
960
|
+
return { event: null, warning: null };
|
|
785
961
|
}
|
|
786
|
-
let
|
|
962
|
+
let content;
|
|
787
963
|
try {
|
|
788
|
-
|
|
789
|
-
} catch
|
|
790
|
-
|
|
964
|
+
content = await readFile4(absPath, "utf8");
|
|
965
|
+
} catch {
|
|
966
|
+
return { event: null, warning: null };
|
|
967
|
+
}
|
|
968
|
+
const newHash = sha256(content);
|
|
969
|
+
const now = Date.now();
|
|
970
|
+
const debounce = lastSyncState.get(absPath);
|
|
971
|
+
if (debounce !== void 0 && newHash === debounce.hash && now - debounce.ts < 500) {
|
|
972
|
+
return { event: null, warning: null };
|
|
973
|
+
}
|
|
974
|
+
if (metaEntry !== void 0 && newHash === metaEntry.content_hash) {
|
|
975
|
+
lastSyncState.set(absPath, { ts: now, hash: newHash });
|
|
976
|
+
return { event: null, warning: null };
|
|
977
|
+
}
|
|
978
|
+
const warning = validateFrontmatter(content, relPath, throwOnInvalidFrontmatter);
|
|
979
|
+
if (warning !== null) {
|
|
980
|
+
lastSyncState.set(absPath, { ts: now, hash: newHash });
|
|
981
|
+
return { event: null, warning };
|
|
982
|
+
}
|
|
983
|
+
const prevHash = metaEntry?.content_hash ?? debounce?.hash ?? null;
|
|
984
|
+
const stableId = metaEntry?.stable_id ?? relPath;
|
|
985
|
+
const eventType = metaEntry === void 0 ? "rule_added" : "rule_content_changed";
|
|
986
|
+
lastSyncState.set(absPath, { ts: now, hash: newHash });
|
|
987
|
+
return {
|
|
988
|
+
event: {
|
|
989
|
+
type: eventType,
|
|
990
|
+
stable_id: stableId,
|
|
991
|
+
path: relPath,
|
|
992
|
+
prev_hash: prevHash,
|
|
993
|
+
new_hash: newHash,
|
|
994
|
+
changed_fields: ["content"],
|
|
995
|
+
source
|
|
996
|
+
},
|
|
997
|
+
warning: null
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
async function appendRuleSyncEvents(projectRoot, events) {
|
|
1001
|
+
if (events.length === 0) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const driftedIds = events.map((e) => e.stable_id);
|
|
1005
|
+
const missingFiles = events.filter((e) => e.type === "rule_removed").map((e) => e.path);
|
|
1006
|
+
const staleFiles = events.filter((e) => e.type !== "rule_removed").map((e) => e.path);
|
|
1007
|
+
if (missingFiles.length > 0 || staleFiles.length > 0) {
|
|
1008
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1009
|
+
event_type: "rule_drift_detected",
|
|
1010
|
+
drifted_stable_ids: driftedIds,
|
|
1011
|
+
missing_files: missingFiles,
|
|
1012
|
+
stale_files: staleFiles
|
|
1013
|
+
});
|
|
791
1014
|
}
|
|
792
|
-
contextCache.set("meta", projectRoot, parsed);
|
|
793
|
-
return parsed;
|
|
794
1015
|
}
|
|
1016
|
+
async function ensureRulesFresh(projectRoot, opts) {
|
|
1017
|
+
const mode = opts?.mode ?? "incremental";
|
|
1018
|
+
const cooldownExpiry = freshSyncCooldown.get(projectRoot);
|
|
1019
|
+
if (cooldownExpiry !== void 0 && Date.now() < cooldownExpiry && mode !== "full") {
|
|
1020
|
+
return { status: "fresh", events: [], warnings: [] };
|
|
1021
|
+
}
|
|
1022
|
+
const throwOnInvalidFrontmatter = opts?.throwOnInvalidFrontmatter ?? false;
|
|
1023
|
+
const source = "ensureRulesFresh";
|
|
1024
|
+
const events = [];
|
|
1025
|
+
const warnings = [];
|
|
1026
|
+
const metaEntries = await readMetaEntries(projectRoot);
|
|
1027
|
+
const ruleFiles = await findRuleFiles(projectRoot);
|
|
1028
|
+
const filesToCheck = ruleFiles;
|
|
1029
|
+
for (const relPath of filesToCheck) {
|
|
1030
|
+
const metaEntry = metaEntries.get(relPath);
|
|
1031
|
+
const result = await processSingleFile(projectRoot, relPath, metaEntry, source, throwOnInvalidFrontmatter);
|
|
1032
|
+
if (result.event !== null) {
|
|
1033
|
+
events.push(result.event);
|
|
1034
|
+
}
|
|
1035
|
+
if (result.warning !== null) {
|
|
1036
|
+
warnings.push(result.warning);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
for (const [relPath, entry] of metaEntries) {
|
|
1040
|
+
if (!ruleFiles.includes(relPath)) {
|
|
1041
|
+
const absPath = join4(projectRoot, relPath);
|
|
1042
|
+
if (!existsSync3(absPath)) {
|
|
1043
|
+
events.push({
|
|
1044
|
+
type: "rule_removed",
|
|
1045
|
+
stable_id: entry.stable_id,
|
|
1046
|
+
path: relPath,
|
|
1047
|
+
prev_hash: entry.content_hash,
|
|
1048
|
+
new_hash: null,
|
|
1049
|
+
changed_fields: ["content"],
|
|
1050
|
+
source
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (events.length === 0 && warnings.length === 0) {
|
|
1056
|
+
freshSyncCooldown.set(projectRoot, Date.now() + SYNC_COOLDOWN_MS);
|
|
1057
|
+
return { status: "fresh", events: [], warnings: [] };
|
|
1058
|
+
}
|
|
1059
|
+
if (events.length > 0) {
|
|
1060
|
+
await appendRuleSyncEvents(projectRoot, events);
|
|
1061
|
+
contextCache.invalidate("file_watch", projectRoot);
|
|
1062
|
+
}
|
|
1063
|
+
freshSyncCooldown.delete(projectRoot);
|
|
1064
|
+
const status = warnings.length > 0 ? "errors" : "reconciled";
|
|
1065
|
+
return {
|
|
1066
|
+
status,
|
|
1067
|
+
events,
|
|
1068
|
+
warnings,
|
|
1069
|
+
reconciled_files: events.map((e) => e.path)
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
async function reconcileRules(projectRoot, opts) {
|
|
1073
|
+
freshSyncCooldown.delete(projectRoot);
|
|
1074
|
+
const trigger = opts?.trigger;
|
|
1075
|
+
const startTime = Date.now();
|
|
1076
|
+
const source = "reconcileRules";
|
|
1077
|
+
const events = [];
|
|
1078
|
+
const warnings = [];
|
|
1079
|
+
const metaEntries = await readMetaEntries(projectRoot);
|
|
1080
|
+
const ruleFiles = await findRuleFiles(projectRoot);
|
|
1081
|
+
for (const relPath of ruleFiles) {
|
|
1082
|
+
const metaEntry = metaEntries.get(relPath);
|
|
1083
|
+
const result = await processSingleFile(projectRoot, relPath, metaEntry, source, false);
|
|
1084
|
+
if (result.event !== null) {
|
|
1085
|
+
events.push(result.event);
|
|
1086
|
+
}
|
|
1087
|
+
if (result.warning !== null) {
|
|
1088
|
+
warnings.push(result.warning);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
for (const [relPath, entry] of metaEntries) {
|
|
1092
|
+
if (!ruleFiles.includes(relPath)) {
|
|
1093
|
+
const absPath = join4(projectRoot, relPath);
|
|
1094
|
+
if (!existsSync3(absPath)) {
|
|
1095
|
+
events.push({
|
|
1096
|
+
type: "rule_removed",
|
|
1097
|
+
stable_id: entry.stable_id,
|
|
1098
|
+
path: relPath,
|
|
1099
|
+
prev_hash: entry.content_hash,
|
|
1100
|
+
new_hash: null,
|
|
1101
|
+
changed_fields: ["content"],
|
|
1102
|
+
source
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
if (events.length > 0) {
|
|
1108
|
+
await writeRuleMeta(projectRoot, { source: "sync_meta" });
|
|
1109
|
+
await appendRuleSyncEvents(projectRoot, events);
|
|
1110
|
+
contextCache.invalidate("file_watch", projectRoot);
|
|
1111
|
+
}
|
|
1112
|
+
const duration_ms = Date.now() - startTime;
|
|
1113
|
+
const reconciledFiles = events.map((e) => e.path);
|
|
1114
|
+
if (trigger !== void 0 && events.length > 0) {
|
|
1115
|
+
if (trigger === "startup") {
|
|
1116
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1117
|
+
event_type: "meta_reconciled_on_startup",
|
|
1118
|
+
reconciled_files: reconciledFiles,
|
|
1119
|
+
duration_ms,
|
|
1120
|
+
source: "reconcileRules"
|
|
1121
|
+
});
|
|
1122
|
+
} else {
|
|
1123
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1124
|
+
event_type: "meta_reconciled",
|
|
1125
|
+
reconciled_files: reconciledFiles,
|
|
1126
|
+
duration_ms,
|
|
1127
|
+
trigger,
|
|
1128
|
+
source: "reconcileRules"
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
if (events.length === 0 && warnings.length === 0) {
|
|
1133
|
+
return { status: "fresh", events: [], warnings: [] };
|
|
1134
|
+
}
|
|
1135
|
+
const status = warnings.length > 0 ? "errors" : "reconciled";
|
|
1136
|
+
return {
|
|
1137
|
+
status,
|
|
1138
|
+
events,
|
|
1139
|
+
warnings,
|
|
1140
|
+
reconciled_files: reconciledFiles
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// src/services/doctor.ts
|
|
1145
|
+
import { existsSync as existsSync4, mkdirSync, readdirSync, readFileSync, rmdirSync, renameSync, statSync as statSync3 } from "fs";
|
|
1146
|
+
import { access, readFile as readFile7, writeFile as writeFile2 } from "fs/promises";
|
|
1147
|
+
import { constants } from "fs";
|
|
1148
|
+
import { isAbsolute as isAbsolute3, join as join8, posix as posix2, resolve as resolve4 } from "path";
|
|
1149
|
+
import {
|
|
1150
|
+
agentsMetaSchema as agentsMetaSchema4,
|
|
1151
|
+
forensicReportSchema,
|
|
1152
|
+
ruleTestIndexSchema as ruleTestIndexSchema2
|
|
1153
|
+
} from "@fenglimg/fabric-shared";
|
|
1154
|
+
import { detectFramework } from "@fenglimg/fabric-shared/node";
|
|
1155
|
+
|
|
1156
|
+
// src/services/rule-sections.ts
|
|
1157
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1158
|
+
import { join as join7 } from "path";
|
|
795
1159
|
|
|
796
1160
|
// src/services/audit-log.ts
|
|
797
|
-
import { open, stat } from "fs/promises";
|
|
798
|
-
import { isAbsolute as isAbsolute2, join as
|
|
1161
|
+
import { open, stat as stat2 } from "fs/promises";
|
|
1162
|
+
import { isAbsolute as isAbsolute2, join as join5, posix, relative as relative3, resolve as resolve3 } from "path";
|
|
799
1163
|
var AUDIT_LOG_FILE = `${FABRIC_DIR}/audit.jsonl`;
|
|
800
1164
|
var DEFAULT_AUDIT_WINDOW_MS = 5 * 60 * 1e3;
|
|
801
1165
|
async function appendGetRulesAuditEvent(projectRoot, input) {
|
|
@@ -842,7 +1206,7 @@ async function appendRuleSelectionAuditEvent(projectRoot, input) {
|
|
|
842
1206
|
function normalizeAuditPath(projectRoot, value) {
|
|
843
1207
|
const normalizedProjectRoot = resolve3(projectRoot);
|
|
844
1208
|
const candidate = isAbsolute2(value) ? resolve3(value) : resolve3(normalizedProjectRoot, value);
|
|
845
|
-
const relativePath =
|
|
1209
|
+
const relativePath = relative3(normalizedProjectRoot, candidate);
|
|
846
1210
|
if (relativePath.length > 0 && relativePath !== "." && !relativePath.startsWith("..") && !isAbsolute2(relativePath)) {
|
|
847
1211
|
return posix.normalize(relativePath.split("\\").join("/"));
|
|
848
1212
|
}
|
|
@@ -899,8 +1263,8 @@ async function appendAuditLogEventLedgerEvents(projectRoot, entries, metadata =
|
|
|
899
1263
|
}
|
|
900
1264
|
|
|
901
1265
|
// src/services/get-rules.ts
|
|
902
|
-
import { readFile as
|
|
903
|
-
import { join as
|
|
1266
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1267
|
+
import { join as join6 } from "path";
|
|
904
1268
|
import { minimatch } from "minimatch";
|
|
905
1269
|
var PRIORITY_ORDER = {
|
|
906
1270
|
high: 0,
|
|
@@ -939,7 +1303,7 @@ async function loadGetRulesContext(projectRoot) {
|
|
|
939
1303
|
return cached;
|
|
940
1304
|
}
|
|
941
1305
|
const meta = await readAgentsMeta(projectRoot);
|
|
942
|
-
const l0Content = await
|
|
1306
|
+
const l0Content = await readFile5(join6(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
|
|
943
1307
|
const context = {
|
|
944
1308
|
meta,
|
|
945
1309
|
l0Content,
|
|
@@ -1078,7 +1442,7 @@ async function readRuleContent(projectRoot, file, fileContentCache) {
|
|
|
1078
1442
|
if (cached !== void 0) {
|
|
1079
1443
|
return await cached;
|
|
1080
1444
|
}
|
|
1081
|
-
const pending =
|
|
1445
|
+
const pending = readFile5(join6(projectRoot, file), "utf8");
|
|
1082
1446
|
fileContentCache.set(file, pending);
|
|
1083
1447
|
return await pending;
|
|
1084
1448
|
}
|
|
@@ -1376,7 +1740,7 @@ async function getRuleSections(projectRoot, input) {
|
|
|
1376
1740
|
const diagnostics = [];
|
|
1377
1741
|
const rules = [];
|
|
1378
1742
|
for (const rule of selectedRules) {
|
|
1379
|
-
const content = await
|
|
1743
|
+
const content = await readFile6(join7(projectRoot, rule.path), "utf8");
|
|
1380
1744
|
const parsedSections = parseRuleSections(content);
|
|
1381
1745
|
const sections = {};
|
|
1382
1746
|
for (const section of input.sections) {
|
|
@@ -1498,6 +1862,9 @@ function pickSelectionReasons(selectedStableIds, reasons) {
|
|
|
1498
1862
|
}
|
|
1499
1863
|
|
|
1500
1864
|
// src/services/doctor.ts
|
|
1865
|
+
import { atomicWriteJson as atomicWriteJson2, atomicWriteText as atomicWriteText3 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
1866
|
+
import { buildBootstrapContent, FABRIC_BOOTSTRAP_PATH } from "@fenglimg/fabric-shared/node/bootstrap-guide";
|
|
1867
|
+
var LEGACY_CLIENT_PATH_KEYS = ["windsurf", "rooCode", "geminiCLI"];
|
|
1501
1868
|
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
1502
1869
|
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
1503
1870
|
".fabric",
|
|
@@ -1539,8 +1906,15 @@ async function runDoctorReport(target) {
|
|
|
1539
1906
|
inspectRuleSections(projectRoot),
|
|
1540
1907
|
inspectRuleTestIndex(projectRoot)
|
|
1541
1908
|
]);
|
|
1542
|
-
const
|
|
1543
|
-
const
|
|
1909
|
+
const mcpConfigInWrongFile = inspectMcpConfigInWrongFile(projectRoot);
|
|
1910
|
+
const metaManuallyDiverged = await inspectMetaManuallyDiverged(projectRoot);
|
|
1911
|
+
const rulesDirUnindexed = inspectRulesDirUnindexed(projectRoot, meta);
|
|
1912
|
+
const stableIdCollision = await inspectStableIdCollisions(projectRoot);
|
|
1913
|
+
const claudeSkillLegacyPath = inspectClaudeSkillLegacyPath(projectRoot);
|
|
1914
|
+
const preexistingRootFiles = inspectPreexistingRootFiles(projectRoot);
|
|
1915
|
+
const legacyClientPaths = inspectLegacyClientPaths(projectRoot);
|
|
1916
|
+
const taxonomyExists = existsSync4(join8(projectRoot, ".fabric", "INITIAL_TAXONOMY.md"));
|
|
1917
|
+
const bootstrapExists = existsSync4(join8(projectRoot, ".fabric", "bootstrap", "README.md"));
|
|
1544
1918
|
const checks = [
|
|
1545
1919
|
createBootstrapCheck(bootstrapExists),
|
|
1546
1920
|
createTaxonomyCheck(taxonomyExists),
|
|
@@ -1550,17 +1924,27 @@ async function runDoctorReport(target) {
|
|
|
1550
1924
|
createRuleContentRefCheck(meta),
|
|
1551
1925
|
createRuleSectionsCheck(ruleSections),
|
|
1552
1926
|
createRuleTestIndexCheck(ruleTestIndex),
|
|
1553
|
-
createEventLedgerCheck(eventLedger)
|
|
1927
|
+
createEventLedgerCheck(eventLedger),
|
|
1928
|
+
createEventLedgerPartialWriteCheck(eventLedger),
|
|
1929
|
+
createMcpConfigInWrongFileCheck(mcpConfigInWrongFile),
|
|
1930
|
+
createMetaManuallyDivergedCheck(metaManuallyDiverged),
|
|
1931
|
+
createRulesDirUnindexedCheck(rulesDirUnindexed),
|
|
1932
|
+
createStableIdCollisionCheck(stableIdCollision),
|
|
1933
|
+
createClaudeSkillLegacyPathCheck(claudeSkillLegacyPath),
|
|
1934
|
+
createPreexistingRootFilesCheck(preexistingRootFiles),
|
|
1935
|
+
createLegacyClientPathCheck(legacyClientPaths)
|
|
1554
1936
|
];
|
|
1555
1937
|
const fixableErrors = collectIssues(checks, "fixable_error");
|
|
1556
1938
|
const manualErrors = collectIssues(checks, "manual_error");
|
|
1557
1939
|
const warnings = collectIssues(checks, "warning");
|
|
1940
|
+
const infos = collectIssues(checks, "info");
|
|
1558
1941
|
return {
|
|
1559
1942
|
status: reduceStatus(checks.map((check) => check.status)),
|
|
1560
1943
|
checks,
|
|
1561
1944
|
fixable_errors: fixableErrors,
|
|
1562
1945
|
manual_errors: manualErrors,
|
|
1563
1946
|
warnings,
|
|
1947
|
+
infos,
|
|
1564
1948
|
summary: {
|
|
1565
1949
|
target: projectRoot,
|
|
1566
1950
|
framework: {
|
|
@@ -1576,8 +1960,9 @@ async function runDoctorReport(target) {
|
|
|
1576
1960
|
fixableErrorCount: fixableErrors.length,
|
|
1577
1961
|
manualErrorCount: manualErrors.length,
|
|
1578
1962
|
warningCount: warnings.length,
|
|
1963
|
+
infoCount: infos.length,
|
|
1579
1964
|
targetFiles: Object.fromEntries(
|
|
1580
|
-
TARGET_FILE_PATHS.map((path) => [path,
|
|
1965
|
+
TARGET_FILE_PATHS.map((path) => [path, existsSync4(join8(projectRoot, path))])
|
|
1581
1966
|
)
|
|
1582
1967
|
}
|
|
1583
1968
|
};
|
|
@@ -1595,16 +1980,53 @@ async function runDoctorFix(target) {
|
|
|
1595
1980
|
fixed.push(findIssue(before.fixable_errors, "event_ledger_missing"));
|
|
1596
1981
|
}
|
|
1597
1982
|
if (before.fixable_errors.some(
|
|
1598
|
-
(issue) => [
|
|
1983
|
+
(issue) => [
|
|
1984
|
+
"agents_meta_missing",
|
|
1985
|
+
"agents_meta_stale",
|
|
1986
|
+
"rule_test_index_missing",
|
|
1987
|
+
"rule_test_index_stale",
|
|
1988
|
+
"content_ref_missing",
|
|
1989
|
+
"rules_dir_unindexed"
|
|
1990
|
+
].includes(issue.code)
|
|
1599
1991
|
)) {
|
|
1600
|
-
await
|
|
1992
|
+
await reconcileRules(projectRoot, { trigger: "doctor" });
|
|
1601
1993
|
for (const issue of before.fixable_errors.filter(
|
|
1602
|
-
(candidate) => [
|
|
1994
|
+
(candidate) => [
|
|
1995
|
+
"agents_meta_missing",
|
|
1996
|
+
"agents_meta_stale",
|
|
1997
|
+
"rule_test_index_missing",
|
|
1998
|
+
"rule_test_index_stale",
|
|
1999
|
+
"content_ref_missing",
|
|
2000
|
+
"rules_dir_unindexed"
|
|
2001
|
+
].includes(candidate.code)
|
|
1603
2002
|
)) {
|
|
1604
2003
|
fixed.push(issue);
|
|
1605
2004
|
}
|
|
1606
2005
|
contextCache.invalidate("meta_write", projectRoot);
|
|
1607
2006
|
}
|
|
2007
|
+
if (before.fixable_errors.some((issue) => issue.code === "event_ledger_partial_write")) {
|
|
2008
|
+
const ledgerPath = getEventLedgerPath(projectRoot);
|
|
2009
|
+
const truncResult = await truncateLedgerToLastNewline(ledgerPath);
|
|
2010
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
2011
|
+
event_type: "event_ledger_truncated",
|
|
2012
|
+
byte_offset: truncResult.truncated_bytes,
|
|
2013
|
+
byte_length: truncResult.truncated_bytes,
|
|
2014
|
+
corrupted_path: truncResult.corrupted_path
|
|
2015
|
+
});
|
|
2016
|
+
fixed.push(findIssue(before.fixable_errors, "event_ledger_partial_write"));
|
|
2017
|
+
}
|
|
2018
|
+
if (before.fixable_errors.some((issue) => issue.code === "mcp_config_in_wrong_file")) {
|
|
2019
|
+
await fixMcpConfigInWrongFile(projectRoot);
|
|
2020
|
+
fixed.push(findIssue(before.fixable_errors, "mcp_config_in_wrong_file"));
|
|
2021
|
+
}
|
|
2022
|
+
if (before.fixable_errors.some((issue) => issue.code === "claude_skill_legacy_path")) {
|
|
2023
|
+
await fixClaudeSkillLegacyPath(projectRoot);
|
|
2024
|
+
fixed.push(findIssue(before.fixable_errors, "claude_skill_legacy_path"));
|
|
2025
|
+
}
|
|
2026
|
+
if (before.warnings.some((issue) => issue.code === "legacy_client_path_present")) {
|
|
2027
|
+
await fixLegacyClientPaths(projectRoot);
|
|
2028
|
+
fixed.push(findIssue(before.warnings, "legacy_client_path_present"));
|
|
2029
|
+
}
|
|
1608
2030
|
const report = await runDoctorReport(projectRoot);
|
|
1609
2031
|
return {
|
|
1610
2032
|
changed: fixed.length > 0,
|
|
@@ -1616,9 +2038,9 @@ async function runDoctorFix(target) {
|
|
|
1616
2038
|
};
|
|
1617
2039
|
}
|
|
1618
2040
|
async function inspectForensic(projectRoot) {
|
|
1619
|
-
const path =
|
|
2041
|
+
const path = join8(projectRoot, ".fabric", "forensic.json");
|
|
1620
2042
|
try {
|
|
1621
|
-
const parsed = forensicReportSchema.parse(JSON.parse(await
|
|
2043
|
+
const parsed = forensicReportSchema.parse(JSON.parse(await readFile7(path, "utf8")));
|
|
1622
2044
|
return { present: true, valid: true, report: parsed };
|
|
1623
2045
|
} catch (error) {
|
|
1624
2046
|
if (isMissingFileError(error)) {
|
|
@@ -1628,9 +2050,9 @@ async function inspectForensic(projectRoot) {
|
|
|
1628
2050
|
}
|
|
1629
2051
|
}
|
|
1630
2052
|
async function inspectInitContext(projectRoot) {
|
|
1631
|
-
const path =
|
|
2053
|
+
const path = join8(projectRoot, ".fabric", "init-context.json");
|
|
1632
2054
|
try {
|
|
1633
|
-
JSON.parse(await
|
|
2055
|
+
JSON.parse(await readFile7(path, "utf8"));
|
|
1634
2056
|
return { exists: true, validJson: true };
|
|
1635
2057
|
} catch (error) {
|
|
1636
2058
|
if (isMissingFileError(error)) {
|
|
@@ -1639,11 +2061,32 @@ async function inspectInitContext(projectRoot) {
|
|
|
1639
2061
|
return { exists: true, validJson: false, error: error instanceof Error ? error.message : String(error) };
|
|
1640
2062
|
}
|
|
1641
2063
|
}
|
|
2064
|
+
function inspectMcpConfigInWrongFile(projectRoot) {
|
|
2065
|
+
const settingsPath = join8(projectRoot, ".claude", "settings.json");
|
|
2066
|
+
if (!existsSync4(settingsPath)) {
|
|
2067
|
+
return { hasWrongEntry: false, settingsPath };
|
|
2068
|
+
}
|
|
2069
|
+
try {
|
|
2070
|
+
const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
2071
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2072
|
+
return { hasWrongEntry: false, settingsPath };
|
|
2073
|
+
}
|
|
2074
|
+
const settings = parsed;
|
|
2075
|
+
const mcpServers = settings.mcpServers;
|
|
2076
|
+
if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
|
|
2077
|
+
return { hasWrongEntry: false, settingsPath };
|
|
2078
|
+
}
|
|
2079
|
+
const hasWrongEntry = "fabric" in mcpServers;
|
|
2080
|
+
return { hasWrongEntry, settingsPath };
|
|
2081
|
+
} catch {
|
|
2082
|
+
return { hasWrongEntry: false, settingsPath };
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
1642
2085
|
async function inspectMeta(projectRoot) {
|
|
1643
|
-
const metaPath =
|
|
2086
|
+
const metaPath = join8(projectRoot, ".fabric", "agents.meta.json");
|
|
1644
2087
|
const built = await tryBuildRuleMeta(projectRoot);
|
|
1645
2088
|
try {
|
|
1646
|
-
const raw = await
|
|
2089
|
+
const raw = await readFile7(metaPath, "utf8");
|
|
1647
2090
|
const meta = agentsMetaSchema4.parse(JSON.parse(raw));
|
|
1648
2091
|
const contentRefIssues = inspectContentRefs(projectRoot, meta);
|
|
1649
2092
|
const changed = built === null ? false : built.changed;
|
|
@@ -1702,7 +2145,7 @@ function inspectContentRefs(projectRoot, meta) {
|
|
|
1702
2145
|
for (const node of Object.values(meta.nodes)) {
|
|
1703
2146
|
const contentRef = normalizePath(node.content_ref ?? node.file);
|
|
1704
2147
|
if (contentRef === ".fabric/bootstrap/README.md") {
|
|
1705
|
-
if (!
|
|
2148
|
+
if (!existsSync4(join8(projectRoot, contentRef))) {
|
|
1706
2149
|
missing.push(contentRef);
|
|
1707
2150
|
}
|
|
1708
2151
|
continue;
|
|
@@ -1711,7 +2154,7 @@ function inspectContentRefs(projectRoot, meta) {
|
|
|
1711
2154
|
invalid.push(contentRef);
|
|
1712
2155
|
continue;
|
|
1713
2156
|
}
|
|
1714
|
-
if (!
|
|
2157
|
+
if (!existsSync4(join8(projectRoot, contentRef))) {
|
|
1715
2158
|
missing.push(contentRef);
|
|
1716
2159
|
}
|
|
1717
2160
|
}
|
|
@@ -1719,19 +2162,23 @@ function inspectContentRefs(projectRoot, meta) {
|
|
|
1719
2162
|
}
|
|
1720
2163
|
async function inspectEventLedger(projectRoot) {
|
|
1721
2164
|
const path = getEventLedgerPath(projectRoot);
|
|
1722
|
-
const exists =
|
|
2165
|
+
const exists = existsSync4(path);
|
|
1723
2166
|
if (!exists) {
|
|
1724
|
-
return { exists: false, writable: false, parseable: false, path };
|
|
2167
|
+
return { exists: false, writable: false, parseable: false, hasPartialWrite: false, partialWriteByteOffset: 0, partialWriteByteLength: 0, path };
|
|
1725
2168
|
}
|
|
1726
2169
|
try {
|
|
1727
2170
|
await access(path, constants.W_OK);
|
|
1728
|
-
await readEventLedger(projectRoot);
|
|
1729
|
-
const raw = await
|
|
2171
|
+
const { warnings } = await readEventLedger(projectRoot);
|
|
2172
|
+
const raw = await readFile7(path, "utf8");
|
|
1730
2173
|
const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
|
|
2174
|
+
const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
|
|
1731
2175
|
return {
|
|
1732
2176
|
exists: true,
|
|
1733
2177
|
writable: true,
|
|
1734
2178
|
parseable: invalidLine === void 0,
|
|
2179
|
+
hasPartialWrite: partialWarning !== void 0,
|
|
2180
|
+
partialWriteByteOffset: partialWarning?.byte_offset ?? 0,
|
|
2181
|
+
partialWriteByteLength: partialWarning?.byte_length ?? 0,
|
|
1735
2182
|
path,
|
|
1736
2183
|
error: invalidLine === void 0 ? void 0 : "events.jsonl contains an invalid JSON line."
|
|
1737
2184
|
};
|
|
@@ -1740,6 +2187,9 @@ async function inspectEventLedger(projectRoot) {
|
|
|
1740
2187
|
exists: true,
|
|
1741
2188
|
writable: false,
|
|
1742
2189
|
parseable: false,
|
|
2190
|
+
hasPartialWrite: false,
|
|
2191
|
+
partialWriteByteOffset: 0,
|
|
2192
|
+
partialWriteByteLength: 0,
|
|
1743
2193
|
path,
|
|
1744
2194
|
error: error instanceof Error ? error.message : String(error)
|
|
1745
2195
|
};
|
|
@@ -1747,10 +2197,10 @@ async function inspectEventLedger(projectRoot) {
|
|
|
1747
2197
|
}
|
|
1748
2198
|
async function inspectRuleSections(projectRoot) {
|
|
1749
2199
|
const invalidFiles = [];
|
|
1750
|
-
const files =
|
|
2200
|
+
const files = findRuleFiles2(projectRoot);
|
|
1751
2201
|
for (const file of files) {
|
|
1752
2202
|
try {
|
|
1753
|
-
parseRuleSections(await
|
|
2203
|
+
parseRuleSections(await readFile7(join8(projectRoot, file), "utf8"));
|
|
1754
2204
|
} catch (error) {
|
|
1755
2205
|
invalidFiles.push({
|
|
1756
2206
|
file,
|
|
@@ -1764,10 +2214,10 @@ async function inspectRuleSections(projectRoot) {
|
|
|
1764
2214
|
};
|
|
1765
2215
|
}
|
|
1766
2216
|
async function inspectRuleTestIndex(projectRoot) {
|
|
1767
|
-
const path =
|
|
2217
|
+
const path = join8(projectRoot, ".fabric", "rule-test.index.json");
|
|
1768
2218
|
const built = await tryBuildRuleMeta(projectRoot);
|
|
1769
2219
|
try {
|
|
1770
|
-
const index = ruleTestIndexSchema2.parse(JSON.parse(await
|
|
2220
|
+
const index = ruleTestIndexSchema2.parse(JSON.parse(await readFile7(path, "utf8")));
|
|
1771
2221
|
return {
|
|
1772
2222
|
present: true,
|
|
1773
2223
|
valid: true,
|
|
@@ -1788,13 +2238,13 @@ async function inspectRuleTestIndex(projectRoot) {
|
|
|
1788
2238
|
}
|
|
1789
2239
|
function createBootstrapCheck(exists) {
|
|
1790
2240
|
if (!exists) {
|
|
1791
|
-
return issueCheck("Bootstrap README", "error", "fixable_error", "bootstrap_missing", ".fabric/bootstrap/README.md is missing.");
|
|
2241
|
+
return issueCheck("Bootstrap README", "error", "fixable_error", "bootstrap_missing", ".fabric/bootstrap/README.md is missing.", "Run `fab doctor --fix` to generate the bootstrap guide.");
|
|
1792
2242
|
}
|
|
1793
2243
|
return okCheck("Bootstrap README", ".fabric/bootstrap/README.md exists.");
|
|
1794
2244
|
}
|
|
1795
2245
|
function createTaxonomyCheck(exists) {
|
|
1796
2246
|
if (!exists) {
|
|
1797
|
-
return issueCheck("Initial taxonomy", "error", "manual_error", "taxonomy_missing", ".fabric/INITIAL_TAXONOMY.md is missing.");
|
|
2247
|
+
return issueCheck("Initial taxonomy", "error", "manual_error", "taxonomy_missing", ".fabric/INITIAL_TAXONOMY.md is missing.", "Run `fab init` to regenerate project scaffolding including INITIAL_TAXONOMY.md.");
|
|
1798
2248
|
}
|
|
1799
2249
|
return okCheck("Initial taxonomy", ".fabric/INITIAL_TAXONOMY.md exists.");
|
|
1800
2250
|
}
|
|
@@ -1805,29 +2255,30 @@ function createForensicCheck(forensic, frameworkKind, entryPointCount) {
|
|
|
1805
2255
|
"error",
|
|
1806
2256
|
"manual_error",
|
|
1807
2257
|
"forensic_missing",
|
|
1808
|
-
`${forensic.error ?? ".fabric/forensic.json is missing."} Live scan detects ${frameworkKind} with ${entryPointCount} entry point${entryPointCount === 1 ? "" : "s"}
|
|
2258
|
+
`${forensic.error ?? ".fabric/forensic.json is missing."} Live scan detects ${frameworkKind} with ${entryPointCount} entry point${entryPointCount === 1 ? "" : "s"}.`,
|
|
2259
|
+
"Run `fab init` to regenerate .fabric/forensic.json."
|
|
1809
2260
|
);
|
|
1810
2261
|
}
|
|
1811
2262
|
if (!forensic.valid) {
|
|
1812
|
-
return issueCheck("Scan evidence", "error", "manual_error", "forensic_invalid", forensic.error ?? ".fabric/forensic.json is invalid.");
|
|
2263
|
+
return issueCheck("Scan evidence", "error", "manual_error", "forensic_invalid", forensic.error ?? ".fabric/forensic.json is invalid.", "Run `fab init` to regenerate .fabric/forensic.json.");
|
|
1813
2264
|
}
|
|
1814
2265
|
return okCheck("Scan evidence", `.fabric/forensic.json is valid for ${forensic.report?.framework.kind ?? "unknown"}.`);
|
|
1815
2266
|
}
|
|
1816
2267
|
function createInitContextCheck(initContext) {
|
|
1817
2268
|
if (!initContext.exists) {
|
|
1818
|
-
return issueCheck("Init context", "error", "manual_error", "init_context_missing", initContext.error ?? ".fabric/init-context.json is missing.");
|
|
2269
|
+
return issueCheck("Init context", "error", "manual_error", "init_context_missing", initContext.error ?? ".fabric/init-context.json is missing.", "Run the fabric-init skill in Claude Code or Codex CLI to complete initialization. See docs/migration-1.8.md FAQ.");
|
|
1819
2270
|
}
|
|
1820
2271
|
if (!initContext.validJson) {
|
|
1821
|
-
return issueCheck("Init context", "error", "manual_error", "init_context_invalid", initContext.error ?? ".fabric/init-context.json is invalid.");
|
|
2272
|
+
return issueCheck("Init context", "error", "manual_error", "init_context_invalid", initContext.error ?? ".fabric/init-context.json is invalid.", "Delete .fabric/init-context.json and run `fab init` to regenerate it.");
|
|
1822
2273
|
}
|
|
1823
2274
|
return okCheck("Init context", ".fabric/init-context.json is valid JSON.");
|
|
1824
2275
|
}
|
|
1825
2276
|
function createMetaCheck(meta) {
|
|
1826
2277
|
if (!meta.present) {
|
|
1827
|
-
return issueCheck("Agents metadata", "error", "fixable_error", "agents_meta_missing", ".fabric/agents.meta.json is missing.");
|
|
2278
|
+
return issueCheck("Agents metadata", "error", "fixable_error", "agents_meta_missing", ".fabric/agents.meta.json is missing.", "Run `fab doctor --fix` to rebuild agents.meta.json from .fabric/rules/.");
|
|
1828
2279
|
}
|
|
1829
2280
|
if (!meta.valid) {
|
|
1830
|
-
return issueCheck("Agents metadata", "error", "manual_error", "agents_meta_invalid", meta.readError ?? ".fabric/agents.meta.json is invalid.");
|
|
2281
|
+
return issueCheck("Agents metadata", "error", "manual_error", "agents_meta_invalid", meta.readError ?? ".fabric/agents.meta.json is invalid.", "Delete .fabric/agents.meta.json and run `fab doctor --fix` to regenerate it.");
|
|
1831
2282
|
}
|
|
1832
2283
|
if (meta.stale) {
|
|
1833
2284
|
return issueCheck(
|
|
@@ -1835,14 +2286,15 @@ function createMetaCheck(meta) {
|
|
|
1835
2286
|
"error",
|
|
1836
2287
|
"fixable_error",
|
|
1837
2288
|
"agents_meta_stale",
|
|
1838
|
-
`.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/rules derived revision ${meta.computedRevision ?? "<unknown>"}
|
|
2289
|
+
`.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/rules derived revision ${meta.computedRevision ?? "<unknown>"}.`,
|
|
2290
|
+
"Run `fab doctor --fix` to reconcile agents.meta.json with the current rule files."
|
|
1839
2291
|
);
|
|
1840
2292
|
}
|
|
1841
2293
|
return okCheck("Agents metadata", `.fabric/agents.meta.json revision ${meta.revision} is aligned with .fabric/rules.`);
|
|
1842
2294
|
}
|
|
1843
2295
|
function createRuleContentRefCheck(meta) {
|
|
1844
2296
|
if (!meta.valid) {
|
|
1845
|
-
return issueCheck("Rule content refs", "error", "manual_error", "content_refs_unavailable", "Cannot inspect content_ref entries until agents.meta.json is valid.");
|
|
2297
|
+
return issueCheck("Rule content refs", "error", "manual_error", "content_refs_unavailable", "Cannot inspect content_ref entries until agents.meta.json is valid.", "Fix agents.meta.json first: run `fab doctor --fix`.");
|
|
1846
2298
|
}
|
|
1847
2299
|
if (meta.invalidContentRefs.length > 0) {
|
|
1848
2300
|
return issueCheck(
|
|
@@ -1850,16 +2302,18 @@ function createRuleContentRefCheck(meta) {
|
|
|
1850
2302
|
"error",
|
|
1851
2303
|
"manual_error",
|
|
1852
2304
|
"content_ref_outside_rules",
|
|
1853
|
-
`${meta.invalidContentRefs.length} content_ref entr${meta.invalidContentRefs.length === 1 ? "y is" : "ies are"} outside .fabric/rules
|
|
2305
|
+
`${meta.invalidContentRefs.length} content_ref entr${meta.invalidContentRefs.length === 1 ? "y is" : "ies are"} outside .fabric/rules.`,
|
|
2306
|
+
"Edit agents.meta.json to ensure all content_ref values point inside .fabric/rules/."
|
|
1854
2307
|
);
|
|
1855
2308
|
}
|
|
1856
2309
|
if (meta.missingContentRefs.length > 0) {
|
|
1857
2310
|
return issueCheck(
|
|
1858
2311
|
"Rule content refs",
|
|
1859
2312
|
"error",
|
|
1860
|
-
"
|
|
2313
|
+
"fixable_error",
|
|
1861
2314
|
"content_ref_missing",
|
|
1862
|
-
`${meta.missingContentRefs.length} content_ref target${meta.missingContentRefs.length === 1 ? "" : "s"} are missing
|
|
2315
|
+
`${meta.missingContentRefs.length} content_ref target${meta.missingContentRefs.length === 1 ? "" : "s"} are missing. Run \`fab doctor --fix\` to reconcile.`,
|
|
2316
|
+
"Run `fab doctor --fix` to reconcile agents.meta.json with the files present in .fabric/rules/."
|
|
1863
2317
|
);
|
|
1864
2318
|
}
|
|
1865
2319
|
return okCheck("Rule content refs", "All content_ref entries resolve to .fabric/rules files or bootstrap README.");
|
|
@@ -1871,46 +2325,77 @@ function createRuleSectionsCheck(snapshot) {
|
|
|
1871
2325
|
"error",
|
|
1872
2326
|
"manual_error",
|
|
1873
2327
|
"rule_sections_invalid",
|
|
1874
|
-
`${snapshot.invalidFiles.length} rule file${snapshot.invalidFiles.length === 1 ? "" : "s"} could not be parsed
|
|
2328
|
+
`${snapshot.invalidFiles.length} rule file${snapshot.invalidFiles.length === 1 ? "" : "s"} could not be parsed.`,
|
|
2329
|
+
"Edit the rule file(s) to fix the section structure, then re-run `fab doctor`."
|
|
1875
2330
|
);
|
|
1876
2331
|
}
|
|
1877
2332
|
return okCheck("Rule sections", `${snapshot.checkedCount} .fabric/rules file${snapshot.checkedCount === 1 ? "" : "s"} parsed.`);
|
|
1878
2333
|
}
|
|
1879
2334
|
function createRuleTestIndexCheck(index) {
|
|
1880
2335
|
if (!index.present) {
|
|
1881
|
-
return issueCheck("Rule-test index", "error", "fixable_error", "rule_test_index_missing", index.error);
|
|
2336
|
+
return issueCheck("Rule-test index", "error", "fixable_error", "rule_test_index_missing", index.error, "Run `fab doctor --fix` to rebuild .fabric/rule-test.index.json.");
|
|
1882
2337
|
}
|
|
1883
2338
|
if (!index.valid) {
|
|
1884
|
-
return issueCheck("Rule-test index", "error", "manual_error", "rule_test_index_invalid", index.error);
|
|
2339
|
+
return issueCheck("Rule-test index", "error", "manual_error", "rule_test_index_invalid", index.error, "Delete .fabric/rule-test.index.json and run `fab doctor --fix` to regenerate it.");
|
|
1885
2340
|
}
|
|
1886
2341
|
if (index.stale) {
|
|
1887
|
-
return issueCheck("Rule-test index", "error", "fixable_error", "rule_test_index_stale", ".fabric/rule-test.index.json is stale.");
|
|
2342
|
+
return issueCheck("Rule-test index", "error", "fixable_error", "rule_test_index_stale", ".fabric/rule-test.index.json is stale.", "Run `fab doctor --fix` to rebuild the rule-test index.");
|
|
1888
2343
|
}
|
|
1889
2344
|
return okCheck("Rule-test index", `${index.linkCount} link${index.linkCount === 1 ? "" : "s"} and ${index.orphanCount} orphan annotation${index.orphanCount === 1 ? "" : "s"} indexed.`);
|
|
1890
2345
|
}
|
|
1891
2346
|
function createEventLedgerCheck(ledger) {
|
|
1892
2347
|
if (!ledger.exists) {
|
|
1893
|
-
return issueCheck("Event ledger", "error", "fixable_error", "event_ledger_missing", ".fabric/events.jsonl is missing.");
|
|
2348
|
+
return issueCheck("Event ledger", "error", "fixable_error", "event_ledger_missing", ".fabric/events.jsonl is missing.", "Run `fab doctor --fix` to create .fabric/events.jsonl.");
|
|
1894
2349
|
}
|
|
1895
2350
|
if (!ledger.writable) {
|
|
1896
|
-
return issueCheck("Event ledger", "error", "manual_error", "event_ledger_not_writable", ledger.error ?? ".fabric/events.jsonl is not writable.");
|
|
2351
|
+
return issueCheck("Event ledger", "error", "manual_error", "event_ledger_not_writable", ledger.error ?? ".fabric/events.jsonl is not writable.", "Check file permissions on .fabric/events.jsonl and ensure no other process holds a write lock.");
|
|
1897
2352
|
}
|
|
1898
2353
|
if (!ledger.parseable) {
|
|
1899
|
-
return issueCheck("Event ledger", "error", "manual_error", "event_ledger_invalid", ledger.error ?? ".fabric/events.jsonl is invalid.");
|
|
2354
|
+
return issueCheck("Event ledger", "error", "manual_error", "event_ledger_invalid", ledger.error ?? ".fabric/events.jsonl is invalid.", "Delete .fabric/events.jsonl and run `fab doctor --fix` to recreate it.");
|
|
1900
2355
|
}
|
|
1901
2356
|
return okCheck("Event ledger", ".fabric/events.jsonl exists, is writable, and is parseable.");
|
|
1902
2357
|
}
|
|
2358
|
+
function createMcpConfigInWrongFileCheck(inspection) {
|
|
2359
|
+
if (inspection.hasWrongEntry) {
|
|
2360
|
+
return issueCheck(
|
|
2361
|
+
"Claude MCP config location",
|
|
2362
|
+
"error",
|
|
2363
|
+
"fixable_error",
|
|
2364
|
+
"mcp_config_in_wrong_file",
|
|
2365
|
+
`.claude/settings.json contains mcpServers.fabric \u2014 this file is for hooks/permissions only. Run --fix to remove it, then re-run fab init to write .mcp.json.`,
|
|
2366
|
+
"Run `fab doctor --fix` to remove mcpServers.fabric from .claude/settings.json, then run `fab init` to write .mcp.json."
|
|
2367
|
+
);
|
|
2368
|
+
}
|
|
2369
|
+
return okCheck("Claude MCP config location", "mcpServers.fabric is not in .claude/settings.json.");
|
|
2370
|
+
}
|
|
2371
|
+
function createEventLedgerPartialWriteCheck(ledger) {
|
|
2372
|
+
if (!ledger.exists || !ledger.writable) {
|
|
2373
|
+
return okCheck("Event ledger partial write", "No partial-write check needed (ledger missing or not writable).");
|
|
2374
|
+
}
|
|
2375
|
+
if (ledger.hasPartialWrite) {
|
|
2376
|
+
return issueCheck(
|
|
2377
|
+
"Event ledger partial write",
|
|
2378
|
+
"error",
|
|
2379
|
+
"fixable_error",
|
|
2380
|
+
"event_ledger_partial_write",
|
|
2381
|
+
`events.jsonl has a partial write at byte offset ${ledger.partialWriteByteOffset} (${ledger.partialWriteByteLength} corrupted bytes). Run --fix to truncate and preserve corrupted bytes.`,
|
|
2382
|
+
"Run `fab doctor --fix` to truncate the partial write and restore events.jsonl to a valid state."
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
return okCheck("Event ledger partial write", "events.jsonl has no partial trailing write.");
|
|
2386
|
+
}
|
|
1903
2387
|
function okCheck(name, message) {
|
|
1904
2388
|
return { name, status: "ok", message };
|
|
1905
2389
|
}
|
|
1906
|
-
function issueCheck(name, status, kind, code, message) {
|
|
2390
|
+
function issueCheck(name, status, kind, code, message, actionHint) {
|
|
1907
2391
|
return {
|
|
1908
2392
|
name,
|
|
1909
2393
|
status,
|
|
1910
2394
|
kind,
|
|
1911
2395
|
code,
|
|
1912
2396
|
fixable: kind === "fixable_error",
|
|
1913
|
-
message
|
|
2397
|
+
message,
|
|
2398
|
+
actionHint
|
|
1914
2399
|
};
|
|
1915
2400
|
}
|
|
1916
2401
|
function collectIssues(checks, kind) {
|
|
@@ -1927,10 +2412,344 @@ function findIssue(issues, code) {
|
|
|
1927
2412
|
message: code
|
|
1928
2413
|
};
|
|
1929
2414
|
}
|
|
2415
|
+
async function inspectMetaManuallyDiverged(projectRoot) {
|
|
2416
|
+
const metaPath = join8(projectRoot, ".fabric", "agents.meta.json");
|
|
2417
|
+
if (!existsSync4(metaPath)) {
|
|
2418
|
+
return { extraMetaEntries: [], hashMismatchEntries: [], readable: false };
|
|
2419
|
+
}
|
|
2420
|
+
let meta;
|
|
2421
|
+
try {
|
|
2422
|
+
const raw = await readFile7(metaPath, "utf8");
|
|
2423
|
+
meta = agentsMetaSchema4.parse(JSON.parse(raw));
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
return {
|
|
2426
|
+
extraMetaEntries: [],
|
|
2427
|
+
hashMismatchEntries: [],
|
|
2428
|
+
readable: false,
|
|
2429
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
const extraMetaEntries = [];
|
|
2433
|
+
const hashMismatchEntries = [];
|
|
2434
|
+
for (const node of Object.values(meta.nodes)) {
|
|
2435
|
+
const contentRef = node.content_ref ?? node.file;
|
|
2436
|
+
const absPath = join8(projectRoot, contentRef);
|
|
2437
|
+
if (!existsSync4(absPath)) {
|
|
2438
|
+
extraMetaEntries.push(contentRef);
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
try {
|
|
2442
|
+
const content = readFileSync(absPath, "utf8");
|
|
2443
|
+
const diskHash = sha256(content);
|
|
2444
|
+
if (node.hash !== "" && node.hash !== diskHash) {
|
|
2445
|
+
hashMismatchEntries.push(contentRef);
|
|
2446
|
+
}
|
|
2447
|
+
} catch {
|
|
2448
|
+
extraMetaEntries.push(contentRef);
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
return { extraMetaEntries, hashMismatchEntries, readable: true };
|
|
2452
|
+
}
|
|
2453
|
+
function inspectRulesDirUnindexed(projectRoot, meta) {
|
|
2454
|
+
const rulesDir = join8(projectRoot, ".fabric", "rules");
|
|
2455
|
+
if (!existsSync4(rulesDir)) {
|
|
2456
|
+
return { unindexedFiles: [] };
|
|
2457
|
+
}
|
|
2458
|
+
const physicalMdFiles = /* @__PURE__ */ new Set();
|
|
2459
|
+
const stack = [rulesDir];
|
|
2460
|
+
while (stack.length > 0) {
|
|
2461
|
+
const dir = stack.pop();
|
|
2462
|
+
if (dir === void 0) {
|
|
2463
|
+
continue;
|
|
2464
|
+
}
|
|
2465
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2466
|
+
const abs = join8(dir, entry.name);
|
|
2467
|
+
if (entry.isDirectory()) {
|
|
2468
|
+
stack.push(abs);
|
|
2469
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2470
|
+
const rel = posix2.join(".fabric/rules", abs.slice(rulesDir.length + 1).replace(/\\/gu, "/"));
|
|
2471
|
+
physicalMdFiles.add(rel);
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
const indexedRefs = /* @__PURE__ */ new Set();
|
|
2476
|
+
if (meta.valid && meta.meta !== null) {
|
|
2477
|
+
for (const node of Object.values(meta.meta.nodes)) {
|
|
2478
|
+
const ref = normalizePath(node.content_ref ?? node.file);
|
|
2479
|
+
indexedRefs.add(ref);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
const unindexedFiles = [...physicalMdFiles].filter((f) => !indexedRefs.has(f)).sort();
|
|
2483
|
+
return { unindexedFiles };
|
|
2484
|
+
}
|
|
2485
|
+
function createRulesDirUnindexedCheck(inspection) {
|
|
2486
|
+
if (inspection.unindexedFiles.length > 0) {
|
|
2487
|
+
return issueCheck(
|
|
2488
|
+
"Rules dir unindexed",
|
|
2489
|
+
"error",
|
|
2490
|
+
"fixable_error",
|
|
2491
|
+
"rules_dir_unindexed",
|
|
2492
|
+
`${inspection.unindexedFiles.length} .md file${inspection.unindexedFiles.length === 1 ? "" : "s"} in .fabric/rules/ not indexed in agents.meta.json. Run \`fab doctor --fix\` to index the missing rule files.`,
|
|
2493
|
+
"Run `fab doctor --fix` to index the missing rule files."
|
|
2494
|
+
);
|
|
2495
|
+
}
|
|
2496
|
+
return okCheck("Rules dir unindexed", "All .fabric/rules/ .md files are indexed in agents.meta.json.");
|
|
2497
|
+
}
|
|
2498
|
+
async function inspectStableIdCollisions(projectRoot) {
|
|
2499
|
+
const rulesDir = join8(projectRoot, ".fabric", "rules");
|
|
2500
|
+
if (!existsSync4(rulesDir)) {
|
|
2501
|
+
return { collisions: [] };
|
|
2502
|
+
}
|
|
2503
|
+
const mdFiles = [];
|
|
2504
|
+
const stack = [rulesDir];
|
|
2505
|
+
while (stack.length > 0) {
|
|
2506
|
+
const dir = stack.pop();
|
|
2507
|
+
if (dir === void 0) {
|
|
2508
|
+
continue;
|
|
2509
|
+
}
|
|
2510
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2511
|
+
const abs = join8(dir, entry.name);
|
|
2512
|
+
if (entry.isDirectory()) {
|
|
2513
|
+
stack.push(abs);
|
|
2514
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2515
|
+
mdFiles.push(abs);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
const stableIdToFiles = /* @__PURE__ */ new Map();
|
|
2520
|
+
const DECLARED_ID_PATTERN = /^(?:\uFEFF)?(?:---\r?\n[\s\S]*?\r?\n---\s*(?:\r?\n|$))?<!--\s*fab:rule-id\s+([A-Za-z0-9][A-Za-z0-9/_-]*)\s*-->\s*(?:\r?\n|$)/u;
|
|
2521
|
+
for (const absPath of mdFiles) {
|
|
2522
|
+
let source;
|
|
2523
|
+
try {
|
|
2524
|
+
source = await readFile7(absPath, "utf8");
|
|
2525
|
+
} catch {
|
|
2526
|
+
continue;
|
|
2527
|
+
}
|
|
2528
|
+
const match = DECLARED_ID_PATTERN.exec(source);
|
|
2529
|
+
if (match === null) {
|
|
2530
|
+
continue;
|
|
2531
|
+
}
|
|
2532
|
+
const stableId = match[1];
|
|
2533
|
+
const relPath = posix2.join(".fabric/rules", absPath.slice(rulesDir.length + 1).replace(/\\/gu, "/"));
|
|
2534
|
+
const existing = stableIdToFiles.get(stableId) ?? [];
|
|
2535
|
+
existing.push(relPath);
|
|
2536
|
+
stableIdToFiles.set(stableId, existing);
|
|
2537
|
+
}
|
|
2538
|
+
const collisions = [];
|
|
2539
|
+
for (const [stable_id, files] of stableIdToFiles) {
|
|
2540
|
+
if (files.length > 1) {
|
|
2541
|
+
collisions.push({ stable_id, files: files.sort() });
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
return { collisions: collisions.sort((a, b) => a.stable_id.localeCompare(b.stable_id)) };
|
|
2545
|
+
}
|
|
2546
|
+
function createStableIdCollisionCheck(inspection) {
|
|
2547
|
+
if (inspection.collisions.length > 0) {
|
|
2548
|
+
const first = inspection.collisions[0];
|
|
2549
|
+
const detail = inspection.collisions.length === 1 ? `stable_id "${first.stable_id}" is declared in ${first.files.length} files: ${first.files.join(", ")}.` : `${inspection.collisions.length} stable_id collision${inspection.collisions.length === 1 ? "" : "s"} detected. First: "${first.stable_id}" in ${first.files.join(", ")}.`;
|
|
2550
|
+
return issueCheck(
|
|
2551
|
+
"Stable ID collision",
|
|
2552
|
+
"warn",
|
|
2553
|
+
"warning",
|
|
2554
|
+
"stable_id_collision",
|
|
2555
|
+
`${detail} Edit one of the rule files to use a unique stable_id.`,
|
|
2556
|
+
"Edit one of the colliding rule files to declare a different `<!-- fab:rule-id X -->` value."
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
return okCheck("Stable ID collision", "No declared stable_id collisions found in .fabric/rules/.");
|
|
2560
|
+
}
|
|
2561
|
+
function createMetaManuallyDivergedCheck(inspection) {
|
|
2562
|
+
if (!inspection.readable) {
|
|
2563
|
+
return okCheck("Meta manual divergence", "agents.meta.json not readable; skipping divergence check.");
|
|
2564
|
+
}
|
|
2565
|
+
if (inspection.extraMetaEntries.length > 0) {
|
|
2566
|
+
return issueCheck(
|
|
2567
|
+
"Meta manual divergence",
|
|
2568
|
+
"warn",
|
|
2569
|
+
"warning",
|
|
2570
|
+
"meta_manually_diverged",
|
|
2571
|
+
`agents.meta.json has ${inspection.extraMetaEntries.length} entr${inspection.extraMetaEntries.length === 1 ? "y" : "ies"} with no backing file on disk. Run --fix to reconcile.`,
|
|
2572
|
+
"Run `fab doctor --fix` to reconcile agents.meta.json with the rule files currently on disk."
|
|
2573
|
+
);
|
|
2574
|
+
}
|
|
2575
|
+
if (inspection.hashMismatchEntries.length > 0) {
|
|
2576
|
+
return issueCheck(
|
|
2577
|
+
"Meta manual divergence",
|
|
2578
|
+
"warn",
|
|
2579
|
+
"warning",
|
|
2580
|
+
"meta_manually_diverged",
|
|
2581
|
+
`agents.meta.json has ${inspection.hashMismatchEntries.length} entr${inspection.hashMismatchEntries.length === 1 ? "y" : "ies"} whose hash does not match the file on disk. Run --fix to reconcile.`,
|
|
2582
|
+
"Run `fab doctor --fix` to reconcile agents.meta.json with the current rule file contents."
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
return okCheck("Meta manual divergence", "agents.meta.json is consistent with rule files on disk.");
|
|
2586
|
+
}
|
|
2587
|
+
function inspectPreexistingRootFiles(projectRoot) {
|
|
2588
|
+
const candidates = ["CLAUDE.md", "AGENTS.md"];
|
|
2589
|
+
const detected = candidates.filter((name) => existsSync4(join8(projectRoot, name)));
|
|
2590
|
+
return { detected };
|
|
2591
|
+
}
|
|
2592
|
+
function createPreexistingRootFilesCheck(inspection) {
|
|
2593
|
+
if (inspection.detected.length === 0) {
|
|
2594
|
+
return okCheck("Preexisting root markdown", "No CLAUDE.md or AGENTS.md detected at project root.");
|
|
2595
|
+
}
|
|
2596
|
+
return {
|
|
2597
|
+
name: "Preexisting root markdown",
|
|
2598
|
+
status: "ok",
|
|
2599
|
+
kind: "info",
|
|
2600
|
+
code: "preexisting_root_claude_md",
|
|
2601
|
+
fixable: false,
|
|
2602
|
+
message: `${inspection.detected.join(", ")} detected at project root. These root files are not auto-loaded by Fabric MCP.`,
|
|
2603
|
+
actionHint: "Move rule content to `.fabric/rules/` if you want it available in MCP responses."
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
function inspectClaudeSkillLegacyPath(projectRoot) {
|
|
2607
|
+
const legacyPath = join8(projectRoot, ".claude", "skills", "agents-md-init", "SKILL.md");
|
|
2608
|
+
const newPath = join8(projectRoot, ".claude", "skills", "fabric-init", "SKILL.md");
|
|
2609
|
+
const hasLegacy = existsSync4(legacyPath) && !existsSync4(newPath);
|
|
2610
|
+
return { hasLegacy, legacyPath, newPath };
|
|
2611
|
+
}
|
|
2612
|
+
function createClaudeSkillLegacyPathCheck(inspection) {
|
|
2613
|
+
if (inspection.hasLegacy) {
|
|
2614
|
+
return issueCheck(
|
|
2615
|
+
"Claude skill path",
|
|
2616
|
+
"error",
|
|
2617
|
+
"fixable_error",
|
|
2618
|
+
"claude_skill_legacy_path",
|
|
2619
|
+
`.claude/skills/agents-md-init/SKILL.md exists at the legacy path. Run --fix to migrate it to .claude/skills/fabric-init/SKILL.md (user edits preserved).`,
|
|
2620
|
+
"Run `fab doctor --fix` to rename agents-md-init/ to fabric-init/, preserving any user edits to SKILL.md."
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
return okCheck("Claude skill path", ".claude/skills/fabric-init/SKILL.md is at the canonical path (or not present).");
|
|
2624
|
+
}
|
|
2625
|
+
async function fixClaudeSkillLegacyPath(projectRoot) {
|
|
2626
|
+
const legacyPath = join8(projectRoot, ".claude", "skills", "agents-md-init", "SKILL.md");
|
|
2627
|
+
const newPath = join8(projectRoot, ".claude", "skills", "fabric-init", "SKILL.md");
|
|
2628
|
+
if (!existsSync4(legacyPath)) {
|
|
2629
|
+
return;
|
|
2630
|
+
}
|
|
2631
|
+
mkdirSync(join8(newPath, ".."), { recursive: true });
|
|
2632
|
+
renameSync(legacyPath, newPath);
|
|
2633
|
+
const legacyDir = join8(legacyPath, "..");
|
|
2634
|
+
try {
|
|
2635
|
+
rmdirSync(legacyDir);
|
|
2636
|
+
} catch {
|
|
2637
|
+
}
|
|
2638
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
2639
|
+
event_type: "claude_skill_path_migrated",
|
|
2640
|
+
from: legacyPath,
|
|
2641
|
+
to: newPath
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
function inspectLegacyClientPaths(projectRoot) {
|
|
2645
|
+
const configPath = join8(projectRoot, "fabric.config.json");
|
|
2646
|
+
if (!existsSync4(configPath)) {
|
|
2647
|
+
return { presentKeys: [] };
|
|
2648
|
+
}
|
|
2649
|
+
try {
|
|
2650
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
2651
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2652
|
+
return { presentKeys: [] };
|
|
2653
|
+
}
|
|
2654
|
+
const config = parsed;
|
|
2655
|
+
const clientPaths = config.clientPaths;
|
|
2656
|
+
if (clientPaths === null || typeof clientPaths !== "object" || Array.isArray(clientPaths)) {
|
|
2657
|
+
return { presentKeys: [] };
|
|
2658
|
+
}
|
|
2659
|
+
const cp = clientPaths;
|
|
2660
|
+
const presentKeys = LEGACY_CLIENT_PATH_KEYS.filter((key) => key in cp);
|
|
2661
|
+
return { presentKeys };
|
|
2662
|
+
} catch {
|
|
2663
|
+
return { presentKeys: [] };
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
function createLegacyClientPathCheck(inspection) {
|
|
2667
|
+
if (inspection.presentKeys.length > 0) {
|
|
2668
|
+
return issueCheck(
|
|
2669
|
+
"Legacy client paths",
|
|
2670
|
+
"warn",
|
|
2671
|
+
"warning",
|
|
2672
|
+
"legacy_client_path_present",
|
|
2673
|
+
`fabric.config.json contains deprecated clientPaths keys: ${inspection.presentKeys.join(", ")}. These clients are removed in 1.8.0; run --fix to clean now or accept the upcoming removal.`,
|
|
2674
|
+
"Run `fab doctor --fix` to remove deprecated clientPaths keys (windsurf, rooCode, geminiCLI) from fabric.config.json."
|
|
2675
|
+
);
|
|
2676
|
+
}
|
|
2677
|
+
return okCheck("Legacy client paths", "No deprecated clientPaths keys found in fabric.config.json.");
|
|
2678
|
+
}
|
|
2679
|
+
async function fixLegacyClientPaths(projectRoot) {
|
|
2680
|
+
const configPath = join8(projectRoot, "fabric.config.json");
|
|
2681
|
+
if (!existsSync4(configPath)) {
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
let config;
|
|
2685
|
+
try {
|
|
2686
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
2687
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
config = parsed;
|
|
2691
|
+
} catch {
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
const clientPaths = config.clientPaths;
|
|
2695
|
+
if (clientPaths === null || typeof clientPaths !== "object" || Array.isArray(clientPaths)) {
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
const cp = clientPaths;
|
|
2699
|
+
const removed = [];
|
|
2700
|
+
for (const key of LEGACY_CLIENT_PATH_KEYS) {
|
|
2701
|
+
if (key in cp) {
|
|
2702
|
+
delete cp[key];
|
|
2703
|
+
removed.push(key);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
if (removed.length === 0) {
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
const updatedConfig = { ...config, clientPaths: cp };
|
|
2710
|
+
await atomicWriteJson2(configPath, updatedConfig, { indent: 2 });
|
|
2711
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
2712
|
+
event_type: "legacy_client_path_present",
|
|
2713
|
+
removed
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
async function fixMcpConfigInWrongFile(projectRoot) {
|
|
2717
|
+
const settingsPath = join8(projectRoot, ".claude", "settings.json");
|
|
2718
|
+
if (!existsSync4(settingsPath)) {
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
let settings;
|
|
2722
|
+
try {
|
|
2723
|
+
const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
2724
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
settings = parsed;
|
|
2728
|
+
} catch {
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
const mcpServers = settings.mcpServers;
|
|
2732
|
+
if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2735
|
+
const { fabric: _removed, ...remainingServers } = mcpServers;
|
|
2736
|
+
const cleaned = { ...settings };
|
|
2737
|
+
if (Object.keys(remainingServers).length === 0) {
|
|
2738
|
+
delete cleaned.mcpServers;
|
|
2739
|
+
} else {
|
|
2740
|
+
cleaned.mcpServers = remainingServers;
|
|
2741
|
+
}
|
|
2742
|
+
await atomicWriteJson2(settingsPath, cleaned, { indent: 2 });
|
|
2743
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
2744
|
+
event_type: "mcp_config_migrated",
|
|
2745
|
+
source: "doctor_fix",
|
|
2746
|
+
removed_from: ".claude/settings.json"
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
1930
2749
|
async function writeDefaultBootstrap(projectRoot) {
|
|
1931
|
-
const path =
|
|
2750
|
+
const path = join8(projectRoot, FABRIC_BOOTSTRAP_PATH);
|
|
1932
2751
|
await ensureParentDirectory(path);
|
|
1933
|
-
await
|
|
2752
|
+
await atomicWriteText3(path, buildBootstrapContent(projectRoot));
|
|
1934
2753
|
}
|
|
1935
2754
|
async function ensureEventLedger(projectRoot) {
|
|
1936
2755
|
const path = getEventLedgerPath(projectRoot);
|
|
@@ -1942,9 +2761,9 @@ function createFixMessage(fixed, report) {
|
|
|
1942
2761
|
const manualText = report.manual_errors.length === 0 ? "No manual errors remain." : `${report.manual_errors.length} manual error${report.manual_errors.length === 1 ? "" : "s"} remain.`;
|
|
1943
2762
|
return `${fixedText} ${manualText}`;
|
|
1944
2763
|
}
|
|
1945
|
-
function
|
|
1946
|
-
const rulesRoot =
|
|
1947
|
-
if (!
|
|
2764
|
+
function findRuleFiles2(projectRoot) {
|
|
2765
|
+
const rulesRoot = join8(projectRoot, ".fabric", "rules");
|
|
2766
|
+
if (!existsSync4(rulesRoot) || !statSync3(rulesRoot).isDirectory()) {
|
|
1948
2767
|
return [];
|
|
1949
2768
|
}
|
|
1950
2769
|
const files = [];
|
|
@@ -1955,7 +2774,7 @@ function findRuleFiles(projectRoot) {
|
|
|
1955
2774
|
continue;
|
|
1956
2775
|
}
|
|
1957
2776
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
1958
|
-
const absolutePath =
|
|
2777
|
+
const absolutePath = join8(current, entry.name);
|
|
1959
2778
|
const relativePath = normalizePath(absolutePath.slice(projectRoot.length + 1));
|
|
1960
2779
|
if (entry.isDirectory()) {
|
|
1961
2780
|
stack.push(absolutePath);
|
|
@@ -1981,7 +2800,7 @@ function normalizePath(path) {
|
|
|
1981
2800
|
return posix2.normalize(path.split("\\").join("/"));
|
|
1982
2801
|
}
|
|
1983
2802
|
function collectEntryPoints(root) {
|
|
1984
|
-
if (!
|
|
2803
|
+
if (!existsSync4(root) || !statSync3(root).isDirectory()) {
|
|
1985
2804
|
return [];
|
|
1986
2805
|
}
|
|
1987
2806
|
const entries = [];
|
|
@@ -1992,7 +2811,7 @@ function collectEntryPoints(root) {
|
|
|
1992
2811
|
continue;
|
|
1993
2812
|
}
|
|
1994
2813
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
1995
|
-
const absolutePath =
|
|
2814
|
+
const absolutePath = join8(current, entry.name);
|
|
1996
2815
|
const relativePath = normalizePath(absolutePath.slice(root.length + 1));
|
|
1997
2816
|
if (relativePath.length === 0) {
|
|
1998
2817
|
continue;
|
|
@@ -2052,8 +2871,6 @@ function isMissingFileError(error) {
|
|
|
2052
2871
|
export {
|
|
2053
2872
|
AGENTS_MD_RESOURCE_URI,
|
|
2054
2873
|
contextCache,
|
|
2055
|
-
AgentsMetaFileMissingError,
|
|
2056
|
-
AgentsMetaInvalidError,
|
|
2057
2874
|
resolveProjectRoot,
|
|
2058
2875
|
readAgentsMeta,
|
|
2059
2876
|
LEDGER_PATH,
|
|
@@ -2066,10 +2883,7 @@ export {
|
|
|
2066
2883
|
isNodeError,
|
|
2067
2884
|
appendEventLedgerEvent,
|
|
2068
2885
|
readEventLedger,
|
|
2069
|
-
|
|
2070
|
-
planContext,
|
|
2071
|
-
RULE_SECTION_NAMES,
|
|
2072
|
-
getRuleSections,
|
|
2886
|
+
flushAndSyncEventLedger,
|
|
2073
2887
|
buildRuleMeta,
|
|
2074
2888
|
writeRuleMeta,
|
|
2075
2889
|
computeRulesBasedAgentsMeta,
|
|
@@ -2078,6 +2892,12 @@ export {
|
|
|
2078
2892
|
deriveRuleMetaTopologyType,
|
|
2079
2893
|
isSameRuleTestIndex,
|
|
2080
2894
|
stableStringify,
|
|
2895
|
+
invalidateRuleSyncCooldown,
|
|
2896
|
+
ensureRulesFresh,
|
|
2897
|
+
reconcileRules,
|
|
2898
|
+
getRules,
|
|
2899
|
+
planContext,
|
|
2900
|
+
getRuleSections,
|
|
2081
2901
|
runDoctorReport,
|
|
2082
2902
|
runDoctorFix
|
|
2083
2903
|
};
|