@fenglimg/fabric-server 1.3.1 → 1.5.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,6 +82,91 @@ 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
+ // src/services/_shared.ts
91
+ import { resolve, sep } from "path";
92
+ import { createHash } from "crypto";
93
+ import { rename, writeFile } from "fs/promises";
94
+ var FABRIC_DIR = ".fabric";
95
+ var HUMAN_LOCK_FILE = "human-lock.json";
96
+ var LEDGER_FILE = ".intent-ledger.jsonl";
97
+ async function atomicWriteText(path, content) {
98
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
99
+ await writeFile(tempPath, content, "utf8");
100
+ await rename(tempPath, path);
101
+ }
102
+ function sha256(content) {
103
+ return `sha256:${createHash("sha256").update(content).digest("hex")}`;
104
+ }
105
+ function isNodeError(error) {
106
+ return error instanceof Error;
107
+ }
108
+ function assertPathWithinProjectRoot(projectRoot, file) {
109
+ const normalizedProjectRoot = resolve(projectRoot);
110
+ const absolutePath = resolve(normalizedProjectRoot, file);
111
+ const rootPrefix = normalizedProjectRoot.endsWith(sep) ? normalizedProjectRoot : `${normalizedProjectRoot}${sep}`;
112
+ if (!absolutePath.startsWith(rootPrefix)) {
113
+ throw new Error(`Path escapes project root: ${file}`);
114
+ }
115
+ return absolutePath;
116
+ }
117
+
118
+ // src/services/read-human-lock.ts
119
+ async function readHumanLock(projectRoot) {
120
+ const document = await readHumanLockDocument(projectRoot);
121
+ return await Promise.all(
122
+ document.locked.map(async (entry) => {
123
+ const currentHash = await hashHumanLockedContent(projectRoot, entry);
124
+ return {
125
+ ...entry,
126
+ drift: currentHash !== entry.hash,
127
+ current_hash: currentHash
128
+ };
129
+ })
130
+ );
131
+ }
132
+ async function readHumanLockEntry(projectRoot, file) {
133
+ const entries = await readHumanLock(projectRoot);
134
+ return entries.find((entry) => entry.file === file) ?? null;
135
+ }
136
+ async function readHumanLockDocument(projectRoot) {
137
+ const humanLockPath = join(projectRoot, FABRIC_DIR, HUMAN_LOCK_FILE);
138
+ const raw = await readFile(humanLockPath, "utf8");
139
+ const parsed = JSON.parse(raw);
140
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
141
+ throw new Error(`Fabric human lock file is invalid: ${humanLockPath}`);
142
+ }
143
+ const rawObject = parsed;
144
+ const lockedResult = humanLockEntrySchema.array().safeParse(rawObject.locked ?? []);
145
+ if (!lockedResult.success) {
146
+ throw new Error(`Fabric human lock file is invalid: ${humanLockPath}`);
147
+ }
148
+ return {
149
+ path: humanLockPath,
150
+ rawObject,
151
+ locked: lockedResult.data
152
+ };
153
+ }
154
+ async function hashHumanLockedContent(projectRoot, entry) {
155
+ const targetPath = assertPathWithinProjectRoot(projectRoot, entry.file);
156
+ let content;
157
+ try {
158
+ content = await readFile(targetPath, "utf8");
159
+ } catch (error) {
160
+ if (isNodeError(error) && error.code === "ENOENT") {
161
+ return "missing";
162
+ }
163
+ throw error;
164
+ }
165
+ const lines = content.split(/\r?\n/);
166
+ const slice = lines.slice(Math.max(entry.start_line - 1, 0), Math.max(entry.end_line, 0)).join("\n");
167
+ return sha256(slice);
168
+ }
169
+
85
170
  // src/services/doctor.ts
86
171
  import { createHash as createHash2 } from "crypto";
87
172
  import { existsSync, readFileSync, readdirSync, statSync } from "fs";
@@ -91,8 +176,8 @@ import { forensicReportSchema } from "@fenglimg/fabric-shared";
91
176
  import { detectFramework } from "@fenglimg/fabric-shared/node";
92
177
 
93
178
  // src/meta-reader.ts
