@fenglimg/fabric-server 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-E3RZ276F.js → chunk-TZCE2K4D.js} +284 -67
- package/dist/{http-BVF4GWIM.js → http-DJCTLGF4.js} +43 -14
- package/dist/index.d.ts +17 -2
- package/dist/index.js +477 -71
- package/dist/static/assets/{index-BRegf31x.js → index-LJh6IezM.js} +7 -7
- package/dist/static/index.html +1 -1
- package/package.json +3 -3
|
@@ -82,23 +82,29 @@ var ContextCache = class {
|
|
|
82
82
|
};
|
|
83
83
|
var contextCache = new ContextCache(5e3);
|
|
84
84
|
|
|
85
|
-
// src/services/read-human-lock.ts
|
|
86
|
-
import { readFile } from "fs/promises";
|
|
87
|
-
import { join } from "path";
|
|
88
|
-
import { humanLockEntrySchema } from "@fenglimg/fabric-shared";
|
|
89
|
-
|
|
90
85
|
// src/services/_shared.ts
|
|
91
|
-
import { resolve, sep } from "path";
|
|
86
|
+
import { dirname, join, resolve, sep } from "path";
|
|
92
87
|
import { createHash } from "crypto";
|
|
93
|
-
import { rename, writeFile } from "fs/promises";
|
|
88
|
+
import { mkdir, rename, writeFile } from "fs/promises";
|
|
94
89
|
var FABRIC_DIR = ".fabric";
|
|
95
90
|
var HUMAN_LOCK_FILE = "human-lock.json";
|
|
96
91
|
var LEDGER_FILE = ".intent-ledger.jsonl";
|
|
92
|
+
var LEDGER_PATH = `${FABRIC_DIR}/${LEDGER_FILE}`;
|
|
93
|
+
var LEGACY_LEDGER_PATH = LEDGER_FILE;
|
|
97
94
|
async function atomicWriteText(path, content) {
|
|
98
95
|
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
99
96
|
await writeFile(tempPath, content, "utf8");
|
|
100
97
|
await rename(tempPath, path);
|
|
101
98
|
}
|
|
99
|
+
function getLedgerPath(projectRoot) {
|
|
100
|
+
return join(projectRoot, LEDGER_PATH);
|
|
101
|
+
}
|
|
102
|
+
function getLegacyLedgerPath(projectRoot) {
|
|
103
|
+
return join(projectRoot, LEGACY_LEDGER_PATH);
|
|
104
|
+
}
|
|
105
|
+
async function ensureParentDirectory(path) {
|
|
106
|
+
await mkdir(dirname(path), { recursive: true });
|
|
107
|
+
}
|
|
102
108
|
function sha256(content) {
|
|
103
109
|
return `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
104
110
|
}
|
|
@@ -116,6 +122,9 @@ function assertPathWithinProjectRoot(projectRoot, file) {
|
|
|
116
122
|
}
|
|
117
123
|
|
|
118
124
|
// src/services/read-human-lock.ts
|
|
125
|
+
import { readFile } from "fs/promises";
|
|
126
|
+
import { join as join2 } from "path";
|
|
127
|
+
import { humanLockEntrySchema } from "@fenglimg/fabric-shared";
|
|
119
128
|
async function readHumanLock(projectRoot) {
|
|
120
129
|
const document = await readHumanLockDocument(projectRoot);
|
|
121
130
|
return await Promise.all(
|
|
@@ -134,7 +143,7 @@ async function readHumanLockEntry(projectRoot, file) {
|
|
|
134
143
|
return entries.find((entry) => entry.file === file) ?? null;
|
|
135
144
|
}
|
|
136
145
|
async function readHumanLockDocument(projectRoot) {
|
|
137
|
-
const humanLockPath =
|
|
146
|
+
const humanLockPath = join2(projectRoot, FABRIC_DIR, HUMAN_LOCK_FILE);
|
|
138
147
|
const raw = await readFile(humanLockPath, "utf8");
|
|
139
148
|
const parsed = JSON.parse(raw);
|
|
140
149
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
@@ -177,7 +186,7 @@ import { detectFramework } from "@fenglimg/fabric-shared/node";
|
|
|
177
186
|
|
|
178
187
|
// src/meta-reader.ts
|
|
179
188
|
import { readFile as readFile2 } from "fs/promises";
|
|
180
|
-
import { join as
|
|
189
|
+
import { join as join3 } from "path";
|
|
181
190
|
import { agentsMetaSchema } from "@fenglimg/fabric-shared";
|
|
182
191
|
import { agentsMetaNodeSchema, agentsMetaSchema as agentsMetaSchema2 } from "@fenglimg/fabric-shared";
|
|
183
192
|
var AgentsMetaFileMissingError = class extends Error {
|
|
@@ -200,7 +209,7 @@ var AgentsMetaInvalidError = class extends Error {
|
|
|
200
209
|
code = "FABRIC_META_INVALID";
|
|
201
210
|
};
|
|
202
211
|
function getAgentsMetaPath(projectRoot) {
|
|
203
|
-
return
|
|
212
|
+
return join3(projectRoot, ".fabric", "agents.meta.json");
|
|
204
213
|
}
|
|
205
214
|
function resolveProjectRoot() {
|
|
206
215
|
return process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
@@ -231,8 +240,8 @@ async function readAgentsMeta(projectRoot) {
|
|
|
231
240
|
}
|
|
232
241
|
|
|
233
242
|
// src/services/audit-log.ts
|
|
234
|
-
import { appendFile, mkdir, open, stat } from "fs/promises";
|
|
235
|
-
import { isAbsolute, join as
|
|
243
|
+
import { appendFile, mkdir as mkdir2, open, stat } from "fs/promises";
|
|
244
|
+
import { isAbsolute, join as join4, posix, relative, resolve as resolve2 } from "path";
|
|
236
245
|
var AUDIT_LOG_FILE = `${FABRIC_DIR}/audit.jsonl`;
|
|
237
246
|
var DEFAULT_AUDIT_WINDOW_MS = 5 * 60 * 1e3;
|
|
238
247
|
async function appendGetRulesAuditEvent(projectRoot, input) {
|
|
@@ -246,6 +255,25 @@ async function appendGetRulesAuditEvent(projectRoot, input) {
|
|
|
246
255
|
await appendAuditLogEntries(projectRoot, [entry]);
|
|
247
256
|
return entry;
|
|
248
257
|
}
|
|
258
|
+
async function appendRuleSelectionAuditEvent(projectRoot, input) {
|
|
259
|
+
const entry = {
|
|
260
|
+
kind: "audit-event",
|
|
261
|
+
event: "rule_selection",
|
|
262
|
+
ts: input.ts ?? Date.now(),
|
|
263
|
+
path: normalizeAuditPath(projectRoot, input.path),
|
|
264
|
+
selection_token: input.selection_token,
|
|
265
|
+
target_paths: input.target_paths.map((path) => normalizeAuditPath(projectRoot, path)),
|
|
266
|
+
required_stable_ids: input.required_stable_ids,
|
|
267
|
+
ai_selectable_stable_ids: input.ai_selectable_stable_ids,
|
|
268
|
+
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
269
|
+
final_stable_ids: input.final_stable_ids,
|
|
270
|
+
ai_selection_reasons: input.ai_selection_reasons,
|
|
271
|
+
rejected_stable_ids: input.rejected_stable_ids,
|
|
272
|
+
ignored_stable_ids: input.ignored_stable_ids
|
|
273
|
+
};
|
|
274
|
+
await appendAuditLogEntries(projectRoot, [entry]);
|
|
275
|
+
return entry;
|
|
276
|
+
}
|
|
249
277
|
async function appendEditIntentAuditEvents(projectRoot, input) {
|
|
250
278
|
const ts = input.ts ?? Date.now();
|
|
251
279
|
const windowMs = input.window_ms ?? DEFAULT_AUDIT_WINDOW_MS;
|
|
@@ -285,7 +313,7 @@ async function readAuditLog(projectRoot, opts) {
|
|
|
285
313
|
return readAuditLogWindowed(projectRoot, opts.ts, opts.windowMs);
|
|
286
314
|
}
|
|
287
315
|
async function readAuditLogFull(projectRoot) {
|
|
288
|
-
const auditPath =
|
|
316
|
+
const auditPath = join4(projectRoot, AUDIT_LOG_FILE);
|
|
289
317
|
let raw;
|
|
290
318
|
try {
|
|
291
319
|
const fileStat = await stat(auditPath);
|
|
@@ -306,7 +334,7 @@ async function readAuditLogFull(projectRoot) {
|
|
|
306
334
|
return parseAuditLogText(raw);
|
|
307
335
|
}
|
|
308
336
|
async function readAuditLogWindowed(projectRoot, ts, windowMs) {
|
|
309
|
-
const auditPath =
|
|
337
|
+
const auditPath = join4(projectRoot, AUDIT_LOG_FILE);
|
|
310
338
|
let fileSize;
|
|
311
339
|
try {
|
|
312
340
|
const fileStat = await stat(auditPath);
|
|
@@ -394,9 +422,9 @@ function isGetRulesAuditEntry(entry) {
|
|
|
394
422
|
return entry.event === "get_rules";
|
|
395
423
|
}
|
|
396
424
|
async function appendAuditLogEntries(projectRoot, entries) {
|
|
397
|
-
const auditPath =
|
|
398
|
-
const auditDir =
|
|
399
|
-
await
|
|
425
|
+
const auditPath = join4(projectRoot, AUDIT_LOG_FILE);
|
|
426
|
+
const auditDir = join4(projectRoot, FABRIC_DIR);
|
|
427
|
+
await mkdir2(auditDir, { recursive: true });
|
|
400
428
|
await appendFile(auditPath, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}
|
|
401
429
|
`, "utf8");
|
|
402
430
|
}
|
|
@@ -428,22 +456,61 @@ function parseAuditLogLine(line) {
|
|
|
428
456
|
window_ms: parsed.window_ms
|
|
429
457
|
};
|
|
430
458
|
}
|
|
459
|
+
if (parsed.event === "rule_selection" && typeof parsed.selection_token === "string" && Array.isArray(parsed.target_paths) && Array.isArray(parsed.required_stable_ids) && Array.isArray(parsed.ai_selectable_stable_ids) && Array.isArray(parsed.ai_selected_stable_ids) && Array.isArray(parsed.final_stable_ids) && isStringRecord(parsed.ai_selection_reasons) && Array.isArray(parsed.rejected_stable_ids) && Array.isArray(parsed.ignored_stable_ids)) {
|
|
460
|
+
return {
|
|
461
|
+
kind: "audit-event",
|
|
462
|
+
event: "rule_selection",
|
|
463
|
+
ts: parsed.ts,
|
|
464
|
+
path: parsed.path,
|
|
465
|
+
selection_token: parsed.selection_token,
|
|
466
|
+
target_paths: parsed.target_paths.filter(isString),
|
|
467
|
+
required_stable_ids: parsed.required_stable_ids.filter(isString),
|
|
468
|
+
ai_selectable_stable_ids: parsed.ai_selectable_stable_ids.filter(isString),
|
|
469
|
+
ai_selected_stable_ids: parsed.ai_selected_stable_ids.filter(isString),
|
|
470
|
+
final_stable_ids: parsed.final_stable_ids.filter(isString),
|
|
471
|
+
ai_selection_reasons: parsed.ai_selection_reasons,
|
|
472
|
+
rejected_stable_ids: parsed.rejected_stable_ids.filter(isString),
|
|
473
|
+
ignored_stable_ids: parsed.ignored_stable_ids.filter(isString)
|
|
474
|
+
};
|
|
475
|
+
}
|
|
431
476
|
return null;
|
|
432
477
|
} catch {
|
|
433
478
|
return null;
|
|
434
479
|
}
|
|
435
480
|
}
|
|
481
|
+
function isString(value) {
|
|
482
|
+
return typeof value === "string";
|
|
483
|
+
}
|
|
484
|
+
function isStringRecord(value) {
|
|
485
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
return Object.values(value).every((entry) => typeof entry === "string");
|
|
489
|
+
}
|
|
436
490
|
|
|
437
491
|
// src/services/read-ledger.ts
|
|
438
492
|
import { randomUUID } from "crypto";
|
|
439
|
-
import { appendFile as appendFile2, readFile as readFile3 } from "fs/promises";
|
|
440
|
-
import { join as join4 } from "path";
|
|
493
|
+
import { access, appendFile as appendFile2, copyFile, readFile as readFile3, rm } from "fs/promises";
|
|
441
494
|
import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
|
|
495
|
+
async function resolveLedgerPaths(projectRoot) {
|
|
496
|
+
const primaryPath = getLedgerPath(projectRoot);
|
|
497
|
+
const legacyPath = getLegacyLedgerPath(projectRoot);
|
|
498
|
+
const [primaryExists, legacyExists] = await Promise.all([
|
|
499
|
+
pathExists(primaryPath),
|
|
500
|
+
pathExists(legacyPath)
|
|
501
|
+
]);
|
|
502
|
+
return {
|
|
503
|
+
primaryPath,
|
|
504
|
+
legacyPath,
|
|
505
|
+
readPath: primaryExists ? primaryPath : legacyPath,
|
|
506
|
+
usingLegacy: !primaryExists && legacyExists
|
|
507
|
+
};
|
|
508
|
+
}
|
|
442
509
|
async function readLedger(projectRoot, options = {}) {
|
|
443
|
-
const
|
|
510
|
+
const { readPath } = await resolveLedgerPaths(projectRoot);
|
|
444
511
|
let raw;
|
|
445
512
|
try {
|
|
446
|
-
raw = await readFile3(
|
|
513
|
+
raw = await readFile3(readPath, "utf8");
|
|
447
514
|
} catch (error) {
|
|
448
515
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
449
516
|
return [];
|
|
@@ -453,15 +520,40 @@ async function readLedger(projectRoot, options = {}) {
|
|
|
453
520
|
return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map((line, index) => parseLedgerLine(line, index)).filter((entry) => entry !== null).filter((entry) => options.source === void 0 || entry.source === options.source).filter((entry) => options.since === void 0 || entry.ts >= options.since);
|
|
454
521
|
}
|
|
455
522
|
async function appendLedgerEntry(projectRoot, entry) {
|
|
456
|
-
const ledgerPath =
|
|
523
|
+
const ledgerPath = getLedgerPath(projectRoot);
|
|
457
524
|
const nextEntry = ledgerEntrySchema.parse({
|
|
458
525
|
...entry,
|
|
459
526
|
id: entry.id ?? `ledger:${randomUUID()}`
|
|
460
527
|
});
|
|
528
|
+
await ensureParentDirectory(ledgerPath);
|
|
461
529
|
await appendFile2(ledgerPath, `${JSON.stringify(nextEntry)}
|
|
462
530
|
`, "utf8");
|
|
463
531
|
return nextEntry;
|
|
464
532
|
}
|
|
533
|
+
async function migrateLegacyLedger(projectRoot) {
|
|
534
|
+
const { primaryPath, legacyPath } = await resolveLedgerPaths(projectRoot);
|
|
535
|
+
const [primaryExists, legacyExists] = await Promise.all([
|
|
536
|
+
pathExists(primaryPath),
|
|
537
|
+
pathExists(legacyPath)
|
|
538
|
+
]);
|
|
539
|
+
if (!legacyExists) {
|
|
540
|
+
return {
|
|
541
|
+
migrated: false,
|
|
542
|
+
from: null,
|
|
543
|
+
to: primaryPath
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
if (!primaryExists) {
|
|
547
|
+
await ensureParentDirectory(primaryPath);
|
|
548
|
+
await copyFile(legacyPath, primaryPath);
|
|
549
|
+
}
|
|
550
|
+
await rm(legacyPath, { force: true });
|
|
551
|
+
return {
|
|
552
|
+
migrated: true,
|
|
553
|
+
from: legacyPath,
|
|
554
|
+
to: primaryPath
|
|
555
|
+
};
|
|
556
|
+
}
|
|
465
557
|
function parseLedgerLine(line, index) {
|
|
466
558
|
try {
|
|
467
559
|
const parsed = JSON.parse(line);
|
|
@@ -483,6 +575,17 @@ function parseLedgerLine(line, index) {
|
|
|
483
575
|
function createDerivedId(index, line) {
|
|
484
576
|
return `ledger:${index + 1}:${sha256(line).slice("sha256:".length)}`;
|
|
485
577
|
}
|
|
578
|
+
async function pathExists(path) {
|
|
579
|
+
try {
|
|
580
|
+
await access(path);
|
|
581
|
+
return true;
|
|
582
|
+
} catch (error) {
|
|
583
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
486
589
|
|
|
487
590
|
// src/services/doctor.ts
|
|
488
591
|
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
@@ -538,6 +641,9 @@ async function runDoctorReport(target) {
|
|
|
538
641
|
lastLedgerEntryTs: ledgerSnapshot.lastEntryTs,
|
|
539
642
|
lastLedgerEntryAgeMs: ledgerSnapshot.lastEntryAgeMs,
|
|
540
643
|
metaRevision: metaSnapshot.revision,
|
|
644
|
+
ledgerPath: ledgerSnapshot.primaryPath,
|
|
645
|
+
legacyLedgerPath: ledgerSnapshot.legacyPath,
|
|
646
|
+
legacyLedgerDetected: ledgerSnapshot.usingLegacy,
|
|
541
647
|
audit: auditReport.skipped ? null : {
|
|
542
648
|
enabled: true,
|
|
543
649
|
mode: auditReport.mode,
|
|
@@ -549,6 +655,17 @@ async function runDoctorReport(target) {
|
|
|
549
655
|
audit: auditReport.skipped ? null : auditReport
|
|
550
656
|
};
|
|
551
657
|
}
|
|
658
|
+
async function runDoctorFix(target) {
|
|
659
|
+
const projectRoot = normalizeTarget(target);
|
|
660
|
+
const migration = await migrateLegacyLedger(projectRoot);
|
|
661
|
+
const report = await runDoctorReport(projectRoot);
|
|
662
|
+
return {
|
|
663
|
+
changed: migration.migrated,
|
|
664
|
+
migratedLedger: migration.migrated,
|
|
665
|
+
message: migration.migrated ? `Migrated legacy ledger from ${migration.from} to ${migration.to}.` : `No legacy ledger migration needed. Canonical ledger path: ${migration.to}.`,
|
|
666
|
+
report
|
|
667
|
+
};
|
|
668
|
+
}
|
|
552
669
|
async function runDoctorAuditReport(target, options = {}) {
|
|
553
670
|
const projectRoot = normalizeTarget(target);
|
|
554
671
|
const mode = options.mode ?? readDoctorAuditMode(projectRoot);
|
|
@@ -567,13 +684,13 @@ async function runDoctorAuditReport(target, options = {}) {
|
|
|
567
684
|
readLedger(projectRoot, { source: "ai" }),
|
|
568
685
|
readAuditLog(projectRoot)
|
|
569
686
|
]);
|
|
570
|
-
const
|
|
571
|
-
(entry) => entry.event === "get_rules"
|
|
687
|
+
const ruleAccessEntries = auditEntries.filter(
|
|
688
|
+
(entry) => entry.event === "get_rules" || entry.event === "rule_selection"
|
|
572
689
|
);
|
|
573
690
|
const { checkedPathCount, violations } = collectAuditViolations(
|
|
574
691
|
projectRoot,
|
|
575
692
|
ledgerEntries,
|
|
576
|
-
|
|
693
|
+
ruleAccessEntries,
|
|
577
694
|
windowMs
|
|
578
695
|
);
|
|
579
696
|
return {
|
|
@@ -647,6 +764,15 @@ function createMetaRevisionCheck(snapshot) {
|
|
|
647
764
|
message: `agents.meta.json revision ${snapshot.revision} is stale: ${parts.join(" \xB7 ")}.`
|
|
648
765
|
};
|
|
649
766
|
}
|
|
767
|
+
if (snapshot.derivedIdentityFiles.length > 0) {
|
|
768
|
+
const [firstFile] = snapshot.derivedIdentityFiles;
|
|
769
|
+
const suffix = snapshot.derivedIdentityFiles.length > 1 ? ` (+${snapshot.derivedIdentityFiles.length - 1} more)` : "";
|
|
770
|
+
return {
|
|
771
|
+
name: "Meta revision",
|
|
772
|
+
status: "warn",
|
|
773
|
+
message: `agents.meta.json revision ${snapshot.revision} matches ${snapshot.nodeCount} tracked AGENTS files, but ${snapshot.derivedIdentityFiles.length} rule node${snapshot.derivedIdentityFiles.length === 1 ? "" : "s"} still use derived identities. Add \`<!-- fab:rule-id ... -->\` to the rule file header instead of editing meta directly (${firstFile}${suffix}).`
|
|
774
|
+
};
|
|
775
|
+
}
|
|
650
776
|
return {
|
|
651
777
|
name: "Meta revision",
|
|
652
778
|
status: "ok",
|
|
@@ -675,6 +801,13 @@ function createProtectedPathsCheck(snapshot) {
|
|
|
675
801
|
};
|
|
676
802
|
}
|
|
677
803
|
function createLedgerCheck(snapshot) {
|
|
804
|
+
if (snapshot.usingLegacy) {
|
|
805
|
+
return {
|
|
806
|
+
name: "Intent ledger",
|
|
807
|
+
status: "warn",
|
|
808
|
+
message: `Legacy ledger path detected at ${snapshot.legacyPath}. Fabric now reads ${snapshot.primaryPath} by default; run fab doctor --fix to migrate.`
|
|
809
|
+
};
|
|
810
|
+
}
|
|
678
811
|
if (snapshot.lastEntryTs === null || snapshot.lastEntryAgeMs === null) {
|
|
679
812
|
return {
|
|
680
813
|
name: "Intent ledger",
|
|
@@ -714,13 +847,13 @@ function createAuditCheck(report) {
|
|
|
714
847
|
return {
|
|
715
848
|
name: "Rules fetch audit",
|
|
716
849
|
status: report.mode === "strict" ? "error" : "warn",
|
|
717
|
-
message: `${report.violationCount} edit path${report.violationCount === 1 ? "" : "s"} lack a preceding
|
|
850
|
+
message: `${report.violationCount} edit path${report.violationCount === 1 ? "" : "s"} lack a preceding rule_selection or get_rules event within ${formatDuration(report.windowMs)}.`
|
|
718
851
|
};
|
|
719
852
|
}
|
|
720
853
|
return {
|
|
721
854
|
name: "Rules fetch audit",
|
|
722
855
|
status: "ok",
|
|
723
|
-
message: `All ${report.checkedPathCount} audited edit path${report.checkedPathCount === 1 ? "" : "s"} have a preceding
|
|
856
|
+
message: `All ${report.checkedPathCount} audited edit path${report.checkedPathCount === 1 ? "" : "s"} have a preceding rule_selection or get_rules event within ${formatDuration(report.windowMs)}.`
|
|
724
857
|
};
|
|
725
858
|
}
|
|
726
859
|
async function readSavedForensic(projectRoot) {
|
|
@@ -756,8 +889,9 @@ async function inspectMetaRevision(projectRoot) {
|
|
|
756
889
|
const meta = await readAgentsMeta(projectRoot);
|
|
757
890
|
const entries = Object.entries(meta.nodes).sort(([left], [right]) => left.localeCompare(right));
|
|
758
891
|
const missingFiles = [];
|
|
892
|
+
const derivedIdentityFiles = [];
|
|
759
893
|
let driftCount = 0;
|
|
760
|
-
const revisionSource = entries.map(([, node]) => {
|
|
894
|
+
const revisionSource = entries.map(([id, node]) => {
|
|
761
895
|
const absolutePath = join5(projectRoot, node.file);
|
|
762
896
|
if (!existsSync(absolutePath)) {
|
|
763
897
|
missingFiles.push(node.file);
|
|
@@ -768,15 +902,19 @@ async function inspectMetaRevision(projectRoot) {
|
|
|
768
902
|
if (actualHash !== node.hash) {
|
|
769
903
|
driftCount += 1;
|
|
770
904
|
}
|
|
771
|
-
|
|
772
|
-
|
|
905
|
+
if (node.file !== ".fabric/bootstrap/README.md" && node.identity_source !== "declared") {
|
|
906
|
+
derivedIdentityFiles.push(node.file);
|
|
907
|
+
}
|
|
908
|
+
return [id, actualHash, node.stable_id ?? "", node.identity_source ?? ""].join("|");
|
|
909
|
+
}).join("\n");
|
|
773
910
|
const revision = sha2562(revisionSource);
|
|
774
911
|
return {
|
|
775
912
|
present: true,
|
|
776
913
|
revision: meta.revision,
|
|
777
914
|
nodeCount: entries.length,
|
|
778
915
|
driftCount: revision === meta.revision ? driftCount : Math.max(driftCount, 1),
|
|
779
|
-
missingFiles
|
|
916
|
+
missingFiles,
|
|
917
|
+
derivedIdentityFiles
|
|
780
918
|
};
|
|
781
919
|
} catch (error) {
|
|
782
920
|
return {
|
|
@@ -785,6 +923,7 @@ async function inspectMetaRevision(projectRoot) {
|
|
|
785
923
|
nodeCount: 0,
|
|
786
924
|
driftCount: 0,
|
|
787
925
|
missingFiles: [],
|
|
926
|
+
derivedIdentityFiles: [],
|
|
788
927
|
unexpectedError: error instanceof Error ? error.message : String(error)
|
|
789
928
|
};
|
|
790
929
|
}
|
|
@@ -815,6 +954,7 @@ async function inspectHumanLock(projectRoot) {
|
|
|
815
954
|
}
|
|
816
955
|
}
|
|
817
956
|
async function inspectLedger(projectRoot) {
|
|
957
|
+
const paths = await resolveLedgerPaths(projectRoot);
|
|
818
958
|
const entries = await readLedger(projectRoot);
|
|
819
959
|
const lastEntry = entries.reduce(
|
|
820
960
|
(latest, entry) => latest === null || entry.ts > latest ? entry.ts : latest,
|
|
@@ -823,16 +963,19 @@ async function inspectLedger(projectRoot) {
|
|
|
823
963
|
return {
|
|
824
964
|
count: entries.length,
|
|
825
965
|
lastEntryTs: lastEntry,
|
|
826
|
-
lastEntryAgeMs: lastEntry === null ? null : Math.max(Date.now() - lastEntry, 0)
|
|
966
|
+
lastEntryAgeMs: lastEntry === null ? null : Math.max(Date.now() - lastEntry, 0),
|
|
967
|
+
primaryPath: paths.primaryPath,
|
|
968
|
+
legacyPath: paths.legacyPath,
|
|
969
|
+
usingLegacy: paths.usingLegacy
|
|
827
970
|
};
|
|
828
971
|
}
|
|
829
|
-
function collectAuditViolations(projectRoot, ledgerEntries,
|
|
972
|
+
function collectAuditViolations(projectRoot, ledgerEntries, ruleAccessEntries, windowMs) {
|
|
830
973
|
let checkedPathCount = 0;
|
|
831
974
|
const violations = [];
|
|
832
975
|
for (const entry of ledgerEntries) {
|
|
833
976
|
for (const affectedPath of entry.affected_paths) {
|
|
834
977
|
const normalizedPath = normalizeAuditPath(projectRoot, affectedPath);
|
|
835
|
-
const matched =
|
|
978
|
+
const matched = findPrecedingRuleAccessEvent(ruleAccessEntries, normalizedPath, entry.ts, windowMs);
|
|
836
979
|
checkedPathCount += 1;
|
|
837
980
|
if (matched !== null) {
|
|
838
981
|
continue;
|
|
@@ -841,7 +984,7 @@ function collectAuditViolations(projectRoot, ledgerEntries, getRulesEntries, win
|
|
|
841
984
|
editTs: entry.ts,
|
|
842
985
|
entryId: entry.id,
|
|
843
986
|
intent: entry.intent,
|
|
844
|
-
|
|
987
|
+
lastRuleAccessTs: findLatestRuleAccessTs(ruleAccessEntries, normalizedPath, entry.ts),
|
|
845
988
|
path: normalizedPath
|
|
846
989
|
});
|
|
847
990
|
}
|
|
@@ -851,16 +994,49 @@ function collectAuditViolations(projectRoot, ledgerEntries, getRulesEntries, win
|
|
|
851
994
|
violations
|
|
852
995
|
};
|
|
853
996
|
}
|
|
854
|
-
function
|
|
997
|
+
function findPrecedingRuleAccessEvent(entries, path, ts, windowMs) {
|
|
998
|
+
const getRulesMatch = findPrecedingGetRulesEvent(entries.filter(isGetRulesAuditEntry2), path, ts, windowMs);
|
|
999
|
+
const ruleSelectionMatch = findPrecedingRuleSelectionEvent(entries.filter(isRuleSelectionAuditEntry), path, ts, windowMs);
|
|
1000
|
+
if (getRulesMatch === null) {
|
|
1001
|
+
return ruleSelectionMatch;
|
|
1002
|
+
}
|
|
1003
|
+
if (ruleSelectionMatch === null) {
|
|
1004
|
+
return getRulesMatch;
|
|
1005
|
+
}
|
|
1006
|
+
return getRulesMatch.ts >= ruleSelectionMatch.ts ? getRulesMatch : ruleSelectionMatch;
|
|
1007
|
+
}
|
|
1008
|
+
function findPrecedingRuleSelectionEvent(entries, path, ts, windowMs) {
|
|
1009
|
+
let matched = null;
|
|
1010
|
+
for (const entry of entries) {
|
|
1011
|
+
if (!entry.target_paths.includes(path) && entry.path !== path) {
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
if (entry.ts > ts || ts - entry.ts > windowMs) {
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
if (matched === null || entry.ts > matched.ts) {
|
|
1018
|
+
matched = entry;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return matched;
|
|
1022
|
+
}
|
|
1023
|
+
function findLatestRuleAccessTs(entries, path, ts) {
|
|
855
1024
|
let latest = null;
|
|
856
1025
|
for (const entry of entries) {
|
|
857
|
-
|
|
1026
|
+
const matchesPath = entry.event === "rule_selection" ? entry.path === path || entry.target_paths.includes(path) : entry.path === path;
|
|
1027
|
+
if (!matchesPath || entry.ts > ts) {
|
|
858
1028
|
continue;
|
|
859
1029
|
}
|
|
860
1030
|
latest = latest === null || entry.ts > latest ? entry.ts : latest;
|
|
861
1031
|
}
|
|
862
1032
|
return latest;
|
|
863
1033
|
}
|
|
1034
|
+
function isGetRulesAuditEntry2(entry) {
|
|
1035
|
+
return entry.event === "get_rules";
|
|
1036
|
+
}
|
|
1037
|
+
function isRuleSelectionAuditEntry(entry) {
|
|
1038
|
+
return entry.event === "rule_selection";
|
|
1039
|
+
}
|
|
864
1040
|
function readDoctorAuditMode(projectRoot) {
|
|
865
1041
|
const configPath = join5(projectRoot, "fabric.config.json");
|
|
866
1042
|
try {
|
|
@@ -1106,56 +1282,76 @@ async function loadGetRulesContext(projectRoot) {
|
|
|
1106
1282
|
return context;
|
|
1107
1283
|
}
|
|
1108
1284
|
async function resolveRulesForPath(projectRoot, context, path, options = {}) {
|
|
1109
|
-
const
|
|
1110
|
-
const
|
|
1111
|
-
return
|
|
1112
|
-
L0: context.l0Content,
|
|
1113
|
-
L1,
|
|
1114
|
-
L2,
|
|
1115
|
-
human_locked_nearby: context.humanLockedNearby,
|
|
1116
|
-
description_stubs: stubs.length > 0 ? dedupeDescriptionStubsByPath(stubs) : void 0
|
|
1117
|
-
};
|
|
1285
|
+
const matchedNodes = matchRuleNodes(context.meta, path);
|
|
1286
|
+
const loaded = await loadMatchedRules(projectRoot, matchedNodes);
|
|
1287
|
+
return buildRulesPayload(context, loaded, options);
|
|
1118
1288
|
}
|
|
1119
1289
|
function normalizeRulesPath(value) {
|
|
1120
1290
|
return value.replaceAll("\\", "/");
|
|
1121
1291
|
}
|
|
1122
|
-
function
|
|
1123
|
-
if (nodeId.startsWith("L1/")) {
|
|
1124
|
-
return "L1";
|
|
1125
|
-
}
|
|
1126
|
-
if (nodeId.startsWith("L2/")) {
|
|
1127
|
-
return "L2";
|
|
1128
|
-
}
|
|
1129
|
-
return null;
|
|
1130
|
-
}
|
|
1131
|
-
async function loadRulesForPath(projectRoot, meta, path) {
|
|
1292
|
+
function matchRuleNodes(meta, path) {
|
|
1132
1293
|
const requestedPath = normalizeRulesPath(path);
|
|
1133
|
-
|
|
1294
|
+
return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
|
|
1134
1295
|
const [leftId, leftNode] = left;
|
|
1135
1296
|
const [rightId, rightNode] = right;
|
|
1136
1297
|
const priorityDelta = PRIORITY_ORDER[leftNode.priority] - PRIORITY_ORDER[rightNode.priority];
|
|
1137
1298
|
return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
|
|
1138
|
-
})
|
|
1299
|
+
}).map(([nodeId, node]) => ({
|
|
1300
|
+
node_id: nodeId,
|
|
1301
|
+
level: classifyNode(nodeId, node),
|
|
1302
|
+
stable_id: node.stable_id ?? nodeId,
|
|
1303
|
+
identity_source: node.identity_source ?? "derived",
|
|
1304
|
+
node
|
|
1305
|
+
}));
|
|
1306
|
+
}
|
|
1307
|
+
async function loadMatchedRules(projectRoot, matchedNodes, fileContentCache = /* @__PURE__ */ new Map()) {
|
|
1139
1308
|
const rules = [];
|
|
1140
1309
|
const stubs = [];
|
|
1141
|
-
for (const
|
|
1142
|
-
if (
|
|
1310
|
+
for (const matchedNode of matchedNodes) {
|
|
1311
|
+
if (matchedNode.level === null) {
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
if (matchedNode.node.activation?.tier === "description") {
|
|
1143
1315
|
stubs.push({
|
|
1144
|
-
|
|
1145
|
-
|
|
1316
|
+
stable_id: matchedNode.stable_id,
|
|
1317
|
+
identity_source: matchedNode.identity_source,
|
|
1318
|
+
level: matchedNode.level,
|
|
1319
|
+
path: matchedNode.node.file,
|
|
1320
|
+
description: matchedNode.node.activation.description ?? ""
|
|
1146
1321
|
});
|
|
1147
1322
|
continue;
|
|
1148
1323
|
}
|
|
1149
1324
|
rules.push({
|
|
1150
|
-
level:
|
|
1325
|
+
level: matchedNode.level,
|
|
1326
|
+
stable_id: matchedNode.stable_id,
|
|
1327
|
+
identity_source: matchedNode.identity_source,
|
|
1151
1328
|
entry: {
|
|
1152
|
-
path: node.file,
|
|
1153
|
-
content: await
|
|
1329
|
+
path: matchedNode.node.file,
|
|
1330
|
+
content: await readRuleContent(projectRoot, matchedNode.node.file, fileContentCache)
|
|
1154
1331
|
}
|
|
1155
1332
|
});
|
|
1156
1333
|
}
|
|
1157
1334
|
return { rules, stubs };
|
|
1158
1335
|
}
|
|
1336
|
+
function buildRulesPayload(context, loaded, options = {}) {
|
|
1337
|
+
const { L1, L2 } = partitionRulesByLevel(loaded.rules, options.dedupeByPath ?? false);
|
|
1338
|
+
return {
|
|
1339
|
+
L0: context.l0Content,
|
|
1340
|
+
L1,
|
|
1341
|
+
L2,
|
|
1342
|
+
human_locked_nearby: context.humanLockedNearby,
|
|
1343
|
+
description_stubs: loaded.stubs.length > 0 ? dedupeDescriptionStubsByPath(loaded.stubs).map(toDescriptionStub) : void 0
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
function classifyNode(nodeId, node) {
|
|
1347
|
+
if (nodeId.startsWith("L1/")) {
|
|
1348
|
+
return "L1";
|
|
1349
|
+
}
|
|
1350
|
+
if (nodeId.startsWith("L2/")) {
|
|
1351
|
+
return "L2";
|
|
1352
|
+
}
|
|
1353
|
+
return node.layer === "L0" ? null : node.layer;
|
|
1354
|
+
}
|
|
1159
1355
|
function partitionRulesByLevel(loadedRules, dedupeByPath) {
|
|
1160
1356
|
const l1 = [];
|
|
1161
1357
|
const l2 = [];
|
|
@@ -1204,6 +1400,21 @@ function dedupeDescriptionStubsByPath(stubs) {
|
|
|
1204
1400
|
return true;
|
|
1205
1401
|
});
|
|
1206
1402
|
}
|
|
1403
|
+
function toDescriptionStub(stub) {
|
|
1404
|
+
return {
|
|
1405
|
+
path: stub.path,
|
|
1406
|
+
description: stub.description
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
async function readRuleContent(projectRoot, file, fileContentCache) {
|
|
1410
|
+
const cached = fileContentCache.get(file);
|
|
1411
|
+
if (cached !== void 0) {
|
|
1412
|
+
return await cached;
|
|
1413
|
+
}
|
|
1414
|
+
const pending = readFile5(join6(projectRoot, file), "utf8");
|
|
1415
|
+
fileContentCache.set(file, pending);
|
|
1416
|
+
return await pending;
|
|
1417
|
+
}
|
|
1207
1418
|
|
|
1208
1419
|
export {
|
|
1209
1420
|
AGENTS_MD_RESOURCE_URI,
|
|
@@ -1213,18 +1424,24 @@ export {
|
|
|
1213
1424
|
resolveProjectRoot,
|
|
1214
1425
|
readAgentsMeta,
|
|
1215
1426
|
FABRIC_DIR,
|
|
1427
|
+
LEDGER_PATH,
|
|
1428
|
+
LEGACY_LEDGER_PATH,
|
|
1216
1429
|
atomicWriteText,
|
|
1430
|
+
getLedgerPath,
|
|
1431
|
+
getLegacyLedgerPath,
|
|
1432
|
+
ensureParentDirectory,
|
|
1217
1433
|
sha256,
|
|
1434
|
+
appendRuleSelectionAuditEvent,
|
|
1218
1435
|
appendEditIntentAuditEvents,
|
|
1436
|
+
resolveLedgerPaths,
|
|
1219
1437
|
readLedger,
|
|
1220
1438
|
appendLedgerEntry,
|
|
1221
1439
|
readHumanLock,
|
|
1222
1440
|
readHumanLockEntry,
|
|
1223
1441
|
getRules,
|
|
1224
|
-
loadGetRulesContext,
|
|
1225
|
-
resolveRulesForPath,
|
|
1226
1442
|
normalizeRulesPath,
|
|
1227
1443
|
runDoctorReport,
|
|
1444
|
+
runDoctorFix,
|
|
1228
1445
|
runDoctorAuditReport,
|
|
1229
1446
|
approveHumanLock
|
|
1230
1447
|
};
|