@fenglimg/fabric-server 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1447 +0,0 @@
1
- // src/constants.ts
2
- var AGENTS_MD_RESOURCE_URI = "fabric://bootstrap-readme";
3
-
4
- // src/cache.ts
5
- var ContextCache = class {
6
- constructor(defaultTtlMs = 5e3) {
7
- this.defaultTtlMs = defaultTtlMs;
8
- }
9
- defaultTtlMs;
10
- // Slot 1: raw AgentsMeta keyed by projectRoot
11
- metaSlot = /* @__PURE__ */ new Map();
12
- // Slot 2: GetRulesContext keyed by projectRoot
13
- contextSlot = /* @__PURE__ */ new Map();
14
- // Slot 3: audit sliding-window cursor keyed by projectRoot
15
- auditSlot = /* @__PURE__ */ new Map();
16
- // ---------------------------------------------------------------------------
17
- // Generic get / set / invalidate
18
- // ---------------------------------------------------------------------------
19
- get(slot, key) {
20
- const store = this.slotStore(slot);
21
- const entry = store.get(key);
22
- if (entry === void 0) {
23
- return void 0;
24
- }
25
- if (entry.expiresAt !== 0 && Date.now() > entry.expiresAt) {
26
- store.delete(key);
27
- return void 0;
28
- }
29
- return entry.value;
30
- }
31
- set(slot, key, value, ttlMs) {
32
- const store = this.slotStore(slot);
33
- const resolvedTtl = ttlMs ?? this.defaultTtlMs;
34
- const expiresAt = resolvedTtl > 0 ? Date.now() + resolvedTtl : 0;
35
- store.set(key, { value, expiresAt });
36
- }
37
- // ---------------------------------------------------------------------------
38
- // Audit cursor (separate API — not TTL-based)
39
- // ---------------------------------------------------------------------------
40
- getAuditCursor(projectRoot) {
41
- return this.auditSlot.get(projectRoot);
42
- }
43
- setAuditCursor(projectRoot, cursor) {
44
- this.auditSlot.set(projectRoot, cursor);
45
- }
46
- resetAuditCursor(projectRoot) {
47
- this.auditSlot.delete(projectRoot);
48
- }
49
- // ---------------------------------------------------------------------------
50
- // Invalidation
51
- // ---------------------------------------------------------------------------
52
- /**
53
- * Invalidate cache slots based on what changed.
54
- *
55
- * @param reason "meta_write" — only the meta slot for this projectRoot
56
- * "file_watch" — meta + context slots (AGENTS.md may have changed)
57
- * @param projectRoot Optional; if omitted, clears ALL keys in affected slots.
58
- */
59
- invalidate(reason, projectRoot) {
60
- if (reason === "meta_write") {
61
- if (projectRoot !== void 0) {
62
- this.metaSlot.delete(projectRoot);
63
- } else {
64
- this.metaSlot.clear();
65
- }
66
- return;
67
- }
68
- if (projectRoot !== void 0) {
69
- this.metaSlot.delete(projectRoot);
70
- this.contextSlot.delete(projectRoot);
71
- } else {
72
- this.metaSlot.clear();
73
- this.contextSlot.clear();
74
- }
75
- }
76
- // ---------------------------------------------------------------------------
77
- // Helpers
78
- // ---------------------------------------------------------------------------
79
- slotStore(slot) {
80
- return slot === "meta" ? this.metaSlot : this.contextSlot;
81
- }
82
- };
83
- var contextCache = new ContextCache(5e3);
84
-
85
- // src/services/_shared.ts
86
- import { dirname, join, resolve, sep } from "path";
87
- import { createHash } from "crypto";
88
- import { mkdir, rename, writeFile } from "fs/promises";
89
- var FABRIC_DIR = ".fabric";
90
- var HUMAN_LOCK_FILE = "human-lock.json";
91
- var LEDGER_FILE = ".intent-ledger.jsonl";
92
- var LEDGER_PATH = `${FABRIC_DIR}/${LEDGER_FILE}`;
93
- var LEGACY_LEDGER_PATH = LEDGER_FILE;
94
- async function atomicWriteText(path, content) {
95
- const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
96
- await writeFile(tempPath, content, "utf8");
97
- await rename(tempPath, path);
98
- }
99
- function getLedgerPath(projectRoot) {
100
- return join(projectRoot, LEDGER_PATH);
101
- }
102
- function getLegacyLedgerPath(projectRoot) {
103
- return join(projectRoot, LEGACY_LEDGER_PATH);
104
- }
105
- async function ensureParentDirectory(path) {
106
- await mkdir(dirname(path), { recursive: true });
107
- }
108
- function sha256(content) {
109
- return `sha256:${createHash("sha256").update(content).digest("hex")}`;
110
- }
111
- function isNodeError(error) {
112
- return error instanceof Error;
113
- }
114
- function assertPathWithinProjectRoot(projectRoot, file) {
115
- const normalizedProjectRoot = resolve(projectRoot);
116
- const absolutePath = resolve(normalizedProjectRoot, file);
117
- const rootPrefix = normalizedProjectRoot.endsWith(sep) ? normalizedProjectRoot : `${normalizedProjectRoot}${sep}`;
118
- if (!absolutePath.startsWith(rootPrefix)) {
119
- throw new Error(`Path escapes project root: ${file}`);
120
- }
121
- return absolutePath;
122
- }
123
-
124
- // src/services/read-human-lock.ts
125
- import { readFile } from "fs/promises";
126
- import { join as join2 } from "path";
127
- import { humanLockEntrySchema } from "@fenglimg/fabric-shared";
128
- async function readHumanLock(projectRoot) {
129
- const document = await readHumanLockDocument(projectRoot);
130
- return await Promise.all(
131
- document.locked.map(async (entry) => {
132
- const currentHash = await hashHumanLockedContent(projectRoot, entry);
133
- return {
134
- ...entry,
135
- drift: currentHash !== entry.hash,
136
- current_hash: currentHash
137
- };
138
- })
139
- );
140
- }
141
- async function readHumanLockEntry(projectRoot, file) {
142
- const entries = await readHumanLock(projectRoot);
143
- return entries.find((entry) => entry.file === file) ?? null;
144
- }
145
- async function readHumanLockDocument(projectRoot) {
146
- const humanLockPath = join2(projectRoot, FABRIC_DIR, HUMAN_LOCK_FILE);
147
- const raw = await readFile(humanLockPath, "utf8");
148
- const parsed = JSON.parse(raw);
149
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
150
- throw new Error(`Fabric human lock file is invalid: ${humanLockPath}`);
151
- }
152
- const rawObject = parsed;
153
- const lockedResult = humanLockEntrySchema.array().safeParse(rawObject.locked ?? []);
154
- if (!lockedResult.success) {
155
- throw new Error(`Fabric human lock file is invalid: ${humanLockPath}`);
156
- }
157
- return {
158
- path: humanLockPath,
159
- rawObject,
160
- locked: lockedResult.data
161
- };
162
- }
163
- async function hashHumanLockedContent(projectRoot, entry) {
164
- const targetPath = assertPathWithinProjectRoot(projectRoot, entry.file);
165
- let content;
166
- try {
167
- content = await readFile(targetPath, "utf8");
168
- } catch (error) {
169
- if (isNodeError(error) && error.code === "ENOENT") {
170
- return "missing";
171
- }
172
- throw error;
173
- }
174
- const lines = content.split(/\r?\n/);
175
- const slice = lines.slice(Math.max(entry.start_line - 1, 0), Math.max(entry.end_line, 0)).join("\n");
176
- return sha256(slice);
177
- }
178
-
179
- // src/services/doctor.ts
180
- import { createHash as createHash2 } from "crypto";
181
- import { existsSync, readFileSync, readdirSync, statSync } from "fs";
182
- import { readFile as readFile4 } from "fs/promises";
183
- import { isAbsolute as isAbsolute2, join as join5, posix as posix2, resolve as resolve3 } from "path";
184
- import { forensicReportSchema } from "@fenglimg/fabric-shared";
185
- import { detectFramework } from "@fenglimg/fabric-shared/node";
186
-
187
- // src/meta-reader.ts
188
- import { readFile as readFile2 } from "fs/promises";
189
- import { join as join3 } from "path";
190
- import { agentsMetaSchema } from "@fenglimg/fabric-shared";
191
- import { agentsMetaNodeSchema, agentsMetaSchema as agentsMetaSchema2 } from "@fenglimg/fabric-shared";
192
- var AgentsMetaFileMissingError = class extends Error {
193
- constructor(metaPath) {
194
- super(`Fabric agents metadata file is missing: ${metaPath}`);
195
- this.metaPath = metaPath;
196
- this.name = "AgentsMetaFileMissingError";
197
- }
198
- metaPath;
199
- code = "FABRIC_META_MISSING";
200
- };
201
- var AgentsMetaInvalidError = class extends Error {
202
- constructor(metaPath, cause) {
203
- const detail = cause instanceof Error ? cause.message : String(cause);
204
- super(`Fabric agents metadata file is invalid: ${metaPath}. ${detail}`);
205
- this.metaPath = metaPath;
206
- this.name = "AgentsMetaInvalidError";
207
- }
208
- metaPath;
209
- code = "FABRIC_META_INVALID";
210
- };
211
- function getAgentsMetaPath(projectRoot) {
212
- return join3(projectRoot, ".fabric", "agents.meta.json");
213
- }
214
- function resolveProjectRoot() {
215
- return process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
216
- }
217
- async function readAgentsMeta(projectRoot) {
218
- const cached = contextCache.get("meta", projectRoot);
219
- if (cached !== void 0) {
220
- return cached;
221
- }
222
- const metaPath = getAgentsMetaPath(projectRoot);
223
- let raw;
224
- try {
225
- raw = await readFile2(metaPath, "utf8");
226
- } catch (error) {
227
- if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
228
- throw new AgentsMetaFileMissingError(metaPath);
229
- }
230
- throw error;
231
- }
232
- let parsed;
233
- try {
234
- parsed = agentsMetaSchema.parse(JSON.parse(raw));
235
- } catch (error) {
236
- throw new AgentsMetaInvalidError(metaPath, error);
237
- }
238
- contextCache.set("meta", projectRoot, parsed);
239
- return parsed;
240
- }
241
-
242
- // src/services/audit-log.ts
243
- import { appendFile, mkdir as mkdir2, open, stat } from "fs/promises";
244
- import { isAbsolute, join as join4, posix, relative, resolve as resolve2 } from "path";
245
- var AUDIT_LOG_FILE = `${FABRIC_DIR}/audit.jsonl`;
246
- var DEFAULT_AUDIT_WINDOW_MS = 5 * 60 * 1e3;
247
- async function appendGetRulesAuditEvent(projectRoot, input) {
248
- const entry = {
249
- kind: "audit-event",
250
- event: "get_rules",
251
- ts: input.ts ?? Date.now(),
252
- path: normalizeAuditPath(projectRoot, input.path),
253
- client_hash: input.client_hash
254
- };
255
- await appendAuditLogEntries(projectRoot, [entry]);
256
- return entry;
257
- }
258
- async function appendRuleSelectionAuditEvent(projectRoot, input) {
259
- const entry = {
260
- kind: "audit-event",
261
- event: "rule_selection",
262
- ts: input.ts ?? Date.now(),
263
- path: normalizeAuditPath(projectRoot, input.path),
264
- selection_token: input.selection_token,
265
- target_paths: input.target_paths.map((path) => normalizeAuditPath(projectRoot, path)),
266
- required_stable_ids: input.required_stable_ids,
267
- ai_selectable_stable_ids: input.ai_selectable_stable_ids,
268
- ai_selected_stable_ids: input.ai_selected_stable_ids,
269
- final_stable_ids: input.final_stable_ids,
270
- ai_selection_reasons: input.ai_selection_reasons,
271
- rejected_stable_ids: input.rejected_stable_ids,
272
- ignored_stable_ids: input.ignored_stable_ids
273
- };
274
- await appendAuditLogEntries(projectRoot, [entry]);
275
- return entry;
276
- }
277
- async function appendEditIntentAuditEvents(projectRoot, input) {
278
- const ts = input.ts ?? Date.now();
279
- const windowMs = input.window_ms ?? DEFAULT_AUDIT_WINDOW_MS;
280
- const getRulesEntries = (await readAuditLog(projectRoot, { windowMs, ts })).filter(
281
- isGetRulesAuditEntry
282
- );
283
- const entries = input.affected_paths.map((affectedPath) => {
284
- const path = normalizeAuditPath(projectRoot, affectedPath);
285
- const matchedGetRules = findPrecedingGetRulesEvent(getRulesEntries, path, ts, windowMs);
286
- return {
287
- kind: "audit-event",
288
- event: "edit_intent",
289
- ts,
290
- path,
291
- compliant: matchedGetRules !== null,
292
- intent: input.intent,
293
- ledger_entry_id: input.ledger_entry_id,
294
- matched_get_rules_ts: matchedGetRules?.ts ?? null,
295
- window_ms: windowMs
296
- };
297
- });
298
- const compliance = {
299
- compliant: entries.length === 0 || entries.every((e) => e.compliant),
300
- matched_get_rules_ts: entries.length > 0 && entries[0].matched_get_rules_ts !== null ? new Date(entries[0].matched_get_rules_ts).toISOString() : null,
301
- window_ms: windowMs
302
- };
303
- if (entries.length === 0) {
304
- return { entries, compliance };
305
- }
306
- await appendAuditLogEntries(projectRoot, entries);
307
- return { entries, compliance };
308
- }
309
- async function readAuditLog(projectRoot, opts) {
310
- if (opts === void 0) {
311
- return readAuditLogFull(projectRoot);
312
- }
313
- return readAuditLogWindowed(projectRoot, opts.ts, opts.windowMs);
314
- }
315
- async function readAuditLogFull(projectRoot) {
316
- const auditPath = join4(projectRoot, AUDIT_LOG_FILE);
317
- let raw;
318
- try {
319
- const fileStat = await stat(auditPath);
320
- const handle = await open(auditPath, "r");
321
- try {
322
- const buffer = Buffer.alloc(fileStat.size);
323
- await handle.read(buffer, 0, fileStat.size, 0);
324
- raw = buffer.toString("utf8");
325
- } finally {
326
- await handle.close();
327
- }
328
- } catch (error) {
329
- if (isNodeError(error) && error.code === "ENOENT") {
330
- return [];
331
- }
332
- throw error;
333
- }
334
- return parseAuditLogText(raw);
335
- }
336
- async function readAuditLogWindowed(projectRoot, ts, windowMs) {
337
- const auditPath = join4(projectRoot, AUDIT_LOG_FILE);
338
- let fileSize;
339
- try {
340
- const fileStat = await stat(auditPath);
341
- fileSize = fileStat.size;
342
- } catch (error) {
343
- if (isNodeError(error) && error.code === "ENOENT") {
344
- return [];
345
- }
346
- throw error;
347
- }
348
- const cursor = contextCache.getAuditCursor(projectRoot);
349
- const startOffset = cursor !== void 0 && cursor.offset <= fileSize ? cursor.offset : 0;
350
- const priorRemainder = startOffset > 0 && cursor !== void 0 ? cursor.remainder : "";
351
- const priorWindowEntries = startOffset > 0 && cursor !== void 0 ? cursor.windowEntries : [];
352
- const effectiveStart = cursor !== void 0 && cursor.offset > fileSize ? 0 : startOffset;
353
- let newEntries = [];
354
- if (fileSize > effectiveStart) {
355
- const length = fileSize - effectiveStart;
356
- let chunk;
357
- try {
358
- const handle = await open(auditPath, "r");
359
- try {
360
- const buffer = Buffer.alloc(length);
361
- await handle.read(buffer, 0, length, effectiveStart);
362
- chunk = `${priorRemainder}${buffer.toString("utf8")}`;
363
- } finally {
364
- await handle.close();
365
- }
366
- } catch (error) {
367
- contextCache.resetAuditCursor(projectRoot);
368
- return (await readAuditLogFull(projectRoot)).filter((entry) => ts - entry.ts <= windowMs && entry.ts <= ts);
369
- }
370
- const lines = chunk.split(/\r?\n/);
371
- const remainder = chunk.endsWith("\n") ? "" : lines.pop() ?? "";
372
- const windowEntries = [...priorWindowEntries, ...parseAuditLogText(lines.join("\n"))].filter(
373
- (entry) => ts - entry.ts <= windowMs && entry.ts <= ts
374
- );
375
- contextCache.setAuditCursor(projectRoot, { offset: fileSize, remainder, windowEntries });
376
- newEntries = windowEntries;
377
- } else {
378
- const windowEntries = priorWindowEntries.filter((entry) => ts - entry.ts <= windowMs && entry.ts <= ts);
379
- contextCache.setAuditCursor(projectRoot, {
380
- offset: fileSize,
381
- remainder: cursor?.remainder ?? "",
382
- windowEntries
383
- });
384
- newEntries = windowEntries;
385
- }
386
- if (effectiveStart === 0 && cursor !== void 0 && cursor.offset > fileSize) {
387
- return newEntries.filter((entry) => ts - entry.ts <= windowMs && entry.ts <= ts);
388
- }
389
- return newEntries;
390
- }
391
- function parseAuditLogText(raw) {
392
- return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map(parseAuditLogLine).filter((entry) => entry !== null);
393
- }
394
- function findPrecedingGetRulesEvent(entries, path, ts, windowMs) {
395
- let matched = null;
396
- for (const entry of entries) {
397
- if (entry.path !== path) {
398
- continue;
399
- }
400
- if (entry.ts > ts) {
401
- continue;
402
- }
403
- if (ts - entry.ts > windowMs) {
404
- continue;
405
- }
406
- if (matched === null || entry.ts > matched.ts) {
407
- matched = entry;
408
- }
409
- }
410
- return matched;
411
- }
412
- function normalizeAuditPath(projectRoot, value) {
413
- const normalizedProjectRoot = resolve2(projectRoot);
414
- const candidate = isAbsolute(value) ? resolve2(value) : resolve2(normalizedProjectRoot, value);
415
- const relativePath = relative(normalizedProjectRoot, candidate);
416
- if (relativePath.length > 0 && relativePath !== "." && !relativePath.startsWith("..") && !isAbsolute(relativePath)) {
417
- return posix.normalize(relativePath.split("\\").join("/"));
418
- }
419
- return posix.normalize(value.replaceAll("\\", "/"));
420
- }
421
- function isGetRulesAuditEntry(entry) {
422
- return entry.event === "get_rules";
423
- }
424
- async function appendAuditLogEntries(projectRoot, entries) {
425
- const auditPath = join4(projectRoot, AUDIT_LOG_FILE);
426
- const auditDir = join4(projectRoot, FABRIC_DIR);
427
- await mkdir2(auditDir, { recursive: true });
428
- await appendFile(auditPath, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}
429
- `, "utf8");
430
- }
431
- function parseAuditLogLine(line) {
432
- try {
433
- const parsed = JSON.parse(line);
434
- if (parsed.kind !== "audit-event" || typeof parsed.ts !== "number" || typeof parsed.path !== "string") {
435
- return null;
436
- }
437
- if (parsed.event === "get_rules") {
438
- return {
439
- kind: "audit-event",
440
- event: "get_rules",
441
- ts: parsed.ts,
442
- path: parsed.path,
443
- client_hash: typeof parsed.client_hash === "string" ? parsed.client_hash : void 0
444
- };
445
- }
446
- if (parsed.event === "edit_intent" && typeof parsed.compliant === "boolean" && typeof parsed.intent === "string" && typeof parsed.ledger_entry_id === "string" && (typeof parsed.matched_get_rules_ts === "number" || parsed.matched_get_rules_ts === null) && typeof parsed.window_ms === "number") {
447
- return {
448
- kind: "audit-event",
449
- event: "edit_intent",
450
- ts: parsed.ts,
451
- path: parsed.path,
452
- compliant: parsed.compliant,
453
- intent: parsed.intent,
454
- ledger_entry_id: parsed.ledger_entry_id,
455
- matched_get_rules_ts: parsed.matched_get_rules_ts,
456
- window_ms: parsed.window_ms
457
- };
458
- }
459
- if (parsed.event === "rule_selection" && typeof parsed.selection_token === "string" && Array.isArray(parsed.target_paths) && Array.isArray(parsed.required_stable_ids) && Array.isArray(parsed.ai_selectable_stable_ids) && Array.isArray(parsed.ai_selected_stable_ids) && Array.isArray(parsed.final_stable_ids) && isStringRecord(parsed.ai_selection_reasons) && Array.isArray(parsed.rejected_stable_ids) && Array.isArray(parsed.ignored_stable_ids)) {
460
- return {
461
- kind: "audit-event",
462
- event: "rule_selection",
463
- ts: parsed.ts,
464
- path: parsed.path,
465
- selection_token: parsed.selection_token,
466
- target_paths: parsed.target_paths.filter(isString),
467
- required_stable_ids: parsed.required_stable_ids.filter(isString),
468
- ai_selectable_stable_ids: parsed.ai_selectable_stable_ids.filter(isString),
469
- ai_selected_stable_ids: parsed.ai_selected_stable_ids.filter(isString),
470
- final_stable_ids: parsed.final_stable_ids.filter(isString),
471
- ai_selection_reasons: parsed.ai_selection_reasons,
472
- rejected_stable_ids: parsed.rejected_stable_ids.filter(isString),
473
- ignored_stable_ids: parsed.ignored_stable_ids.filter(isString)
474
- };
475
- }
476
- return null;
477
- } catch {
478
- return null;
479
- }
480
- }
481
- function isString(value) {
482
- return typeof value === "string";
483
- }
484
- function isStringRecord(value) {
485
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
486
- return false;
487
- }
488
- return Object.values(value).every((entry) => typeof entry === "string");
489
- }
490
-
491
- // src/services/read-ledger.ts
492
- import { randomUUID } from "crypto";
493
- import { access, appendFile as appendFile2, copyFile, readFile as readFile3, rm } from "fs/promises";
494
- import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
495
- async function resolveLedgerPaths(projectRoot) {
496
- const primaryPath = getLedgerPath(projectRoot);
497
- const legacyPath = getLegacyLedgerPath(projectRoot);
498
- const [primaryExists, legacyExists] = await Promise.all([
499
- pathExists(primaryPath),
500
- pathExists(legacyPath)
501
- ]);
502
- return {
503
- primaryPath,
504
- legacyPath,
505
- readPath: primaryExists ? primaryPath : legacyPath,
506
- usingLegacy: !primaryExists && legacyExists
507
- };
508
- }
509
- async function readLedger(projectRoot, options = {}) {
510
- const { readPath } = await resolveLedgerPaths(projectRoot);
511
- let raw;
512
- try {
513
- raw = await readFile3(readPath, "utf8");
514
- } catch (error) {
515
- if (isNodeError(error) && error.code === "ENOENT") {
516
- return [];
517
- }
518
- throw error;
519
- }
520
- return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map((line, index) => parseLedgerLine(line, index)).filter((entry) => entry !== null).filter((entry) => options.source === void 0 || entry.source === options.source).filter((entry) => options.since === void 0 || entry.ts >= options.since);
521
- }
522
- async function appendLedgerEntry(projectRoot, entry) {
523
- const ledgerPath = getLedgerPath(projectRoot);
524
- const nextEntry = ledgerEntrySchema.parse({
525
- ...entry,
526
- id: entry.id ?? `ledger:${randomUUID()}`
527
- });
528
- await ensureParentDirectory(ledgerPath);
529
- await appendFile2(ledgerPath, `${JSON.stringify(nextEntry)}
530
- `, "utf8");
531
- return nextEntry;
532
- }
533
- async function migrateLegacyLedger(projectRoot) {
534
- const { primaryPath, legacyPath } = await resolveLedgerPaths(projectRoot);
535
- const [primaryExists, legacyExists] = await Promise.all([
536
- pathExists(primaryPath),
537
- pathExists(legacyPath)
538
- ]);
539
- if (!legacyExists) {
540
- return {
541
- migrated: false,
542
- from: null,
543
- to: primaryPath
544
- };
545
- }
546
- if (!primaryExists) {
547
- await ensureParentDirectory(primaryPath);
548
- await copyFile(legacyPath, primaryPath);
549
- }
550
- await rm(legacyPath, { force: true });
551
- return {
552
- migrated: true,
553
- from: legacyPath,
554
- to: primaryPath
555
- };
556
- }
557
- function parseLedgerLine(line, index) {
558
- try {
559
- const parsed = JSON.parse(line);
560
- if (parsed.kind === "mcp-event") {
561
- return null;
562
- }
563
- const result = ledgerEntrySchema.safeParse(parsed);
564
- if (!result.success) {
565
- return null;
566
- }
567
- return {
568
- ...result.data,
569
- id: result.data.id ?? createDerivedId(index, line)
570
- };
571
- } catch {
572
- return null;
573
- }
574
- }
575
- function createDerivedId(index, line) {
576
- return `ledger:${index + 1}:${sha256(line).slice("sha256:".length)}`;
577
- }
578
- async function pathExists(path) {
579
- try {
580
- await access(path);
581
- return true;
582
- } catch (error) {
583
- if (isNodeError(error) && error.code === "ENOENT") {
584
- return false;
585
- }
586
- throw error;
587
- }
588
- }
589
-
590
- // src/services/doctor.ts
591
- var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
592
- var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
593
- ".fabric",
594
- ".git",
595
- ".next",
596
- ".turbo",
597
- "Library",
598
- "Temp",
599
- "build",
600
- "coverage",
601
- "dist",
602
- "node_modules"
603
- ]);
604
- var LEDGER_WARN_AFTER_MS = 3 * 24 * 60 * 60 * 1e3;
605
- var LEDGER_ERROR_AFTER_MS = 7 * 24 * 60 * 60 * 1e3;
606
- async function runDoctorReport(target) {
607
- const projectRoot = normalizeTarget(target);
608
- const framework = detectFramework(projectRoot);
609
- const entryPoints = collectEntryPoints(projectRoot);
610
- const [savedForensic, metaSnapshot, humanLockSnapshot, ledgerSnapshot, auditReport] = await Promise.all([
611
- readSavedForensic(projectRoot),
612
- inspectMetaRevision(projectRoot),
613
- inspectHumanLock(projectRoot),
614
- inspectLedger(projectRoot),
615
- runDoctorAuditReport(projectRoot)
616
- ]);
617
- const checks = [
618
- createForensicCheck(savedForensic, framework, entryPoints),
619
- createFrameworkCheck(savedForensic, framework, entryPoints),
620
- createMetaRevisionCheck(metaSnapshot),
621
- createProtectedPathsCheck(humanLockSnapshot),
622
- createLedgerCheck(ledgerSnapshot)
623
- ];
624
- if (!auditReport.skipped) {
625
- checks.push(createAuditCheck(auditReport));
626
- }
627
- return {
628
- status: reduceStatus(checks.map((check) => check.status)),
629
- checks,
630
- summary: {
631
- target: projectRoot,
632
- framework: {
633
- kind: framework.kind,
634
- version: framework.version,
635
- subkind: framework.subkind
636
- },
637
- entryPoints,
638
- driftCount: humanLockSnapshot.driftCount,
639
- protectedPathCount: humanLockSnapshot.protectedPathCount,
640
- protectedPathsIntact: humanLockSnapshot.present && humanLockSnapshot.driftCount === 0,
641
- lastLedgerEntryTs: ledgerSnapshot.lastEntryTs,
642
- lastLedgerEntryAgeMs: ledgerSnapshot.lastEntryAgeMs,
643
- metaRevision: metaSnapshot.revision,
644
- ledgerPath: ledgerSnapshot.primaryPath,
645
- legacyLedgerPath: ledgerSnapshot.legacyPath,
646
- legacyLedgerDetected: ledgerSnapshot.usingLegacy,
647
- audit: auditReport.skipped ? null : {
648
- enabled: true,
649
- mode: auditReport.mode,
650
- checkedPathCount: auditReport.checkedPathCount,
651
- violationCount: auditReport.violationCount,
652
- windowMs: auditReport.windowMs
653
- }
654
- },
655
- audit: auditReport.skipped ? null : auditReport
656
- };
657
- }
658
- async function runDoctorFix(target) {
659
- const projectRoot = normalizeTarget(target);
660
- const migration = await migrateLegacyLedger(projectRoot);
661
- const report = await runDoctorReport(projectRoot);
662
- return {
663
- changed: migration.migrated,
664
- migratedLedger: migration.migrated,
665
- message: migration.migrated ? `Migrated legacy ledger from ${migration.from} to ${migration.to}.` : `No legacy ledger migration needed. Canonical ledger path: ${migration.to}.`,
666
- report
667
- };
668
- }
669
- async function runDoctorAuditReport(target, options = {}) {
670
- const projectRoot = normalizeTarget(target);
671
- const mode = options.mode ?? readDoctorAuditMode(projectRoot);
672
- const windowMs = options.windowMs ?? DEFAULT_AUDIT_WINDOW_MS;
673
- if (mode === "off" && options.force !== true) {
674
- return {
675
- mode,
676
- skipped: true,
677
- windowMs,
678
- checkedPathCount: 0,
679
- violationCount: 0,
680
- violations: []
681
- };
682
- }
683
- const [ledgerEntries, auditEntries] = await Promise.all([
684
- readLedger(projectRoot, { source: "ai" }),
685
- readAuditLog(projectRoot)
686
- ]);
687
- const ruleAccessEntries = auditEntries.filter(
688
- (entry) => entry.event === "get_rules" || entry.event === "rule_selection"
689
- );
690
- const { checkedPathCount, violations } = collectAuditViolations(
691
- projectRoot,
692
- ledgerEntries,
693
- ruleAccessEntries,
694
- windowMs
695
- );
696
- return {
697
- mode,
698
- skipped: false,
699
- windowMs,
700
- checkedPathCount,
701
- violationCount: violations.length,
702
- violations
703
- };
704
- }
705
- function createForensicCheck(forensic, framework, entryPoints) {
706
- if (!forensic.present) {
707
- return {
708
- name: "Forensic snapshot",
709
- status: "error",
710
- message: `${forensic.reason} Live scan detects ${formatFramework(framework)} with ${entryPoints.length} entry point${entryPoints.length === 1 ? "" : "s"}.`
711
- };
712
- }
713
- return {
714
- name: "Forensic snapshot",
715
- status: "ok",
716
- message: `Loaded .fabric/forensic.json for ${formatFramework(forensic.report.framework)} with ${forensic.report.entry_points.length} recorded entry point${forensic.report.entry_points.length === 1 ? "" : "s"}.`
717
- };
718
- }
719
- function createFrameworkCheck(forensic, framework, entryPoints) {
720
- if (framework.kind === "unknown") {
721
- return {
722
- name: "Framework fingerprint",
723
- status: "warn",
724
- message: "Unable to identify the project framework from current files."
725
- };
726
- }
727
- if (!forensic.present) {
728
- return {
729
- name: "Framework fingerprint",
730
- status: "warn",
731
- message: `Live detection sees ${formatFramework(framework)} and ${entryPoints.length} entry point${entryPoints.length === 1 ? "" : "s"}, but no forensic baseline exists yet.`
732
- };
733
- }
734
- const matches = forensic.report.framework.kind === framework.kind && forensic.report.framework.version === framework.version && forensic.report.framework.subkind === framework.subkind;
735
- if (!matches) {
736
- return {
737
- name: "Framework fingerprint",
738
- status: "warn",
739
- message: `Forensic baseline says ${formatFramework(forensic.report.framework)}; live scan says ${formatFramework(framework)}.`
740
- };
741
- }
742
- return {
743
- name: "Framework fingerprint",
744
- status: "ok",
745
- message: `Framework baseline matches live scan: ${formatFramework(framework)} \xB7 ${entryPoints.length} current entry point${entryPoints.length === 1 ? "" : "s"}.`
746
- };
747
- }
748
- function createMetaRevisionCheck(snapshot) {
749
- if (!snapshot.present) {
750
- return {
751
- name: "Meta revision",
752
- status: "error",
753
- message: snapshot.unexpectedError ?? "agents.meta.json is missing."
754
- };
755
- }
756
- if (snapshot.driftCount > 0 || snapshot.missingFiles.length > 0) {
757
- const parts = [
758
- `${snapshot.driftCount} tracked AGENTS file drift`,
759
- snapshot.missingFiles.length > 0 ? `${snapshot.missingFiles.length} missing tracked file` : null
760
- ].filter((part) => part !== null);
761
- return {
762
- name: "Meta revision",
763
- status: "error",
764
- message: `agents.meta.json revision ${snapshot.revision} is stale: ${parts.join(" \xB7 ")}.`
765
- };
766
- }
767
- if (snapshot.derivedIdentityFiles.length > 0) {
768
- const [firstFile] = snapshot.derivedIdentityFiles;
769
- const suffix = snapshot.derivedIdentityFiles.length > 1 ? ` (+${snapshot.derivedIdentityFiles.length - 1} more)` : "";
770
- return {
771
- name: "Meta revision",
772
- status: "warn",
773
- message: `agents.meta.json revision ${snapshot.revision} matches ${snapshot.nodeCount} tracked AGENTS files, but ${snapshot.derivedIdentityFiles.length} rule node${snapshot.derivedIdentityFiles.length === 1 ? "" : "s"} still use derived identities. Add \`<!-- fab:rule-id ... -->\` to the rule file header instead of editing meta directly (${firstFile}${suffix}).`
774
- };
775
- }
776
- return {
777
- name: "Meta revision",
778
- status: "ok",
779
- message: `agents.meta.json revision ${snapshot.revision} matches ${snapshot.nodeCount} tracked AGENTS file${snapshot.nodeCount === 1 ? "" : "s"}.`
780
- };
781
- }
782
- function createProtectedPathsCheck(snapshot) {
783
- if (!snapshot.present) {
784
- return {
785
- name: "Protected paths",
786
- status: "warn",
787
- message: snapshot.reason
788
- };
789
- }
790
- if (snapshot.driftCount > 0) {
791
- return {
792
- name: "Protected paths",
793
- status: "warn",
794
- message: `${snapshot.driftCount} of ${snapshot.protectedPathCount} protected path${snapshot.protectedPathCount === 1 ? "" : "s"} drifted from approved hashes.`
795
- };
796
- }
797
- return {
798
- name: "Protected paths",
799
- status: "ok",
800
- message: `${snapshot.protectedPathCount} protected path${snapshot.protectedPathCount === 1 ? "" : "s"} intact with zero hash drift.`
801
- };
802
- }
803
- function createLedgerCheck(snapshot) {
804
- if (snapshot.usingLegacy) {
805
- return {
806
- name: "Intent ledger",
807
- status: "warn",
808
- message: `Legacy ledger path detected at ${snapshot.legacyPath}. Fabric now reads ${snapshot.primaryPath} by default; run fab doctor --fix to migrate.`
809
- };
810
- }
811
- if (snapshot.lastEntryTs === null || snapshot.lastEntryAgeMs === null) {
812
- return {
813
- name: "Intent ledger",
814
- status: "warn",
815
- message: "No ledger entries recorded yet."
816
- };
817
- }
818
- if (snapshot.lastEntryAgeMs >= LEDGER_ERROR_AFTER_MS) {
819
- return {
820
- name: "Intent ledger",
821
- status: "error",
822
- message: `Last ledger entry is ${formatAge(snapshot.lastEntryAgeMs)} old (${new Date(snapshot.lastEntryTs).toISOString()}).`
823
- };
824
- }
825
- if (snapshot.lastEntryAgeMs >= LEDGER_WARN_AFTER_MS) {
826
- return {
827
- name: "Intent ledger",
828
- status: "warn",
829
- message: `Last ledger entry is ${formatAge(snapshot.lastEntryAgeMs)} old (${new Date(snapshot.lastEntryTs).toISOString()}).`
830
- };
831
- }
832
- return {
833
- name: "Intent ledger",
834
- status: "ok",
835
- message: `Last ledger entry is ${formatAge(snapshot.lastEntryAgeMs)} old (${snapshot.count} total entr${snapshot.count === 1 ? "y" : "ies"}).`
836
- };
837
- }
838
- function createAuditCheck(report) {
839
- if (report.checkedPathCount === 0) {
840
- return {
841
- name: "Rules fetch audit",
842
- status: "warn",
843
- message: "No AI edit intents recorded yet for compliance audit."
844
- };
845
- }
846
- if (report.violationCount > 0) {
847
- return {
848
- name: "Rules fetch audit",
849
- status: report.mode === "strict" ? "error" : "warn",
850
- message: `${report.violationCount} edit path${report.violationCount === 1 ? "" : "s"} lack a preceding rule_selection or get_rules event within ${formatDuration(report.windowMs)}.`
851
- };
852
- }
853
- return {
854
- name: "Rules fetch audit",
855
- status: "ok",
856
- message: `All ${report.checkedPathCount} audited edit path${report.checkedPathCount === 1 ? "" : "s"} have a preceding rule_selection or get_rules event within ${formatDuration(report.windowMs)}.`
857
- };
858
- }
859
- async function readSavedForensic(projectRoot) {
860
- const forensicPath = join5(projectRoot, ".fabric", "forensic.json");
861
- try {
862
- const raw = await readFile4(forensicPath, "utf8");
863
- const parsed = forensicReportSchema.safeParse(JSON.parse(raw));
864
- if (!parsed.success) {
865
- return {
866
- present: false,
867
- reason: "forensic.json is invalid."
868
- };
869
- }
870
- return {
871
- present: true,
872
- report: parsed.data
873
- };
874
- } catch (error) {
875
- if (isMissingFileError(error)) {
876
- return {
877
- present: false,
878
- reason: ".fabric/forensic.json is missing."
879
- };
880
- }
881
- return {
882
- present: false,
883
- reason: error instanceof Error ? error.message : String(error)
884
- };
885
- }
886
- }
887
- async function inspectMetaRevision(projectRoot) {
888
- try {
889
- const meta = await readAgentsMeta(projectRoot);
890
- const entries = Object.entries(meta.nodes).sort(([left], [right]) => left.localeCompare(right));
891
- const missingFiles = [];
892
- const derivedIdentityFiles = [];
893
- let driftCount = 0;
894
- const revisionSource = entries.map(([id, node]) => {
895
- const absolutePath = join5(projectRoot, node.file);
896
- if (!existsSync(absolutePath)) {
897
- missingFiles.push(node.file);
898
- driftCount += 1;
899
- return "missing";
900
- }
901
- const actualHash = sha2562(readFileSync(absolutePath, "utf8"));
902
- if (actualHash !== node.hash) {
903
- driftCount += 1;
904
- }
905
- if (node.file !== ".fabric/bootstrap/README.md" && node.identity_source !== "declared") {
906
- derivedIdentityFiles.push(node.file);
907
- }
908
- return [id, actualHash, node.stable_id ?? "", node.identity_source ?? ""].join("|");
909
- }).join("\n");
910
- const revision = sha2562(revisionSource);
911
- return {
912
- present: true,
913
- revision: meta.revision,
914
- nodeCount: entries.length,
915
- driftCount: revision === meta.revision ? driftCount : Math.max(driftCount, 1),
916
- missingFiles,
917
- derivedIdentityFiles
918
- };
919
- } catch (error) {
920
- return {
921
- present: false,
922
- revision: null,
923
- nodeCount: 0,
924
- driftCount: 0,
925
- missingFiles: [],
926
- derivedIdentityFiles: [],
927
- unexpectedError: error instanceof Error ? error.message : String(error)
928
- };
929
- }
930
- }
931
- async function inspectHumanLock(projectRoot) {
932
- try {
933
- const entries = await readHumanLock(projectRoot);
934
- return {
935
- present: true,
936
- driftCount: entries.filter((entry) => entry.drift).length,
937
- protectedPathCount: entries.length
938
- };
939
- } catch (error) {
940
- if (isMissingFileError(error)) {
941
- return {
942
- present: false,
943
- driftCount: 0,
944
- protectedPathCount: 0,
945
- reason: ".fabric/human-lock.json is missing; no protected paths are being tracked."
946
- };
947
- }
948
- return {
949
- present: false,
950
- driftCount: 0,
951
- protectedPathCount: 0,
952
- reason: error instanceof Error ? error.message : String(error)
953
- };
954
- }
955
- }
956
- async function inspectLedger(projectRoot) {
957
- const paths = await resolveLedgerPaths(projectRoot);
958
- const entries = await readLedger(projectRoot);
959
- const lastEntry = entries.reduce(
960
- (latest, entry) => latest === null || entry.ts > latest ? entry.ts : latest,
961
- null
962
- );
963
- return {
964
- count: entries.length,
965
- lastEntryTs: lastEntry,
966
- lastEntryAgeMs: lastEntry === null ? null : Math.max(Date.now() - lastEntry, 0),
967
- primaryPath: paths.primaryPath,
968
- legacyPath: paths.legacyPath,
969
- usingLegacy: paths.usingLegacy
970
- };
971
- }
972
- function collectAuditViolations(projectRoot, ledgerEntries, ruleAccessEntries, windowMs) {
973
- let checkedPathCount = 0;
974
- const violations = [];
975
- for (const entry of ledgerEntries) {
976
- for (const affectedPath of entry.affected_paths) {
977
- const normalizedPath = normalizeAuditPath(projectRoot, affectedPath);
978
- const matched = findPrecedingRuleAccessEvent(ruleAccessEntries, normalizedPath, entry.ts, windowMs);
979
- checkedPathCount += 1;
980
- if (matched !== null) {
981
- continue;
982
- }
983
- violations.push({
984
- editTs: entry.ts,
985
- entryId: entry.id,
986
- intent: entry.intent,
987
- lastRuleAccessTs: findLatestRuleAccessTs(ruleAccessEntries, normalizedPath, entry.ts),
988
- path: normalizedPath
989
- });
990
- }
991
- }
992
- return {
993
- checkedPathCount,
994
- violations
995
- };
996
- }
997
- function findPrecedingRuleAccessEvent(entries, path, ts, windowMs) {
998
- const getRulesMatch = findPrecedingGetRulesEvent(entries.filter(isGetRulesAuditEntry2), path, ts, windowMs);
999
- const ruleSelectionMatch = findPrecedingRuleSelectionEvent(entries.filter(isRuleSelectionAuditEntry), path, ts, windowMs);
1000
- if (getRulesMatch === null) {
1001
- return ruleSelectionMatch;
1002
- }
1003
- if (ruleSelectionMatch === null) {
1004
- return getRulesMatch;
1005
- }
1006
- return getRulesMatch.ts >= ruleSelectionMatch.ts ? getRulesMatch : ruleSelectionMatch;
1007
- }
1008
- function findPrecedingRuleSelectionEvent(entries, path, ts, windowMs) {
1009
- let matched = null;
1010
- for (const entry of entries) {
1011
- if (!entry.target_paths.includes(path) && entry.path !== path) {
1012
- continue;
1013
- }
1014
- if (entry.ts > ts || ts - entry.ts > windowMs) {
1015
- continue;
1016
- }
1017
- if (matched === null || entry.ts > matched.ts) {
1018
- matched = entry;
1019
- }
1020
- }
1021
- return matched;
1022
- }
1023
- function findLatestRuleAccessTs(entries, path, ts) {
1024
- let latest = null;
1025
- for (const entry of entries) {
1026
- const matchesPath = entry.event === "rule_selection" ? entry.path === path || entry.target_paths.includes(path) : entry.path === path;
1027
- if (!matchesPath || entry.ts > ts) {
1028
- continue;
1029
- }
1030
- latest = latest === null || entry.ts > latest ? entry.ts : latest;
1031
- }
1032
- return latest;
1033
- }
1034
- function isGetRulesAuditEntry2(entry) {
1035
- return entry.event === "get_rules";
1036
- }
1037
- function isRuleSelectionAuditEntry(entry) {
1038
- return entry.event === "rule_selection";
1039
- }
1040
- function readDoctorAuditMode(projectRoot) {
1041
- const configPath = join5(projectRoot, "fabric.config.json");
1042
- try {
1043
- const parsed = JSON.parse(readFileSync(configPath, "utf8"));
1044
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1045
- return "off";
1046
- }
1047
- const configuredMode = readAuditModeValue(parsed.auditMode) ?? readAuditModeValue(parsed.audit_mode);
1048
- return configuredMode ?? "off";
1049
- } catch (error) {
1050
- if (isMissingFileError(error)) {
1051
- return "off";
1052
- }
1053
- return "off";
1054
- }
1055
- }
1056
- function normalizeTarget(targetInput) {
1057
- return isAbsolute2(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
1058
- }
1059
- function collectEntryPoints(root) {
1060
- if (!existsSync(root) || !statSync(root).isDirectory()) {
1061
- return [];
1062
- }
1063
- const entries = [];
1064
- const stack = [root];
1065
- while (stack.length > 0) {
1066
- const current = stack.pop();
1067
- if (current === void 0) {
1068
- continue;
1069
- }
1070
- for (const entry of readdirSync(current, { withFileTypes: true })) {
1071
- const absolutePath = join5(current, entry.name);
1072
- const relativePath = posix2.normalize(absolutePath.slice(root.length + 1).split("\\").join("/"));
1073
- if (relativePath.length === 0) {
1074
- continue;
1075
- }
1076
- if (entry.isDirectory()) {
1077
- if (IGNORED_DIRECTORIES.has(entry.name)) {
1078
- continue;
1079
- }
1080
- stack.push(absolutePath);
1081
- continue;
1082
- }
1083
- if (!entry.isFile()) {
1084
- continue;
1085
- }
1086
- const reason = getEntryPointReason(relativePath);
1087
- if (reason !== null) {
1088
- entries.push({
1089
- path: relativePath,
1090
- reason
1091
- });
1092
- }
1093
- }
1094
- }
1095
- return entries.sort((left, right) => left.path.localeCompare(right.path));
1096
- }
1097
- function getEntryPointReason(relativePath) {
1098
- const extension = relativePath.slice(relativePath.lastIndexOf("."));
1099
- if (!SCRIPT_EXTENSIONS.has(extension)) {
1100
- return null;
1101
- }
1102
- const directory = posix2.dirname(relativePath);
1103
- const fileName = posix2.basename(relativePath);
1104
- const fileBase = fileName.slice(0, Math.max(fileName.lastIndexOf("."), 0));
1105
- if (directory === "assets/scripts" || directory === "scripts") {
1106
- return "top-level script";
1107
- }
1108
- if (directory === "src" && /^(App|app|index|main)$/.test(fileBase)) {
1109
- return "application entry";
1110
- }
1111
- if ((directory === "app" || directory.startsWith("app/")) && /^(layout|page|route)$/.test(fileBase)) {
1112
- return "next app route";
1113
- }
1114
- if ((directory === "pages" || directory.startsWith("pages/")) && fileName !== "_app.d.ts") {
1115
- return "next page route";
1116
- }
1117
- return null;
1118
- }
1119
- function reduceStatus(statuses) {
1120
- if (statuses.includes("error")) {
1121
- return "error";
1122
- }
1123
- if (statuses.includes("warn")) {
1124
- return "warn";
1125
- }
1126
- return "ok";
1127
- }
1128
- function formatFramework(framework) {
1129
- const pieces = [framework.kind, framework.version !== "unknown" ? framework.version : null, framework.subkind].filter((piece) => piece !== null && piece !== "unknown");
1130
- return pieces.length > 0 ? pieces.join(" \xB7 ") : "unknown";
1131
- }
1132
- function formatAge(ageMs) {
1133
- const seconds = Math.floor(ageMs / 1e3);
1134
- if (seconds < 60) {
1135
- return `${seconds}s`;
1136
- }
1137
- const minutes = Math.floor(seconds / 60);
1138
- if (minutes < 60) {
1139
- return `${minutes}m`;
1140
- }
1141
- const hours = Math.floor(minutes / 60);
1142
- if (hours < 48) {
1143
- return `${hours}h`;
1144
- }
1145
- const days = Math.floor(hours / 24);
1146
- if (days < 14) {
1147
- return `${days}d`;
1148
- }
1149
- return `${Math.floor(days / 7)}w`;
1150
- }
1151
- function formatDuration(durationMs) {
1152
- const minutes = Math.floor(durationMs / (60 * 1e3));
1153
- if (minutes < 1) {
1154
- return `${Math.max(Math.floor(durationMs / 1e3), 1)}s`;
1155
- }
1156
- if (minutes < 60) {
1157
- return `${minutes}m`;
1158
- }
1159
- const hours = Math.floor(minutes / 60);
1160
- if (hours < 24) {
1161
- return `${hours}h`;
1162
- }
1163
- return `${Math.floor(hours / 24)}d`;
1164
- }
1165
- function readAuditModeValue(value) {
1166
- if (value === "strict" || value === "warn" || value === "off") {
1167
- return value;
1168
- }
1169
- return null;
1170
- }
1171
- function sha2562(content) {
1172
- return `sha256:${createHash2("sha256").update(content).digest("hex")}`;
1173
- }
1174
- function isMissingFileError(error) {
1175
- return error instanceof Error && "code" in error && error.code === "ENOENT";
1176
- }
1177
-
1178
- // src/services/approve-human-lock.ts
1179
- async function approveHumanLock(projectRoot, input) {
1180
- assertPathWithinProjectRoot(projectRoot, input.file);
1181
- const document = await readHumanLockDocument(projectRoot);
1182
- const index = document.locked.findIndex(
1183
- (entry) => entry.file === input.file && entry.start_line === input.start_line && entry.end_line === input.end_line
1184
- );
1185
- if (index === -1) {
1186
- throw new Error(`Cannot find human lock entry: ${input.file}:${input.start_line}-${input.end_line}`);
1187
- }
1188
- const currentEntry = document.locked[index];
1189
- if (currentEntry === void 0) {
1190
- throw new Error(`Cannot find human lock entry: ${input.file}:${input.start_line}-${input.end_line}`);
1191
- }
1192
- const nextEntry = {
1193
- ...currentEntry,
1194
- hash: input.new_hash
1195
- };
1196
- if (currentEntry.hash === input.new_hash) {
1197
- const currentHash2 = await hashHumanLockedContent(projectRoot, nextEntry);
1198
- return {
1199
- updated: false,
1200
- entry: {
1201
- ...nextEntry,
1202
- drift: currentHash2 !== nextEntry.hash,
1203
- current_hash: currentHash2
1204
- }
1205
- };
1206
- }
1207
- const nextLocked = document.locked.slice();
1208
- nextLocked[index] = nextEntry;
1209
- const nextRawObject = {
1210
- ...document.rawObject,
1211
- locked: nextLocked
1212
- };
1213
- await atomicWriteText(document.path, `${JSON.stringify(nextRawObject, null, 2)}
1214
- `);
1215
- const currentHash = await hashHumanLockedContent(projectRoot, nextEntry);
1216
- const ledgerEntry = await appendLedgerEntry(projectRoot, createApproveLedgerEntry(input));
1217
- return {
1218
- updated: true,
1219
- entry: {
1220
- ...nextEntry,
1221
- drift: currentHash !== nextEntry.hash,
1222
- current_hash: currentHash
1223
- },
1224
- ledger_entry: ledgerEntry
1225
- };
1226
- }
1227
- function createApproveLedgerEntry(input) {
1228
- return {
1229
- ts: Date.now(),
1230
- source: "human",
1231
- parent_sha: "human-lock:approve",
1232
- intent: `approve human lock ${input.file}:${input.start_line}-${input.end_line}`,
1233
- affected_paths: [input.file, ".fabric/human-lock.json"],
1234
- diff_stat: `updated approved hash to ${input.new_hash}`
1235
- };
1236
- }
1237
-
1238
- // src/services/get-rules.ts
1239
- import { readFile as readFile5 } from "fs/promises";
1240
- import { join as join6 } from "path";
1241
- import { minimatch } from "minimatch";
1242
- var PRIORITY_ORDER = {
1243
- high: 0,
1244
- medium: 1,
1245
- low: 2
1246
- };
1247
- async function getRules(projectRoot, input) {
1248
- const context = await loadGetRulesContext(projectRoot);
1249
- const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
1250
- const rules = await resolveRulesForPath(projectRoot, context, input.path);
1251
- const result = {
1252
- revision_hash: context.meta.revision,
1253
- stale,
1254
- rules
1255
- };
1256
- try {
1257
- await appendGetRulesAuditEvent(projectRoot, {
1258
- path: input.path,
1259
- client_hash: input.client_hash
1260
- });
1261
- } catch {
1262
- }
1263
- return result;
1264
- }
1265
- async function loadGetRulesContext(projectRoot) {
1266
- const cached = contextCache.get("context", projectRoot);
1267
- if (cached !== void 0) {
1268
- return cached;
1269
- }
1270
- const meta = await readAgentsMeta(projectRoot);
1271
- const l0Content = await readFile5(join6(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
1272
- const humanLockedNearby = (await readHumanLock(projectRoot)).map((entry) => ({
1273
- file: entry.file,
1274
- excerpt: JSON.stringify(entry)
1275
- }));
1276
- const context = {
1277
- meta,
1278
- l0Content,
1279
- humanLockedNearby
1280
- };
1281
- contextCache.set("context", projectRoot, context);
1282
- return context;
1283
- }
1284
- async function resolveRulesForPath(projectRoot, context, path, options = {}) {
1285
- const matchedNodes = matchRuleNodes(context.meta, path);
1286
- const loaded = await loadMatchedRules(projectRoot, matchedNodes);
1287
- return buildRulesPayload(context, loaded, options);
1288
- }
1289
- function normalizeRulesPath(value) {
1290
- return value.replaceAll("\\", "/");
1291
- }
1292
- function matchRuleNodes(meta, path) {
1293
- const requestedPath = normalizeRulesPath(path);
1294
- return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
1295
- const [leftId, leftNode] = left;
1296
- const [rightId, rightNode] = right;
1297
- const priorityDelta = PRIORITY_ORDER[leftNode.priority] - PRIORITY_ORDER[rightNode.priority];
1298
- return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
1299
- }).map(([nodeId, node]) => ({
1300
- node_id: nodeId,
1301
- level: classifyNode(nodeId, node),
1302
- stable_id: node.stable_id ?? nodeId,
1303
- identity_source: node.identity_source ?? "derived",
1304
- node
1305
- }));
1306
- }
1307
- async function loadMatchedRules(projectRoot, matchedNodes, fileContentCache = /* @__PURE__ */ new Map()) {
1308
- const rules = [];
1309
- const stubs = [];
1310
- for (const matchedNode of matchedNodes) {
1311
- if (matchedNode.level === null) {
1312
- continue;
1313
- }
1314
- if (matchedNode.node.activation?.tier === "description") {
1315
- stubs.push({
1316
- stable_id: matchedNode.stable_id,
1317
- identity_source: matchedNode.identity_source,
1318
- level: matchedNode.level,
1319
- path: matchedNode.node.file,
1320
- description: matchedNode.node.activation.description ?? ""
1321
- });
1322
- continue;
1323
- }
1324
- rules.push({
1325
- level: matchedNode.level,
1326
- stable_id: matchedNode.stable_id,
1327
- identity_source: matchedNode.identity_source,
1328
- entry: {
1329
- path: matchedNode.node.file,
1330
- content: await readRuleContent(projectRoot, matchedNode.node.file, fileContentCache)
1331
- }
1332
- });
1333
- }
1334
- return { rules, stubs };
1335
- }
1336
- function buildRulesPayload(context, loaded, options = {}) {
1337
- const { L1, L2 } = partitionRulesByLevel(loaded.rules, options.dedupeByPath ?? false);
1338
- return {
1339
- L0: context.l0Content,
1340
- L1,
1341
- L2,
1342
- human_locked_nearby: context.humanLockedNearby,
1343
- description_stubs: loaded.stubs.length > 0 ? dedupeDescriptionStubsByPath(loaded.stubs).map(toDescriptionStub) : void 0
1344
- };
1345
- }
1346
- function classifyNode(nodeId, node) {
1347
- if (nodeId.startsWith("L1/")) {
1348
- return "L1";
1349
- }
1350
- if (nodeId.startsWith("L2/")) {
1351
- return "L2";
1352
- }
1353
- return node.layer === "L0" ? null : node.layer;
1354
- }
1355
- function partitionRulesByLevel(loadedRules, dedupeByPath) {
1356
- const l1 = [];
1357
- const l2 = [];
1358
- for (const rule of loadedRules) {
1359
- if (rule.level === "L1") {
1360
- l1.push(rule.entry);
1361
- continue;
1362
- }
1363
- if (rule.level === "L2") {
1364
- l2.push(rule.entry);
1365
- }
1366
- }
1367
- return {
1368
- L1: dedupeByPath ? dedupeEntriesByPath(l1) : l1,
1369
- L2: dedupeByPath ? dedupeEntriesByPath(l2) : l2
1370
- };
1371
- }
1372
- function dedupeEntriesByPath(entries) {
1373
- const seenPaths = /* @__PURE__ */ new Set();
1374
- return entries.filter((entry) => {
1375
- if (seenPaths.has(entry.path)) {
1376
- return false;
1377
- }
1378
- seenPaths.add(entry.path);
1379
- return true;
1380
- });
1381
- }
1382
- function shouldLoadNodeForPath(requestedPath, node) {
1383
- switch (node.activation?.tier) {
1384
- case "always":
1385
- return true;
1386
- case "description":
1387
- return true;
1388
- case "path":
1389
- case void 0:
1390
- return minimatch(requestedPath, normalizeRulesPath(node.scope_glob), { dot: true });
1391
- }
1392
- }
1393
- function dedupeDescriptionStubsByPath(stubs) {
1394
- const seenPaths = /* @__PURE__ */ new Set();
1395
- return stubs.filter((stub) => {
1396
- if (seenPaths.has(stub.path)) {
1397
- return false;
1398
- }
1399
- seenPaths.add(stub.path);
1400
- return true;
1401
- });
1402
- }
1403
- function toDescriptionStub(stub) {
1404
- return {
1405
- path: stub.path,
1406
- description: stub.description
1407
- };
1408
- }
1409
- async function readRuleContent(projectRoot, file, fileContentCache) {
1410
- const cached = fileContentCache.get(file);
1411
- if (cached !== void 0) {
1412
- return await cached;
1413
- }
1414
- const pending = readFile5(join6(projectRoot, file), "utf8");
1415
- fileContentCache.set(file, pending);
1416
- return await pending;
1417
- }
1418
-
1419
- export {
1420
- AGENTS_MD_RESOURCE_URI,
1421
- contextCache,
1422
- AgentsMetaFileMissingError,
1423
- AgentsMetaInvalidError,
1424
- resolveProjectRoot,
1425
- readAgentsMeta,
1426
- FABRIC_DIR,
1427
- LEDGER_PATH,
1428
- LEGACY_LEDGER_PATH,
1429
- atomicWriteText,
1430
- getLedgerPath,
1431
- getLegacyLedgerPath,
1432
- ensureParentDirectory,
1433
- sha256,
1434
- appendRuleSelectionAuditEvent,
1435
- appendEditIntentAuditEvents,
1436
- resolveLedgerPaths,
1437
- readLedger,
1438
- appendLedgerEntry,
1439
- readHumanLock,
1440
- readHumanLockEntry,
1441
- getRules,
1442
- normalizeRulesPath,
1443
- runDoctorReport,
1444
- runDoctorFix,
1445
- runDoctorAuditReport,
1446
- approveHumanLock
1447
- };