94
- import { readFile } from "fs/promises";
95
- import { join } from "path";
179
+ import { readFile as readFile2 } from "fs/promises";
180
+ import { join as join2 } from "path";
96
181
  import { agentsMetaSchema } from "@fenglimg/fabric-shared";
97
182
  import { agentsMetaNodeSchema, agentsMetaSchema as agentsMetaSchema2 } from "@fenglimg/fabric-shared";
98
183
  var AgentsMetaFileMissingError = class extends Error {
@@ -115,7 +200,7 @@ var AgentsMetaInvalidError = class extends Error {
115
200
  code = "FABRIC_META_INVALID";
116
201
  };
117
202
  function getAgentsMetaPath(projectRoot) {
118
- return join(projectRoot, ".fabric", "agents.meta.json");
203
+ return join2(projectRoot, ".fabric", "agents.meta.json");
119
204
  }
120
205
  function resolveProjectRoot() {
121
206
  return process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
@@ -128,7 +213,7 @@ async function readAgentsMeta(projectRoot) {
128
213
  const metaPath = getAgentsMetaPath(projectRoot);
129
214
  let raw;
130
215
  try {
131
- raw = await readFile(metaPath, "utf8");
216
+ raw = await readFile2(metaPath, "utf8");
132
217
  } catch (error) {
133
218
  if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
134
219
  throw new AgentsMetaFileMissingError(metaPath);
@@ -147,37 +232,7 @@ async function readAgentsMeta(projectRoot) {
147
232
 
148
233
  // src/services/audit-log.ts
149
234
  import { appendFile, mkdir, open, stat } from "fs/promises";
150
- import { isAbsolute, join as join2, posix, relative, resolve as resolve2 } from "path";
151
-
152
- // src/services/_shared.ts
153
- import { resolve, sep } from "path";
154
- import { createHash } from "crypto";
155
- import { rename, writeFile } from "fs/promises";
156
- var FABRIC_DIR = ".fabric";
157
- var HUMAN_LOCK_FILE = "human-lock.json";
158
- var LEDGER_FILE = ".intent-ledger.jsonl";
159
- async function atomicWriteText(path, content) {
160
- const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
161
- await writeFile(tempPath, content, "utf8");
162
- await rename(tempPath, path);
163
- }
164
- function sha256(content) {
165
- return `sha256:${createHash("sha256").update(content).digest("hex")}`;
166
- }
167
- function isNodeError(error) {
168
- return error instanceof Error;
169
- }
170
- function assertPathWithinProjectRoot(projectRoot, file) {
171
- const normalizedProjectRoot = resolve(projectRoot);
172
- const absolutePath = resolve(normalizedProjectRoot, file);
173
- const rootPrefix = normalizedProjectRoot.endsWith(sep) ? normalizedProjectRoot : `${normalizedProjectRoot}${sep}`;
174
- if (!absolutePath.startsWith(rootPrefix)) {
175
- throw new Error(`Path escapes project root: ${file}`);
176
- }
177
- return absolutePath;
178
- }
179
-
180
- // src/services/audit-log.ts
235
+ import { isAbsolute, join as join3, posix, relative, resolve as resolve2 } from "path";
181
236
  var AUDIT_LOG_FILE = `${FABRIC_DIR}/audit.jsonl`;
182
237
  var DEFAULT_AUDIT_WINDOW_MS = 5 * 60 * 1e3;
183
238
  async function appendGetRulesAuditEvent(projectRoot, input) {
@@ -230,7 +285,7 @@ async function readAuditLog(projectRoot, opts) {
230
285
  return readAuditLogWindowed(projectRoot, opts.ts, opts.windowMs);
231
286
  }
232
287
  async function readAuditLogFull(projectRoot) {
233
- const auditPath = join2(projectRoot, AUDIT_LOG_FILE);
288
+ const auditPath = join3(projectRoot, AUDIT_LOG_FILE);
234
289
  let raw;
235
290
  try {
236
291
  const fileStat = await stat(auditPath);
@@ -251,7 +306,7 @@ async function readAuditLogFull(projectRoot) {
251
306
  return parseAuditLogText(raw);
252
307
  }
253
308
  async function readAuditLogWindowed(projectRoot, ts, windowMs) {
254
- const auditPath = join2(projectRoot, AUDIT_LOG_FILE);
309
+ const auditPath = join3(projectRoot, AUDIT_LOG_FILE);
255
310
  let fileSize;
256
311
  try {
257
312
  const fileStat = await stat(auditPath);
@@ -339,8 +394,8 @@ function isGetRulesAuditEntry(entry) {
339
394
  return entry.event === "get_rules";
340
395
  }
341
396
  async function appendAuditLogEntries(projectRoot, entries) {
342
- const auditPath = join2(projectRoot, AUDIT_LOG_FILE);
343
- const auditDir = join2(projectRoot, FABRIC_DIR);
397
+ const auditPath = join3(projectRoot, AUDIT_LOG_FILE);
398
+ const auditDir = join3(projectRoot, FABRIC_DIR);
344
399
  await mkdir(auditDir, { recursive: true });
345
400
  await appendFile(auditPath, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}
346
401
  `, "utf8");
@@ -379,61 +434,6 @@ function parseAuditLogLine(line) {
379
434
  }
380
435
  }
381
436
 
382
- // src/services/read-human-lock.ts
383
- import { readFile as readFile2 } from "fs/promises";
384
- import { join as join3 } from "path";
385
- import { humanLockEntrySchema } from "@fenglimg/fabric-shared";
386
- async function readHumanLock(projectRoot) {
387
- const document = await readHumanLockDocument(projectRoot);
388
- return await Promise.all(
389
- document.locked.map(async (entry) => {
390
- const currentHash = await hashHumanLockedContent(projectRoot, entry);
391
- return {
392
- ...entry,
393
- drift: currentHash !== entry.hash,
394
- current_hash: currentHash
395
- };
396
- })
397
- );
398
- }
399
- async function readHumanLockEntry(projectRoot, file) {
400
- const entries = await readHumanLock(projectRoot);
401
- return entries.find((entry) => entry.file === file) ?? null;
402
- }
403
- async function readHumanLockDocument(projectRoot) {
404
- const humanLockPath = join3(projectRoot, FABRIC_DIR, HUMAN_LOCK_FILE);
405
- const raw = await readFile2(humanLockPath, "utf8");
406
- const parsed = JSON.parse(raw);
407
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
408
- throw new Error(`Fabric human lock file is invalid: ${humanLockPath}`);
409
- }
410
- const rawObject = parsed;
411
- const lockedResult = humanLockEntrySchema.array().safeParse(rawObject.locked ?? []);
412
- if (!lockedResult.success) {
413
- throw new Error(`Fabric human lock file is invalid: ${humanLockPath}`);
414
- }
415
- return {
416
- path: humanLockPath,
417
- rawObject,
418
- locked: lockedResult.data
419
- };
420
- }
421
- async function hashHumanLockedContent(projectRoot, entry) {
422
- const targetPath = assertPathWithinProjectRoot(projectRoot, entry.file);
423
- let content;
424
- try {
425
- content = await readFile2(targetPath, "utf8");
426
- } catch (error) {
427
- if (isNodeError(error) && error.code === "ENOENT") {
428
- return "missing";
429
- }
430
- throw error;
431
- }
432
- const lines = content.split(/\r?\n/);
433
- const slice = lines.slice(Math.max(entry.start_line - 1, 0), Math.max(entry.end_line, 0)).join("\n");
434
- return sha256(slice);
435
- }
436
-
437
437
  // src/services/read-ledger.ts
438
438
  import { randomUUID } from "crypto";
439
439
  import { appendFile as appendFile2, readFile as readFile3 } from "fs/promises";
@@ -999,6 +999,212 @@ function isMissingFileError(error) {
999
999
  return error instanceof Error && "code" in error && error.code === "ENOENT";
1000
1000
  }
1001
1001
 
1002
+ // src/services/approve-human-lock.ts
1003
+ async function approveHumanLock(projectRoot, input) {
1004
+ assertPathWithinProjectRoot(projectRoot, input.file);
1005
+ const document = await readHumanLockDocument(projectRoot);
1006
+ const index = document.locked.findIndex(
1007
+ (entry) => entry.file === input.file && entry.start_line === input.start_line && entry.end_line === input.end_line
1008
+ );
1009
+ if (index === -1) {
1010
+ throw new Error(`Cannot find human lock entry: ${input.file}:${input.start_line}-${input.end_line}`);
1011
+ }
1012
+ const currentEntry = document.locked[index];
1013
+ if (currentEntry === void 0) {
1014
+ throw new Error(`Cannot find human lock entry: ${input.file}:${input.start_line}-${input.end_line}`);
1015
+ }
1016
+ const nextEntry = {
1017
+ ...currentEntry,
1018
+ hash: input.new_hash
1019
+ };
1020
+ if (currentEntry.hash === input.new_hash) {
1021
+ const currentHash2 = await hashHumanLockedContent(projectRoot, nextEntry);
1022
+ return {
1023
+ updated: false,
1024
+ entry: {
1025
+ ...nextEntry,
1026
+ drift: currentHash2 !== nextEntry.hash,
1027
+ current_hash: currentHash2
1028
+ }
1029
+ };
1030
+ }
1031
+ const nextLocked = document.locked.slice();
1032
+ nextLocked[index] = nextEntry;
1033
+ const nextRawObject = {
1034
+ ...document.rawObject,
1035
+ locked: nextLocked
1036
+ };
1037
+ await atomicWriteText(document.path, `${JSON.stringify(nextRawObject, null, 2)}
1038
+ `);
1039
+ const currentHash = await hashHumanLockedContent(projectRoot, nextEntry);
1040
+ const ledgerEntry = await appendLedgerEntry(projectRoot, createApproveLedgerEntry(input));
1041
+ return {
1042
+ updated: true,
1043
+ entry: {
1044
+ ...nextEntry,
1045
+ drift: currentHash !== nextEntry.hash,
1046
+ current_hash: currentHash
1047
+ },
1048
+ ledger_entry: ledgerEntry
1049
+ };
1050
+ }
1051
+ function createApproveLedgerEntry(input) {
1052
+ return {
1053
+ ts: Date.now(),
1054
+ source: "human",
1055
+ parent_sha: "human-lock:approve",
1056
+ intent: `approve human lock ${input.file}:${input.start_line}-${input.end_line}`,
1057
+ affected_paths: [input.file, ".fabric/human-lock.json"],
1058
+ diff_stat: `updated approved hash to ${input.new_hash}`
1059
+ };
1060
+ }
1061
+
1062
+ // src/services/get-rules.ts
1063
+ import { readFile as readFile5 } from "fs/promises";
1064
+ import { join as join6 } from "path";
1065
+ import { minimatch } from "minimatch";
1066
+ var PRIORITY_ORDER = {
1067
+ high: 0,
1068
+ medium: 1,
1069
+ low: 2
1070
+ };
1071
+ async function getRules(projectRoot, input) {
1072
+ const context = await loadGetRulesContext(projectRoot);
1073
+ const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
1074
+ const rules = await resolveRulesForPath(projectRoot, context, input.path);
1075
+ const result = {
1076
+ revision_hash: context.meta.revision,
1077
+ stale,
1078
+ rules
1079
+ };
1080
+ try {
1081
+ await appendGetRulesAuditEvent(projectRoot, {
1082
+ path: input.path,
1083
+ client_hash: input.client_hash
1084
+ });
1085
+ } catch {
1086
+ }
1087
+ return result;
1088
+ }
1089
+ async function loadGetRulesContext(projectRoot) {
1090
+ const cached = contextCache.get("context", projectRoot);
1091
+ if (cached !== void 0) {
1092
+ return cached;
1093
+ }
1094
+ const meta = await readAgentsMeta(projectRoot);
1095
+ const l0Content = await readFile5(join6(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
1096
+ const humanLockedNearby = (await readHumanLock(projectRoot)).map((entry) => ({
1097
+ file: entry.file,
1098
+ excerpt: JSON.stringify(entry)
1099
+ }));
1100
+ const context = {
1101
+ meta,
1102
+ l0Content,
1103
+ humanLockedNearby
1104
+ };
1105
+ contextCache.set("context", projectRoot, context);
1106
+ return context;
1107
+ }
1108
+ 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
+ };
1118
+ }
1119
+ function normalizeRulesPath(value) {
1120
+ return value.replaceAll("\\", "/");
1121
+ }
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) {
1132
+ const requestedPath = normalizeRulesPath(path);
1133
+ const matchedNodes = Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
1134
+ const [leftId, leftNode] = left;
1135
+ const [rightId, rightNode] = right;
1136
+ const priorityDelta = PRIORITY_ORDER[leftNode.priority] - PRIORITY_ORDER[rightNode.priority];
1137
+ return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
1138
+ });
1139
+ const rules = [];
1140
+ const stubs = [];
1141
+ for (const [nodeId, node] of matchedNodes) {
1142
+ if (node.activation?.tier === "description") {
1143
+ stubs.push({
1144
+ path: node.file,
1145
+ description: node.activation.description ?? ""
1146
+ });
1147
+ continue;
1148
+ }
1149
+ rules.push({
1150
+ level: classifyNode(nodeId),
1151
+ entry: {
1152
+ path: node.file,
1153
+ content: await readFile5(join6(projectRoot, node.file), "utf8")
1154
+ }
1155
+ });
1156
+ }
1157
+ return { rules, stubs };
1158
+ }
1159
+ function partitionRulesByLevel(loadedRules, dedupeByPath) {
1160
+ const l1 = [];
1161
+ const l2 = [];
1162
+ for (const rule of loadedRules) {
1163
+ if (rule.level === "L1") {
1164
+ l1.push(rule.entry);
1165
+ continue;
1166
+ }
1167
+ if (rule.level === "L2") {
1168
+ l2.push(rule.entry);
1169
+ }
1170
+ }
1171
+ return {
1172
+ L1: dedupeByPath ? dedupeEntriesByPath(l1) : l1,
1173
+ L2: dedupeByPath ? dedupeEntriesByPath(l2) : l2
1174
+ };
1175
+ }
1176
+ function dedupeEntriesByPath(entries) {
1177
+ const seenPaths = /* @__PURE__ */ new Set();
1178
+ return entries.filter((entry) => {
1179
+ if (seenPaths.has(entry.path)) {
1180
+ return false;
1181
+ }
1182
+ seenPaths.add(entry.path);
1183
+ return true;
1184
+ });
1185
+ }
1186
+ function shouldLoadNodeForPath(requestedPath, node) {
1187
+ switch (node.activation?.tier) {
1188
+ case "always":
1189
+ return true;
1190
+ case "description":
1191
+ return true;
1192
+ case "path":
1193
+ case void 0:
1194
+ return minimatch(requestedPath, normalizeRulesPath(node.scope_glob), { dot: true });
1195
+ }
1196
+ }
1197
+ function dedupeDescriptionStubsByPath(stubs) {
1198
+ const seenPaths = /* @__PURE__ */ new Set();
1199
+ return stubs.filter((stub) => {
1200
+ if (seenPaths.has(stub.path)) {
1201
+ return false;
1202
+ }
1203
+ seenPaths.add(stub.path);
1204
+ return true;
1205
+ });
1206
+ }
1207
+
1002
1208
  export {
1003
1209
  AGENTS_MD_RESOURCE_URI,
1004
1210
  contextCache,
@@ -1009,15 +1215,16 @@ export {
1009
1215
  FABRIC_DIR,
1010
1216
  atomicWriteText,
1011
1217
  sha256,
1012
- assertPathWithinProjectRoot,
1013
- appendGetRulesAuditEvent,
1014
1218
  appendEditIntentAuditEvents,
1015
1219
  readLedger,
1016
1220
  appendLedgerEntry,
1017
1221
  readHumanLock,
1018
1222
  readHumanLockEntry,
1019
- readHumanLockDocument,
1020
- hashHumanLockedContent,
1223
+ getRules,
1224
+ loadGetRulesContext,
1225
+ resolveRulesForPath,
1226
+ normalizeRulesPath,
1021
1227
  runDoctorReport,
1022
- runDoctorAuditReport
1228
+ runDoctorAuditReport,
1229
+ approveHumanLock
1023
1230
  };
@@ -3,17 +3,15 @@ import {
3
3
  AgentsMetaFileMissingError,
4
4
  AgentsMetaInvalidError,
5
5
  appendLedgerEntry,
6
- assertPathWithinProjectRoot,
7
- atomicWriteText,
6
+ approveHumanLock,
8
7
  contextCache,
9
- hashHumanLockedContent,
8
+ getRules,
10
9
  readAgentsMeta,
11
10
  readHumanLock,
12
- readHumanLockDocument,
13
11
  readHumanLockEntry,
14
12
  readLedger,
15
13
  runDoctorReport
16
- } from "./chunk-GU7AMRM3.js";
14
+ } from "./chunk-E3RZ276F.js";
17
15
 
18
16
  // src/http.ts
19
17
  import { randomUUID } from "crypto";
@@ -731,68 +729,6 @@ function registerHistoryApi(app, projectRoot) {
731
729
 
732
730
  // src/api/human-lock.ts
733
731
  import { humanLockApproveRequestSchema, humanLockFileParamsSchema } from "@fenglimg/fabric-shared";
734
-
735
- // src/services/approve-human-lock.ts
736
- async function approveHumanLock(projectRoot, input) {
737
- assertPathWithinProjectRoot(projectRoot, input.file);
738
- const document = await readHumanLockDocument(projectRoot);
739
- const index = document.locked.findIndex(
740
- (entry) => entry.file === input.file && entry.start_line === input.start_line && entry.end_line === input.end_line
741
- );
742
- if (index === -1) {
743
- throw new Error(`Cannot find human lock entry: ${input.file}:${input.start_line}-${input.end_line}`);
744
- }
745
- const currentEntry = document.locked[index];
746
- if (currentEntry === void 0) {
747
- throw new Error(`Cannot find human lock entry: ${input.file}:${input.start_line}-${input.end_line}`);
748
- }
749
- const nextEntry = {
750
- ...currentEntry,
751
- hash: input.new_hash
752
- };
753
- if (currentEntry.hash === input.new_hash) {
754
- const currentHash2 = await hashHumanLockedContent(projectRoot, nextEntry);
755
- return {
756
- updated: false,
757
- entry: {
758
- ...nextEntry,
759
- drift: currentHash2 !== nextEntry.hash,
760
- current_hash: currentHash2
761
- }
762
- };
763
- }
764
- const nextLocked = document.locked.slice();
765
- nextLocked[index] = nextEntry;
766
- const nextRawObject = {
767
- ...document.rawObject,
768
- locked: nextLocked
769
- };
770
- await atomicWriteText(document.path, `${JSON.stringify(nextRawObject, null, 2)}
771
- `);
772
- const currentHash = await hashHumanLockedContent(projectRoot, nextEntry);
773
- const ledgerEntry = await appendLedgerEntry(projectRoot, createApproveLedgerEntry(input));
774
- return {
775
- updated: true,
776
- entry: {
777
- ...nextEntry,
778
- drift: currentHash !== nextEntry.hash,
779
- current_hash: currentHash
780
- },
781
- ledger_entry: ledgerEntry
782
- };
783
- }
784
- function createApproveLedgerEntry(input) {
785
- return {
786
- ts: Date.now(),
787
- source: "human",
788
- parent_sha: "human-lock:approve",
789
- intent: `approve human lock ${input.file}:${input.start_line}-${input.end_line}`,
790
- affected_paths: [input.file, ".fabric/human-lock.json"],
791
- diff_stat: `updated approved hash to ${input.new_hash}`
792
- };
793
- }
794
-
795
- // src/api/human-lock.ts
796
732
  function registerHumanLockApi(app, projectRoot) {
797
733
  app.get("/api/human-lock", async (_req, res) => {
798
734
  try {
@@ -929,6 +865,27 @@ function registerRulesApi(app, projectRoot) {
929
865
  });
930
866
  }
931
867
 
868
+ // src/api/rules-context.ts
869
+ function registerRulesContextApi(app, projectRoot) {
870
+ app.get("/api/rules/context", async (req, res) => {
871
+ const path = typeof req.query.path === "string" ? req.query.path.trim() : "";
872
+ if (path.length === 0) {
873
+ sendValidationError(res, "Missing required query parameter: path", {
874
+ fieldErrors: {
875
+ path: ["Expected a non-empty path query parameter."]
876
+ }
877
+ });
878
+ return;
879
+ }
880
+ try {
881
+ const result = await getRules(projectRoot, { path });
882
+ res.json(result.rules);
883
+ } catch (error) {
884
+ sendUnknownError(res, error);
885
+ }
886
+ });
887
+ }
888
+
932
889
  // src/api/scan.ts
933
890
  import { existsSync, readdirSync, readFileSync, statSync } from "fs";
934
891
  import { isAbsolute, join as join2, relative, resolve, sep } from "path";
@@ -948,13 +905,13 @@ var DEFAULT_IGNORES = [
948
905
  function registerScanApi(app, projectRoot) {
949
906
  app.get("/api/scan", async (_req, res) => {
950
907
  try {
951
- res.json(createScanReport(projectRoot));
908
+ res.json(await createScanReport(projectRoot));
952
909
  } catch (error) {
953
910
  sendUnknownError(res, error);
954
911
  }
955
912
  });
956
913
  }
957
- function createScanReport(targetInput = process.cwd()) {
914
+ async function createScanReport(targetInput = process.cwd()) {
958
915
  const target = normalizeTarget(targetInput);
959
916
  const framework = detectFramework(target);
960
917
  const readmeQuality = getReadmeQuality(target);
@@ -1235,6 +1192,7 @@ function createFabricHttpApp(options) {
1235
1192
  app.use("/mcp", bearerAuth);
1236
1193
  }
1237
1194
  registerRulesApi(app, projectRoot);
1195
+ registerRulesContextApi(app, projectRoot);
1238
1196
  registerLedgerApi(app, projectRoot);
1239
1197
  registerHistoryApi(app, projectRoot);
1240
1198
  registerScanApi(app, projectRoot);
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Server } from 'node:http';
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import { AuditMode } from '@fenglimg/fabric-shared';
3
+ import { AuditMode, LedgerEntry, HumanLockEntry } from '@fenglimg/fabric-shared';
4
4
 
5
5
  type DoctorStatus = "ok" | "warn" | "error";
6
6
  type DoctorCheck = {
@@ -61,6 +61,30 @@ declare function runDoctorAuditReport(target: string, options?: {
61
61
  windowMs?: number;
62
62
  }): Promise<DoctorAuditReport>;
63
63
 
64
+ type StoredLedgerEntry = LedgerEntry & {
65
+ id: string;
66
+ };
67
+
68
+ type HumanLockStatus = HumanLockEntry & {
69
+ drift: boolean;
70
+ current_hash: string;
71
+ };
72
+ declare function readHumanLock(projectRoot: string): Promise<HumanLockStatus[]>;
73
+ declare function readHumanLockEntry(projectRoot: string, file: string): Promise<HumanLockStatus | null>;
74
+
75
+ type ApproveHumanLockInput = {
76
+ file: string;
77
+ start_line: number;
78
+ end_line: number;
79
+ new_hash: string;
80
+ };
81
+ type ApproveHumanLockResult = {
82
+ updated: boolean;
83
+ entry: HumanLockStatus;
84
+ ledger_entry?: StoredLedgerEntry;
85
+ };
86
+ declare function approveHumanLock(projectRoot: string, input: ApproveHumanLockInput): Promise<ApproveHumanLockResult>;
87
+
64
88
  /**
65
89
  * Shared constants used across the server package.
66
90
  */
@@ -78,4 +102,4 @@ declare function startHttpServer(options: {
78
102
  dev?: boolean;
79
103
  }): Promise<Server>;
80
104
 
81
- export { AGENTS_MD_RESOURCE_URI, type DoctorAuditReport, type DoctorReport, createFabricServer, runDoctorAuditReport, runDoctorReport, startHttpServer, startStdioServer };
105
+ export { AGENTS_MD_RESOURCE_URI, type ApproveHumanLockInput, type ApproveHumanLockResult, type DoctorAuditReport, type DoctorReport, type HumanLockStatus, approveHumanLock, createFabricServer, readHumanLock, readHumanLockEntry, runDoctorAuditReport, runDoctorReport, startHttpServer, startStdioServer };