@fenglimg/fabric-server 1.5.0 → 1.5.2

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) {
@@ -285,7 +294,7 @@ async function readAuditLog(projectRoot, opts) {
285
294
  return readAuditLogWindowed(projectRoot, opts.ts, opts.windowMs);
286
295
  }
287
296
  async function readAuditLogFull(projectRoot) {
288
- const auditPath = join3(projectRoot, AUDIT_LOG_FILE);
297
+ const auditPath = join4(projectRoot, AUDIT_LOG_FILE);
289
298
  let raw;
290
299
  try {
291
300
  const fileStat = await stat(auditPath);
@@ -306,7 +315,7 @@ async function readAuditLogFull(projectRoot) {
306
315
  return parseAuditLogText(raw);
307
316
  }
308
317
  async function readAuditLogWindowed(projectRoot, ts, windowMs) {
309
- const auditPath = join3(projectRoot, AUDIT_LOG_FILE);
318
+ const auditPath = join4(projectRoot, AUDIT_LOG_FILE);
310
319
  let fileSize;
311
320
  try {
312
321
  const fileStat = await stat(auditPath);
@@ -394,9 +403,9 @@ function isGetRulesAuditEntry(entry) {
394
403
  return entry.event === "get_rules";
395
404
  }
396
405
  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 });
406
+ const auditPath = join4(projectRoot, AUDIT_LOG_FILE);
407
+ const auditDir = join4(projectRoot, FABRIC_DIR);
408
+ await mkdir2(auditDir, { recursive: true });
400
409
  await appendFile(auditPath, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}
401
410
  `, "utf8");
402
411
  }
@@ -436,14 +445,27 @@ function parseAuditLogLine(line) {
436
445
 
437
446
  // src/services/read-ledger.ts
438
447
  import { randomUUID } from "crypto";
439
- import { appendFile as appendFile2, readFile as readFile3 } from "fs/promises";
440
- import { join as join4 } from "path";
448
+ import { access, appendFile as appendFile2, copyFile, readFile as readFile3, rm } from "fs/promises";
441
449
  import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
450
+ async function resolveLedgerPaths(projectRoot) {
451
+ const primaryPath = getLedgerPath(projectRoot);
452
+ const legacyPath = getLegacyLedgerPath(projectRoot);
453
+ const [primaryExists, legacyExists] = await Promise.all([
454
+ pathExists(primaryPath),
455
+ pathExists(legacyPath)
456
+ ]);
457
+ return {
458
+ primaryPath,
459
+ legacyPath,
460
+ readPath: primaryExists ? primaryPath : legacyPath,
461
+ usingLegacy: !primaryExists && legacyExists
462
+ };
463
+ }
442
464
  async function readLedger(projectRoot, options = {}) {
443
- const ledgerPath = join4(projectRoot, LEDGER_FILE);
465
+ const { readPath } = await resolveLedgerPaths(projectRoot);
444
466
  let raw;
445
467
  try {
446
- raw = await readFile3(ledgerPath, "utf8");
468
+ raw = await readFile3(readPath, "utf8");
447
469
  } catch (error) {
448
470
  if (isNodeError(error) && error.code === "ENOENT") {
449
471
  return [];
@@ -453,15 +475,40 @@ async function readLedger(projectRoot, options = {}) {
453
475
  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
476
  }
455
477
  async function appendLedgerEntry(projectRoot, entry) {
456
- const ledgerPath = join4(projectRoot, LEDGER_FILE);
478
+ const ledgerPath = getLedgerPath(projectRoot);
457
479
  const nextEntry = ledgerEntrySchema.parse({
458
480
  ...entry,
459
481
  id: entry.id ?? `ledger:${randomUUID()}`
460
482
  });
483
+ await ensureParentDirectory(ledgerPath);
461
484
  await appendFile2(ledgerPath, `${JSON.stringify(nextEntry)}
462
485
  `, "utf8");
463
486
  return nextEntry;
464
487
  }
488
+ async function migrateLegacyLedger(projectRoot) {
489
+ const { primaryPath, legacyPath } = await resolveLedgerPaths(projectRoot);
490
+ const [primaryExists, legacyExists] = await Promise.all([
491
+ pathExists(primaryPath),
492
+ pathExists(legacyPath)
493
+ ]);
494
+ if (!legacyExists) {
495
+ return {
496
+ migrated: false,
497
+ from: null,
498
+ to: primaryPath
499
+ };
500
+ }
501
+ if (!primaryExists) {
502
+ await ensureParentDirectory(primaryPath);
503
+ await copyFile(legacyPath, primaryPath);
504
+ }
505
+ await rm(legacyPath, { force: true });
506
+ return {
507
+ migrated: true,
508
+ from: legacyPath,
509
+ to: primaryPath
510
+ };
511
+ }
465
512
  function parseLedgerLine(line, index) {
466
513
  try {
467
514
  const parsed = JSON.parse(line);
@@ -483,6 +530,17 @@ function parseLedgerLine(line, index) {
483
530
  function createDerivedId(index, line) {
484
531
  return `ledger:${index + 1}:${sha256(line).slice("sha256:".length)}`;
485
532
  }
533
+ async function pathExists(path) {
534
+ try {
535
+ await access(path);
536
+ return true;
537
+ } catch (error) {
538
+ if (isNodeError(error) && error.code === "ENOENT") {
539
+ return false;
540
+ }
541
+ throw error;
542
+ }
543
+ }
486
544
 
487
545
  // src/services/doctor.ts
488
546
  var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
@@ -538,6 +596,9 @@ async function runDoctorReport(target) {
538
596
  lastLedgerEntryTs: ledgerSnapshot.lastEntryTs,
539
597
  lastLedgerEntryAgeMs: ledgerSnapshot.lastEntryAgeMs,
540
598
  metaRevision: metaSnapshot.revision,
599
+ ledgerPath: ledgerSnapshot.primaryPath,
600
+ legacyLedgerPath: ledgerSnapshot.legacyPath,
601
+ legacyLedgerDetected: ledgerSnapshot.usingLegacy,
541
602
  audit: auditReport.skipped ? null : {
542
603
  enabled: true,
543
604
  mode: auditReport.mode,
@@ -549,6 +610,17 @@ async function runDoctorReport(target) {
549
610
  audit: auditReport.skipped ? null : auditReport
550
611
  };
551
612
  }
613
+ async function runDoctorFix(target) {
614
+ const projectRoot = normalizeTarget(target);
615
+ const migration = await migrateLegacyLedger(projectRoot);
616
+ const report = await runDoctorReport(projectRoot);
617
+ return {
618
+ changed: migration.migrated,
619
+ migratedLedger: migration.migrated,
620
+ message: migration.migrated ? `Migrated legacy ledger from ${migration.from} to ${migration.to}.` : `No legacy ledger migration needed. Canonical ledger path: ${migration.to}.`,
621
+ report
622
+ };
623
+ }
552
624
  async function runDoctorAuditReport(target, options = {}) {
553
625
  const projectRoot = normalizeTarget(target);
554
626
  const mode = options.mode ?? readDoctorAuditMode(projectRoot);
@@ -647,6 +719,15 @@ function createMetaRevisionCheck(snapshot) {
647
719
  message: `agents.meta.json revision ${snapshot.revision} is stale: ${parts.join(" \xB7 ")}.`
648
720
  };
649
721
  }
722
+ if (snapshot.derivedIdentityFiles.length > 0) {
723
+ const [firstFile] = snapshot.derivedIdentityFiles;
724
+ const suffix = snapshot.derivedIdentityFiles.length > 1 ? ` (+${snapshot.derivedIdentityFiles.length - 1} more)` : "";
725
+ return {
726
+ name: "Meta revision",
727
+ status: "warn",
728
+ 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}).`
729
+ };
730
+ }
650
731
  return {
651
732
  name: "Meta revision",
652
733
  status: "ok",
@@ -675,6 +756,13 @@ function createProtectedPathsCheck(snapshot) {
675
756
  };
676
757
  }
677
758
  function createLedgerCheck(snapshot) {
759
+ if (snapshot.usingLegacy) {
760
+ return {
761
+ name: "Intent ledger",
762
+ status: "warn",
763
+ message: `Legacy ledger path detected at ${snapshot.legacyPath}. Fabric now reads ${snapshot.primaryPath} by default; run fab doctor --fix to migrate.`
764
+ };
765
+ }
678
766
  if (snapshot.lastEntryTs === null || snapshot.lastEntryAgeMs === null) {
679
767
  return {
680
768
  name: "Intent ledger",
@@ -756,8 +844,9 @@ async function inspectMetaRevision(projectRoot) {
756
844
  const meta = await readAgentsMeta(projectRoot);
757
845
  const entries = Object.entries(meta.nodes).sort(([left], [right]) => left.localeCompare(right));
758
846
  const missingFiles = [];
847
+ const derivedIdentityFiles = [];
759
848
  let driftCount = 0;
760
- const revisionSource = entries.map(([, node]) => {
849
+ const revisionSource = entries.map(([id, node]) => {
761
850
  const absolutePath = join5(projectRoot, node.file);
762
851
  if (!existsSync(absolutePath)) {
763
852
  missingFiles.push(node.file);
@@ -768,15 +857,19 @@ async function inspectMetaRevision(projectRoot) {
768
857
  if (actualHash !== node.hash) {
769
858
  driftCount += 1;
770
859
  }
771
- return actualHash;
772
- }).join("");
860
+ if (node.file !== ".fabric/bootstrap/README.md" && node.identity_source !== "declared") {
861
+ derivedIdentityFiles.push(node.file);
862
+ }
863
+ return [id, actualHash, node.stable_id ?? "", node.identity_source ?? ""].join("|");
864
+ }).join("\n");
773
865
  const revision = sha2562(revisionSource);
774
866
  return {
775
867
  present: true,
776
868
  revision: meta.revision,
777
869
  nodeCount: entries.length,
778
870
  driftCount: revision === meta.revision ? driftCount : Math.max(driftCount, 1),
779
- missingFiles
871
+ missingFiles,
872
+ derivedIdentityFiles
780
873
  };
781
874
  } catch (error) {
782
875
  return {
@@ -785,6 +878,7 @@ async function inspectMetaRevision(projectRoot) {
785
878
  nodeCount: 0,
786
879
  driftCount: 0,
787
880
  missingFiles: [],
881
+ derivedIdentityFiles: [],
788
882
  unexpectedError: error instanceof Error ? error.message : String(error)
789
883
  };
790
884
  }
@@ -815,6 +909,7 @@ async function inspectHumanLock(projectRoot) {
815
909
  }
816
910
  }
817
911
  async function inspectLedger(projectRoot) {
912
+ const paths = await resolveLedgerPaths(projectRoot);
818
913
  const entries = await readLedger(projectRoot);
819
914
  const lastEntry = entries.reduce(
820
915
  (latest, entry) => latest === null || entry.ts > latest ? entry.ts : latest,
@@ -823,7 +918,10 @@ async function inspectLedger(projectRoot) {
823
918
  return {
824
919
  count: entries.length,
825
920
  lastEntryTs: lastEntry,
826
- lastEntryAgeMs: lastEntry === null ? null : Math.max(Date.now() - lastEntry, 0)
921
+ lastEntryAgeMs: lastEntry === null ? null : Math.max(Date.now() - lastEntry, 0),
922
+ primaryPath: paths.primaryPath,
923
+ legacyPath: paths.legacyPath,
924
+ usingLegacy: paths.usingLegacy
827
925
  };
828
926
  }
829
927
  function collectAuditViolations(projectRoot, ledgerEntries, getRulesEntries, windowMs) {
@@ -1106,56 +1204,76 @@ async function loadGetRulesContext(projectRoot) {
1106
1204
  return context;
1107
1205
  }
1108
1206
  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
- };
1207
+ const matchedNodes = matchRuleNodes(context.meta, path);
1208
+ const loaded = await loadMatchedRules(projectRoot, matchedNodes);
1209
+ return buildRulesPayload(context, loaded, options);
1118
1210
  }
1119
1211
  function normalizeRulesPath(value) {
1120
1212
  return value.replaceAll("\\", "/");
1121
1213
  }
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) {
1214
+ function matchRuleNodes(meta, path) {
1132
1215
  const requestedPath = normalizeRulesPath(path);
1133
- const matchedNodes = Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
1216
+ return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
1134
1217
  const [leftId, leftNode] = left;
1135
1218
  const [rightId, rightNode] = right;
1136
1219
  const priorityDelta = PRIORITY_ORDER[leftNode.priority] - PRIORITY_ORDER[rightNode.priority];
1137
1220
  return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
1138
- });
1221
+ }).map(([nodeId, node]) => ({
1222
+ node_id: nodeId,
1223
+ level: classifyNode(nodeId, node),
1224
+ stable_id: node.stable_id ?? nodeId,
1225
+ identity_source: node.identity_source ?? "derived",
1226
+ node
1227
+ }));
1228
+ }
1229
+ async function loadMatchedRules(projectRoot, matchedNodes, fileContentCache = /* @__PURE__ */ new Map()) {
1139
1230
  const rules = [];
1140
1231
  const stubs = [];
1141
- for (const [nodeId, node] of matchedNodes) {
1142
- if (node.activation?.tier === "description") {
1232
+ for (const matchedNode of matchedNodes) {
1233
+ if (matchedNode.level === null) {
1234
+ continue;
1235
+ }
1236
+ if (matchedNode.node.activation?.tier === "description") {
1143
1237
  stubs.push({
1144
- path: node.file,
1145
- description: node.activation.description ?? ""
1238
+ stable_id: matchedNode.stable_id,
1239
+ identity_source: matchedNode.identity_source,
1240
+ level: matchedNode.level,
1241
+ path: matchedNode.node.file,
1242
+ description: matchedNode.node.activation.description ?? ""
1146
1243
  });
1147
1244
  continue;
1148
1245
  }
1149
1246
  rules.push({
1150
- level: classifyNode(nodeId),
1247
+ level: matchedNode.level,
1248
+ stable_id: matchedNode.stable_id,
1249
+ identity_source: matchedNode.identity_source,
1151
1250
  entry: {
1152
- path: node.file,
1153
- content: await readFile5(join6(projectRoot, node.file), "utf8")
1251
+ path: matchedNode.node.file,
1252
+ content: await readRuleContent(projectRoot, matchedNode.node.file, fileContentCache)
1154
1253
  }
1155
1254
  });
1156
1255
  }
1157
1256
  return { rules, stubs };
1158
1257
  }
1258
+ function buildRulesPayload(context, loaded, options = {}) {
1259
+ const { L1, L2 } = partitionRulesByLevel(loaded.rules, options.dedupeByPath ?? false);
1260
+ return {
1261
+ L0: context.l0Content,
1262
+ L1,
1263
+ L2,
1264
+ human_locked_nearby: context.humanLockedNearby,
1265
+ description_stubs: loaded.stubs.length > 0 ? dedupeDescriptionStubsByPath(loaded.stubs).map(toDescriptionStub) : void 0
1266
+ };
1267
+ }
1268
+ function classifyNode(nodeId, node) {
1269
+ if (nodeId.startsWith("L1/")) {
1270
+ return "L1";
1271
+ }
1272
+ if (nodeId.startsWith("L2/")) {
1273
+ return "L2";
1274
+ }
1275
+ return node.layer === "L0" ? null : node.layer;
1276
+ }
1159
1277
  function partitionRulesByLevel(loadedRules, dedupeByPath) {
1160
1278
  const l1 = [];
1161
1279
  const l2 = [];
@@ -1204,6 +1322,21 @@ function dedupeDescriptionStubsByPath(stubs) {
1204
1322
  return true;
1205
1323
  });
1206
1324
  }
1325
+ function toDescriptionStub(stub) {
1326
+ return {
1327
+ path: stub.path,
1328
+ description: stub.description
1329
+ };
1330
+ }
1331
+ async function readRuleContent(projectRoot, file, fileContentCache) {
1332
+ const cached = fileContentCache.get(file);
1333
+ if (cached !== void 0) {
1334
+ return await cached;
1335
+ }
1336
+ const pending = readFile5(join6(projectRoot, file), "utf8");
1337
+ fileContentCache.set(file, pending);
1338
+ return await pending;
1339
+ }
1207
1340
 
1208
1341
  export {
1209
1342
  AGENTS_MD_RESOURCE_URI,
@@ -1213,18 +1346,27 @@ export {
1213
1346
  resolveProjectRoot,
1214
1347
  readAgentsMeta,
1215
1348
  FABRIC_DIR,
1349
+ LEDGER_PATH,
1350
+ LEGACY_LEDGER_PATH,
1216
1351
  atomicWriteText,
1352
+ getLedgerPath,
1353
+ getLegacyLedgerPath,
1354
+ ensureParentDirectory,
1217
1355
  sha256,
1218
1356
  appendEditIntentAuditEvents,
1357
+ resolveLedgerPaths,
1219
1358
  readLedger,
1220
1359
  appendLedgerEntry,
1221
1360
  readHumanLock,
1222
1361
  readHumanLockEntry,
1223
1362
  getRules,
1224
1363
  loadGetRulesContext,
1225
- resolveRulesForPath,
1226
1364
  normalizeRulesPath,
1365
+ matchRuleNodes,
1366
+ loadMatchedRules,
1367
+ buildRulesPayload,
1227
1368
  runDoctorReport,
1369
+ runDoctorFix,
1228
1370
  runDoctorAuditReport,
1229
1371
  approveHumanLock
1230
1372
  };
@@ -2,21 +2,26 @@ import {
2
2
  AGENTS_MD_RESOURCE_URI,
3
3
  AgentsMetaFileMissingError,
4
4
  AgentsMetaInvalidError,
5
+ LEDGER_PATH,
6
+ LEGACY_LEDGER_PATH,
5
7
  appendLedgerEntry,
6
8
  approveHumanLock,
7
9
  contextCache,
10
+ ensureParentDirectory,
11
+ getLedgerPath,
12
+ getLegacyLedgerPath,
8
13
  getRules,
9
14
  readAgentsMeta,
10
15
  readHumanLock,
11
16
  readHumanLockEntry,
12
17
  readLedger,
18
+ resolveLedgerPaths,
13
19
  runDoctorReport
14
- } from "./chunk-E3RZ276F.js";
20
+ } from "./chunk-4G6VFG5N.js";
15
21
 
16
22
  // src/http.ts
17
23
  import { randomUUID } from "crypto";
18
24
  import { appendFile, readFile as readFile2 } from "fs/promises";
19
- import { join as join3 } from "path";
20
25
  import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
21
26
  import {
22
27
  StreamableHTTPServerTransport
@@ -126,8 +131,7 @@ import chokidar from "chokidar";
126
131
  var AGENTS_META_PATH = ".fabric/agents.meta.json";
127
132
  var HUMAN_LOCK_PATH = ".fabric/human-lock.json";
128
133
  var FORENSIC_PATH = ".fabric/forensic.json";
129
- var LEDGER_PATH = ".intent-ledger.jsonl";
130
- var WATCHED_PATHS = [AGENTS_META_PATH, HUMAN_LOCK_PATH, FORENSIC_PATH, LEDGER_PATH];
134
+ var WATCHED_PATHS = [AGENTS_META_PATH, HUMAN_LOCK_PATH, FORENSIC_PATH, LEDGER_PATH, LEGACY_LEDGER_PATH];
131
135
  var CONNECTION_LIMIT = 10;
132
136
  var HEARTBEAT_INTERVAL_MS = 3e4;
133
137
  var WATCH_DEBOUNCE_MS = 75;
@@ -166,6 +170,7 @@ function createEventsHandler(options) {
166
170
  const state = {
167
171
  clients: /* @__PURE__ */ new Set(),
168
172
  pendingTimers: /* @__PURE__ */ new Map(),
173
+ activeLedgerPath: getLedgerPath(projectRoot),
169
174
  ledgerOffset: 0,
170
175
  ledgerRemainder: "",
171
176
  humanLockSnapshot: createEmptyHumanLockSnapshot(),
@@ -243,10 +248,12 @@ async function ensureWatcher(state, projectRoot) {
243
248
  if (state.watcher !== void 0) {
244
249
  return;
245
250
  }
246
- state.ledgerOffset = await readFileSize(join(projectRoot, LEDGER_PATH));
251
+ const ledgerState = await resolveLedgerWatchState(projectRoot);
252
+ state.activeLedgerPath = ledgerState.path;
253
+ state.ledgerOffset = ledgerState.size;
247
254
  state.ledgerRemainder = "";
248
255
  state.humanLockSnapshot = await readHumanLockSnapshot(projectRoot);
249
- const watcher = chokidar.watch([...WATCHED_PATHS], {
256
+ const watcher = chokidar.watch(WATCHED_PATHS, {
250
257
  cwd: projectRoot,
251
258
  ignoreInitial: true,
252
259
  awaitWriteFinish: {
@@ -306,7 +313,7 @@ async function readEventsForFile(state, projectRoot, relativePath) {
306
313
  const event = await readDriftDetectedEvent(projectRoot);
307
314
  return event === null ? [] : [event];
308
315
  }
309
- if (relativePath === LEDGER_PATH) {
316
+ if (relativePath === LEDGER_PATH || relativePath === LEGACY_LEDGER_PATH) {
310
317
  return await readLedgerAppendedEvents(state, projectRoot);
311
318
  }
312
319
  return [];
@@ -370,8 +377,14 @@ async function readHumanLockEvents(state, projectRoot) {
370
377
  return events;
371
378
  }
372
379
  async function readLedgerAppendedEvents(state, projectRoot) {
373
- const ledgerPath = join(projectRoot, LEDGER_PATH);
374
- const nextSize = await readFileSize(ledgerPath);
380
+ const ledgerState = await resolveLedgerWatchState(projectRoot);
381
+ const ledgerPath = ledgerState.path;
382
+ const nextSize = ledgerState.size;
383
+ if (ledgerPath !== state.activeLedgerPath) {
384
+ state.activeLedgerPath = ledgerPath;
385
+ state.ledgerOffset = 0;
386
+ state.ledgerRemainder = "";
387
+ }
375
388
  if (nextSize < state.ledgerOffset) {
376
389
  state.ledgerOffset = 0;
377
390
  state.ledgerRemainder = "";
@@ -394,6 +407,12 @@ async function readLedgerAppendedEvents(state, projectRoot) {
394
407
  await handle.close();
395
408
  }
396
409
  }
410
+ async function resolveLedgerWatchState(projectRoot) {
411
+ const paths = await resolveLedgerPaths(projectRoot);
412
+ const path = paths.usingLegacy ? paths.legacyPath : paths.primaryPath;
413
+ const size = await readFileSize(path);
414
+ return { path, size };
415
+ }
397
416
  function parseLedgerAppendedEvent(line) {
398
417
  try {
399
418
  const parsed = JSON.parse(line);
@@ -1084,12 +1103,13 @@ function hashToken(token) {
1084
1103
 
1085
1104
  // src/http.ts
1086
1105
  var DEFAULT_HOST = "127.0.0.1";
1087
- var LEDGER_FILE = ".intent-ledger.jsonl";
1088
1106
  var NOTIFY_DEBOUNCE_MS = 200;
1089
1107
  var JsonlEventStore = class {
1090
- constructor(ledgerPath) {
1108
+ constructor(projectRoot, ledgerPath) {
1109
+ this.projectRoot = projectRoot;
1091
1110
  this.ledgerPath = ledgerPath;
1092
1111
  }
1112
+ projectRoot;
1093
1113
  ledgerPath;
1094
1114
  async storeEvent(streamId, message) {
1095
1115
  const eventId = randomUUID();
@@ -1099,6 +1119,7 @@ var JsonlEventStore = class {
1099
1119
  streamId,
1100
1120
  message
1101
1121
  };
1122
+ await ensureParentDirectory(this.ledgerPath);
1102
1123
  await appendFile(this.ledgerPath, `${JSON.stringify(entry)}
1103
1124
  `, "utf8");
1104
1125
  return eventId;
@@ -1131,9 +1152,17 @@ var JsonlEventStore = class {
1131
1152
  raw = await readFile2(this.ledgerPath, "utf8");
1132
1153
  } catch (error) {
1133
1154
  if (isNodeError2(error) && error.code === "ENOENT") {
1134
- return [];
1155
+ try {
1156
+ raw = await readFile2(getLegacyLedgerPath(this.projectRoot), "utf8");
1157
+ } catch (legacyError) {
1158
+ if (isNodeError2(legacyError) && legacyError.code === "ENOENT") {
1159
+ return [];
1160
+ }
1161
+ throw legacyError;
1162
+ }
1163
+ } else {
1164
+ throw error;
1135
1165
  }
1136
- throw error;
1137
1166
  }
1138
1167
  return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => parseStoredMcpEvent(line)).filter((event) => event !== null);
1139
1168
  }
@@ -1141,8 +1170,8 @@ var JsonlEventStore = class {
1141
1170
  function createFabricHttpApp(options) {
1142
1171
  const { projectRoot, host = DEFAULT_HOST, authToken, dashboardDistPath, dev } = options;
1143
1172
  const app = createMcpExpressApp({ host });
1144
- const ledgerPath = join3(projectRoot, LEDGER_FILE);
1145
- const eventStore = new JsonlEventStore(ledgerPath);
1173
+ const ledgerPath = getLedgerPath(projectRoot);
1174
+ const eventStore = new JsonlEventStore(projectRoot, ledgerPath);
1146
1175
  const sessions = /* @__PURE__ */ new Map();
1147
1176
  process.env.FABRIC_PROJECT_ROOT = projectRoot;
1148
1177
  const cacheWatcher = chokidar2.watch(
package/dist/index.d.ts CHANGED
@@ -2,6 +2,11 @@ import { Server } from 'node:http';
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { AuditMode, LedgerEntry, HumanLockEntry } from '@fenglimg/fabric-shared';
4
4
 
5
+ declare const LEDGER_PATH = ".fabric/.intent-ledger.jsonl";
6
+ declare const LEGACY_LEDGER_PATH = ".intent-ledger.jsonl";
7
+ declare function getLedgerPath(projectRoot: string): string;
8
+ declare function getLegacyLedgerPath(projectRoot: string): string;
9
+
5
10
  type DoctorStatus = "ok" | "warn" | "error";
6
11
  type DoctorCheck = {
7
12
  name: string;
@@ -25,6 +30,9 @@ type DoctorSummary = {
25
30
  lastLedgerEntryTs: number | null;
26
31
  lastLedgerEntryAgeMs: number | null;
27
32
  metaRevision: string | null;
33
+ ledgerPath: string;
34
+ legacyLedgerPath: string;
35
+ legacyLedgerDetected: boolean;
28
36
  audit: {
29
37
  enabled: boolean;
30
38
  mode: AuditMode;
@@ -39,6 +47,12 @@ type DoctorReport = {
39
47
  summary: DoctorSummary;
40
48
  audit: DoctorAuditReport | null;
41
49
  };
50
+ type DoctorFixReport = {
51
+ changed: boolean;
52
+ migratedLedger: boolean;
53
+ message: string;
54
+ report: DoctorReport;
55
+ };
42
56
  type DoctorAuditViolation = {
43
57
  editTs: number;
44
58
  entryId: string;
@@ -55,6 +69,7 @@ type DoctorAuditReport = {
55
69
  violations: DoctorAuditViolation[];
56
70
  };
57
71
  declare function runDoctorReport(target: string): Promise<DoctorReport>;
72
+ declare function runDoctorFix(target: string): Promise<DoctorFixReport>;
58
73
  declare function runDoctorAuditReport(target: string, options?: {
59
74
  force?: boolean;
60
75
  mode?: AuditMode;
@@ -102,4 +117,4 @@ declare function startHttpServer(options: {
102
117
  dev?: boolean;
103
118
  }): Promise<Server>;
104
119
 
105
- export { AGENTS_MD_RESOURCE_URI, type ApproveHumanLockInput, type ApproveHumanLockResult, type DoctorAuditReport, type DoctorReport, type HumanLockStatus, approveHumanLock, createFabricServer, readHumanLock, readHumanLockEntry, runDoctorAuditReport, runDoctorReport, startHttpServer, startStdioServer };
120
+ export { AGENTS_MD_RESOURCE_URI, type ApproveHumanLockInput, type ApproveHumanLockResult, type DoctorAuditReport, type DoctorFixReport, type DoctorReport, type HumanLockStatus, LEDGER_PATH, LEGACY_LEDGER_PATH, approveHumanLock, createFabricServer, getLedgerPath, getLegacyLedgerPath, readHumanLock, readHumanLockEntry, runDoctorAuditReport, runDoctorFix, runDoctorReport, startHttpServer, startStdioServer };