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