@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/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
- getRules,
10
- loadGetRulesContext,
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-E3RZ276F.js";
23
+ } from "./chunk-TZCE2K4D.js";
21
24
 
22
25
  // src/index.ts
23
- import { readFile } from "fs/promises";
24
- import { join as join2, resolve } from "path";
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/get-rules.ts
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
- path: z2.string().describe("Target file path to query rules for"),
108
- client_hash: z2.string().optional().describe("Revision hash from prior fab_get_rules response; enables stale detection")
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 rulesEntrySchema = z2.object({ path: z2.string(), content: z2.string() });
111
- var humanLockedSchema = z2.object({ file: z2.string(), excerpt: z2.string() });
112
- var descriptionStubSchema = z2.object({ path: z2.string(), description: z2.string() });
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
- rules: z2.object({
117
- L0: z2.string(),
118
- L1: z2.array(rulesEntrySchema),
119
- L2: z2.array(rulesEntrySchema),
120
- human_locked_nearby: z2.array(humanLockedSchema),
121
- description_stubs: z2.array(descriptionStubSchema).optional()
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 registerGetRules(server) {
384
+ function registerPlanContext(server) {
125
385
  server.registerTool(
126
- "fab_get_rules",
386
+ "fab_plan_context",
127
387
  {
128
- description: "Call before modifying any file to retrieve Fabric rules for a target path.",
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 ({ path, client_hash }) => {
393
+ async ({ paths, intent, known_tech, detected_entities, client_hash }) => {
134
394
  const projectRoot = resolveProjectRoot();
135
- const result = await getRules(projectRoot, { path, client_hash });
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/plan-context.ts
404
+ // src/tools/rule-sections.ts
145
405
  import { z as z3 } from "zod";
146
406
 
147
- // src/services/plan-context.ts
148
- async function planContext(projectRoot, input) {
149
- const context = await loadGetRulesContext(projectRoot);
150
- const stale = input.client_hash !== void 0 && input.client_hash !== context.meta.revision;
151
- const uniquePaths = dedupePaths(input.paths);
152
- const entries = await Promise.all(
153
- uniquePaths.map(async (path) => ({
154
- path,
155
- rules: await resolveRulesForPath(projectRoot, context, path, { dedupeByPath: true })
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
- return {
159
- revision_hash: context.meta.revision,
160
- stale,
161
- entries
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 dedupePaths(paths) {
165
- const seenPaths = /* @__PURE__ */ new Set();
166
- return paths.flatMap((path) => {
167
- const normalizedPath = normalizeRulesPath(path);
168
- if (seenPaths.has(normalizedPath)) {
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
- seenPaths.add(normalizedPath);
172
- return [normalizedPath];
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/plan-context.ts
571
+ // src/tools/rule-sections.ts
177
572
  var inputSchema3 = {
178
- paths: z3.array(z3.string()).min(2).describe("Candidate file paths to query rules for during planning or architecture review"),
179
- client_hash: z3.string().optional().describe("Revision hash from a prior fab_plan_context response; enables stale detection")
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
- stale: z3.boolean(),
192
- entries: z3.array(
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
- rules: rulesPayloadSchema
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 registerPlanContext(server) {
600
+ function registerRuleSections(server) {
200
601
  server.registerTool(
201
- "fab_plan_context",
602
+ "fab_get_rule_sections",
202
603
  {
203
- description: "Use during plan or architecture phases to batch-query Fabric rules for multiple candidate paths in one round-trip. Use fab_get_rules for single-file queries; use fab_plan_context for 2+ files.",
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 ({ paths, client_hash }) => {
609
+ async (input) => {
209
610
  const projectRoot = resolveProjectRoot();
210
- const result = await planContext(projectRoot, { paths, client_hash });
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 = join(projectRoot, FABRIC_DIR, "agents.meta.json");
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.5.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 readFile(join2(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
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-BVF4GWIM.js");
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