@fenglimg/fabric-server 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-E3RZ276F.js → chunk-TZCE2K4D.js} +284 -67
- package/dist/{http-BVF4GWIM.js → http-DJCTLGF4.js} +43 -14
- package/dist/index.d.ts +17 -2
- package/dist/index.js +477 -71
- package/dist/static/assets/{index-BRegf31x.js → index-LJh6IezM.js} +7 -7
- package/dist/static/index.html +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AGENTS_MD_RESOURCE_URI,
|
|
3
3
|
FABRIC_DIR,
|
|
4
|
+
LEDGER_PATH,
|
|
5
|
+
LEGACY_LEDGER_PATH,
|
|
4
6
|
appendEditIntentAuditEvents,
|
|
5
7
|
appendLedgerEntry,
|
|
8
|
+
appendRuleSelectionAuditEvent,
|
|
6
9
|
approveHumanLock,
|
|
7
10
|
atomicWriteText,
|
|
8
11
|
contextCache,
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
getLedgerPath,
|
|
13
|
+
getLegacyLedgerPath,
|
|
11
14
|
normalizeRulesPath,
|
|
12
15
|
readAgentsMeta,
|
|
13
16
|
readHumanLock,
|
|
14
17
|
readHumanLockEntry,
|
|
15
18
|
resolveProjectRoot,
|
|
16
|
-
resolveRulesForPath,
|
|
17
19
|
runDoctorAuditReport,
|
|
20
|
+
runDoctorFix,
|
|
18
21
|
runDoctorReport,
|
|
19
22
|
sha256
|
|
20
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-TZCE2K4D.js";
|
|
21
24
|
|
|
22
25
|
// src/index.ts
|
|
23
|
-
import { readFile } from "fs/promises";
|
|
24
|
-
import { join as
|
|
26
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
27
|
+
import { join as join3, resolve } from "path";
|
|
25
28
|
import { fileURLToPath } from "url";
|
|
26
29
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
27
30
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -101,38 +104,295 @@ function registerAppendIntent(server) {
|
|
|
101
104
|
);
|
|
102
105
|
}
|
|
103
106
|
|
|
104
|
-
// src/tools/
|
|
107
|
+
// src/tools/plan-context.ts
|
|
105
108
|
import { z as z2 } from "zod";
|
|
109
|
+
|
|
110
|
+
// src/services/plan-context.ts
|
|
111
|
+
var SELECTION_TOKEN_TTL_MS = 5 * 60 * 1e3;
|
|
112
|
+
var selectionTokenCache = /* @__PURE__ */ new Map();
|
|
113
|
+
async function planContext(projectRoot, input) {
|
|
114
|
+
const meta = await readAgentsMeta(projectRoot);
|
|
115
|
+
const stale = input.client_hash !== void 0 && input.client_hash !== meta.revision;
|
|
116
|
+
const uniquePaths = dedupePaths(input.paths);
|
|
117
|
+
const allDescriptions = buildDescriptionIndex(meta);
|
|
118
|
+
const entries = uniquePaths.map((path) => {
|
|
119
|
+
const profile = buildRequirementProfile(path, input);
|
|
120
|
+
const descriptionIndex = allDescriptions.filter((item) => shouldIncludeIndexItemForPath(item, meta, path));
|
|
121
|
+
const requiredStableIds2 = descriptionIndex.filter((item) => item.required).map((item) => item.stable_id);
|
|
122
|
+
const aiSelectableStableIds2 = descriptionIndex.filter((item) => item.selectable).map((item) => item.stable_id);
|
|
123
|
+
return {
|
|
124
|
+
path,
|
|
125
|
+
requirement_profile: profile,
|
|
126
|
+
description_index: descriptionIndex,
|
|
127
|
+
required_stable_ids: requiredStableIds2,
|
|
128
|
+
ai_selectable_stable_ids: aiSelectableStableIds2,
|
|
129
|
+
initial_selected_stable_ids: requiredStableIds2,
|
|
130
|
+
selection_policy: {
|
|
131
|
+
required_levels: ["L0", "L2"],
|
|
132
|
+
ai_selectable_levels: ["L1"],
|
|
133
|
+
final_fetch_rule: "required_stable_ids + ai_selected_l1_stable_ids"
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
const requiredStableIds = dedupeStableIds(entries.flatMap((entry) => entry.required_stable_ids));
|
|
138
|
+
const aiSelectableStableIds = dedupeStableIds(entries.flatMap((entry) => entry.ai_selectable_stable_ids));
|
|
139
|
+
const sharedDescriptionIndex = dedupeDescriptionIndex(entries.flatMap((entry) => entry.description_index));
|
|
140
|
+
const selectionToken = createSelectionToken(meta.revision, uniquePaths, requiredStableIds, aiSelectableStableIds);
|
|
141
|
+
return {
|
|
142
|
+
revision_hash: meta.revision,
|
|
143
|
+
stale,
|
|
144
|
+
selection_token: selectionToken,
|
|
145
|
+
entries,
|
|
146
|
+
shared: {
|
|
147
|
+
required_stable_ids: requiredStableIds,
|
|
148
|
+
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
149
|
+
description_index: sharedDescriptionIndex,
|
|
150
|
+
preflight_diagnostics: buildPreflightDiagnostics(meta)
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function readSelectionToken(token, now = Date.now()) {
|
|
155
|
+
const state = selectionTokenCache.get(token);
|
|
156
|
+
if (state === void 0) {
|
|
157
|
+
return void 0;
|
|
158
|
+
}
|
|
159
|
+
if (state.expires_at <= now) {
|
|
160
|
+
selectionTokenCache.delete(token);
|
|
161
|
+
return void 0;
|
|
162
|
+
}
|
|
163
|
+
return state;
|
|
164
|
+
}
|
|
165
|
+
function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSelectableStableIds, now = Date.now()) {
|
|
166
|
+
const token = `selection:${revisionHash}:${now.toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
167
|
+
selectionTokenCache.set(token, {
|
|
168
|
+
token,
|
|
169
|
+
revision_hash: revisionHash,
|
|
170
|
+
target_paths: targetPaths,
|
|
171
|
+
required_stable_ids: requiredStableIds,
|
|
172
|
+
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
173
|
+
created_at: now,
|
|
174
|
+
expires_at: now + SELECTION_TOKEN_TTL_MS
|
|
175
|
+
});
|
|
176
|
+
return token;
|
|
177
|
+
}
|
|
178
|
+
function dedupePaths(paths) {
|
|
179
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
180
|
+
return paths.flatMap((path) => {
|
|
181
|
+
const normalizedPath = normalizeRulesPath(path);
|
|
182
|
+
if (seenPaths.has(normalizedPath)) {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
seenPaths.add(normalizedPath);
|
|
186
|
+
return [normalizedPath];
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function buildRequirementProfile(path, input) {
|
|
190
|
+
const normalizedPath = normalizeRulesPath(path);
|
|
191
|
+
const extensionMatch = /(\.[^./\\]+)$/u.exec(normalizedPath);
|
|
192
|
+
const knownTech = dedupeStableIds([
|
|
193
|
+
...input.known_tech ?? [],
|
|
194
|
+
...extensionMatch?.[1] === ".ts" ? ["TypeScript"] : []
|
|
195
|
+
]);
|
|
196
|
+
return {
|
|
197
|
+
target_path: normalizedPath,
|
|
198
|
+
path_segments: normalizedPath.split("/").filter(Boolean),
|
|
199
|
+
extension: extensionMatch?.[1] ?? "",
|
|
200
|
+
inferred_domain: inferDomains(normalizedPath),
|
|
201
|
+
known_tech: knownTech,
|
|
202
|
+
user_intent: input.intent ?? "",
|
|
203
|
+
intent_tokens: tokenizeIntent(input.intent ?? ""),
|
|
204
|
+
impact_hints: inferImpactHints(input.intent ?? ""),
|
|
205
|
+
detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path] ?? []
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function buildDescriptionIndex(meta) {
|
|
209
|
+
return Object.entries(meta.nodes).flatMap(([nodeId, node]) => {
|
|
210
|
+
const level = node.level ?? node.layer;
|
|
211
|
+
const description = node.description ?? descriptionFromLegacyActivation(node.activation?.description);
|
|
212
|
+
if (description === void 0) {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
return [{
|
|
216
|
+
stable_id: node.stable_id ?? nodeId,
|
|
217
|
+
level,
|
|
218
|
+
required: level === "L0" || level === "L2",
|
|
219
|
+
selectable: level === "L1",
|
|
220
|
+
description
|
|
221
|
+
}];
|
|
222
|
+
}).sort(compareDescriptionIndexItems);
|
|
223
|
+
}
|
|
224
|
+
function descriptionFromLegacyActivation(summary) {
|
|
225
|
+
if (summary === void 0) {
|
|
226
|
+
return void 0;
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
summary,
|
|
230
|
+
intent_clues: [],
|
|
231
|
+
tech_stack: [],
|
|
232
|
+
impact: [],
|
|
233
|
+
must_read_if: summary
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function shouldIncludeIndexItemForPath(item, meta, path) {
|
|
237
|
+
if (item.level === "L0" || item.level === "L1") {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
const node = Object.values(meta.nodes).find((candidate) => candidate.stable_id === item.stable_id);
|
|
241
|
+
if (node === void 0) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
return node.scope_glob === path || minimatchSimple(path, node.scope_glob);
|
|
245
|
+
}
|
|
246
|
+
function minimatchSimple(path, glob) {
|
|
247
|
+
if (glob === "**") {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
if (glob.endsWith("/**")) {
|
|
251
|
+
return path.startsWith(glob.slice(0, -3));
|
|
252
|
+
}
|
|
253
|
+
return path === glob;
|
|
254
|
+
}
|
|
255
|
+
function buildPreflightDiagnostics(meta) {
|
|
256
|
+
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();
|
|
257
|
+
if (missingDescriptionStableIds.length === 0) {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
return [{
|
|
261
|
+
code: "missing_description",
|
|
262
|
+
severity: "warn",
|
|
263
|
+
stable_ids: missingDescriptionStableIds,
|
|
264
|
+
message: `Resolved registry includes ${missingDescriptionStableIds.length} node(s) without structured descriptions.`
|
|
265
|
+
}];
|
|
266
|
+
}
|
|
267
|
+
function inferDomains(path) {
|
|
268
|
+
const domains = [];
|
|
269
|
+
if (path.includes("/ui/") || path.toLowerCase().includes("ui")) {
|
|
270
|
+
domains.push("UI");
|
|
271
|
+
}
|
|
272
|
+
if (path.includes("assets/scripts")) {
|
|
273
|
+
domains.push("Gameplay");
|
|
274
|
+
}
|
|
275
|
+
if (path.includes("resources") || path.includes("assets/resources")) {
|
|
276
|
+
domains.push("Asset");
|
|
277
|
+
}
|
|
278
|
+
return domains;
|
|
279
|
+
}
|
|
280
|
+
function tokenizeIntent(intent) {
|
|
281
|
+
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()));
|
|
282
|
+
return dedupeStableIds(tokens);
|
|
283
|
+
}
|
|
284
|
+
function inferImpactHints(intent) {
|
|
285
|
+
return /性能|优化|drawcall|渲染|卡顿|闪烁/iu.test(intent) ? ["Performance"] : [];
|
|
286
|
+
}
|
|
287
|
+
function dedupeStableIds(stableIds) {
|
|
288
|
+
return Array.from(new Set(stableIds));
|
|
289
|
+
}
|
|
290
|
+
function dedupeDescriptionIndex(items) {
|
|
291
|
+
const seenStableIds = /* @__PURE__ */ new Set();
|
|
292
|
+
return items.filter((item) => {
|
|
293
|
+
if (seenStableIds.has(item.stable_id)) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
seenStableIds.add(item.stable_id);
|
|
297
|
+
return true;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function compareDescriptionIndexItems(left, right) {
|
|
301
|
+
const levelDelta = levelOrder(left.level) - levelOrder(right.level);
|
|
302
|
+
return levelDelta !== 0 ? levelDelta : left.stable_id.localeCompare(right.stable_id);
|
|
303
|
+
}
|
|
304
|
+
function levelOrder(level) {
|
|
305
|
+
switch (level) {
|
|
306
|
+
case "L0":
|
|
307
|
+
return 0;
|
|
308
|
+
case "L1":
|
|
309
|
+
return 1;
|
|
310
|
+
case "L2":
|
|
311
|
+
return 2;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/tools/plan-context.ts
|
|
106
316
|
var inputSchema2 = {
|
|
107
|
-
|
|
108
|
-
|
|
317
|
+
paths: z2.array(z2.string()).min(1).describe("Candidate file paths to build neutral rule selection context for"),
|
|
318
|
+
intent: z2.string().optional().describe("User-stated requirement or implementation intent; used only to build a neutral requirement profile"),
|
|
319
|
+
known_tech: z2.array(z2.string()).optional().describe("Known technologies involved in the requirement profile"),
|
|
320
|
+
detected_entities: z2.record(z2.array(z2.string())).optional().describe("Optional path-keyed detected entities for the requirement profile"),
|
|
321
|
+
client_hash: z2.string().optional().describe("Revision hash from a prior fab_plan_context response; enables stale detection")
|
|
109
322
|
};
|
|
110
|
-
var
|
|
111
|
-
|
|
112
|
-
|
|
323
|
+
var ruleDescriptionSchema = z2.object({
|
|
324
|
+
summary: z2.string(),
|
|
325
|
+
intent_clues: z2.array(z2.string()),
|
|
326
|
+
tech_stack: z2.array(z2.string()),
|
|
327
|
+
impact: z2.array(z2.string()),
|
|
328
|
+
must_read_if: z2.string(),
|
|
329
|
+
entities: z2.array(z2.string()).optional()
|
|
330
|
+
});
|
|
331
|
+
var descriptionIndexItemSchema = z2.object({
|
|
332
|
+
stable_id: z2.string(),
|
|
333
|
+
level: z2.enum(["L0", "L1", "L2"]),
|
|
334
|
+
required: z2.boolean(),
|
|
335
|
+
selectable: z2.boolean(),
|
|
336
|
+
description: ruleDescriptionSchema
|
|
337
|
+
});
|
|
338
|
+
var requirementProfileSchema = z2.object({
|
|
339
|
+
target_path: z2.string(),
|
|
340
|
+
path_segments: z2.array(z2.string()),
|
|
341
|
+
extension: z2.string(),
|
|
342
|
+
inferred_domain: z2.array(z2.string()),
|
|
343
|
+
known_tech: z2.array(z2.string()),
|
|
344
|
+
user_intent: z2.string(),
|
|
345
|
+
intent_tokens: z2.array(z2.string()),
|
|
346
|
+
impact_hints: z2.array(z2.string()),
|
|
347
|
+
detected_entities: z2.array(z2.string())
|
|
348
|
+
});
|
|
349
|
+
var selectionPolicySchema = z2.object({
|
|
350
|
+
required_levels: z2.tuple([z2.literal("L0"), z2.literal("L2")]),
|
|
351
|
+
ai_selectable_levels: z2.tuple([z2.literal("L1")]),
|
|
352
|
+
final_fetch_rule: z2.literal("required_stable_ids + ai_selected_l1_stable_ids")
|
|
353
|
+
});
|
|
113
354
|
var outputSchema2 = z2.object({
|
|
114
355
|
revision_hash: z2.string(),
|
|
115
356
|
stale: z2.boolean(),
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
357
|
+
selection_token: z2.string(),
|
|
358
|
+
entries: z2.array(
|
|
359
|
+
z2.object({
|
|
360
|
+
path: z2.string(),
|
|
361
|
+
requirement_profile: requirementProfileSchema,
|
|
362
|
+
description_index: z2.array(descriptionIndexItemSchema),
|
|
363
|
+
required_stable_ids: z2.array(z2.string()),
|
|
364
|
+
ai_selectable_stable_ids: z2.array(z2.string()),
|
|
365
|
+
initial_selected_stable_ids: z2.array(z2.string()),
|
|
366
|
+
selection_policy: selectionPolicySchema
|
|
367
|
+
})
|
|
368
|
+
),
|
|
369
|
+
shared: z2.object({
|
|
370
|
+
required_stable_ids: z2.array(z2.string()),
|
|
371
|
+
ai_selectable_stable_ids: z2.array(z2.string()),
|
|
372
|
+
description_index: z2.array(descriptionIndexItemSchema),
|
|
373
|
+
preflight_diagnostics: z2.array(
|
|
374
|
+
z2.object({
|
|
375
|
+
code: z2.literal("missing_description"),
|
|
376
|
+
severity: z2.literal("warn"),
|
|
377
|
+
message: z2.string(),
|
|
378
|
+
stable_ids: z2.array(z2.string()).optional(),
|
|
379
|
+
path: z2.string().optional()
|
|
380
|
+
})
|
|
381
|
+
)
|
|
122
382
|
})
|
|
123
383
|
});
|
|
124
|
-
function
|
|
384
|
+
function registerPlanContext(server) {
|
|
125
385
|
server.registerTool(
|
|
126
|
-
"
|
|
386
|
+
"fab_plan_context",
|
|
127
387
|
{
|
|
128
|
-
description: "
|
|
388
|
+
description: "Use during plan or architecture phases to build a neutral Fabric rule description index and selection token before fetching rule sections.",
|
|
129
389
|
inputSchema: inputSchema2,
|
|
130
390
|
outputSchema: outputSchema2,
|
|
131
391
|
annotations: { readOnlyHint: true }
|
|
132
392
|
},
|
|
133
|
-
async ({
|
|
393
|
+
async ({ paths, intent, known_tech, detected_entities, client_hash }) => {
|
|
134
394
|
const projectRoot = resolveProjectRoot();
|
|
135
|
-
const result = await
|
|
395
|
+
const result = await planContext(projectRoot, { paths, intent, known_tech, detected_entities, client_hash });
|
|
136
396
|
return {
|
|
137
397
|
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
138
398
|
structuredContent: result
|
|
@@ -141,73 +401,214 @@ function registerGetRules(server) {
|
|
|
141
401
|
);
|
|
142
402
|
}
|
|
143
403
|
|
|
144
|
-
// src/tools/
|
|
404
|
+
// src/tools/rule-sections.ts
|
|
145
405
|
import { z as z3 } from "zod";
|
|
146
406
|
|
|
147
|
-
// src/services/
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
407
|
+
// src/services/rule-sections.ts
|
|
408
|
+
import { readFile } from "fs/promises";
|
|
409
|
+
import { join } from "path";
|
|
410
|
+
var RULE_SECTION_NAMES = ["MANDATORY_INJECTION", "CONTEXT_INFO"];
|
|
411
|
+
var PRIORITY_ORDER = {
|
|
412
|
+
high: 0,
|
|
413
|
+
medium: 1,
|
|
414
|
+
low: 2
|
|
415
|
+
};
|
|
416
|
+
function parseRuleSections(content) {
|
|
417
|
+
const sections = /* @__PURE__ */ new Map();
|
|
418
|
+
const lines = content.split(/\r?\n/u);
|
|
419
|
+
let activeSection;
|
|
420
|
+
let buffer = [];
|
|
421
|
+
const flush = () => {
|
|
422
|
+
if (activeSection === void 0) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const text = buffer.join("\n").trim();
|
|
426
|
+
if (text.length === 0) {
|
|
427
|
+
buffer = [];
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
sections.set(activeSection, [...sections.get(activeSection) ?? [], text]);
|
|
431
|
+
buffer = [];
|
|
432
|
+
};
|
|
433
|
+
for (const line of lines) {
|
|
434
|
+
const heading = /^(#{2,6})\s+\[([A-Z_]+)\]\s*$/u.exec(line.trim());
|
|
435
|
+
if (heading !== null) {
|
|
436
|
+
flush();
|
|
437
|
+
activeSection = isRuleSectionName(heading[2]) ? heading[2] : void 0;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (/^#{1,6}\s+/u.test(line)) {
|
|
441
|
+
flush();
|
|
442
|
+
activeSection = void 0;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (activeSection !== void 0) {
|
|
446
|
+
buffer.push(line);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
flush();
|
|
450
|
+
return new Map(
|
|
451
|
+
Array.from(sections.entries()).map(([section, values]) => [section, values.join("\n\n")])
|
|
157
452
|
);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
453
|
+
}
|
|
454
|
+
async function getRuleSections(projectRoot, input) {
|
|
455
|
+
const token = readSelectionToken(input.selection_token);
|
|
456
|
+
if (token === void 0) {
|
|
457
|
+
throw new Error("selection_token is missing or expired");
|
|
458
|
+
}
|
|
459
|
+
validateAiSelections(token.ai_selectable_stable_ids, input.ai_selected_stable_ids, input.ai_selection_reasons);
|
|
460
|
+
const meta = await readAgentsMeta(projectRoot);
|
|
461
|
+
const selectedStableIds = [...token.required_stable_ids, ...input.ai_selected_stable_ids];
|
|
462
|
+
const selectedRules = sortRuleNodes(selectedStableIds.map((stableId) => findRuleNode(meta, stableId)));
|
|
463
|
+
const diagnostics = [];
|
|
464
|
+
const rules = [];
|
|
465
|
+
for (const rule of selectedRules) {
|
|
466
|
+
const content = await readFile(join(projectRoot, rule.path), "utf8");
|
|
467
|
+
const parsedSections = parseRuleSections(content);
|
|
468
|
+
const sections = {};
|
|
469
|
+
for (const section of input.sections) {
|
|
470
|
+
const sectionContent = parsedSections.get(section);
|
|
471
|
+
sections[section] = sectionContent ?? "";
|
|
472
|
+
if (sectionContent === void 0) {
|
|
473
|
+
diagnostics.push({
|
|
474
|
+
code: "missing_section",
|
|
475
|
+
severity: "warn",
|
|
476
|
+
stable_id: rule.stable_id,
|
|
477
|
+
section,
|
|
478
|
+
message: `Rule ${rule.stable_id} does not define section ${section}.`
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
rules.push({
|
|
483
|
+
stable_id: rule.stable_id,
|
|
484
|
+
level: rule.level,
|
|
485
|
+
path: rule.path,
|
|
486
|
+
sections
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
const result = {
|
|
490
|
+
revision_hash: meta.revision,
|
|
491
|
+
precedence: ["L2", "L1", "L0"],
|
|
492
|
+
selected_stable_ids: rules.map((rule) => rule.stable_id),
|
|
493
|
+
rules,
|
|
494
|
+
diagnostics
|
|
162
495
|
};
|
|
496
|
+
await appendRuleSelectionAuditEvent(projectRoot, {
|
|
497
|
+
path: token.target_paths[0] ?? "",
|
|
498
|
+
selection_token: input.selection_token,
|
|
499
|
+
target_paths: token.target_paths,
|
|
500
|
+
required_stable_ids: token.required_stable_ids,
|
|
501
|
+
ai_selectable_stable_ids: token.ai_selectable_stable_ids,
|
|
502
|
+
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
503
|
+
final_stable_ids: result.selected_stable_ids,
|
|
504
|
+
ai_selection_reasons: pickSelectionReasons(input.ai_selected_stable_ids, input.ai_selection_reasons),
|
|
505
|
+
rejected_stable_ids: [],
|
|
506
|
+
ignored_stable_ids: []
|
|
507
|
+
});
|
|
508
|
+
return result;
|
|
163
509
|
}
|
|
164
|
-
function
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return [];
|
|
510
|
+
function validateAiSelections(aiSelectableStableIds, aiSelectedStableIds, aiSelectionReasons) {
|
|
511
|
+
const selectable = new Set(aiSelectableStableIds);
|
|
512
|
+
for (const stableId of aiSelectedStableIds) {
|
|
513
|
+
if (!selectable.has(stableId)) {
|
|
514
|
+
throw new Error(`Invalid L1 rule selection: ${stableId}`);
|
|
170
515
|
}
|
|
171
|
-
|
|
172
|
-
|
|
516
|
+
if (aiSelectionReasons[stableId]?.trim() === "") {
|
|
517
|
+
throw new Error(`Missing AI selection reason for ${stableId}`);
|
|
518
|
+
}
|
|
519
|
+
if (aiSelectionReasons[stableId] === void 0) {
|
|
520
|
+
throw new Error(`Missing AI selection reason for ${stableId}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function findRuleNode(meta, stableId) {
|
|
525
|
+
for (const [nodeId, node] of Object.entries(meta.nodes)) {
|
|
526
|
+
const nodeStableId = node.stable_id ?? nodeId;
|
|
527
|
+
if (nodeStableId !== stableId) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const level = node.level ?? node.layer;
|
|
531
|
+
return {
|
|
532
|
+
stable_id: nodeStableId,
|
|
533
|
+
level,
|
|
534
|
+
path: normalizeRulesPath(node.content_ref ?? node.file),
|
|
535
|
+
priority: node.priority,
|
|
536
|
+
node
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
throw new Error(`Selected rule is not present in agents.meta.json: ${stableId}`);
|
|
540
|
+
}
|
|
541
|
+
function sortRuleNodes(rules) {
|
|
542
|
+
return [...rules].sort((left, right) => {
|
|
543
|
+
const levelDelta = outputLevelOrder(left.level) - outputLevelOrder(right.level);
|
|
544
|
+
if (levelDelta !== 0) {
|
|
545
|
+
return levelDelta;
|
|
546
|
+
}
|
|
547
|
+
const priorityDelta = PRIORITY_ORDER[left.priority] - PRIORITY_ORDER[right.priority];
|
|
548
|
+
if (priorityDelta !== 0) {
|
|
549
|
+
return priorityDelta;
|
|
550
|
+
}
|
|
551
|
+
return left.stable_id.localeCompare(right.stable_id);
|
|
173
552
|
});
|
|
174
553
|
}
|
|
554
|
+
function outputLevelOrder(level) {
|
|
555
|
+
switch (level) {
|
|
556
|
+
case "L0":
|
|
557
|
+
return 0;
|
|
558
|
+
case "L1":
|
|
559
|
+
return 1;
|
|
560
|
+
case "L2":
|
|
561
|
+
return 2;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function isRuleSectionName(value) {
|
|
565
|
+
return RULE_SECTION_NAMES.includes(value);
|
|
566
|
+
}
|
|
567
|
+
function pickSelectionReasons(selectedStableIds, reasons) {
|
|
568
|
+
return Object.fromEntries(selectedStableIds.map((stableId) => [stableId, reasons[stableId] ?? ""]));
|
|
569
|
+
}
|
|
175
570
|
|
|
176
|
-
// src/tools/
|
|
571
|
+
// src/tools/rule-sections.ts
|
|
177
572
|
var inputSchema3 = {
|
|
178
|
-
|
|
179
|
-
|
|
573
|
+
selection_token: z3.string().min(1).describe("Selection token returned by fab_plan_context"),
|
|
574
|
+
sections: z3.array(z3.enum(RULE_SECTION_NAMES)).min(1).describe("Structured rule sections to fetch"),
|
|
575
|
+
ai_selected_stable_ids: z3.array(z3.string()).describe("AI-selected L1 stable_ids chosen from fab_plan_context ai_selectable_stable_ids"),
|
|
576
|
+
ai_selection_reasons: z3.record(z3.string().min(1)).describe("Reason for each AI-selected L1 stable_id")
|
|
180
577
|
};
|
|
181
|
-
var rulesEntrySchema2 = z3.object({ path: z3.string(), content: z3.string() });
|
|
182
|
-
var humanLockedSchema2 = z3.object({ file: z3.string(), excerpt: z3.string() });
|
|
183
|
-
var rulesPayloadSchema = z3.object({
|
|
184
|
-
L0: z3.string(),
|
|
185
|
-
L1: z3.array(rulesEntrySchema2),
|
|
186
|
-
L2: z3.array(rulesEntrySchema2),
|
|
187
|
-
human_locked_nearby: z3.array(humanLockedSchema2)
|
|
188
|
-
});
|
|
189
578
|
var outputSchema3 = z3.object({
|
|
190
579
|
revision_hash: z3.string(),
|
|
191
|
-
|
|
192
|
-
|
|
580
|
+
precedence: z3.tuple([z3.literal("L2"), z3.literal("L1"), z3.literal("L0")]),
|
|
581
|
+
selected_stable_ids: z3.array(z3.string()),
|
|
582
|
+
rules: z3.array(
|
|
193
583
|
z3.object({
|
|
584
|
+
stable_id: z3.string(),
|
|
585
|
+
level: z3.enum(["L0", "L1", "L2"]),
|
|
194
586
|
path: z3.string(),
|
|
195
|
-
|
|
587
|
+
sections: z3.record(z3.string())
|
|
588
|
+
})
|
|
589
|
+
),
|
|
590
|
+
diagnostics: z3.array(
|
|
591
|
+
z3.object({
|
|
592
|
+
code: z3.literal("missing_section"),
|
|
593
|
+
severity: z3.literal("warn"),
|
|
594
|
+
stable_id: z3.string(),
|
|
595
|
+
section: z3.enum(RULE_SECTION_NAMES),
|
|
596
|
+
message: z3.string()
|
|
196
597
|
})
|
|
197
598
|
)
|
|
198
599
|
});
|
|
199
|
-
function
|
|
600
|
+
function registerRuleSections(server) {
|
|
200
601
|
server.registerTool(
|
|
201
|
-
"
|
|
602
|
+
"fab_get_rule_sections",
|
|
202
603
|
{
|
|
203
|
-
description: "
|
|
604
|
+
description: "Fetch structured Fabric rule sections after fab_plan_context. Required L0/L2 rules are merged with AI-selected L1 rules server-side.",
|
|
204
605
|
inputSchema: inputSchema3,
|
|
205
606
|
outputSchema: outputSchema3,
|
|
206
607
|
annotations: { readOnlyHint: true }
|
|
207
608
|
},
|
|
208
|
-
async (
|
|
609
|
+
async (input) => {
|
|
209
610
|
const projectRoot = resolveProjectRoot();
|
|
210
|
-
const result = await
|
|
611
|
+
const result = await getRuleSections(projectRoot, input);
|
|
211
612
|
return {
|
|
212
613
|
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
213
614
|
structuredContent: result
|
|
@@ -222,9 +623,9 @@ import { z as z4 } from "zod";
|
|
|
222
623
|
|
|
223
624
|
// src/services/update-registry.ts
|
|
224
625
|
import { agentsMetaNodeSchema } from "@fenglimg/fabric-shared";
|
|
225
|
-
import { join } from "path";
|
|
626
|
+
import { join as join2 } from "path";
|
|
226
627
|
async function updateRegistry(projectRoot, input) {
|
|
227
|
-
const metaPath =
|
|
628
|
+
const metaPath = join2(projectRoot, FABRIC_DIR, "agents.meta.json");
|
|
228
629
|
const currentMeta = await readAgentsMeta(projectRoot);
|
|
229
630
|
const nextMeta = applyRegistryOperation(currentMeta, input.op, input.node_id, input.data);
|
|
230
631
|
const newRevision = computeRevision(nextMeta);
|
|
@@ -339,10 +740,10 @@ function formatError(error) {
|
|
|
339
740
|
function createFabricServer() {
|
|
340
741
|
const server = new McpServer({
|
|
341
742
|
name: "fabric-context-server",
|
|
342
|
-
version: "1.
|
|
743
|
+
version: "1.6.0"
|
|
343
744
|
});
|
|
344
|
-
registerGetRules(server);
|
|
345
745
|
registerPlanContext(server);
|
|
746
|
+
registerRuleSections(server);
|
|
346
747
|
registerAppendIntent(server);
|
|
347
748
|
registerUpdateRegistry(server);
|
|
348
749
|
server.registerResource(
|
|
@@ -354,7 +755,7 @@ function createFabricServer() {
|
|
|
354
755
|
},
|
|
355
756
|
async (_uri) => {
|
|
356
757
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
357
|
-
const content = await
|
|
758
|
+
const content = await readFile2(join3(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
|
|
358
759
|
return {
|
|
359
760
|
contents: [
|
|
360
761
|
{
|
|
@@ -374,7 +775,7 @@ async function startStdioServer() {
|
|
|
374
775
|
await server.connect(transport);
|
|
375
776
|
}
|
|
376
777
|
async function startHttpServer(options) {
|
|
377
|
-
const { createFabricHttpApp } = await import("./http-
|
|
778
|
+
const { createFabricHttpApp } = await import("./http-DJCTLGF4.js");
|
|
378
779
|
const { port, projectRoot, host = "127.0.0.1", authToken, dashboardDistPath, dev } = options;
|
|
379
780
|
const app = createFabricHttpApp({ projectRoot, host, authToken, dashboardDistPath, dev });
|
|
380
781
|
return await new Promise((resolveServer, rejectServer) => {
|
|
@@ -401,11 +802,16 @@ if (isMainModule) {
|
|
|
401
802
|
}
|
|
402
803
|
export {
|
|
403
804
|
AGENTS_MD_RESOURCE_URI,
|
|
805
|
+
LEDGER_PATH,
|
|
806
|
+
LEGACY_LEDGER_PATH,
|
|
404
807
|
approveHumanLock,
|
|
405
808
|
createFabricServer,
|
|
809
|
+
getLedgerPath,
|
|
810
|
+
getLegacyLedgerPath,
|
|
406
811
|
readHumanLock,
|
|
407
812
|
readHumanLockEntry,
|
|
408
813
|
runDoctorAuditReport,
|
|
814
|
+
runDoctorFix,
|
|
409
815
|
runDoctorReport,
|
|
410
816
|
startHttpServer,
|
|
411
817
|
startStdioServer
|