@fenglimg/fabric-server 1.5.1 → 1.6.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.
@@ -82,23 +82,29 @@ var ContextCache = class {
82
82
  };
83
83
  var contextCache = new ContextCache(5e3);
84
84
 
85
- // src/services/read-human-lock.ts
86
- import { readFile } from "fs/promises";
87
- import { join } from "path";
88
- import { humanLockEntrySchema } from "@fenglimg/fabric-shared";
89
-
90
85
  // src/services/_shared.ts
91
- import { resolve, sep } from "path";
86
+ import { dirname, join, resolve, sep } from "path";
92
87
  import { createHash } from "crypto";
93
- import { rename, writeFile } from "fs/promises";
88
+ import { mkdir, rename, writeFile } from "fs/promises";
94
89
  var FABRIC_DIR = ".fabric";
95
90
  var HUMAN_LOCK_FILE = "human-lock.json";
96
91
  var LEDGER_FILE = ".intent-ledger.jsonl";
92
+ var LEDGER_PATH = `${FABRIC_DIR}/${LEDGER_FILE}`;
93
+ var LEGACY_LEDGER_PATH = LEDGER_FILE;
97
94
  async function atomicWriteText(path, content) {
98
95
  const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
99
96
  await writeFile(tempPath, content, "utf8");
100
97
  await rename(tempPath, path);
101
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
+ }
102
108
  function sha256(content) {
103
109
  return `sha256:${createHash("sha256").update(content).digest("hex")}`;
104
110
  }
@@ -116,6 +122,9 @@ function assertPathWithinProjectRoot(projectRoot, file) {
116
122
  }
117
123
 
