@fenglimg/fabric-server 1.5.2 → 1.7.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-PTFSYO4Y.js +2083 -0
- package/dist/{http-U4S7BUP4.js → http-6LFZLHCN.js} +270 -199
- package/dist/index.d.ts +42 -60
- package/dist/index.js +141 -431
- package/dist/static/assets/index-DEc8gkD_.js +10 -0
- package/dist/static/assets/index-FoBU5Kta.css +1 -0
- package/dist/static/index.html +9 -7
- package/package.json +3 -3
- package/dist/chunk-4G6VFG5N.js +0 -1372
- package/dist/static/assets/index-B5hhHHl2.css +0 -1
- package/dist/static/assets/index-D22ASzsl.js +0 -14
|
@@ -0,0 +1,2083 @@
|
|
|
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 LEDGER_FILE = ".intent-ledger.jsonl";
|
|
91
|
+
var LEDGER_PATH = `${FABRIC_DIR}/${LEDGER_FILE}`;
|
|
92
|
+
var LEGACY_LEDGER_PATH = LEDGER_FILE;
|
|
93
|
+
var EVENT_LEDGER_FILE = "events.jsonl";
|
|
94
|
+
var EVENT_LEDGER_PATH = `${FABRIC_DIR}/${EVENT_LEDGER_FILE}`;
|
|
95
|
+
async function atomicWriteText(path, content) {
|
|
96
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
97
|
+
await writeFile(tempPath, content, "utf8");
|
|
98
|
+
await rename(tempPath, path);
|
|
99
|
+
}
|
|
100
|
+
function getLedgerPath(projectRoot) {
|
|
101
|
+
return join(projectRoot, LEDGER_PATH);
|
|
102
|
+
}
|
|
103
|
+
function getLegacyLedgerPath(projectRoot) {
|
|
104
|
+
return join(projectRoot, LEGACY_LEDGER_PATH);
|
|
105
|
+
}
|
|
106
|
+
function getEventLedgerPath(projectRoot) {
|
|
107
|
+
return join(projectRoot, EVENT_LEDGER_PATH);
|
|
108
|
+
}
|
|
109
|
+
async function ensureParentDirectory(path) {
|
|
110
|
+
await mkdir(dirname(path), { recursive: true });
|
|
111
|
+
}
|
|
112
|
+
function sha256(content) {
|
|
113
|
+
return `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
114
|
+
}
|
|
115
|
+
function isNodeError(error) {
|
|
116
|
+
return error instanceof Error;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/services/event-ledger.ts
|
|
120
|
+
import { randomUUID } from "crypto";
|
|
121
|
+
import { appendFile, readFile } from "fs/promises";
|
|
122
|
+
import {
|
|
123
|
+
eventLedgerEventSchema
|
|
124
|
+
} from "@fenglimg/fabric-shared";
|
|
125
|
+
async function appendEventLedgerEvent(projectRoot, event) {
|
|
126
|
+
const eventPath = getEventLedgerPath(projectRoot);
|
|
127
|
+
const nextEvent = eventLedgerEventSchema.parse({
|
|
128
|
+
...event,
|
|
129
|
+
kind: "fabric-event",
|
|
130
|
+
id: event.id ?? `event:${randomUUID()}`,
|
|
131
|
+
ts: event.ts ?? Date.now(),
|
|
132
|
+
schema_version: 1
|
|
133
|
+
});
|
|
134
|
+
await ensureParentDirectory(eventPath);
|
|
135
|
+
await appendFile(eventPath, `${JSON.stringify(nextEvent)}
|
|
136
|
+
`, "utf8");
|
|
137
|
+
return nextEvent;
|
|
138
|
+
}
|
|
139
|
+
async function readEventLedger(projectRoot, options = {}) {
|
|
140
|
+
const eventPath = getEventLedgerPath(projectRoot);
|
|
141
|
+
let raw;
|
|
142
|
+
try {
|
|
143
|
+
raw = await readFile(eventPath, "utf8");
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
return raw.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0).map((line, index) => parseEventLedgerLine(line, index)).filter((entry) => entry !== null).filter((entry) => options.event_type === void 0 || entry.event_type === options.event_type).filter((entry) => options.since === void 0 || entry.ts >= options.since).filter((entry) => options.correlation_id === void 0 || entry.correlation_id === options.correlation_id).filter((entry) => options.session_id === void 0 || entry.session_id === options.session_id);
|
|
151
|
+
}
|
|
152
|
+
function parseEventLedgerLine(line, index) {
|
|
153
|
+
try {
|
|
154
|
+
const parsed = JSON.parse(line);
|
|
155
|
+
const result = eventLedgerEventSchema.safeParse(parsed);
|
|
156
|
+
if (!result.success) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
...result.data,
|
|
161
|
+
id: result.data.id || createDerivedId(index, line)
|
|
162
|
+
};
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function createDerivedId(index, line) {
|
|
168
|
+
return `event:${index + 1}:${sha256(line).slice("sha256:".length)}`;
|
|
169
|
+
}
|
|
170
|
+
function isNodeError2(error) {
|
|
171
|
+
return error instanceof Error;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/services/rule-meta-builder.ts
|
|
175
|
+
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
176
|
+
import { existsSync, statSync } from "fs";
|
|
177
|
+
import { isAbsolute, join as join2, relative, resolve as resolve2, sep as sep2 } from "path";
|
|
178
|
+
import {
|
|
179
|
+
RULE_TEST_INDEX_SCHEMA_VERSION,
|
|
180
|
+
agentsMetaSchema,
|
|
181
|
+
deriveAgentsMetaLayer,
|
|
182
|
+
deriveAgentsMetaStableId,
|
|
183
|
+
deriveAgentsMetaTopologyType,
|
|
184
|
+
ruleTestIndexSchema
|
|
185
|
+
} from "@fenglimg/fabric-shared";
|
|
186
|
+
async function buildRuleMeta(projectRootInput) {
|
|
187
|
+
const projectRoot = normalizeProjectRoot(projectRootInput);
|
|
188
|
+
assertExistingDirectory(projectRoot);
|
|
189
|
+
const metaPath = join2(projectRoot, ".fabric", "agents.meta.json");
|
|
190
|
+
const ruleTestIndexPath = join2(projectRoot, ".fabric", "rule-test.index.json");
|
|
191
|
+
const existingMeta = await readExistingMeta(metaPath);
|
|
192
|
+
const existingRuleTestIndex = await readExistingRuleTestIndex(ruleTestIndexPath);
|
|
193
|
+
const meta = await computeRulesBasedAgentsMeta(projectRoot, existingMeta);
|
|
194
|
+
const ruleTestIndex = await computeRuleTestIndex(projectRoot, meta, existingRuleTestIndex);
|
|
195
|
+
return {
|
|
196
|
+
meta,
|
|
197
|
+
ruleTestIndex,
|
|
198
|
+
changed: existingMeta === void 0 || stableStringify(existingMeta) !== stableStringify(meta) || existingRuleTestIndex === void 0 || !isSameRuleTestIndex(existingRuleTestIndex, ruleTestIndex)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async function writeRuleMeta(projectRootInput, options) {
|
|
202
|
+
const projectRoot = normalizeProjectRoot(projectRootInput);
|
|
203
|
+
const metaPath = join2(projectRoot, ".fabric", "agents.meta.json");
|
|
204
|
+
const ruleTestIndexPath = join2(projectRoot, ".fabric", "rule-test.index.json");
|
|
205
|
+
const existingMeta = await readExistingMeta(metaPath);
|
|
206
|
+
const result = await buildRuleMeta(projectRoot);
|
|
207
|
+
if (!result.changed) {
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
await ensureParentDirectory(metaPath);
|
|
211
|
+
await atomicWriteText(metaPath, `${JSON.stringify(result.meta, null, 2)}
|
|
212
|
+
`);
|
|
213
|
+
await atomicWriteText(ruleTestIndexPath, `${JSON.stringify(result.ruleTestIndex, null, 2)}
|
|
214
|
+
`);
|
|
215
|
+
if (existingMeta === void 0 || stableStringify(existingMeta) !== stableStringify(result.meta)) {
|
|
216
|
+
await recordBaselineSynced(projectRoot, {
|
|
217
|
+
previousRevision: existingMeta?.revision,
|
|
218
|
+
revision: result.meta.revision,
|
|
219
|
+
syncedFiles: collectSyncedFiles(existingMeta, result.meta),
|
|
220
|
+
acceptedStableIds: collectStableIds(result.meta),
|
|
221
|
+
driftDetails: collectDriftDetails(existingMeta, result.meta),
|
|
222
|
+
source: options.source
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
async function computeRulesBasedAgentsMeta(projectRootInput, existingMeta) {
|
|
228
|
+
const projectRoot = normalizeProjectRoot(projectRootInput);
|
|
229
|
+
assertExistingDirectory(projectRoot);
|
|
230
|
+
const previousMeta = existingMeta ?? await readExistingMeta(join2(projectRoot, ".fabric", "agents.meta.json"));
|
|
231
|
+
const existingByContentRef = indexExistingNodesByContentRef(previousMeta);
|
|
232
|
+
const ruleFiles = await findFabricRuleFiles(projectRoot);
|
|
233
|
+
const nodes = {};
|
|
234
|
+
const bootstrapNode = await createBootstrapNode(projectRoot, existingByContentRef.get(".fabric/bootstrap/README.md")?.node);
|
|
235
|
+
if (bootstrapNode !== void 0) {
|
|
236
|
+
nodes.L0 = bootstrapNode;
|
|
237
|
+
}
|
|
238
|
+
for (const contentRef of ruleFiles) {
|
|
239
|
+
const source = await readFile2(join2(projectRoot, contentRef), "utf8");
|
|
240
|
+
const existing = existingByContentRef.get(contentRef);
|
|
241
|
+
const id = deriveNodeId(contentRef);
|
|
242
|
+
const hash = sha256(source);
|
|
243
|
+
const defaults = createDefaultNodeMeta(contentRef);
|
|
244
|
+
const identity = deriveRuleIdentity(contentRef, source, existing?.node);
|
|
245
|
+
nodes[id] = {
|
|
246
|
+
...defaults,
|
|
247
|
+
...existing?.node,
|
|
248
|
+
file: contentRef,
|
|
249
|
+
content_ref: contentRef,
|
|
250
|
+
hash,
|
|
251
|
+
stable_id: identity.stableId,
|
|
252
|
+
identity_source: identity.identitySource,
|
|
253
|
+
description: extractRuleDescription(source) ?? existing?.node.description,
|
|
254
|
+
sections: extractRuleSections(source)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
...previousMeta ?? {},
|
|
259
|
+
revision: computeRevision(nodes),
|
|
260
|
+
nodes: sortNodes(nodes)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
async function computeRuleTestIndex(projectRootInput, computedMeta, previousIndex) {
|
|
264
|
+
const projectRoot = normalizeProjectRoot(projectRootInput);
|
|
265
|
+
assertExistingDirectory(projectRoot);
|
|
266
|
+
const previousLinks = indexPreviousRuleTestEntries(previousIndex?.links ?? []);
|
|
267
|
+
const previousOrphans = indexPreviousRuleTestEntries(previousIndex?.orphan_annotations ?? []);
|
|
268
|
+
const rulesByStableId = indexRulesByStableId(computedMeta);
|
|
269
|
+
const links = [];
|
|
270
|
+
const orphanAnnotations = [];
|
|
271
|
+
for (const annotation of await findFabricVerifyAnnotations(projectRoot)) {
|
|
272
|
+
const rule = rulesByStableId.get(annotation.stableId);
|
|
273
|
+
const key = createRuleTestEntryKey(annotation.stableId, annotation.testFile, annotation.line);
|
|
274
|
+
if (rule === void 0) {
|
|
275
|
+
const previous2 = previousOrphans.get(key) ?? previousLinks.get(key);
|
|
276
|
+
orphanAnnotations.push({
|
|
277
|
+
rule_stable_id: annotation.stableId,
|
|
278
|
+
test_file: annotation.testFile,
|
|
279
|
+
test_hash: annotation.testHash,
|
|
280
|
+
previous_test_hash: getPreviousTestHash(previous2, annotation.testHash),
|
|
281
|
+
annotation_line: annotation.line
|
|
282
|
+
});
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const previous = previousLinks.get(key) ?? previousOrphans.get(key);
|
|
286
|
+
const previousHashes = getPreviousRuleTestHashes(previous, rule.hash, annotation.testHash);
|
|
287
|
+
links.push({
|
|
288
|
+
rule_stable_id: annotation.stableId,
|
|
289
|
+
rule_file: rule.content_ref ?? rule.file,
|
|
290
|
+
rule_hash: rule.hash,
|
|
291
|
+
previous_rule_hash: previousHashes.previousRuleHash,
|
|
292
|
+
test_file: annotation.testFile,
|
|
293
|
+
test_hash: annotation.testHash,
|
|
294
|
+
previous_test_hash: previousHashes.previousTestHash,
|
|
295
|
+
annotation_line: annotation.line
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
schema_version: RULE_TEST_INDEX_SCHEMA_VERSION,
|
|
300
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
301
|
+
revision: computedMeta.revision,
|
|
302
|
+
previous_revision: previousIndex?.revision !== void 0 && previousIndex.revision !== computedMeta.revision ? previousIndex.revision : previousIndex?.previous_revision,
|
|
303
|
+
links: links.sort(compareRuleTestEntries),
|
|
304
|
+
orphan_annotations: orphanAnnotations.sort(compareRuleTestEntries)
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function deriveRuleMetaLayer(relativePath) {
|
|
308
|
+
return deriveAgentsMetaLayer(toAgentsCompatiblePath(relativePath));
|
|
309
|
+
}
|
|
310
|
+
function deriveRuleMetaTopologyType(relativePath) {
|
|
311
|
+
return deriveAgentsMetaTopologyType(toAgentsCompatiblePath(relativePath));
|
|
312
|
+
}
|
|
313
|
+
function isSameRuleTestIndex(left, right) {
|
|
314
|
+
return stableStringify(toComparableRuleTestIndex(left)) === stableStringify(toComparableRuleTestIndex(right));
|
|
315
|
+
}
|
|
316
|
+
function stableStringify(value) {
|
|
317
|
+
return JSON.stringify(value, Object.keys(flattenKeys(value)).sort());
|
|
318
|
+
}
|
|
319
|
+
function normalizeProjectRoot(projectRoot) {
|
|
320
|
+
return isAbsolute(projectRoot) ? projectRoot : resolve2(process.cwd(), projectRoot);
|
|
321
|
+
}
|
|
322
|
+
function assertExistingDirectory(projectRoot) {
|
|
323
|
+
if (!existsSync(projectRoot) || !statSync(projectRoot).isDirectory()) {
|
|
324
|
+
throw new Error(`Target directory does not exist: ${projectRoot}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function readExistingMeta(metaPath) {
|
|
328
|
+
let raw;
|
|
329
|
+
try {
|
|
330
|
+
raw = await readFile2(metaPath, "utf8");
|
|
331
|
+
} catch (error) {
|
|
332
|
+
if (isNodeError3(error) && error.code === "ENOENT") {
|
|
333
|
+
return void 0;
|
|
334
|
+
}
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
return agentsMetaSchema.parse(JSON.parse(raw));
|
|
339
|
+
} catch {
|
|
340
|
+
return void 0;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function readExistingRuleTestIndex(indexPath) {
|
|
344
|
+
let raw;
|
|
345
|
+
try {
|
|
346
|
+
raw = await readFile2(indexPath, "utf8");
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (isNodeError3(error) && error.code === "ENOENT") {
|
|
349
|
+
return void 0;
|
|
350
|
+
}
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
return ruleTestIndexSchema.parse(JSON.parse(raw));
|
|
355
|
+
} catch {
|
|
356
|
+
return void 0;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function findFabricRuleFiles(projectRoot) {
|
|
360
|
+
const rulesRoot = join2(projectRoot, ".fabric", "rules");
|
|
361
|
+
if (!existsSync(rulesRoot) || !statSync(rulesRoot).isDirectory()) {
|
|
362
|
+
return [];
|
|
363
|
+
}
|
|
364
|
+
const files = [];
|
|
365
|
+
const stack = [rulesRoot];
|
|
366
|
+
while (stack.length > 0) {
|
|
367
|
+
const current = stack.pop();
|
|
368
|
+
if (current === void 0) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
372
|
+
const absolutePath = join2(current, entry.name);
|
|
373
|
+
const relativePath = toPosixPath(relative(projectRoot, absolutePath));
|
|
374
|
+
if (entry.isDirectory()) {
|
|
375
|
+
stack.push(absolutePath);
|
|
376
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
377
|
+
files.push(relativePath);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return files.sort();
|
|
382
|
+
}
|
|
383
|
+
async function findFabricVerifyAnnotations(projectRoot) {
|
|
384
|
+
const files = await findTestFiles(projectRoot);
|
|
385
|
+
const annotations = [];
|
|
386
|
+
const annotationPattern = /^\s*\/\/\s*@fabric-verify\s+([A-Za-z0-9][A-Za-z0-9/_-]*)\s*$/u;
|
|
387
|
+
for (const testFile of files) {
|
|
388
|
+
const source = await readFile2(join2(projectRoot, testFile), "utf8");
|
|
389
|
+
const testHash = sha256(source);
|
|
390
|
+
const lines = source.split(/\r?\n/u);
|
|
391
|
+
for (const [index, line] of lines.entries()) {
|
|
392
|
+
const match = annotationPattern.exec(line);
|
|
393
|
+
if (match === null) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
annotations.push({
|
|
397
|
+
stableId: match[1],
|
|
398
|
+
testFile,
|
|
399
|
+
testHash,
|
|
400
|
+
line: index + 1
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return annotations.sort(compareAnnotationEntries);
|
|
405
|
+
}
|
|
406
|
+
async function findTestFiles(projectRoot) {
|
|
407
|
+
const ignoredRootSegments = /* @__PURE__ */ new Set([".git", ".fabric", "node_modules", "dist", "build", "coverage"]);
|
|
408
|
+
const files = [];
|
|
409
|
+
const stack = [projectRoot];
|
|
410
|
+
while (stack.length > 0) {
|
|
411
|
+
const current = stack.pop();
|
|
412
|
+
if (current === void 0) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
416
|
+
const absolutePath = join2(current, entry.name);
|
|
417
|
+
const relativePath = toPosixPath(relative(projectRoot, absolutePath));
|
|
418
|
+
const [rootSegment] = relativePath.split("/");
|
|
419
|
+
if (entry.isDirectory()) {
|
|
420
|
+
if (!ignoredRootSegments.has(rootSegment) && !ignoredRootSegments.has(entry.name)) {
|
|
421
|
+
stack.push(absolutePath);
|
|
422
|
+
}
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (entry.isFile() && isTestFile(relativePath)) {
|
|
426
|
+
files.push(relativePath);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return files.sort();
|
|
431
|
+
}
|
|
432
|
+
function isTestFile(relativePath) {
|
|
433
|
+
return /\.(?:test|spec)\.[cm]?[jt]sx?$/u.test(relativePath);
|
|
434
|
+
}
|
|
435
|
+
function indexRulesByStableId(meta) {
|
|
436
|
+
const rules = /* @__PURE__ */ new Map();
|
|
437
|
+
for (const node of Object.values(meta.nodes)) {
|
|
438
|
+
if (node.stable_id !== void 0) {
|
|
439
|
+
rules.set(node.stable_id, node);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return rules;
|
|
443
|
+
}
|
|
444
|
+
function indexPreviousRuleTestEntries(entries) {
|
|
445
|
+
const previous = /* @__PURE__ */ new Map();
|
|
446
|
+
for (const entry of entries) {
|
|
447
|
+
previous.set(createRuleTestEntryKey(entry.rule_stable_id, entry.test_file, entry.annotation_line), {
|
|
448
|
+
rule_hash: entry.rule_hash,
|
|
449
|
+
previous_rule_hash: entry.previous_rule_hash,
|
|
450
|
+
test_hash: entry.test_hash,
|
|
451
|
+
previous_test_hash: entry.previous_test_hash
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
return previous;
|
|
455
|
+
}
|
|
456
|
+
function createRuleTestEntryKey(stableId, testFile, line) {
|
|
457
|
+
return `${stableId}\0${testFile}\0${line}`;
|
|
458
|
+
}
|
|
459
|
+
function getPreviousRuleTestHashes(previous, ruleHash, testHash) {
|
|
460
|
+
if (previous === void 0) {
|
|
461
|
+
return {};
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
previousRuleHash: previous.rule_hash !== void 0 && previous.rule_hash !== ruleHash ? previous.rule_hash : previous.previous_rule_hash,
|
|
465
|
+
previousTestHash: previous.test_hash !== testHash ? previous.test_hash : previous.previous_test_hash
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function getPreviousTestHash(previous, testHash) {
|
|
469
|
+
if (previous === void 0) {
|
|
470
|
+
return void 0;
|
|
471
|
+
}
|
|
472
|
+
return previous.test_hash !== testHash ? previous.test_hash : previous.previous_test_hash;
|
|
473
|
+
}
|
|
474
|
+
function compareRuleTestEntries(left, right) {
|
|
475
|
+
return left.rule_stable_id.localeCompare(right.rule_stable_id) || left.test_file.localeCompare(right.test_file) || left.annotation_line - right.annotation_line;
|
|
476
|
+
}
|
|
477
|
+
function compareAnnotationEntries(left, right) {
|
|
478
|
+
return left.stableId.localeCompare(right.stableId) || left.testFile.localeCompare(right.testFile) || left.line - right.line;
|
|
479
|
+
}
|
|
480
|
+
function toComparableRuleTestIndex(index) {
|
|
481
|
+
const { generated_at: _generatedAt, ...comparable } = index;
|
|
482
|
+
return comparable;
|
|
483
|
+
}
|
|
484
|
+
function indexExistingNodesByContentRef(existingMeta) {
|
|
485
|
+
const byContentRef = /* @__PURE__ */ new Map();
|
|
486
|
+
for (const [id, node] of Object.entries(existingMeta?.nodes ?? {})) {
|
|
487
|
+
byContentRef.set(toPosixPath(node.content_ref ?? node.file), { id, node });
|
|
488
|
+
}
|
|
489
|
+
return byContentRef;
|
|
490
|
+
}
|
|
491
|
+
function deriveNodeId(file) {
|
|
492
|
+
if (file === ".fabric/bootstrap/README.md") {
|
|
493
|
+
return "L0";
|
|
494
|
+
}
|
|
495
|
+
const layer = deriveRuleMetaLayer(file);
|
|
496
|
+
const relativeStem = getRuleRelativeStem(file);
|
|
497
|
+
return `${layer}/${relativeStem}`;
|
|
498
|
+
}
|
|
499
|
+
function createDefaultNodeMeta(contentRef) {
|
|
500
|
+
const layer = deriveRuleMetaLayer(contentRef);
|
|
501
|
+
const topologyType = deriveRuleMetaTopologyType(contentRef);
|
|
502
|
+
return {
|
|
503
|
+
file: contentRef,
|
|
504
|
+
content_ref: contentRef,
|
|
505
|
+
scope_glob: deriveScopeGlob(contentRef),
|
|
506
|
+
deps: layer === "L0" ? [] : ["L0"],
|
|
507
|
+
priority: layer === "L0" ? "high" : "medium",
|
|
508
|
+
level: layer,
|
|
509
|
+
layer,
|
|
510
|
+
topology_type: topologyType,
|
|
511
|
+
hash: ""
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
async function createBootstrapNode(projectRoot, existing) {
|
|
515
|
+
const contentRef = ".fabric/bootstrap/README.md";
|
|
516
|
+
const bootstrapPath = join2(projectRoot, contentRef);
|
|
517
|
+
if (!existsSync(bootstrapPath)) {
|
|
518
|
+
return void 0;
|
|
519
|
+
}
|
|
520
|
+
const hash = sha256(await readFile2(bootstrapPath, "utf8"));
|
|
521
|
+
const identity = {
|
|
522
|
+
stableId: existing?.stable_id ?? deriveAgentsMetaStableId(contentRef),
|
|
523
|
+
identitySource: existing?.identity_source ?? "derived"
|
|
524
|
+
};
|
|
525
|
+
return {
|
|
526
|
+
...createDefaultNodeMeta(contentRef),
|
|
527
|
+
...existing,
|
|
528
|
+
file: contentRef,
|
|
529
|
+
content_ref: contentRef,
|
|
530
|
+
hash,
|
|
531
|
+
stable_id: identity.stableId,
|
|
532
|
+
identity_source: identity.identitySource
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function deriveScopeGlob(contentRef) {
|
|
536
|
+
if (contentRef === ".fabric/bootstrap/README.md") {
|
|
537
|
+
return "**";
|
|
538
|
+
}
|
|
539
|
+
const stem = getRuleRelativeStem(contentRef);
|
|
540
|
+
const segments = stem.split("/").filter(Boolean);
|
|
541
|
+
if (segments.length === 0 || stem === "root") {
|
|
542
|
+
return "**";
|
|
543
|
+
}
|
|
544
|
+
if (segments[0] === "_cross") {
|
|
545
|
+
return "**";
|
|
546
|
+
}
|
|
547
|
+
if (segments.at(-1) === "rules") {
|
|
548
|
+
segments.pop();
|
|
549
|
+
}
|
|
550
|
+
const scopePath = segments.join("/");
|
|
551
|
+
return scopePath === "" ? "**" : `${scopePath}/**`;
|
|
552
|
+
}
|
|
553
|
+
function getRuleRelativeStem(contentRef) {
|
|
554
|
+
return contentRef.replace(/^\.fabric\/rules\//u, "").replace(/\.md$/u, "");
|
|
555
|
+
}
|
|
556
|
+
function toAgentsCompatiblePath(contentRef) {
|
|
557
|
+
return contentRef.replace(/^\.fabric\/rules\//u, ".fabric/agents/");
|
|
558
|
+
}
|
|
559
|
+
function sortNodes(nodes) {
|
|
560
|
+
return Object.fromEntries(Object.entries(nodes).sort(([left], [right]) => left.localeCompare(right)));
|
|
561
|
+
}
|
|
562
|
+
function computeRevision(nodes) {
|
|
563
|
+
const revisionSource = Object.entries(sortNodes(nodes)).map(([id, node]) => [id, node.hash, node.stable_id ?? "", node.identity_source ?? ""].join("|")).join("\n");
|
|
564
|
+
return sha256(revisionSource);
|
|
565
|
+
}
|
|
566
|
+
function collectSyncedFiles(existingMeta, computedMeta) {
|
|
567
|
+
if (existingMeta === void 0) {
|
|
568
|
+
return Object.values(computedMeta.nodes).map((node) => node.content_ref ?? node.file).sort();
|
|
569
|
+
}
|
|
570
|
+
const existingByContentRef = indexExistingNodesByContentRef(existingMeta);
|
|
571
|
+
return Object.values(computedMeta.nodes).filter((node) => {
|
|
572
|
+
const existing = existingByContentRef.get(node.content_ref ?? node.file)?.node;
|
|
573
|
+
return existing === void 0 || existing.hash !== node.hash || existing.stable_id !== node.stable_id || existing.identity_source !== node.identity_source;
|
|
574
|
+
}).map((node) => node.content_ref ?? node.file).sort();
|
|
575
|
+
}
|
|
576
|
+
function collectStableIds(meta) {
|
|
577
|
+
return Object.values(meta.nodes).map((node) => node.stable_id).filter((stableId) => stableId !== void 0).sort();
|
|
578
|
+
}
|
|
579
|
+
function collectDriftDetails(existingMeta, computedMeta) {
|
|
580
|
+
if (existingMeta === void 0) {
|
|
581
|
+
return [];
|
|
582
|
+
}
|
|
583
|
+
const computedByContentRef = indexExistingNodesByContentRef(computedMeta);
|
|
584
|
+
return Object.values(existingMeta.nodes).map((existingNode) => {
|
|
585
|
+
const contentRef = existingNode.content_ref ?? existingNode.file;
|
|
586
|
+
const computedNode = computedByContentRef.get(contentRef)?.node;
|
|
587
|
+
const stableId = existingNode.stable_id ?? computedNode?.stable_id;
|
|
588
|
+
if (computedNode === void 0 || stableId === void 0 || existingNode.hash === computedNode.hash) {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
file: contentRef,
|
|
593
|
+
stable_id: stableId,
|
|
594
|
+
expected_hash: existingNode.hash,
|
|
595
|
+
actual_hash: computedNode.hash
|
|
596
|
+
};
|
|
597
|
+
}).filter((detail) => detail !== null);
|
|
598
|
+
}
|
|
599
|
+
async function recordBaselineSynced(projectRoot, input) {
|
|
600
|
+
if (input.driftDetails.length > 0) {
|
|
601
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
602
|
+
event_type: "rule_drift_detected",
|
|
603
|
+
revision: input.previousRevision ?? input.revision,
|
|
604
|
+
drifted_stable_ids: input.driftDetails.map((detail) => detail.stable_id),
|
|
605
|
+
missing_files: input.driftDetails.filter((detail) => detail.actual_hash === null).map((detail) => detail.file),
|
|
606
|
+
stale_files: input.driftDetails.filter((detail) => detail.actual_hash !== null).map((detail) => detail.file),
|
|
607
|
+
details: input.driftDetails
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
611
|
+
event_type: "rule_baseline_accepted",
|
|
612
|
+
revision: input.revision,
|
|
613
|
+
previous_revision: input.previousRevision,
|
|
614
|
+
accepted_stable_ids: input.acceptedStableIds,
|
|
615
|
+
source: input.source
|
|
616
|
+
});
|
|
617
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
618
|
+
event_type: "baseline_synced",
|
|
619
|
+
revision: input.revision,
|
|
620
|
+
previous_revision: input.previousRevision,
|
|
621
|
+
synced_files: input.syncedFiles,
|
|
622
|
+
accepted_stable_ids: input.acceptedStableIds,
|
|
623
|
+
source: input.source
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
function flattenKeys(value, keys = {}) {
|
|
627
|
+
if (value && typeof value === "object") {
|
|
628
|
+
for (const [key, child] of Object.entries(value)) {
|
|
629
|
+
keys[key] = true;
|
|
630
|
+
flattenKeys(child, keys);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return keys;
|
|
634
|
+
}
|
|
635
|
+
function toPosixPath(path) {
|
|
636
|
+
return path.split(sep2).join("/");
|
|
637
|
+
}
|
|
638
|
+
function deriveRuleIdentity(file, source, existing) {
|
|
639
|
+
const declaredStableId = extractDeclaredStableId(source);
|
|
640
|
+
const derivedStableId = deriveAgentsMetaStableId(toAgentsCompatiblePath(file));
|
|
641
|
+
if (declaredStableId !== void 0) {
|
|
642
|
+
return {
|
|
643
|
+
stableId: declaredStableId,
|
|
644
|
+
identitySource: "declared"
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
if (existing?.identity_source === "declared" && existing.stable_id !== void 0 && existing.stable_id !== derivedStableId) {
|
|
648
|
+
return {
|
|
649
|
+
stableId: existing.stable_id,
|
|
650
|
+
identitySource: "declared"
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
stableId: derivedStableId,
|
|
655
|
+
identitySource: "derived"
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function extractDeclaredStableId(source) {
|
|
659
|
+
const match = /^(?:\uFEFF)?(?:---\r?\n[\s\S]*?\r?\n---\s*(?:\r?\n|$))?<!--\s*fab:rule-id\s+([A-Za-z0-9][A-Za-z0-9/_-]*)\s*-->\s*(?:\r?\n|$)/u.exec(source);
|
|
660
|
+
return match?.[1];
|
|
661
|
+
}
|
|
662
|
+
function extractRuleDescription(source) {
|
|
663
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/u.exec(source);
|
|
664
|
+
const description = frontmatter === null ? void 0 : extractDescriptionFromFrontmatter(frontmatter[1]);
|
|
665
|
+
if (description !== void 0) {
|
|
666
|
+
return description;
|
|
667
|
+
}
|
|
668
|
+
const heading = /^#\s+(.+?)\s*$/mu.exec(source);
|
|
669
|
+
const summary = heading?.[1]?.trim();
|
|
670
|
+
if (summary === void 0 || summary.length === 0) {
|
|
671
|
+
return void 0;
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
summary,
|
|
675
|
+
intent_clues: [],
|
|
676
|
+
tech_stack: [],
|
|
677
|
+
impact: [],
|
|
678
|
+
must_read_if: summary
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function extractRuleSections(source) {
|
|
682
|
+
const sections = Array.from(source.matchAll(/^(?:#{2,6})\s+\[([A-Z_]+)\]\s*$/gmu)).map((match) => match[1]).filter((section, index, all) => all.indexOf(section) === index);
|
|
683
|
+
return sections.length > 0 ? sections : void 0;
|
|
684
|
+
}
|
|
685
|
+
function extractDescriptionFromFrontmatter(frontmatter) {
|
|
686
|
+
const summary = extractScalar(frontmatter, "summary") ?? extractScalar(frontmatter, "description");
|
|
687
|
+
if (summary === void 0) {
|
|
688
|
+
return void 0;
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
summary,
|
|
692
|
+
intent_clues: extractInlineArray(frontmatter, "intent_clues"),
|
|
693
|
+
tech_stack: extractInlineArray(frontmatter, "tech_stack"),
|
|
694
|
+
impact: extractInlineArray(frontmatter, "impact"),
|
|
695
|
+
must_read_if: extractScalar(frontmatter, "must_read_if") ?? summary,
|
|
696
|
+
entities: extractInlineArray(frontmatter, "entities")
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
function extractScalar(frontmatter, key) {
|
|
700
|
+
const pattern = new RegExp(`^${escapeRegExp(key)}:\\s*(.+?)\\s*$`, "mu");
|
|
701
|
+
const match = pattern.exec(frontmatter);
|
|
702
|
+
if (match === null) {
|
|
703
|
+
return void 0;
|
|
704
|
+
}
|
|
705
|
+
return unquote(match[1].trim());
|
|
706
|
+
}
|
|
707
|
+
function extractInlineArray(frontmatter, key) {
|
|
708
|
+
const pattern = new RegExp(`^${escapeRegExp(key)}:\\s*\\[(.*?)\\]\\s*$`, "mu");
|
|
709
|
+
const match = pattern.exec(frontmatter);
|
|
710
|
+
if (match === null) {
|
|
711
|
+
return [];
|
|
712
|
+
}
|
|
713
|
+
return match[1].split(",").map((item) => unquote(item.trim())).filter((item) => item.length > 0);
|
|
714
|
+
}
|
|
715
|
+
function unquote(value) {
|
|
716
|
+
return value.replace(/^["'](.*)["']$/u, "$1");
|
|
717
|
+
}
|
|
718
|
+
function escapeRegExp(value) {
|
|
719
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
720
|
+
}
|
|
721
|
+
function isNodeError3(error) {
|
|
722
|
+
return error instanceof Error;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/services/doctor.ts
|
|
726
|
+
import { existsSync as existsSync2, readdirSync, statSync as statSync2 } from "fs";
|
|
727
|
+
import { access, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
|
|
728
|
+
import { constants } from "fs";
|
|
729
|
+
import { isAbsolute as isAbsolute3, join as join7, posix as posix2, resolve as resolve4 } from "path";
|
|
730
|
+
import {
|
|
731
|
+
agentsMetaSchema as agentsMetaSchema4,
|
|
732
|
+
forensicReportSchema,
|
|
733
|
+
ruleTestIndexSchema as ruleTestIndexSchema2
|
|
734
|
+
} from "@fenglimg/fabric-shared";
|
|
735
|
+
import { detectFramework } from "@fenglimg/fabric-shared/node";
|
|
736
|
+
|
|
737
|
+
// src/services/rule-sections.ts
|
|
738
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
739
|
+
import { join as join6 } from "path";
|
|
740
|
+
|
|
741
|
+
// src/meta-reader.ts
|
|
742
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
743
|
+
import { join as join3 } from "path";
|
|
744
|
+
import { agentsMetaSchema as agentsMetaSchema2 } from "@fenglimg/fabric-shared";
|
|
745
|
+
import { agentsMetaNodeSchema, agentsMetaSchema as agentsMetaSchema3 } from "@fenglimg/fabric-shared";
|
|
746
|
+
var AgentsMetaFileMissingError = class extends Error {
|
|
747
|
+
constructor(metaPath) {
|
|
748
|
+
super(`Fabric agents metadata file is missing: ${metaPath}`);
|
|
749
|
+
this.metaPath = metaPath;
|
|
750
|
+
this.name = "AgentsMetaFileMissingError";
|
|
751
|
+
}
|
|
752
|
+
metaPath;
|
|
753
|
+
code = "FABRIC_META_MISSING";
|
|
754
|
+
};
|
|
755
|
+
var AgentsMetaInvalidError = class extends Error {
|
|
756
|
+
constructor(metaPath, cause) {
|
|
757
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
758
|
+
super(`Fabric agents metadata file is invalid: ${metaPath}. ${detail}`);
|
|
759
|
+
this.metaPath = metaPath;
|
|
760
|
+
this.name = "AgentsMetaInvalidError";
|
|
761
|
+
}
|
|
762
|
+
metaPath;
|
|
763
|
+
code = "FABRIC_META_INVALID";
|
|
764
|
+
};
|
|
765
|
+
function getAgentsMetaPath(projectRoot) {
|
|
766
|
+
return join3(projectRoot, ".fabric", "agents.meta.json");
|
|
767
|
+
}
|
|
768
|
+
function resolveProjectRoot() {
|
|
769
|
+
return process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
770
|
+
}
|
|
771
|
+
async function readAgentsMeta(projectRoot) {
|
|
772
|
+
const cached = contextCache.get("meta", projectRoot);
|
|
773
|
+
if (cached !== void 0) {
|
|
774
|
+
return cached;
|
|
775
|
+
}
|
|
776
|
+
const metaPath = getAgentsMetaPath(projectRoot);
|
|
777
|
+
let raw;
|
|
778
|
+
try {
|
|
779
|
+
raw = await readFile3(metaPath, "utf8");
|
|
780
|
+
} catch (error) {
|
|
781
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
782
|
+
throw new AgentsMetaFileMissingError(metaPath);
|
|
783
|
+
}
|
|
784
|
+
throw error;
|
|
785
|
+
}
|
|
786
|
+
let parsed;
|
|
787
|
+
try {
|
|
788
|
+
parsed = agentsMetaSchema2.parse(JSON.parse(raw));
|
|
789
|
+
} catch (error) {
|
|
790
|
+
throw new AgentsMetaInvalidError(metaPath, error);
|
|
791
|
+
}
|
|
792
|
+
contextCache.set("meta", projectRoot, parsed);
|
|
793
|
+
return parsed;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/services/audit-log.ts
|
|
797
|
+
import { open, stat } from "fs/promises";
|
|
798
|
+
import { isAbsolute as isAbsolute2, join as join4, posix, relative as relative2, resolve as resolve3 } from "path";
|
|
799
|
+
var AUDIT_LOG_FILE = `${FABRIC_DIR}/audit.jsonl`;
|
|
800
|
+
var DEFAULT_AUDIT_WINDOW_MS = 5 * 60 * 1e3;
|
|
801
|
+
async function appendGetRulesAuditEvent(projectRoot, input) {
|
|
802
|
+
const entry = {
|
|
803
|
+
kind: "audit-event",
|
|
804
|
+
event: "get_rules",
|
|
805
|
+
ts: input.ts ?? Date.now(),
|
|
806
|
+
path: normalizeAuditPath(projectRoot, input.path),
|
|
807
|
+
client_hash: input.client_hash
|
|
808
|
+
};
|
|
809
|
+
await appendAuditLogEventLedgerEvents(projectRoot, [entry], {
|
|
810
|
+
rule_context: {
|
|
811
|
+
required_stable_ids: input.required_stable_ids,
|
|
812
|
+
ai_selectable_stable_ids: input.ai_selectable_stable_ids,
|
|
813
|
+
final_stable_ids: input.final_stable_ids
|
|
814
|
+
},
|
|
815
|
+
correlation_id: input.correlation_id,
|
|
816
|
+
session_id: input.session_id
|
|
817
|
+
});
|
|
818
|
+
return entry;
|
|
819
|
+
}
|
|
820
|
+
async function appendRuleSelectionAuditEvent(projectRoot, input) {
|
|
821
|
+
const entry = {
|
|
822
|
+
kind: "audit-event",
|
|
823
|
+
event: "rule_selection",
|
|
824
|
+
ts: input.ts ?? Date.now(),
|
|
825
|
+
path: normalizeAuditPath(projectRoot, input.path),
|
|
826
|
+
selection_token: input.selection_token,
|
|
827
|
+
target_paths: input.target_paths.map((path) => normalizeAuditPath(projectRoot, path)),
|
|
828
|
+
required_stable_ids: input.required_stable_ids,
|
|
829
|
+
ai_selectable_stable_ids: input.ai_selectable_stable_ids,
|
|
830
|
+
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
831
|
+
final_stable_ids: input.final_stable_ids,
|
|
832
|
+
ai_selection_reasons: input.ai_selection_reasons,
|
|
833
|
+
rejected_stable_ids: input.rejected_stable_ids,
|
|
834
|
+
ignored_stable_ids: input.ignored_stable_ids
|
|
835
|
+
};
|
|
836
|
+
await appendAuditLogEventLedgerEvents(projectRoot, [entry], {
|
|
837
|
+
correlation_id: input.correlation_id,
|
|
838
|
+
session_id: input.session_id
|
|
839
|
+
});
|
|
840
|
+
return entry;
|
|
841
|
+
}
|
|
842
|
+
function normalizeAuditPath(projectRoot, value) {
|
|
843
|
+
const normalizedProjectRoot = resolve3(projectRoot);
|
|
844
|
+
const candidate = isAbsolute2(value) ? resolve3(value) : resolve3(normalizedProjectRoot, value);
|
|
845
|
+
const relativePath = relative2(normalizedProjectRoot, candidate);
|
|
846
|
+
if (relativePath.length > 0 && relativePath !== "." && !relativePath.startsWith("..") && !isAbsolute2(relativePath)) {
|
|
847
|
+
return posix.normalize(relativePath.split("\\").join("/"));
|
|
848
|
+
}
|
|
849
|
+
return posix.normalize(value.replaceAll("\\", "/"));
|
|
850
|
+
}
|
|
851
|
+
async function appendAuditLogEventLedgerEvents(projectRoot, entries, metadata = {}) {
|
|
852
|
+
for (const entry of entries) {
|
|
853
|
+
if (entry.event === "get_rules") {
|
|
854
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
855
|
+
event_type: "rule_context_planned",
|
|
856
|
+
ts: entry.ts,
|
|
857
|
+
target_paths: [entry.path],
|
|
858
|
+
required_stable_ids: metadata.rule_context?.required_stable_ids ?? [],
|
|
859
|
+
ai_selectable_stable_ids: metadata.rule_context?.ai_selectable_stable_ids ?? [],
|
|
860
|
+
final_stable_ids: metadata.rule_context?.final_stable_ids ?? [],
|
|
861
|
+
client_hash: entry.client_hash,
|
|
862
|
+
correlation_id: metadata.correlation_id,
|
|
863
|
+
session_id: metadata.session_id
|
|
864
|
+
});
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
if (entry.event === "rule_selection") {
|
|
868
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
869
|
+
event_type: "rule_selection",
|
|
870
|
+
ts: entry.ts,
|
|
871
|
+
selection_token: entry.selection_token,
|
|
872
|
+
target_paths: entry.target_paths,
|
|
873
|
+
required_stable_ids: entry.required_stable_ids,
|
|
874
|
+
ai_selectable_stable_ids: entry.ai_selectable_stable_ids,
|
|
875
|
+
ai_selected_stable_ids: entry.ai_selected_stable_ids,
|
|
876
|
+
final_stable_ids: entry.final_stable_ids,
|
|
877
|
+
ai_selection_reasons: entry.ai_selection_reasons,
|
|
878
|
+
rejected_stable_ids: entry.rejected_stable_ids,
|
|
879
|
+
ignored_stable_ids: entry.ignored_stable_ids,
|
|
880
|
+
correlation_id: metadata.correlation_id,
|
|
881
|
+
session_id: metadata.session_id
|
|
882
|
+
});
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
886
|
+
event_type: "edit_intent_checked",
|
|
887
|
+
ts: entry.ts,
|
|
888
|
+
path: entry.path,
|
|
889
|
+
compliant: entry.compliant,
|
|
890
|
+
intent: entry.intent,
|
|
891
|
+
ledger_entry_id: entry.ledger_entry_id,
|
|
892
|
+
ledger_source: "ai",
|
|
893
|
+
matched_rule_context_ts: entry.matched_get_rules_ts,
|
|
894
|
+
window_ms: entry.window_ms,
|
|
895
|
+
correlation_id: metadata.correlation_id,
|
|
896
|
+
session_id: metadata.session_id
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// src/services/get-rules.ts
|
|
902
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
903
|
+
import { join as join5 } from "path";
|
|
904
|
+
import { minimatch } from "minimatch";
|
|
905
|
+
var PRIORITY_ORDER = {
|
|
906
|
+
high: 0,
|
|
907
|
+
medium: 1,
|
|
908
|
+
low: 2
|
|
909
|
+
};
|
|
910
|
+
async function getRules(projectRoot, input) {
|
|
911
|
+
const context = await loadGetRulesContext(projectRoot);
|
|
912
|
+
const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
|
|
913
|
+
const matchedNodes = matchRuleNodes(context.meta, input.path);
|
|
914
|
+
const requiredStableIds = matchedNodes.filter((node) => node.level === "L2").map((node) => node.stable_id);
|
|
915
|
+
const aiSelectableStableIds = matchedNodes.filter((node) => node.level === "L1").map((node) => node.stable_id);
|
|
916
|
+
const rules = await resolveRulesForPath(projectRoot, context, input.path);
|
|
917
|
+
const result = {
|
|
918
|
+
revision_hash: context.meta.revision,
|
|
919
|
+
stale,
|
|
920
|
+
rules
|
|
921
|
+
};
|
|
922
|
+
try {
|
|
923
|
+
await appendGetRulesAuditEvent(projectRoot, {
|
|
924
|
+
path: input.path,
|
|
925
|
+
client_hash: input.client_hash,
|
|
926
|
+
required_stable_ids: requiredStableIds,
|
|
927
|
+
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
928
|
+
final_stable_ids: [...requiredStableIds, ...aiSelectableStableIds],
|
|
929
|
+
correlation_id: input.correlation_id,
|
|
930
|
+
session_id: input.session_id
|
|
931
|
+
});
|
|
932
|
+
} catch {
|
|
933
|
+
}
|
|
934
|
+
return result;
|
|
935
|
+
}
|
|
936
|
+
async function loadGetRulesContext(projectRoot) {
|
|
937
|
+
const cached = contextCache.get("context", projectRoot);
|
|
938
|
+
if (cached !== void 0) {
|
|
939
|
+
return cached;
|
|
940
|
+
}
|
|
941
|
+
const meta = await readAgentsMeta(projectRoot);
|
|
942
|
+
const l0Content = await readFile4(join5(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
|
|
943
|
+
const context = {
|
|
944
|
+
meta,
|
|
945
|
+
l0Content,
|
|
946
|
+
humanLockedNearby: []
|
|
947
|
+
};
|
|
948
|
+
contextCache.set("context", projectRoot, context);
|
|
949
|
+
return context;
|
|
950
|
+
}
|
|
951
|
+
async function resolveRulesForPath(projectRoot, context, path, options = {}) {
|
|
952
|
+
const matchedNodes = matchRuleNodes(context.meta, path);
|
|
953
|
+
const loaded = await loadMatchedRules(projectRoot, matchedNodes);
|
|
954
|
+
return buildRulesPayload(context, loaded, options);
|
|
955
|
+
}
|
|
956
|
+
function normalizeRulesPath(value) {
|
|
957
|
+
return value.replaceAll("\\", "/");
|
|
958
|
+
}
|
|
959
|
+
function matchRuleNodes(meta, path) {
|
|
960
|
+
const requestedPath = normalizeRulesPath(path);
|
|
961
|
+
return Object.entries(meta.nodes).filter(([, node]) => shouldLoadNodeForPath(requestedPath, node)).sort((left, right) => {
|
|
962
|
+
const [leftId, leftNode] = left;
|
|
963
|
+
const [rightId, rightNode] = right;
|
|
964
|
+
const priorityDelta = PRIORITY_ORDER[leftNode.priority] - PRIORITY_ORDER[rightNode.priority];
|
|
965
|
+
return priorityDelta !== 0 ? priorityDelta : leftId.localeCompare(rightId);
|
|
966
|
+
}).map(([nodeId, node]) => ({
|
|
967
|
+
node_id: nodeId,
|
|
968
|
+
level: classifyNode(nodeId, node),
|
|
969
|
+
stable_id: node.stable_id ?? nodeId,
|
|
970
|
+
identity_source: node.identity_source ?? "derived",
|
|
971
|
+
node
|
|
972
|
+
}));
|
|
973
|
+
}
|
|
974
|
+
async function loadMatchedRules(projectRoot, matchedNodes, fileContentCache = /* @__PURE__ */ new Map()) {
|
|
975
|
+
const rules = [];
|
|
976
|
+
const stubs = [];
|
|
977
|
+
for (const matchedNode of matchedNodes) {
|
|
978
|
+
if (matchedNode.level === null) {
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
if (matchedNode.node.activation?.tier === "description") {
|
|
982
|
+
stubs.push({
|
|
983
|
+
stable_id: matchedNode.stable_id,
|
|
984
|
+
identity_source: matchedNode.identity_source,
|
|
985
|
+
level: matchedNode.level,
|
|
986
|
+
path: matchedNode.node.file,
|
|
987
|
+
description: matchedNode.node.activation.description ?? ""
|
|
988
|
+
});
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
rules.push({
|
|
992
|
+
level: matchedNode.level,
|
|
993
|
+
stable_id: matchedNode.stable_id,
|
|
994
|
+
identity_source: matchedNode.identity_source,
|
|
995
|
+
entry: {
|
|
996
|
+
path: matchedNode.node.file,
|
|
997
|
+
content: await readRuleContent(projectRoot, matchedNode.node.file, fileContentCache)
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
return { rules, stubs };
|
|
1002
|
+
}
|
|
1003
|
+
function buildRulesPayload(context, loaded, options = {}) {
|
|
1004
|
+
const { L1, L2 } = partitionRulesByLevel(loaded.rules, options.dedupeByPath ?? false);
|
|
1005
|
+
return {
|
|
1006
|
+
L0: context.l0Content,
|
|
1007
|
+
L1,
|
|
1008
|
+
L2,
|
|
1009
|
+
human_locked_nearby: context.humanLockedNearby,
|
|
1010
|
+
description_stubs: loaded.stubs.length > 0 ? dedupeDescriptionStubsByPath(loaded.stubs).map(toDescriptionStub) : void 0
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
function classifyNode(nodeId, node) {
|
|
1014
|
+
if (nodeId.startsWith("L1/")) {
|
|
1015
|
+
return "L1";
|
|
1016
|
+
}
|
|
1017
|
+
if (nodeId.startsWith("L2/")) {
|
|
1018
|
+
return "L2";
|
|
1019
|
+
}
|
|
1020
|
+
return node.layer === "L0" ? null : node.layer;
|
|
1021
|
+
}
|
|
1022
|
+
function partitionRulesByLevel(loadedRules, dedupeByPath) {
|
|
1023
|
+
const l1 = [];
|
|
1024
|
+
const l2 = [];
|
|
1025
|
+
for (const rule of loadedRules) {
|
|
1026
|
+
if (rule.level === "L1") {
|
|
1027
|
+
l1.push(rule.entry);
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (rule.level === "L2") {
|
|
1031
|
+
l2.push(rule.entry);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return {
|
|
1035
|
+
L1: dedupeByPath ? dedupeEntriesByPath(l1) : l1,
|
|
1036
|
+
L2: dedupeByPath ? dedupeEntriesByPath(l2) : l2
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
function dedupeEntriesByPath(entries) {
|
|
1040
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
1041
|
+
return entries.filter((entry) => {
|
|
1042
|
+
if (seenPaths.has(entry.path)) {
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
seenPaths.add(entry.path);
|
|
1046
|
+
return true;
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
function shouldLoadNodeForPath(requestedPath, node) {
|
|
1050
|
+
switch (node.activation?.tier) {
|
|
1051
|
+
case "always":
|
|
1052
|
+
return true;
|
|
1053
|
+
case "description":
|
|
1054
|
+
return true;
|
|
1055
|
+
case "path":
|
|
1056
|
+
case void 0:
|
|
1057
|
+
return minimatch(requestedPath, normalizeRulesPath(node.scope_glob), { dot: true });
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
function dedupeDescriptionStubsByPath(stubs) {
|
|
1061
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
1062
|
+
return stubs.filter((stub) => {
|
|
1063
|
+
if (seenPaths.has(stub.path)) {
|
|
1064
|
+
return false;
|
|
1065
|
+
}
|
|
1066
|
+
seenPaths.add(stub.path);
|
|
1067
|
+
return true;
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
function toDescriptionStub(stub) {
|
|
1071
|
+
return {
|
|
1072
|
+
path: stub.path,
|
|
1073
|
+
description: stub.description
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
async function readRuleContent(projectRoot, file, fileContentCache) {
|
|
1077
|
+
const cached = fileContentCache.get(file);
|
|
1078
|
+
if (cached !== void 0) {
|
|
1079
|
+
return await cached;
|
|
1080
|
+
}
|
|
1081
|
+
const pending = readFile4(join5(projectRoot, file), "utf8");
|
|
1082
|
+
fileContentCache.set(file, pending);
|
|
1083
|
+
return await pending;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/services/plan-context.ts
|
|
1087
|
+
var SELECTION_TOKEN_TTL_MS = 5 * 60 * 1e3;
|
|
1088
|
+
var selectionTokenCache = /* @__PURE__ */ new Map();
|
|
1089
|
+
async function planContext(projectRoot, input) {
|
|
1090
|
+
const meta = await readAgentsMeta(projectRoot);
|
|
1091
|
+
const stale = input.client_hash !== void 0 && input.client_hash !== meta.revision;
|
|
1092
|
+
const uniquePaths = dedupePaths(input.paths);
|
|
1093
|
+
const allDescriptions = buildDescriptionIndex(meta);
|
|
1094
|
+
const entries = uniquePaths.map((path) => {
|
|
1095
|
+
const profile = buildRequirementProfile(path, input);
|
|
1096
|
+
const descriptionIndex = allDescriptions.filter((item) => shouldIncludeIndexItemForPath(item, meta, path));
|
|
1097
|
+
const requiredStableIds2 = descriptionIndex.filter((item) => item.required).map((item) => item.stable_id);
|
|
1098
|
+
const aiSelectableStableIds2 = descriptionIndex.filter((item) => item.selectable).map((item) => item.stable_id);
|
|
1099
|
+
return {
|
|
1100
|
+
path,
|
|
1101
|
+
requirement_profile: profile,
|
|
1102
|
+
description_index: descriptionIndex,
|
|
1103
|
+
required_stable_ids: requiredStableIds2,
|
|
1104
|
+
ai_selectable_stable_ids: aiSelectableStableIds2,
|
|
1105
|
+
initial_selected_stable_ids: requiredStableIds2,
|
|
1106
|
+
selection_policy: {
|
|
1107
|
+
required_levels: ["L0", "L2"],
|
|
1108
|
+
ai_selectable_levels: ["L1"],
|
|
1109
|
+
final_fetch_rule: "required_stable_ids + ai_selected_l1_stable_ids"
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
});
|
|
1113
|
+
const requiredStableIds = dedupeStableIds(entries.flatMap((entry) => entry.required_stable_ids));
|
|
1114
|
+
const aiSelectableStableIds = dedupeStableIds(entries.flatMap((entry) => entry.ai_selectable_stable_ids));
|
|
1115
|
+
const sharedDescriptionIndex = dedupeDescriptionIndex(entries.flatMap((entry) => entry.description_index));
|
|
1116
|
+
const selectionToken = createSelectionToken(meta.revision, uniquePaths, requiredStableIds, aiSelectableStableIds);
|
|
1117
|
+
const result = {
|
|
1118
|
+
revision_hash: meta.revision,
|
|
1119
|
+
stale,
|
|
1120
|
+
selection_token: selectionToken,
|
|
1121
|
+
entries,
|
|
1122
|
+
shared: {
|
|
1123
|
+
required_stable_ids: requiredStableIds,
|
|
1124
|
+
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
1125
|
+
description_index: sharedDescriptionIndex,
|
|
1126
|
+
preflight_diagnostics: buildPreflightDiagnostics(meta)
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
try {
|
|
1130
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1131
|
+
event_type: "rule_context_planned",
|
|
1132
|
+
target_paths: uniquePaths,
|
|
1133
|
+
required_stable_ids: requiredStableIds,
|
|
1134
|
+
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
1135
|
+
final_stable_ids: requiredStableIds,
|
|
1136
|
+
selection_token: selectionToken,
|
|
1137
|
+
client_hash: input.client_hash,
|
|
1138
|
+
intent: input.intent,
|
|
1139
|
+
known_tech: input.known_tech,
|
|
1140
|
+
diagnostics: result.shared.preflight_diagnostics,
|
|
1141
|
+
correlation_id: input.correlation_id,
|
|
1142
|
+
session_id: input.session_id
|
|
1143
|
+
});
|
|
1144
|
+
} catch {
|
|
1145
|
+
}
|
|
1146
|
+
return result;
|
|
1147
|
+
}
|
|
1148
|
+
function readSelectionToken(token, now = Date.now()) {
|
|
1149
|
+
const state = selectionTokenCache.get(token);
|
|
1150
|
+
if (state === void 0) {
|
|
1151
|
+
return void 0;
|
|
1152
|
+
}
|
|
1153
|
+
if (state.expires_at <= now) {
|
|
1154
|
+
selectionTokenCache.delete(token);
|
|
1155
|
+
return void 0;
|
|
1156
|
+
}
|
|
1157
|
+
return state;
|
|
1158
|
+
}
|
|
1159
|
+
function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSelectableStableIds, now = Date.now()) {
|
|
1160
|
+
const token = `selection:${revisionHash}:${now.toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
1161
|
+
selectionTokenCache.set(token, {
|
|
1162
|
+
token,
|
|
1163
|
+
revision_hash: revisionHash,
|
|
1164
|
+
target_paths: targetPaths,
|
|
1165
|
+
required_stable_ids: requiredStableIds,
|
|
1166
|
+
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
1167
|
+
created_at: now,
|
|
1168
|
+
expires_at: now + SELECTION_TOKEN_TTL_MS
|
|
1169
|
+
});
|
|
1170
|
+
return token;
|
|
1171
|
+
}
|
|
1172
|
+
function dedupePaths(paths) {
|
|
1173
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
1174
|
+
return paths.flatMap((path) => {
|
|
1175
|
+
const normalizedPath = normalizeRulesPath(path);
|
|
1176
|
+
if (seenPaths.has(normalizedPath)) {
|
|
1177
|
+
return [];
|
|
1178
|
+
}
|
|
1179
|
+
seenPaths.add(normalizedPath);
|
|
1180
|
+
return [normalizedPath];
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
function buildRequirementProfile(path, input) {
|
|
1184
|
+
const normalizedPath = normalizeRulesPath(path);
|
|
1185
|
+
const extensionMatch = /(\.[^./\\]+)$/u.exec(normalizedPath);
|
|
1186
|
+
const knownTech = dedupeStableIds([
|
|
1187
|
+
...input.known_tech ?? [],
|
|
1188
|
+
...extensionMatch?.[1] === ".ts" ? ["TypeScript"] : []
|
|
1189
|
+
]);
|
|
1190
|
+
return {
|
|
1191
|
+
target_path: normalizedPath,
|
|
1192
|
+
path_segments: normalizedPath.split("/").filter(Boolean),
|
|
1193
|
+
extension: extensionMatch?.[1] ?? "",
|
|
1194
|
+
inferred_domain: inferDomains(normalizedPath),
|
|
1195
|
+
known_tech: knownTech,
|
|
1196
|
+
user_intent: input.intent ?? "",
|
|
1197
|
+
intent_tokens: tokenizeIntent(input.intent ?? ""),
|
|
1198
|
+
impact_hints: inferImpactHints(input.intent ?? ""),
|
|
1199
|
+
detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path] ?? []
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
function buildDescriptionIndex(meta) {
|
|
1203
|
+
return Object.entries(meta.nodes).flatMap(([nodeId, node]) => {
|
|
1204
|
+
const level = node.level ?? node.layer;
|
|
1205
|
+
const description = node.description ?? descriptionFromLegacyActivation(node.activation?.description);
|
|
1206
|
+
if (description === void 0) {
|
|
1207
|
+
return [];
|
|
1208
|
+
}
|
|
1209
|
+
return [{
|
|
1210
|
+
stable_id: node.stable_id ?? nodeId,
|
|
1211
|
+
level,
|
|
1212
|
+
required: level === "L0" || level === "L2",
|
|
1213
|
+
selectable: level === "L1",
|
|
1214
|
+
description
|
|
1215
|
+
}];
|
|
1216
|
+
}).sort(compareDescriptionIndexItems);
|
|
1217
|
+
}
|
|
1218
|
+
function descriptionFromLegacyActivation(summary) {
|
|
1219
|
+
if (summary === void 0) {
|
|
1220
|
+
return void 0;
|
|
1221
|
+
}
|
|
1222
|
+
return {
|
|
1223
|
+
summary,
|
|
1224
|
+
intent_clues: [],
|
|
1225
|
+
tech_stack: [],
|
|
1226
|
+
impact: [],
|
|
1227
|
+
must_read_if: summary
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
function shouldIncludeIndexItemForPath(item, meta, path) {
|
|
1231
|
+
if (item.level === "L0" || item.level === "L1") {
|
|
1232
|
+
return true;
|
|
1233
|
+
}
|
|
1234
|
+
const node = Object.values(meta.nodes).find((candidate) => candidate.stable_id === item.stable_id);
|
|
1235
|
+
if (node === void 0) {
|
|
1236
|
+
return false;
|
|
1237
|
+
}
|
|
1238
|
+
return node.scope_glob === path || minimatchSimple(path, node.scope_glob);
|
|
1239
|
+
}
|
|
1240
|
+
function minimatchSimple(path, glob) {
|
|
1241
|
+
if (glob === "**") {
|
|
1242
|
+
return true;
|
|
1243
|
+
}
|
|
1244
|
+
if (glob.endsWith("/**")) {
|
|
1245
|
+
return path.startsWith(glob.slice(0, -3));
|
|
1246
|
+
}
|
|
1247
|
+
return path === glob;
|
|
1248
|
+
}
|
|
1249
|
+
function buildPreflightDiagnostics(meta) {
|
|
1250
|
+
const missingDescriptionStableIds = Object.entries(meta.nodes).filter(([, node]) => node.description === void 0 && node.activation?.description === void 0).map(([nodeId, node]) => node.stable_id ?? nodeId).sort();
|
|
1251
|
+
if (missingDescriptionStableIds.length === 0) {
|
|
1252
|
+
return [];
|
|
1253
|
+
}
|
|
1254
|
+
return [{
|
|
1255
|
+
code: "missing_description",
|
|
1256
|
+
severity: "warn",
|
|
1257
|
+
stable_ids: missingDescriptionStableIds,
|
|
1258
|
+
message: `Resolved registry includes ${missingDescriptionStableIds.length} node(s) without structured descriptions.`
|
|
1259
|
+
}];
|
|
1260
|
+
}
|
|
1261
|
+
function inferDomains(path) {
|
|
1262
|
+
const domains = [];
|
|
1263
|
+
if (path.includes("/ui/") || path.toLowerCase().includes("ui")) {
|
|
1264
|
+
domains.push("UI");
|
|
1265
|
+
}
|
|
1266
|
+
if (path.includes("assets/scripts")) {
|
|
1267
|
+
domains.push("Gameplay");
|
|
1268
|
+
}
|
|
1269
|
+
if (path.includes("resources") || path.includes("assets/resources")) {
|
|
1270
|
+
domains.push("Asset");
|
|
1271
|
+
}
|
|
1272
|
+
return domains;
|
|
1273
|
+
}
|
|
1274
|
+
function tokenizeIntent(intent) {
|
|
1275
|
+
const tokens = ["\u6027\u80FD", "\u4F18\u5316", "drawcall", "\u6E32\u67D3", "\u5361\u987F", "\u95EA\u70C1", "\u754C\u9762", "UI", "\u8D44\u6E90", "\u56FE\u96C6"].filter((token) => intent.toLowerCase().includes(token.toLowerCase()));
|
|
1276
|
+
return dedupeStableIds(tokens);
|
|
1277
|
+
}
|
|
1278
|
+
function inferImpactHints(intent) {
|
|
1279
|
+
return /性能|优化|drawcall|渲染|卡顿|闪烁/iu.test(intent) ? ["Performance"] : [];
|
|
1280
|
+
}
|
|
1281
|
+
function dedupeStableIds(stableIds) {
|
|
1282
|
+
return Array.from(new Set(stableIds));
|
|
1283
|
+
}
|
|
1284
|
+
function dedupeDescriptionIndex(items) {
|
|
1285
|
+
const seenStableIds = /* @__PURE__ */ new Set();
|
|
1286
|
+
return items.filter((item) => {
|
|
1287
|
+
if (seenStableIds.has(item.stable_id)) {
|
|
1288
|
+
return false;
|
|
1289
|
+
}
|
|
1290
|
+
seenStableIds.add(item.stable_id);
|
|
1291
|
+
return true;
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
function compareDescriptionIndexItems(left, right) {
|
|
1295
|
+
const levelDelta = levelOrder(left.level) - levelOrder(right.level);
|
|
1296
|
+
return levelDelta !== 0 ? levelDelta : left.stable_id.localeCompare(right.stable_id);
|
|
1297
|
+
}
|
|
1298
|
+
function levelOrder(level) {
|
|
1299
|
+
switch (level) {
|
|
1300
|
+
case "L0":
|
|
1301
|
+
return 0;
|
|
1302
|
+
case "L1":
|
|
1303
|
+
return 1;
|
|
1304
|
+
case "L2":
|
|
1305
|
+
return 2;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// src/services/rule-sections.ts
|
|
1310
|
+
var RULE_SECTION_NAMES = [
|
|
1311
|
+
"MISSION_STATEMENT",
|
|
1312
|
+
"MANDATORY_INJECTION",
|
|
1313
|
+
"BUSINESS_LOGIC_CHUNKS",
|
|
1314
|
+
"CONTEXT_INFO"
|
|
1315
|
+
];
|
|
1316
|
+
var PRIORITY_ORDER2 = {
|
|
1317
|
+
high: 0,
|
|
1318
|
+
medium: 1,
|
|
1319
|
+
low: 2
|
|
1320
|
+
};
|
|
1321
|
+
function parseRuleSections(content) {
|
|
1322
|
+
const sections = /* @__PURE__ */ new Map();
|
|
1323
|
+
const lines = content.split(/\r?\n/u);
|
|
1324
|
+
let activeSection;
|
|
1325
|
+
let activeSectionDepth = 0;
|
|
1326
|
+
let buffer = [];
|
|
1327
|
+
const flush = () => {
|
|
1328
|
+
if (activeSection === void 0) {
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
const text = buffer.join("\n").trim();
|
|
1332
|
+
if (text.length === 0) {
|
|
1333
|
+
buffer = [];
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
sections.set(activeSection, [...sections.get(activeSection) ?? [], text]);
|
|
1337
|
+
buffer = [];
|
|
1338
|
+
};
|
|
1339
|
+
for (const line of lines) {
|
|
1340
|
+
const heading = /^(#{2,6})\s+\[([A-Z_]+)\]\s*$/u.exec(line.trim());
|
|
1341
|
+
if (heading !== null) {
|
|
1342
|
+
flush();
|
|
1343
|
+
activeSection = isRuleSectionName(heading[2]) ? heading[2] : void 0;
|
|
1344
|
+
activeSectionDepth = activeSection === void 0 ? 0 : heading[1].length;
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
const ordinaryHeading = /^(#{1,6})\s+/u.exec(line.trim());
|
|
1348
|
+
if (ordinaryHeading !== null) {
|
|
1349
|
+
if (activeSection !== void 0 && ordinaryHeading[1].length > activeSectionDepth) {
|
|
1350
|
+
buffer.push(line);
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
flush();
|
|
1354
|
+
activeSection = void 0;
|
|
1355
|
+
activeSectionDepth = 0;
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
if (activeSection !== void 0) {
|
|
1359
|
+
buffer.push(line);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
flush();
|
|
1363
|
+
return new Map(
|
|
1364
|
+
Array.from(sections.entries()).map(([section, values]) => [section, values.join("\n\n")])
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
async function getRuleSections(projectRoot, input) {
|
|
1368
|
+
const token = readSelectionToken(input.selection_token);
|
|
1369
|
+
if (token === void 0) {
|
|
1370
|
+
throw new Error("selection_token is missing or expired");
|
|
1371
|
+
}
|
|
1372
|
+
validateAiSelections(token.ai_selectable_stable_ids, input.ai_selected_stable_ids, input.ai_selection_reasons);
|
|
1373
|
+
const meta = await readAgentsMeta(projectRoot);
|
|
1374
|
+
const selectedStableIds = [...token.required_stable_ids, ...input.ai_selected_stable_ids];
|
|
1375
|
+
const selectedRules = sortRuleNodes(selectedStableIds.map((stableId) => findRuleNode(meta, stableId)));
|
|
1376
|
+
const diagnostics = [];
|
|
1377
|
+
const rules = [];
|
|
1378
|
+
for (const rule of selectedRules) {
|
|
1379
|
+
const content = await readFile5(join6(projectRoot, rule.path), "utf8");
|
|
1380
|
+
const parsedSections = parseRuleSections(content);
|
|
1381
|
+
const sections = {};
|
|
1382
|
+
for (const section of input.sections) {
|
|
1383
|
+
const sectionContent = parsedSections.get(section);
|
|
1384
|
+
sections[section] = sectionContent ?? "";
|
|
1385
|
+
if (sectionContent === void 0) {
|
|
1386
|
+
diagnostics.push({
|
|
1387
|
+
code: "missing_section",
|
|
1388
|
+
severity: "warn",
|
|
1389
|
+
stable_id: rule.stable_id,
|
|
1390
|
+
section,
|
|
1391
|
+
message: `Rule ${rule.stable_id} does not define section ${section}.`
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
rules.push({
|
|
1396
|
+
stable_id: rule.stable_id,
|
|
1397
|
+
level: rule.level,
|
|
1398
|
+
path: rule.path,
|
|
1399
|
+
sections
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
const result = {
|
|
1403
|
+
revision_hash: meta.revision,
|
|
1404
|
+
precedence: ["L2", "L1", "L0"],
|
|
1405
|
+
selected_stable_ids: rules.map((rule) => rule.stable_id),
|
|
1406
|
+
rules,
|
|
1407
|
+
diagnostics
|
|
1408
|
+
};
|
|
1409
|
+
await appendRuleSelectionAuditEvent(projectRoot, {
|
|
1410
|
+
path: token.target_paths[0] ?? "",
|
|
1411
|
+
selection_token: input.selection_token,
|
|
1412
|
+
target_paths: token.target_paths,
|
|
1413
|
+
required_stable_ids: token.required_stable_ids,
|
|
1414
|
+
ai_selectable_stable_ids: token.ai_selectable_stable_ids,
|
|
1415
|
+
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
1416
|
+
final_stable_ids: result.selected_stable_ids,
|
|
1417
|
+
ai_selection_reasons: pickSelectionReasons(input.ai_selected_stable_ids, input.ai_selection_reasons),
|
|
1418
|
+
rejected_stable_ids: [],
|
|
1419
|
+
ignored_stable_ids: [],
|
|
1420
|
+
correlation_id: input.correlation_id,
|
|
1421
|
+
session_id: input.session_id
|
|
1422
|
+
});
|
|
1423
|
+
try {
|
|
1424
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1425
|
+
event_type: "rule_sections_fetched",
|
|
1426
|
+
selection_token: input.selection_token,
|
|
1427
|
+
target_paths: token.target_paths,
|
|
1428
|
+
requested_sections: input.sections,
|
|
1429
|
+
final_stable_ids: result.selected_stable_ids,
|
|
1430
|
+
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
1431
|
+
diagnostics,
|
|
1432
|
+
correlation_id: input.correlation_id,
|
|
1433
|
+
session_id: input.session_id
|
|
1434
|
+
});
|
|
1435
|
+
} catch {
|
|
1436
|
+
}
|
|
1437
|
+
return result;
|
|
1438
|
+
}
|
|
1439
|
+
function validateAiSelections(aiSelectableStableIds, aiSelectedStableIds, aiSelectionReasons) {
|
|
1440
|
+
const selectable = new Set(aiSelectableStableIds);
|
|
1441
|
+
for (const stableId of aiSelectedStableIds) {
|
|
1442
|
+
if (!selectable.has(stableId)) {
|
|
1443
|
+
throw new Error(`Invalid L1 rule selection: ${stableId}`);
|
|
1444
|
+
}
|
|
1445
|
+
if (aiSelectionReasons[stableId]?.trim() === "") {
|
|
1446
|
+
throw new Error(`Missing AI selection reason for ${stableId}`);
|
|
1447
|
+
}
|
|
1448
|
+
if (aiSelectionReasons[stableId] === void 0) {
|
|
1449
|
+
throw new Error(`Missing AI selection reason for ${stableId}`);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
function findRuleNode(meta, stableId) {
|
|
1454
|
+
for (const [nodeId, node] of Object.entries(meta.nodes)) {
|
|
1455
|
+
const nodeStableId = node.stable_id ?? nodeId;
|
|
1456
|
+
if (nodeStableId !== stableId) {
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
const level = node.level ?? node.layer;
|
|
1460
|
+
return {
|
|
1461
|
+
stable_id: nodeStableId,
|
|
1462
|
+
level,
|
|
1463
|
+
path: normalizeRulesPath(node.content_ref ?? node.file),
|
|
1464
|
+
priority: node.priority,
|
|
1465
|
+
node
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
throw new Error(`Selected rule is not present in agents.meta.json: ${stableId}`);
|
|
1469
|
+
}
|
|
1470
|
+
function sortRuleNodes(rules) {
|
|
1471
|
+
return [...rules].sort((left, right) => {
|
|
1472
|
+
const levelDelta = outputLevelOrder(left.level) - outputLevelOrder(right.level);
|
|
1473
|
+
if (levelDelta !== 0) {
|
|
1474
|
+
return levelDelta;
|
|
1475
|
+
}
|
|
1476
|
+
const priorityDelta = PRIORITY_ORDER2[left.priority] - PRIORITY_ORDER2[right.priority];
|
|
1477
|
+
if (priorityDelta !== 0) {
|
|
1478
|
+
return priorityDelta;
|
|
1479
|
+
}
|
|
1480
|
+
return left.stable_id.localeCompare(right.stable_id);
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
function outputLevelOrder(level) {
|
|
1484
|
+
switch (level) {
|
|
1485
|
+
case "L0":
|
|
1486
|
+
return 0;
|
|
1487
|
+
case "L1":
|
|
1488
|
+
return 1;
|
|
1489
|
+
case "L2":
|
|
1490
|
+
return 2;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
function isRuleSectionName(value) {
|
|
1494
|
+
return RULE_SECTION_NAMES.includes(value);
|
|
1495
|
+
}
|
|
1496
|
+
function pickSelectionReasons(selectedStableIds, reasons) {
|
|
1497
|
+
return Object.fromEntries(selectedStableIds.map((stableId) => [stableId, reasons[stableId] ?? ""]));
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// src/services/doctor.ts
|
|
1501
|
+
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
1502
|
+
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
1503
|
+
".fabric",
|
|
1504
|
+
".git",
|
|
1505
|
+
".next",
|
|
1506
|
+
".turbo",
|
|
1507
|
+
"Library",
|
|
1508
|
+
"Temp",
|
|
1509
|
+
"build",
|
|
1510
|
+
"coverage",
|
|
1511
|
+
"dist",
|
|
1512
|
+
"node_modules"
|
|
1513
|
+
]);
|
|
1514
|
+
var TARGET_FILE_PATHS = [
|
|
1515
|
+
".fabric/bootstrap/README.md",
|
|
1516
|
+
".fabric/INITIAL_TAXONOMY.md",
|
|
1517
|
+
".fabric/forensic.json",
|
|
1518
|
+
".fabric/init-context.json",
|
|
1519
|
+
".fabric/agents.meta.json",
|
|
1520
|
+
".fabric/rule-test.index.json",
|
|
1521
|
+
".fabric/events.jsonl"
|
|
1522
|
+
];
|
|
1523
|
+
async function runDoctorReport(target) {
|
|
1524
|
+
const projectRoot = normalizeTarget(target);
|
|
1525
|
+
const framework = detectFramework(projectRoot);
|
|
1526
|
+
const entryPoints = collectEntryPoints(projectRoot);
|
|
1527
|
+
const [
|
|
1528
|
+
forensic,
|
|
1529
|
+
initContext,
|
|
1530
|
+
meta,
|
|
1531
|
+
eventLedger,
|
|
1532
|
+
ruleSections,
|
|
1533
|
+
ruleTestIndex
|
|
1534
|
+
] = await Promise.all([
|
|
1535
|
+
inspectForensic(projectRoot),
|
|
1536
|
+
inspectInitContext(projectRoot),
|
|
1537
|
+
inspectMeta(projectRoot),
|
|
1538
|
+
inspectEventLedger(projectRoot),
|
|
1539
|
+
inspectRuleSections(projectRoot),
|
|
1540
|
+
inspectRuleTestIndex(projectRoot)
|
|
1541
|
+
]);
|
|
1542
|
+
const taxonomyExists = existsSync2(join7(projectRoot, ".fabric", "INITIAL_TAXONOMY.md"));
|
|
1543
|
+
const bootstrapExists = existsSync2(join7(projectRoot, ".fabric", "bootstrap", "README.md"));
|
|
1544
|
+
const checks = [
|
|
1545
|
+
createBootstrapCheck(bootstrapExists),
|
|
1546
|
+
createTaxonomyCheck(taxonomyExists),
|
|
1547
|
+
createForensicCheck(forensic, framework.kind, entryPoints.length),
|
|
1548
|
+
createInitContextCheck(initContext),
|
|
1549
|
+
createMetaCheck(meta),
|
|
1550
|
+
createRuleContentRefCheck(meta),
|
|
1551
|
+
createRuleSectionsCheck(ruleSections),
|
|
1552
|
+
createRuleTestIndexCheck(ruleTestIndex),
|
|
1553
|
+
createEventLedgerCheck(eventLedger)
|
|
1554
|
+
];
|
|
1555
|
+
const fixableErrors = collectIssues(checks, "fixable_error");
|
|
1556
|
+
const manualErrors = collectIssues(checks, "manual_error");
|
|
1557
|
+
const warnings = collectIssues(checks, "warning");
|
|
1558
|
+
return {
|
|
1559
|
+
status: reduceStatus(checks.map((check) => check.status)),
|
|
1560
|
+
checks,
|
|
1561
|
+
fixable_errors: fixableErrors,
|
|
1562
|
+
manual_errors: manualErrors,
|
|
1563
|
+
warnings,
|
|
1564
|
+
summary: {
|
|
1565
|
+
target: projectRoot,
|
|
1566
|
+
framework: {
|
|
1567
|
+
kind: framework.kind,
|
|
1568
|
+
version: framework.version,
|
|
1569
|
+
subkind: framework.subkind
|
|
1570
|
+
},
|
|
1571
|
+
entryPoints,
|
|
1572
|
+
metaRevision: meta.revision,
|
|
1573
|
+
computedMetaRevision: meta.computedRevision,
|
|
1574
|
+
ruleCount: meta.ruleCount,
|
|
1575
|
+
eventLedgerPath: eventLedger.path,
|
|
1576
|
+
fixableErrorCount: fixableErrors.length,
|
|
1577
|
+
manualErrorCount: manualErrors.length,
|
|
1578
|
+
warningCount: warnings.length,
|
|
1579
|
+
targetFiles: Object.fromEntries(
|
|
1580
|
+
TARGET_FILE_PATHS.map((path) => [path, existsSync2(join7(projectRoot, path))])
|
|
1581
|
+
)
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
async function runDoctorFix(target) {
|
|
1586
|
+
const projectRoot = normalizeTarget(target);
|
|
1587
|
+
const before = await runDoctorReport(projectRoot);
|
|
1588
|
+
const fixed = [];
|
|
1589
|
+
if (before.fixable_errors.some((issue) => issue.code === "bootstrap_missing")) {
|
|
1590
|
+
await writeDefaultBootstrap(projectRoot);
|
|
1591
|
+
fixed.push(findIssue(before.fixable_errors, "bootstrap_missing"));
|
|
1592
|
+
}
|
|
1593
|
+
if (before.fixable_errors.some((issue) => issue.code === "event_ledger_missing")) {
|
|
1594
|
+
await ensureEventLedger(projectRoot);
|
|
1595
|
+
fixed.push(findIssue(before.fixable_errors, "event_ledger_missing"));
|
|
1596
|
+
}
|
|
1597
|
+
if (before.fixable_errors.some(
|
|
1598
|
+
(issue) => ["agents_meta_missing", "agents_meta_stale", "rule_test_index_missing", "rule_test_index_stale"].includes(issue.code)
|
|
1599
|
+
)) {
|
|
1600
|
+
await writeRuleMeta(projectRoot, { source: "doctor_fix" });
|
|
1601
|
+
for (const issue of before.fixable_errors.filter(
|
|
1602
|
+
(candidate) => ["agents_meta_missing", "agents_meta_stale", "rule_test_index_missing", "rule_test_index_stale"].includes(candidate.code)
|
|
1603
|
+
)) {
|
|
1604
|
+
fixed.push(issue);
|
|
1605
|
+
}
|
|
1606
|
+
contextCache.invalidate("meta_write", projectRoot);
|
|
1607
|
+
}
|
|
1608
|
+
const report = await runDoctorReport(projectRoot);
|
|
1609
|
+
return {
|
|
1610
|
+
changed: fixed.length > 0,
|
|
1611
|
+
fixed,
|
|
1612
|
+
remaining_manual_errors: report.manual_errors,
|
|
1613
|
+
warnings: report.warnings,
|
|
1614
|
+
message: createFixMessage(fixed, report),
|
|
1615
|
+
report
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
async function inspectForensic(projectRoot) {
|
|
1619
|
+
const path = join7(projectRoot, ".fabric", "forensic.json");
|
|
1620
|
+
try {
|
|
1621
|
+
const parsed = forensicReportSchema.parse(JSON.parse(await readFile6(path, "utf8")));
|
|
1622
|
+
return { present: true, valid: true, report: parsed };
|
|
1623
|
+
} catch (error) {
|
|
1624
|
+
if (isMissingFileError(error)) {
|
|
1625
|
+
return { present: false, valid: false, report: null, error: ".fabric/forensic.json is missing." };
|
|
1626
|
+
}
|
|
1627
|
+
return { present: true, valid: false, report: null, error: error instanceof Error ? error.message : String(error) };
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
async function inspectInitContext(projectRoot) {
|
|
1631
|
+
const path = join7(projectRoot, ".fabric", "init-context.json");
|
|
1632
|
+
try {
|
|
1633
|
+
JSON.parse(await readFile6(path, "utf8"));
|
|
1634
|
+
return { exists: true, validJson: true };
|
|
1635
|
+
} catch (error) {
|
|
1636
|
+
if (isMissingFileError(error)) {
|
|
1637
|
+
return { exists: false, validJson: false, error: ".fabric/init-context.json is missing." };
|
|
1638
|
+
}
|
|
1639
|
+
return { exists: true, validJson: false, error: error instanceof Error ? error.message : String(error) };
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
async function inspectMeta(projectRoot) {
|
|
1643
|
+
const metaPath = join7(projectRoot, ".fabric", "agents.meta.json");
|
|
1644
|
+
const built = await tryBuildRuleMeta(projectRoot);
|
|
1645
|
+
try {
|
|
1646
|
+
const raw = await readFile6(metaPath, "utf8");
|
|
1647
|
+
const meta = agentsMetaSchema4.parse(JSON.parse(raw));
|
|
1648
|
+
const contentRefIssues = inspectContentRefs(projectRoot, meta);
|
|
1649
|
+
const changed = built === null ? false : built.changed;
|
|
1650
|
+
return {
|
|
1651
|
+
present: true,
|
|
1652
|
+
valid: true,
|
|
1653
|
+
meta,
|
|
1654
|
+
revision: meta.revision,
|
|
1655
|
+
computedRevision: built?.meta.revision ?? null,
|
|
1656
|
+
ruleCount: Object.values(meta.nodes).filter((node) => (node.content_ref ?? node.file).startsWith(".fabric/rules/")).length,
|
|
1657
|
+
missingContentRefs: contentRefIssues.missing,
|
|
1658
|
+
invalidContentRefs: contentRefIssues.invalid,
|
|
1659
|
+
stale: changed || built !== null && meta.revision !== built.meta.revision,
|
|
1660
|
+
changed
|
|
1661
|
+
};
|
|
1662
|
+
} catch (error) {
|
|
1663
|
+
if (isMissingFileError(error)) {
|
|
1664
|
+
return {
|
|
1665
|
+
present: false,
|
|
1666
|
+
valid: false,
|
|
1667
|
+
meta: null,
|
|
1668
|
+
revision: null,
|
|
1669
|
+
computedRevision: built?.meta.revision ?? null,
|
|
1670
|
+
ruleCount: 0,
|
|
1671
|
+
missingContentRefs: [],
|
|
1672
|
+
invalidContentRefs: [],
|
|
1673
|
+
stale: true,
|
|
1674
|
+
changed: built?.changed ?? true
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
return {
|
|
1678
|
+
present: true,
|
|
1679
|
+
valid: false,
|
|
1680
|
+
meta: null,
|
|
1681
|
+
revision: null,
|
|
1682
|
+
computedRevision: built?.meta.revision ?? null,
|
|
1683
|
+
ruleCount: 0,
|
|
1684
|
+
missingContentRefs: [],
|
|
1685
|
+
invalidContentRefs: [],
|
|
1686
|
+
stale: true,
|
|
1687
|
+
changed: built?.changed ?? true,
|
|
1688
|
+
readError: error instanceof Error ? error.message : String(error)
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
async function tryBuildRuleMeta(projectRoot) {
|
|
1693
|
+
try {
|
|
1694
|
+
return await buildRuleMeta(projectRoot);
|
|
1695
|
+
} catch {
|
|
1696
|
+
return null;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
function inspectContentRefs(projectRoot, meta) {
|
|
1700
|
+
const missing = [];
|
|
1701
|
+
const invalid = [];
|
|
1702
|
+
for (const node of Object.values(meta.nodes)) {
|
|
1703
|
+
const contentRef = normalizePath(node.content_ref ?? node.file);
|
|
1704
|
+
if (contentRef === ".fabric/bootstrap/README.md") {
|
|
1705
|
+
if (!existsSync2(join7(projectRoot, contentRef))) {
|
|
1706
|
+
missing.push(contentRef);
|
|
1707
|
+
}
|
|
1708
|
+
continue;
|
|
1709
|
+
}
|
|
1710
|
+
if (!contentRef.startsWith(".fabric/rules/")) {
|
|
1711
|
+
invalid.push(contentRef);
|
|
1712
|
+
continue;
|
|
1713
|
+
}
|
|
1714
|
+
if (!existsSync2(join7(projectRoot, contentRef))) {
|
|
1715
|
+
missing.push(contentRef);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
return { missing, invalid };
|
|
1719
|
+
}
|
|
1720
|
+
async function inspectEventLedger(projectRoot) {
|
|
1721
|
+
const path = getEventLedgerPath(projectRoot);
|
|
1722
|
+
const exists = existsSync2(path);
|
|
1723
|
+
if (!exists) {
|
|
1724
|
+
return { exists: false, writable: false, parseable: false, path };
|
|
1725
|
+
}
|
|
1726
|
+
try {
|
|
1727
|
+
await access(path, constants.W_OK);
|
|
1728
|
+
await readEventLedger(projectRoot);
|
|
1729
|
+
const raw = await readFile6(path, "utf8");
|
|
1730
|
+
const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
|
|
1731
|
+
return {
|
|
1732
|
+
exists: true,
|
|
1733
|
+
writable: true,
|
|
1734
|
+
parseable: invalidLine === void 0,
|
|
1735
|
+
path,
|
|
1736
|
+
error: invalidLine === void 0 ? void 0 : "events.jsonl contains an invalid JSON line."
|
|
1737
|
+
};
|
|
1738
|
+
} catch (error) {
|
|
1739
|
+
return {
|
|
1740
|
+
exists: true,
|
|
1741
|
+
writable: false,
|
|
1742
|
+
parseable: false,
|
|
1743
|
+
path,
|
|
1744
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
async function inspectRuleSections(projectRoot) {
|
|
1749
|
+
const invalidFiles = [];
|
|
1750
|
+
const files = findRuleFiles(projectRoot);
|
|
1751
|
+
for (const file of files) {
|
|
1752
|
+
try {
|
|
1753
|
+
parseRuleSections(await readFile6(join7(projectRoot, file), "utf8"));
|
|
1754
|
+
} catch (error) {
|
|
1755
|
+
invalidFiles.push({
|
|
1756
|
+
file,
|
|
1757
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
return {
|
|
1762
|
+
checkedCount: files.length,
|
|
1763
|
+
invalidFiles
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
async function inspectRuleTestIndex(projectRoot) {
|
|
1767
|
+
const path = join7(projectRoot, ".fabric", "rule-test.index.json");
|
|
1768
|
+
const built = await tryBuildRuleMeta(projectRoot);
|
|
1769
|
+
try {
|
|
1770
|
+
const index = ruleTestIndexSchema2.parse(JSON.parse(await readFile6(path, "utf8")));
|
|
1771
|
+
return {
|
|
1772
|
+
present: true,
|
|
1773
|
+
valid: true,
|
|
1774
|
+
stale: built === null ? false : !isSameRuleTestIndex(index, built.ruleTestIndex),
|
|
1775
|
+
linkCount: index.links.length,
|
|
1776
|
+
orphanCount: index.orphan_annotations.length
|
|
1777
|
+
};
|
|
1778
|
+
} catch (error) {
|
|
1779
|
+
return {
|
|
1780
|
+
present: !isMissingFileError(error),
|
|
1781
|
+
valid: false,
|
|
1782
|
+
stale: true,
|
|
1783
|
+
linkCount: 0,
|
|
1784
|
+
orphanCount: 0,
|
|
1785
|
+
error: isMissingFileError(error) ? ".fabric/rule-test.index.json is missing." : error instanceof Error ? error.message : String(error)
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
function createBootstrapCheck(exists) {
|
|
1790
|
+
if (!exists) {
|
|
1791
|
+
return issueCheck("Bootstrap README", "error", "fixable_error", "bootstrap_missing", ".fabric/bootstrap/README.md is missing.");
|
|
1792
|
+
}
|
|
1793
|
+
return okCheck("Bootstrap README", ".fabric/bootstrap/README.md exists.");
|
|
1794
|
+
}
|
|
1795
|
+
function createTaxonomyCheck(exists) {
|
|
1796
|
+
if (!exists) {
|
|
1797
|
+
return issueCheck("Initial taxonomy", "error", "manual_error", "taxonomy_missing", ".fabric/INITIAL_TAXONOMY.md is missing.");
|
|
1798
|
+
}
|
|
1799
|
+
return okCheck("Initial taxonomy", ".fabric/INITIAL_TAXONOMY.md exists.");
|
|
1800
|
+
}
|
|
1801
|
+
function createForensicCheck(forensic, frameworkKind, entryPointCount) {
|
|
1802
|
+
if (!forensic.present) {
|
|
1803
|
+
return issueCheck(
|
|
1804
|
+
"Scan evidence",
|
|
1805
|
+
"error",
|
|
1806
|
+
"manual_error",
|
|
1807
|
+
"forensic_missing",
|
|
1808
|
+
`${forensic.error ?? ".fabric/forensic.json is missing."} Live scan detects ${frameworkKind} with ${entryPointCount} entry point${entryPointCount === 1 ? "" : "s"}.`
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
if (!forensic.valid) {
|
|
1812
|
+
return issueCheck("Scan evidence", "error", "manual_error", "forensic_invalid", forensic.error ?? ".fabric/forensic.json is invalid.");
|
|
1813
|
+
}
|
|
1814
|
+
return okCheck("Scan evidence", `.fabric/forensic.json is valid for ${forensic.report?.framework.kind ?? "unknown"}.`);
|
|
1815
|
+
}
|
|
1816
|
+
function createInitContextCheck(initContext) {
|
|
1817
|
+
if (!initContext.exists) {
|
|
1818
|
+
return issueCheck("Init context", "error", "manual_error", "init_context_missing", initContext.error ?? ".fabric/init-context.json is missing.");
|
|
1819
|
+
}
|
|
1820
|
+
if (!initContext.validJson) {
|
|
1821
|
+
return issueCheck("Init context", "error", "manual_error", "init_context_invalid", initContext.error ?? ".fabric/init-context.json is invalid.");
|
|
1822
|
+
}
|
|
1823
|
+
return okCheck("Init context", ".fabric/init-context.json is valid JSON.");
|
|
1824
|
+
}
|
|
1825
|
+
function createMetaCheck(meta) {
|
|
1826
|
+
if (!meta.present) {
|
|
1827
|
+
return issueCheck("Agents metadata", "error", "fixable_error", "agents_meta_missing", ".fabric/agents.meta.json is missing.");
|
|
1828
|
+
}
|
|
1829
|
+
if (!meta.valid) {
|
|
1830
|
+
return issueCheck("Agents metadata", "error", "manual_error", "agents_meta_invalid", meta.readError ?? ".fabric/agents.meta.json is invalid.");
|
|
1831
|
+
}
|
|
1832
|
+
if (meta.stale) {
|
|
1833
|
+
return issueCheck(
|
|
1834
|
+
"Agents metadata",
|
|
1835
|
+
"error",
|
|
1836
|
+
"fixable_error",
|
|
1837
|
+
"agents_meta_stale",
|
|
1838
|
+
`.fabric/agents.meta.json revision ${meta.revision} does not match .fabric/rules derived revision ${meta.computedRevision ?? "<unknown>"}.`
|
|
1839
|
+
);
|
|
1840
|
+
}
|
|
1841
|
+
return okCheck("Agents metadata", `.fabric/agents.meta.json revision ${meta.revision} is aligned with .fabric/rules.`);
|
|
1842
|
+
}
|
|
1843
|
+
function createRuleContentRefCheck(meta) {
|
|
1844
|
+
if (!meta.valid) {
|
|
1845
|
+
return issueCheck("Rule content refs", "error", "manual_error", "content_refs_unavailable", "Cannot inspect content_ref entries until agents.meta.json is valid.");
|
|
1846
|
+
}
|
|
1847
|
+
if (meta.invalidContentRefs.length > 0) {
|
|
1848
|
+
return issueCheck(
|
|
1849
|
+
"Rule content refs",
|
|
1850
|
+
"error",
|
|
1851
|
+
"manual_error",
|
|
1852
|
+
"content_ref_outside_rules",
|
|
1853
|
+
`${meta.invalidContentRefs.length} content_ref entr${meta.invalidContentRefs.length === 1 ? "y is" : "ies are"} outside .fabric/rules.`
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
if (meta.missingContentRefs.length > 0) {
|
|
1857
|
+
return issueCheck(
|
|
1858
|
+
"Rule content refs",
|
|
1859
|
+
"error",
|
|
1860
|
+
"manual_error",
|
|
1861
|
+
"content_ref_missing",
|
|
1862
|
+
`${meta.missingContentRefs.length} content_ref target${meta.missingContentRefs.length === 1 ? "" : "s"} are missing.`
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
return okCheck("Rule content refs", "All content_ref entries resolve to .fabric/rules files or bootstrap README.");
|
|
1866
|
+
}
|
|
1867
|
+
function createRuleSectionsCheck(snapshot) {
|
|
1868
|
+
if (snapshot.invalidFiles.length > 0) {
|
|
1869
|
+
return issueCheck(
|
|
1870
|
+
"Rule sections",
|
|
1871
|
+
"error",
|
|
1872
|
+
"manual_error",
|
|
1873
|
+
"rule_sections_invalid",
|
|
1874
|
+
`${snapshot.invalidFiles.length} rule file${snapshot.invalidFiles.length === 1 ? "" : "s"} could not be parsed.`
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
return okCheck("Rule sections", `${snapshot.checkedCount} .fabric/rules file${snapshot.checkedCount === 1 ? "" : "s"} parsed.`);
|
|
1878
|
+
}
|
|
1879
|
+
function createRuleTestIndexCheck(index) {
|
|
1880
|
+
if (!index.present) {
|
|
1881
|
+
return issueCheck("Rule-test index", "error", "fixable_error", "rule_test_index_missing", index.error);
|
|
1882
|
+
}
|
|
1883
|
+
if (!index.valid) {
|
|
1884
|
+
return issueCheck("Rule-test index", "error", "manual_error", "rule_test_index_invalid", index.error);
|
|
1885
|
+
}
|
|
1886
|
+
if (index.stale) {
|
|
1887
|
+
return issueCheck("Rule-test index", "error", "fixable_error", "rule_test_index_stale", ".fabric/rule-test.index.json is stale.");
|
|
1888
|
+
}
|
|
1889
|
+
return okCheck("Rule-test index", `${index.linkCount} link${index.linkCount === 1 ? "" : "s"} and ${index.orphanCount} orphan annotation${index.orphanCount === 1 ? "" : "s"} indexed.`);
|
|
1890
|
+
}
|
|
1891
|
+
function createEventLedgerCheck(ledger) {
|
|
1892
|
+
if (!ledger.exists) {
|
|
1893
|
+
return issueCheck("Event ledger", "error", "fixable_error", "event_ledger_missing", ".fabric/events.jsonl is missing.");
|
|
1894
|
+
}
|
|
1895
|
+
if (!ledger.writable) {
|
|
1896
|
+
return issueCheck("Event ledger", "error", "manual_error", "event_ledger_not_writable", ledger.error ?? ".fabric/events.jsonl is not writable.");
|
|
1897
|
+
}
|
|
1898
|
+
if (!ledger.parseable) {
|
|
1899
|
+
return issueCheck("Event ledger", "error", "manual_error", "event_ledger_invalid", ledger.error ?? ".fabric/events.jsonl is invalid.");
|
|
1900
|
+
}
|
|
1901
|
+
return okCheck("Event ledger", ".fabric/events.jsonl exists, is writable, and is parseable.");
|
|
1902
|
+
}
|
|
1903
|
+
function okCheck(name, message) {
|
|
1904
|
+
return { name, status: "ok", message };
|
|
1905
|
+
}
|
|
1906
|
+
function issueCheck(name, status, kind, code, message) {
|
|
1907
|
+
return {
|
|
1908
|
+
name,
|
|
1909
|
+
status,
|
|
1910
|
+
kind,
|
|
1911
|
+
code,
|
|
1912
|
+
fixable: kind === "fixable_error",
|
|
1913
|
+
message
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
function collectIssues(checks, kind) {
|
|
1917
|
+
return checks.filter((check) => check.kind === kind).map((check) => ({
|
|
1918
|
+
code: check.code ?? check.name,
|
|
1919
|
+
name: check.name,
|
|
1920
|
+
message: check.message
|
|
1921
|
+
}));
|
|
1922
|
+
}
|
|
1923
|
+
function findIssue(issues, code) {
|
|
1924
|
+
return issues.find((issue) => issue.code === code) ?? {
|
|
1925
|
+
code,
|
|
1926
|
+
name: code,
|
|
1927
|
+
message: code
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
async function writeDefaultBootstrap(projectRoot) {
|
|
1931
|
+
const path = join7(projectRoot, ".fabric", "bootstrap", "README.md");
|
|
1932
|
+
await ensureParentDirectory(path);
|
|
1933
|
+
await atomicWriteText(path, "# Fabric Bootstrap\n\nProject-specific Fabric bootstrap notes.\n");
|
|
1934
|
+
}
|
|
1935
|
+
async function ensureEventLedger(projectRoot) {
|
|
1936
|
+
const path = getEventLedgerPath(projectRoot);
|
|
1937
|
+
await ensureParentDirectory(path);
|
|
1938
|
+
await writeFile2(path, "", { encoding: "utf8", flag: "a" });
|
|
1939
|
+
}
|
|
1940
|
+
function createFixMessage(fixed, report) {
|
|
1941
|
+
const fixedText = fixed.length === 0 ? "No deterministic doctor fixes were needed." : `Applied ${fixed.length} deterministic doctor fix${fixed.length === 1 ? "" : "es"}.`;
|
|
1942
|
+
const manualText = report.manual_errors.length === 0 ? "No manual errors remain." : `${report.manual_errors.length} manual error${report.manual_errors.length === 1 ? "" : "s"} remain.`;
|
|
1943
|
+
return `${fixedText} ${manualText}`;
|
|
1944
|
+
}
|
|
1945
|
+
function findRuleFiles(projectRoot) {
|
|
1946
|
+
const rulesRoot = join7(projectRoot, ".fabric", "rules");
|
|
1947
|
+
if (!existsSync2(rulesRoot) || !statSync2(rulesRoot).isDirectory()) {
|
|
1948
|
+
return [];
|
|
1949
|
+
}
|
|
1950
|
+
const files = [];
|
|
1951
|
+
const stack = [rulesRoot];
|
|
1952
|
+
while (stack.length > 0) {
|
|
1953
|
+
const current = stack.pop();
|
|
1954
|
+
if (current === void 0) {
|
|
1955
|
+
continue;
|
|
1956
|
+
}
|
|
1957
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
1958
|
+
const absolutePath = join7(current, entry.name);
|
|
1959
|
+
const relativePath = normalizePath(absolutePath.slice(projectRoot.length + 1));
|
|
1960
|
+
if (entry.isDirectory()) {
|
|
1961
|
+
stack.push(absolutePath);
|
|
1962
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1963
|
+
files.push(relativePath);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
return files.sort();
|
|
1968
|
+
}
|
|
1969
|
+
function isValidJsonLine(line) {
|
|
1970
|
+
try {
|
|
1971
|
+
JSON.parse(line);
|
|
1972
|
+
return true;
|
|
1973
|
+
} catch {
|
|
1974
|
+
return false;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
function normalizeTarget(targetInput) {
|
|
1978
|
+
return isAbsolute3(targetInput) ? targetInput : resolve4(process.cwd(), targetInput);
|
|
1979
|
+
}
|
|
1980
|
+
function normalizePath(path) {
|
|
1981
|
+
return posix2.normalize(path.split("\\").join("/"));
|
|
1982
|
+
}
|
|
1983
|
+
function collectEntryPoints(root) {
|
|
1984
|
+
if (!existsSync2(root) || !statSync2(root).isDirectory()) {
|
|
1985
|
+
return [];
|
|
1986
|
+
}
|
|
1987
|
+
const entries = [];
|
|
1988
|
+
const stack = [root];
|
|
1989
|
+
while (stack.length > 0) {
|
|
1990
|
+
const current = stack.pop();
|
|
1991
|
+
if (current === void 0) {
|
|
1992
|
+
continue;
|
|
1993
|
+
}
|
|
1994
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
1995
|
+
const absolutePath = join7(current, entry.name);
|
|
1996
|
+
const relativePath = normalizePath(absolutePath.slice(root.length + 1));
|
|
1997
|
+
if (relativePath.length === 0) {
|
|
1998
|
+
continue;
|
|
1999
|
+
}
|
|
2000
|
+
if (entry.isDirectory()) {
|
|
2001
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) {
|
|
2002
|
+
stack.push(absolutePath);
|
|
2003
|
+
}
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
if (!entry.isFile()) {
|
|
2007
|
+
continue;
|
|
2008
|
+
}
|
|
2009
|
+
const reason = getEntryPointReason(relativePath);
|
|
2010
|
+
if (reason !== null) {
|
|
2011
|
+
entries.push({ path: relativePath, reason });
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
return entries.sort((left, right) => left.path.localeCompare(right.path));
|
|
2016
|
+
}
|
|
2017
|
+
function getEntryPointReason(relativePath) {
|
|
2018
|
+
const extension = relativePath.slice(relativePath.lastIndexOf("."));
|
|
2019
|
+
if (!SCRIPT_EXTENSIONS.has(extension)) {
|
|
2020
|
+
return null;
|
|
2021
|
+
}
|
|
2022
|
+
const directory = posix2.dirname(relativePath);
|
|
2023
|
+
const fileName = posix2.basename(relativePath);
|
|
2024
|
+
const fileBase = fileName.slice(0, Math.max(fileName.lastIndexOf("."), 0));
|
|
2025
|
+
if (directory === "assets/scripts" || directory === "scripts") {
|
|
2026
|
+
return "top-level script";
|
|
2027
|
+
}
|
|
2028
|
+
if (directory === "src" && /^(App|app|index|main)$/.test(fileBase)) {
|
|
2029
|
+
return "application entry";
|
|
2030
|
+
}
|
|
2031
|
+
if ((directory === "app" || directory.startsWith("app/")) && /^(layout|page|route)$/.test(fileBase)) {
|
|
2032
|
+
return "next app route";
|
|
2033
|
+
}
|
|
2034
|
+
if ((directory === "pages" || directory.startsWith("pages/")) && fileName !== "_app.d.ts") {
|
|
2035
|
+
return "next page route";
|
|
2036
|
+
}
|
|
2037
|
+
return null;
|
|
2038
|
+
}
|
|
2039
|
+
function reduceStatus(statuses) {
|
|
2040
|
+
if (statuses.includes("error")) {
|
|
2041
|
+
return "error";
|
|
2042
|
+
}
|
|
2043
|
+
if (statuses.includes("warn")) {
|
|
2044
|
+
return "warn";
|
|
2045
|
+
}
|
|
2046
|
+
return "ok";
|
|
2047
|
+
}
|
|
2048
|
+
function isMissingFileError(error) {
|
|
2049
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
export {
|
|
2053
|
+
AGENTS_MD_RESOURCE_URI,
|
|
2054
|
+
contextCache,
|
|
2055
|
+
AgentsMetaFileMissingError,
|
|
2056
|
+
AgentsMetaInvalidError,
|
|
2057
|
+
resolveProjectRoot,
|
|
2058
|
+
readAgentsMeta,
|
|
2059
|
+
LEDGER_PATH,
|
|
2060
|
+
LEGACY_LEDGER_PATH,
|
|
2061
|
+
EVENT_LEDGER_PATH,
|
|
2062
|
+
getLedgerPath,
|
|
2063
|
+
getLegacyLedgerPath,
|
|
2064
|
+
getEventLedgerPath,
|
|
2065
|
+
sha256,
|
|
2066
|
+
isNodeError,
|
|
2067
|
+
appendEventLedgerEvent,
|
|
2068
|
+
readEventLedger,
|
|
2069
|
+
getRules,
|
|
2070
|
+
planContext,
|
|
2071
|
+
RULE_SECTION_NAMES,
|
|
2072
|
+
getRuleSections,
|
|
2073
|
+
buildRuleMeta,
|
|
2074
|
+
writeRuleMeta,
|
|
2075
|
+
computeRulesBasedAgentsMeta,
|
|
2076
|
+
computeRuleTestIndex,
|
|
2077
|
+
deriveRuleMetaLayer,
|
|
2078
|
+
deriveRuleMetaTopologyType,
|
|
2079
|
+
isSameRuleTestIndex,
|
|
2080
|
+
stableStringify,
|
|
2081
|
+
runDoctorReport,
|
|
2082
|
+
runDoctorFix
|
|
2083
|
+
};
|