@fenglimg/fabric-server 1.4.0 → 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.
- package/dist/{chunk-GU7AMRM3.js → chunk-E3RZ276F.js} +306 -99
- package/dist/{http-7OHSKCPN.js → http-SK2HEFK4.js} +27 -69
- package/dist/index.d.ts +26 -2
- package/dist/index.js +20 -126
- package/dist/static/assets/index-B5hhHHl2.css +1 -0
- package/dist/static/assets/index-Mf8rv3Zc.js +14 -0
- package/dist/static/index.html +2 -2
- package/package.json +3 -3
- package/dist/static/assets/index-CWkgSd3Y.js +0 -14
- package/dist/static/assets/index-D45wW11O.css +0 -1
|
@@ -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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
343
|
-
const auditDir =
|
|
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
|
-
|
|
1020
|
-
|
|
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
|
-
|
|
7
|
-
atomicWriteText,
|
|
6
|
+
approveHumanLock,
|
|
8
7
|
contextCache,
|
|
9
|
-
|
|
8
|
+
getRules,
|
|
10
9
|
readAgentsMeta,
|
|
11
10
|
readHumanLock,
|
|
12
|
-
readHumanLockDocument,
|
|
13
11
|
readHumanLockEntry,
|
|
14
12
|
readLedger,
|
|
15
13
|
runDoctorReport
|
|
16
|
-
} from "./chunk-
|
|
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 };
|