118
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";
119
128
  async function readHumanLock(projectRoot) {
120
129
  const document = await readHumanLockDocument(projectRoot);
121
130
  return await Promise.all(
@@ -134,7 +143,7 @@ async function readHumanLockEntry(projectRoot, file) {
134
143
  return entries.find((entry) => entry.file === file) ?? null;
135
144
  }
136
145
  async function readHumanLockDocument(projectRoot) {
137
- const humanLockPath = join(projectRoot, FABRIC_DIR, HUMAN_LOCK_FILE);
146
+ const humanLockPath = join2(projectRoot, FABRIC_DIR, HUMAN_LOCK_FILE);
138
147
  const raw = await readFile(humanLockPath, "utf8");
139
148
  const parsed = JSON.parse(raw);
140
149
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
@@ -177,7 +186,7 @@ import { detectFramework } from "@fenglimg/fabric-shared/node";
177
186
 
178
187
  // src/meta-reader.ts
179
188
  import { readFile as readFile2 } from "fs/promises";
180
- import { join as join2 } from "path";
189
+ import { join as join3 } from "path";
181
190
  import { agentsMetaSchema } from "@fenglimg/fabric-shared";
182
191
  import { agentsMetaNodeSchema, agentsMetaSchema as agentsMetaSchema2 } from "@fenglimg/fabric-shared";
183
192
  var AgentsMetaFileMissingError = class extends Error {
@@ -200,7 +209,7 @@ var AgentsMetaInvalidError = class extends Error {
200
209
  code = "FABRIC_META_INVALID";
201
210
  };
202
211
  function getAgentsMetaPath(projectRoot) {
203
- return join2(projectRoot, ".fabric", "agents.meta.json");
212
+ return join3(projectRoot, ".fabric", "agents.meta.json");
204
213
  }
205
214
  function resolveProjectRoot() {
206
215
  return process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
@@ -231,8 +240,8 @@ async function readAgentsMeta(projectRoot) {
231
240
  }
232
241
 
233
242
  // src/services/audit-log.ts
234
- import { appendFile, mkdir, open, stat } from "fs/promises";
235
- import { isAbsolute, join as join3, posix, relative, resolve as resolve2 } from "path";
243
+ import { appendFile, mkdir as mkdir2, open, stat } from "fs/promises";
244
+ import { isAbsolute, join as join4, posix, relative, resolve as resolve2 } from "path";
236
245
  var AUDIT_LOG_FILE = `${FABRIC_DIR}/audit.jsonl`;
237
246
  var DEFAULT_AUDIT_WINDOW_MS = 5 * 60 * 1e3;
238
247
  async function appendGetRulesAuditEvent(projectRoot, input) {
@@ -246,6 +255,25 @@ async function appendGetRulesAuditEvent(projectRoot, input) {
246
255
  await appendAuditLogEntries(projectRoot, [entry]);
247
256
  return entry;
248
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
+ }
249
277
  async function appendEditIntentAuditEvents(projectRoot, input) {
250
278
  const ts = input.ts ?? Date.now();
251
279
  const windowMs = input.window_ms ?? DEFAULT_AUDIT_WINDOW_MS;
@@ -285,7 +313,7 @@ async function readAuditLog(projectRoot, opts) {
285
313
  return readAuditLogWindowed(projectRoot, opts.ts, opts.windowMs);
286
314
  }
287
315
  async function readAuditLogFull(projectRoot) {
288
- const auditPath = join3(projectRoot, AUDIT_LOG_FILE);
316
+ const auditPath = join4(projectRoot, AUDIT_LOG_FILE);
289
317
  let raw;
290
318
  try {
291
319
  const fileStat = await stat(auditPath);
@@ -306,7 +334,7 @@ async function readAuditLogFull(projectRoot) {
306
334
  return parseAuditLogText(raw);
307
335
  }
308
336
  async function readAuditLogWindowed(projectRoot, ts, windowMs) {
309
- const auditPath = join3(projectRoot, AUDIT_LOG_FILE);
337
+ const auditPath = join4(projectRoot, AUDIT_LOG_FILE);
310
338
  let fileSize;
311
339
  try {
312
340
  const fileStat = await stat(auditPath);
@@ -394,9 +422,9 @@ function isGetRulesAuditEntry(entry) {
394
422
  return entry.event === "get_rules";
395
423
  }
396
424
  async function appendAuditLogEntries(projectRoot, entries) {
397
- const auditPath = join3(projectRoot, AUDIT_LOG_FILE);
398
- const auditDir = join3(projectRoot, FABRIC_DIR);
399
- await mkdir(auditDir, { recursive: true });
425
+ const auditPath = join4(projectRoot, AUDIT_LOG_FILE);
426
+ const auditDir = join4(projectRoot, FABRIC_DIR);
427
+ await mkdir2(auditDir, { recursive: true });
400
428
  await appendFile(auditPath, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}
401
429
  `, "utf8");
402
430
  }
@@ -428,22 +456,61 @@ function parseAuditLogLine(line) {
428
456
  window_ms: parsed.window_ms
429
457
  };
430
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
+ }
431
476
  return null;
432
477
  } catch {
433
478
  return null;
434
479
  }
435
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
+ }
436
490
 
437
491
  // src/services/read-ledger.ts
438
492
  import { randomUUID } from "crypto";
439
- import { appendFile as appendFile2, readFile as readFile3 } from "fs/promises";
440
- import { join as join4 } from "path";
493
+ import { access, appendFile as appendFile2, copyFile, readFile as readFile3, rm } from "fs/promises";
441
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
+ }
442
509
  async function readLedger(projectRoot, options = {}) {
443
- const ledgerPath = join4(projectRoot, LEDGER_FILE);
510
+ const { readPath } = await resolveLedgerPaths(projectRoot);
444
511
  let raw;
445
512
  try {
446
- raw = await readFile3(ledgerPath, "utf8");
513
+ raw = await readFile3(readPath, "utf8");
447
514
  } catch (error) {
448
515
  if (isNodeError(error) && error.code === "ENOENT") {
449
516
  return [];
@@ -453,15 +520,40 @@ async function readLedger(projectRoot, options = {}) {
453
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);
454
521
  }
455
522
  async function appendLedgerEntry(projectRoot, entry) {
456
- const ledgerPath = join4(projectRoot, LEDGER_FILE);
523
+ const ledgerPath = getLedgerPath(projectRoot);
457
524
  const nextEntry = ledgerEntrySchema.parse({
458
525
  ...entry,
459
526
  id: entry.id ?? `ledger:${randomUUID()}`
460
527
  });
528
+ await ensureParentDirectory(ledgerPath);
461
529
  await appendFile2(ledgerPath, `${JSON.stringify(nextEntry)}
462
530
  `, "utf8");
463
531
  return nextEntry;
464
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
+ }
465
557
  function parseLedgerLine(line, index) {
466
558
  try {
467
559
  const parsed = JSON.parse(line);
@@ -483,6 +575,17 @@ function parseLedgerLine(line, index) {
483
575
  function createDerivedId(index, line) {
484
576
  return `ledger:${index + 1}:${sha256(line).slice("sha256:".length)}`;
485
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
+ }
486
589
 
487
590
  // src/services/doctor.ts
488
591
  var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
@@ -538,6 +641,9 @@ async function runDoctorReport(target) {
538
641
  lastLedgerEntryTs: ledgerSnapshot.lastEntryTs,
539
642
  lastLedgerEntryAgeMs: ledgerSnapshot.lastEntryAgeMs,
540
643
  metaRevision: metaSnapshot.revision,
644
+ ledgerPath: ledgerSnapshot.primaryPath,
645
+ legacyLedgerPath: ledgerSnapshot.legacyPath,
646
+ legacyLedgerDetected: ledgerSnapshot.usingLegacy,
541
647
  audit: auditReport.skipped ? null : {
542
648
  enabled: true,
543
649
  mode: auditReport.mode,
@@ -549,6 +655,17 @@ async function runDoctorReport(target) {
549
655
  audit: auditReport.skipped ? null : auditReport
550
656
  };
551
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
+ }
552
669
  async function runDoctorAuditReport(target, options = {}) {
553
670
  const projectRoot = normalizeTarget(target);
554
671
  const mode = options.mode ?? readDoctorAuditMode(projectRoot);
@@ -567,13 +684,13 @@ async function runDoctorAuditReport(target, options = {}) {
567
684
  readLedger(projectRoot, { source: "ai" }),
568
685
  readAuditLog(projectRoot)
569
686
  ]);
570
- const getRulesEntries = auditEntries.filter(
571
- (entry) => entry.event === "get_rules"
687
+ const ruleAccessEntries = auditEntries.filter(
688
+ (entry) => entry.event === "get_rules" || entry.event === "rule_selection"
572
689
  );
573
690
  const { checkedPathCount, violations } = collectAuditViolations(
574
691
  projectRoot,
575
692
  ledgerEntries,
576
- getRulesEntries,
693
+ ruleAccessEntries,
577
694
  windowMs
578
695
  );
579
696
  return {
@@ -647,6 +764,15 @@ function createMetaRevisionCheck(snapshot) {
647
764
  message: `agents.meta.json revision ${snapshot.revision} is stale: ${parts.join(" \xB7 ")}.`
648
765
  };
649
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
+ }
650
776
  return {
651
777
  name: "Meta revision",
652
778
  status: "ok",
@@ -675,6 +801,13 @@ function createProtectedPathsCheck(snapshot) {
675
801
  };
676
802
  }
677
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
+ }
678
811
  if (snapshot.lastEntryTs === null || snapshot.lastEntryAgeMs === null) {
679
812
  return {
680
813
  name: "Intent ledger",
@@ -714,13 +847,13 @@ function createAuditCheck(report) {
714
847
  return {
715
848
  name: "Rules fetch audit",
716
849
  status: report.mode === "strict" ? "error" : "warn",
717
- message: `${report.violationCount} edit path${report.violationCount === 1 ? "" : "s"} lack a preceding fab_get_rules call within ${formatDuration(report.windowMs)}.`
850
+ message: `${report.violationCount} edit path${report.violationCount === 1 ? "" : "s"} lack a preceding rule_selection or get_rules event within ${formatDuration(report.windowMs)}.`
718
851
  };
719
852
  }
720
853
  return {
721
854
  name: "Rules fetch audit",
722
855
  status: "ok",
723
- message: `All ${report.checkedPathCount} audited edit path${report.checkedPathCount === 1 ? "" : "s"} have a preceding fab_get_rules call within ${formatDuration(report.windowMs)}.`
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)}.`
724
857
  };
725
858
  }
726
859
  async function readSavedForensic(projectRoot) {
@@ -756,8 +889,9 @@ async function inspectMetaRevision(projectRoot) {
756
889
  const meta = await readAgentsMeta(projectRoot);
757
890
  const entries = Object.entries(meta.nodes).sort(([left], [right]) => left.localeCompare(right));
758
891
  const missingFiles = [];
892
+ const derivedIdentityFiles = [];
759
893
  let driftCount = 0;
760
- const revisionSource = entries.map(([, node]) => {
894
+ const revisionSource = entries.map(([id, node]) => {
761
895
  const absolutePath = join5(projectRoot, node.file);
762
896
  if (!existsSync(absolutePath)) {
763
897
  missingFiles.push(node.file);
@@ -768,15 +902,19 @@ async function inspectMetaRevision(projectRoot) {
768
902
  if (actualHash !== node.hash) {
769
903
  driftCount += 1;
770
904
  }
771
- return actualHash;
772
- }).join("");
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");
773
910
  const revision = sha2562(revisionSource);
774
911
  return {
775
912
  present: true,
776
913
  revision: meta.revision,
777
914
  nodeCount: entries.length,
778
915
  driftCount: revision === meta.revision ? driftCount : Math.max(driftCount, 1),
779
- missingFiles
916
+ missingFiles,
917
+ derivedIdentityFiles
780
918
  };
781
919
  } catch (error) {
782
920
  return {
@@ -785,6 +923,7 @@ async function inspectMetaRevision(projectRoot) {
785
923
  nodeCount: 0,
786
924
  driftCount: 0,
787
925
  missingFiles: [],
926
+ derivedIdentityFiles: [],
788
927
  unexpectedError: error instanceof Error ? error.message : String(error)
789
928
  };
790
929
  }
@@ -815,6 +954,7 @@ async function inspectHumanLock(projectRoot) {
815
954
  }
816
955
  }
817
956
  async function inspectLedger(projectRoot) {
957
+ const paths = await resolveLedgerPaths(projectRoot);
818
958
  const entries = await readLedger(projectRoot);
819
959
  const lastEntry = entries.reduce(
820
960
  (latest, entry) => latest === null || entry.ts > latest ? entry.ts : latest,
@@ -823,16 +963,19 @@ async function inspectLedger(projectRoot) {
823
963
  return {
824
964
  count: entries.length,
825
965
  lastEntryTs: lastEntry,
826
- lastEntryAgeMs: lastEntry === null ? null : Math.max(Date.now() - lastEntry, 0)
966
+ lastEntryAgeMs: lastEntry === null ? null : Math.max(Date.now() - lastEntry, 0),
967
+ primaryPath: paths.primaryPath,
968
+ legacyPath: paths.legacyPath,
969
+ usingLegacy: paths.usingLegacy
827
970
  };
828
971
  }
829
- function collectAuditViolations(projectRoot, ledgerEntries, getRulesEntries, windowMs) {
972
+ function collectAuditViolations(projectRoot, ledgerEntries, ruleAccessEntries, windowMs) {
830
973
  let checkedPathCount = 0;
831
974
  const violations = [];
832
975
  for (const entry of ledgerEntries) {
833
976
  for (const affectedPath of entry.affected_paths) {
834
977
  const normalizedPath = normalizeAuditPath(projectRoot, affectedPath);
835
- const matched = findPrecedingGetRulesEvent(getRulesEntries, normalizedPath, entry.ts, windowMs);
978
+ const matched = findPrecedingRuleAccessEvent(ruleAccessEntries, normalizedPath, entry.ts, windowMs);
836
979
  checkedPathCount += 1;
837
980
  if (matched !== null) {
838
981
  continue;
@@ -841,7 +984,7 @@ function collectAuditViolations(projectRoot, ledgerEntries, getRulesEntries, win
841
984
  editTs: entry.ts,
842
985
  entryId: entry.id,
843
986
  intent: entry.intent,
844
- lastGetRulesTs: findLatestGetRulesTs(getRulesEntries, normalizedPath, entry.ts),
987
+ lastRuleAccessTs: findLatestRuleAccessTs(ruleAccessEntries, normalizedPath, entry.ts),
845
988
  path: normalizedPath
846
989
  });
847
990
  }
@@ -851,16 +994,49 @@ function collectAuditViolations(projectRoot, ledgerEntries, getRulesEntries, win
851
994
  violations
852
995
  };
853
996
  }
854
- function findLatestGetRulesTs(entries, path, ts) {
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) {
855
1024
  let latest = null;
856
1025
  for (const entry of entries) {
857
- if (entry.path !== path || entry.ts > ts) {
1026
+ const matchesPath = entry.event === "rule_selection" ? entry.path === path || entry.target_paths.includes(path) : entry.path === path;
1027
+ if (!matchesPath || entry.ts > ts) {
858
1028
  continue;
859
1029
  }
860
1030
  latest = latest === null || entry.ts > latest ? entry.ts : latest;
861
1031
  }
862
1032
  return latest;
863
1033
  }
1034
+ function isGetRulesAuditEntry2(entry) {
1035
+ return entry.event === "get_rules";
1036
+ }
1037
+ function isRuleSelectionAuditEntry(entry) {
1038
+ return entry.event === "rule_selection";
1039
+ }
864
1040
  function readDoctorAuditMode(projectRoot) {
865
1041
  const configPath = join5(projectRoot, "fabric.config.json");
866
1042
  try {
@@ -1106,56 +1282,76 @@ async function loadGetRulesContext(projectRoot) {
1106
1282
  return context;
1107
1283
  }
1108
1284
  async function resolveRulesForPath(projectRoot, context, path, options = {}) {
1109
- const { rules: loadedRules, stubs } = await loadRulesForPath(projectRoot, context.meta, path);
1110
- const { L1, L2 } = partitionRulesByLevel(loadedRules, options.dedupeByPath ?? false);
1111
- return {
1112
- L0: context.l0Content,
1113
- L1,
1114
- L2,
1115
- human_locked_nearby: context.humanLockedNearby,
1116
- description_stubs: stubs.length > 0 ? dedupeDescriptionStubsByPath(stubs) : void 0
1117
- };
1285
+ const matchedNodes = matchRuleNodes(context.meta, path);
1286
+ const loaded = await loadMatchedRules(projectRoot, matchedNodes);
1287
+ return buildRulesPayload(context, loaded, options);
1118
1288
  }
1119
1289
  function normalizeRulesPath(value) {
1120
1290
  return value.replaceAll("\\", "/");
1121
1291
  }
1122
- function classifyNode(nodeId) {
1123
- if (nodeId.startsWith("L1/")) {
1124
- return "L1";
1125
- }
1126
- if (nodeId.startsWith("L2/")) {
1127
- return "L2";
1128
- }
1129
- return null;
1130
- }
1131
- async function loadRulesForPath(projectRoot, meta, path) {
1292
+ function matchRuleNodes(meta, path) {
1132
1293
  const requestedPath = normalizeRulesPath(path);
1133
- const matchedNodes = Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
1294
+ return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
1134
1295
  const [leftId, leftNode] = left;
1135
1296
  const [rightId, rightNode] = right;
1136
1297
  const priorityDelta = PRIORITY_ORDER[leftNode.priority] - PRIORITY_ORDER[rightNode.priority];
1137
1298
  return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
1138
- });
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()) {
1139
1308
  const rules = [];
1140
1309
  const stubs = [];
1141
- for (const [nodeId, node] of matchedNodes) {
1142
- if (node.activation?.tier === "description") {
1310
+ for (const matchedNode of matchedNodes) {
1311
+ if (matchedNode.level === null) {
1312
+ continue;
1313
+ }
1314
+ if (matchedNode.node.activation?.tier === "description") {
1143
1315
  stubs.push({
1144
- path: node.file,
1145
- description: node.activation.description ?? ""
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 ?? ""
1146
1321
  });
1147
1322
  continue;
1148
1323
  }
1149
1324
  rules.push({
1150
- level: classifyNode(nodeId),
1325
+ level: matchedNode.level,
1326
+ stable_id: matchedNode.stable_id,
1327
+ identity_source: matchedNode.identity_source,
1151
1328
  entry: {
1152
- path: node.file,
1153
- content: await readFile5(join6(projectRoot, node.file), "utf8")
1329
+ path: matchedNode.node.file,
1330
+ content: await readRuleContent(projectRoot, matchedNode.node.file, fileContentCache)
1154
1331
  }
1155
1332
  });
1156
1333
  }
1157
1334
  return { rules, stubs };
1158
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
+ }
1159
1355
  function partitionRulesByLevel(loadedRules, dedupeByPath) {
1160
1356
  const l1 = [];
1161
1357
  const l2 = [];
@@ -1204,6 +1400,21 @@ function dedupeDescriptionStubsByPath(stubs) {
1204
1400
  return true;
1205
1401
  });
1206
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
+ }
1207
1418
 
1208
1419
  export {
1209
1420
  AGENTS_MD_RESOURCE_URI,
@@ -1213,18 +1424,24 @@ export {
1213
1424
  resolveProjectRoot,
1214
1425
  readAgentsMeta,
1215
1426
  FABRIC_DIR,
1427
+ LEDGER_PATH,
1428
+ LEGACY_LEDGER_PATH,
1216
1429
  atomicWriteText,
1430
+ getLedgerPath,
1431
+ getLegacyLedgerPath,
1432
+ ensureParentDirectory,
1217
1433
  sha256,
1434
+ appendRuleSelectionAuditEvent,
1218
1435
  appendEditIntentAuditEvents,
1436
+ resolveLedgerPaths,
1219
1437
  readLedger,
1220
1438
  appendLedgerEntry,
1221
1439
  readHumanLock,
1222
1440
  readHumanLockEntry,
1223
1441
  getRules,
1224
- loadGetRulesContext,
1225
- resolveRulesForPath,
1226
1442
  normalizeRulesPath,
1227
1443
  runDoctorReport,
1444
+ runDoctorFix,
1228
1445
  runDoctorAuditReport,
1229
1446
  approveHumanLock
1230
1447
  };