@fenglimg/fabric-server 1.6.0 → 1.8.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,727 +1,292 @@
1
1
  import {
2
2
  AGENTS_MD_RESOURCE_URI,
3
- FABRIC_DIR,
3
+ EVENT_LEDGER_PATH,
4
4
  LEDGER_PATH,
5
5
  LEGACY_LEDGER_PATH,
6
- appendEditIntentAuditEvents,
7
- appendLedgerEntry,
8
- appendRuleSelectionAuditEvent,
9
- approveHumanLock,
10
- atomicWriteText,
11
- contextCache,
6
+ buildRuleMeta,
7
+ computeRuleTestIndex,
8
+ computeRulesBasedAgentsMeta,
9
+ deriveRuleMetaLayer,
10
+ deriveRuleMetaTopologyType,
11
+ ensureRulesFresh,
12
+ flushAndSyncEventLedger,
13
+ getEventLedgerPath,
12
14
  getLedgerPath,
13
15
  getLegacyLedgerPath,
14
- normalizeRulesPath,
15
- readAgentsMeta,
16
- readHumanLock,
17
- readHumanLockEntry,
16
+ getRuleSections,
17
+ isSameRuleTestIndex,
18
+ planContext,
19
+ reconcileRules,
18
20
  resolveProjectRoot,
19
- runDoctorAuditReport,
20
21
  runDoctorFix,
21
22
  runDoctorReport,
22
- sha256
23
- } from "./chunk-TZCE2K4D.js";
23
+ stableStringify,
24
+ writeRuleMeta
25
+ } from "./chunk-E3BHIUIW.js";
24
26
 
25
27
  // src/index.ts
26
- import { readFile as readFile2 } from "fs/promises";
27
- import { join as join3, resolve } from "path";
28
+ import { existsSync as existsSync2 } from "fs";
29
+ import { readFile } from "fs/promises";
30
+ import { join as join2, resolve } from "path";
28
31
  import { fileURLToPath } from "url";
29
32
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
30
33
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
31
34
 
32
- // src/tools/append-intent.ts
33
- import { aiLedgerEntrySchema } from "@fenglimg/fabric-shared";
34
- import { z } from "zod";
35
-
36
- // src/services/append-intent.ts
37
- async function appendIntent(projectRoot, input) {
38
- const ts = Date.now();
39
- const entry = await appendLedgerEntry(projectRoot, {
40
- ...input.entry,
41
- ts,
42
- source: "ai"
43
- });
44
- let compliance;
45
- try {
46
- const auditResult = await appendEditIntentAuditEvents(projectRoot, {
47
- affected_paths: entry.affected_paths,
48
- intent: entry.intent,
49
- ledger_entry_id: entry.id,
50
- ts
51
- });
52
- compliance = auditResult.compliance;
53
- } catch {
54
- }
35
+ // src/services/in-flight-tracker.ts
36
+ function createInFlightTracker() {
37
+ const active = /* @__PURE__ */ new Map();
38
+ let resolveDrained = null;
55
39
  return {
56
- success: true,
57
- timestamp: ts,
58
- entry,
59
- compliance
60
- };
61
- }
62
-
63
- // src/tools/append-intent.ts
64
- var inputSchema = {
65
- entry: aiLedgerEntrySchema.omit({
66
- id: true,
67
- source: true,
68
- ts: true
69
- })
70
- };
71
- var outputSchema = z.object({
72
- success: z.boolean(),
73
- timestamp: z.number(),
74
- entry: z.record(z.unknown()),
75
- compliance: z.object({
76
- compliant: z.boolean(),
77
- matched_get_rules_ts: z.string().nullable(),
78
- window_ms: z.number()
79
- }).optional()
80
- });
81
- function registerAppendIntent(server) {
82
- server.registerTool(
83
- "fab_append_intent",
84
- {
85
- description: "Call after a completed task to append an intent ledger entry for Fabric.",
86
- inputSchema,
87
- outputSchema,
88
- annotations: { readOnlyHint: false }
40
+ enter(id) {
41
+ active.set(id, Date.now());
89
42
  },
90
- async ({ entry }) => {
91
- const projectRoot = resolveProjectRoot();
92
- const result = await appendIntent(projectRoot, { entry });
93
- const structuredContent = {
94
- success: result.success,
95
- timestamp: result.timestamp,
96
- entry: { ...result.entry },
97
- compliance: result.compliance
98
- };
99
- return {
100
- content: [{ type: "text", text: JSON.stringify(result) }],
101
- structuredContent
102
- };
43
+ exit(id) {
44
+ active.delete(id);
45
+ if (active.size === 0 && resolveDrained) {
46
+ resolveDrained();
47
+ resolveDrained = null;
48
+ }
49
+ },
50
+ drain(deadlineMs) {
51
+ const startedWith = active.size;
52
+ if (startedWith === 0) return Promise.resolve({ drained: 0, timed_out: 0 });
53
+ return new Promise((resolve2) => {
54
+ const timer = setTimeout(() => {
55
+ const drainedCount = startedWith - active.size;
56
+ resolveDrained = null;
57
+ resolve2({ drained: drainedCount, timed_out: active.size });
58
+ }, deadlineMs);
59
+ resolveDrained = () => {
60
+ clearTimeout(timer);
61
+ resolve2({ drained: startedWith, timed_out: 0 });
62
+ };
63
+ });
64
+ },
65
+ size() {
66
+ return active.size;
103
67
  }
104
- );
68
+ };
105
69
  }
106
70
 
107
71
  // src/tools/plan-context.ts
108
- import { z as z2 } from "zod";
72
+ import { randomUUID } from "crypto";
73
+ import {
74
+ planContextAnnotations,
75
+ planContextInputSchema,
76
+ planContextOutputSchema
77
+ } from "@fenglimg/fabric-shared/schemas/api-contracts";
78
+ import { enforcePayloadLimit } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
109
79
 
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");
80
+ // src/config-loader.ts
81
+ import { existsSync, readFileSync } from "fs";
82
+ import { join } from "path";
83
+ function readFabricConfig(projectRoot) {
84
+ const configPath = join(projectRoot, "fabric.config.json");
85
+ if (!existsSync(configPath)) {
86
+ return {};
274
87
  }
275
- if (path.includes("resources") || path.includes("assets/resources")) {
276
- domains.push("Asset");
88
+ const parsed = JSON.parse(readFileSync(configPath, "utf8"));
89
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
90
+ throw new Error(`Expected object in ${configPath}`);
277
91
  }
278
- return domains;
92
+ return parsed;
279
93
  }
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
- }
94
+ function readPayloadLimits(projectRoot) {
95
+ return readFabricConfig(projectRoot).mcpPayloadLimits;
313
96
  }
314
97
 
315
98
  // src/tools/plan-context.ts
316
- var inputSchema2 = {
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")
322
- };
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
- });
354
- var outputSchema2 = z2.object({
355
- revision_hash: z2.string(),
356
- stale: z2.boolean(),
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
- )
382
- })
383
- });
384
- function registerPlanContext(server) {
99
+ function registerPlanContext(server, tracker) {
385
100
  server.registerTool(
386
101
  "fab_plan_context",
387
102
  {
388
103
  description: "Use during plan or architecture phases to build a neutral Fabric rule description index and selection token before fetching rule sections.",
389
- inputSchema: inputSchema2,
390
- outputSchema: outputSchema2,
391
- annotations: { readOnlyHint: true }
104
+ inputSchema: planContextInputSchema,
105
+ outputSchema: planContextOutputSchema,
106
+ annotations: planContextAnnotations
392
107
  },
393
- async ({ paths, intent, known_tech, detected_entities, client_hash }) => {
394
- const projectRoot = resolveProjectRoot();
395
- const result = await planContext(projectRoot, { paths, intent, known_tech, detected_entities, client_hash });
396
- return {
397
- content: [{ type: "text", text: JSON.stringify(result) }],
398
- structuredContent: result
399
- };
400
- }
401
- );
402
- }
403
-
404
- // src/tools/rule-sections.ts
405
- import { z as z3 } from "zod";
406
-
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")])
452
- );
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}.`
108
+ async ({ paths, intent, known_tech, detected_entities, client_hash, correlation_id, session_id }) => {
109
+ const requestId = randomUUID();
110
+ tracker?.enter(requestId);
111
+ try {
112
+ const projectRoot = resolveProjectRoot();
113
+ const syncReport = await ensureRulesFresh(projectRoot);
114
+ const result = await planContext(projectRoot, {
115
+ paths,
116
+ intent,
117
+ known_tech,
118
+ detected_entities,
119
+ client_hash,
120
+ correlation_id,
121
+ session_id
479
122
  });
123
+ const response = {
124
+ ...result,
125
+ warnings: [...syncReport.warnings]
126
+ };
127
+ const payloadLimits = readPayloadLimits(projectRoot);
128
+ const serialized = JSON.stringify(response);
129
+ const guardResult = enforcePayloadLimit(serialized, payloadLimits);
130
+ if (guardResult.warning) {
131
+ response.warnings = [
132
+ ...response.warnings,
133
+ {
134
+ code: guardResult.warning.code,
135
+ file: "<response>",
136
+ action_hint: "Consider narrowing the request scope to reduce response size"
137
+ }
138
+ ];
139
+ }
140
+ return {
141
+ content: [{ type: "text", text: JSON.stringify(response) }],
142
+ structuredContent: response
143
+ };
144
+ } finally {
145
+ tracker?.exit(requestId);
480
146
  }
481
147
  }
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
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;
509
- }
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}`);
515
- }
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);
552
- });
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] ?? ""]));
148
+ );
569
149
  }
570
150
 
571
151
  // src/tools/rule-sections.ts
572
- var inputSchema3 = {
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")
577
- };
578
- var outputSchema3 = z3.object({
579
- revision_hash: z3.string(),
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(
583
- z3.object({
584
- stable_id: z3.string(),
585
- level: z3.enum(["L0", "L1", "L2"]),
586
- path: z3.string(),
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()
597
- })
598
- )
599
- });
600
- function registerRuleSections(server) {
152
+ import { randomUUID as randomUUID2 } from "crypto";
153
+ import {
154
+ ruleSectionsAnnotations,
155
+ ruleSectionsInputSchema,
156
+ ruleSectionsOutputSchema
157
+ } from "@fenglimg/fabric-shared/schemas/api-contracts";
158
+ import { enforcePayloadLimit as enforcePayloadLimit2 } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
159
+ function registerRuleSections(server, tracker) {
601
160
  server.registerTool(
602
161
  "fab_get_rule_sections",
603
162
  {
604
163
  description: "Fetch structured Fabric rule sections after fab_plan_context. Required L0/L2 rules are merged with AI-selected L1 rules server-side.",
605
- inputSchema: inputSchema3,
606
- outputSchema: outputSchema3,
607
- annotations: { readOnlyHint: true }
164
+ inputSchema: ruleSectionsInputSchema,
165
+ outputSchema: ruleSectionsOutputSchema,
166
+ annotations: ruleSectionsAnnotations
608
167
  },
609
168
  async (input) => {
610
- const projectRoot = resolveProjectRoot();
611
- const result = await getRuleSections(projectRoot, input);
612
- return {
613
- content: [{ type: "text", text: JSON.stringify(result) }],
614
- structuredContent: result
615
- };
169
+ const requestId = randomUUID2();
170
+ tracker?.enter(requestId);
171
+ try {
172
+ const projectRoot = resolveProjectRoot();
173
+ const syncReport = await ensureRulesFresh(projectRoot);
174
+ const result = await getRuleSections(projectRoot, input);
175
+ const response = {
176
+ ...result,
177
+ warnings: [...syncReport.warnings]
178
+ };
179
+ const payloadLimits = readPayloadLimits(projectRoot);
180
+ const serialized = JSON.stringify(response);
181
+ const guardResult = enforcePayloadLimit2(serialized, payloadLimits);
182
+ if (guardResult.warning) {
183
+ response.warnings = [
184
+ ...response.warnings,
185
+ {
186
+ code: guardResult.warning.code,
187
+ file: "<response>",
188
+ action_hint: "Consider narrowing the request scope to reduce response size"
189
+ }
190
+ ];
191
+ }
192
+ return {
193
+ content: [{ type: "text", text: JSON.stringify(response) }],
194
+ structuredContent: response
195
+ };
196
+ } finally {
197
+ tracker?.exit(requestId);
198
+ }
616
199
  }
617
200
  );
618
201
  }
619
202
 
620
- // src/tools/update-registry.ts
621
- import { agentsLayerSchema, agentsTopologyTypeSchema } from "@fenglimg/fabric-shared";
622
- import { z as z4 } from "zod";
623
-
624
- // src/services/update-registry.ts
625
- import { agentsMetaNodeSchema } from "@fenglimg/fabric-shared";
626
- import { join as join2 } from "path";
627
- async function updateRegistry(projectRoot, input) {
628
- const metaPath = join2(projectRoot, FABRIC_DIR, "agents.meta.json");
629
- const currentMeta = await readAgentsMeta(projectRoot);
630
- const nextMeta = applyRegistryOperation(currentMeta, input.op, input.node_id, input.data);
631
- const newRevision = computeRevision(nextMeta);
632
- await atomicWriteText(
633
- metaPath,
634
- `${JSON.stringify(
635
- {
636
- ...nextMeta,
637
- revision: newRevision
638
- },
639
- null,
640
- 2
641
- )}
642
- `
643
- );
644
- contextCache.invalidate("meta_write", projectRoot);
645
- return {
646
- revision_hash: newRevision,
647
- success: true
648
- };
649
- }
650
- function computeRevision(meta) {
651
- const joinedHashes = Object.entries(meta.nodes).sort(([leftId], [rightId]) => leftId.localeCompare(rightId)).map(([, node]) => node.hash).join("");
652
- return sha256(joinedHashes);
203
+ // src/services/serve-lock.ts
204
+ import fs from "fs";
205
+ import path from "path";
206
+ import { IOFabricError } from "@fenglimg/fabric-shared/errors";
207
+ var LOCK_FILENAME = ".serve.lock";
208
+ var ServeLockHeldError = class extends IOFabricError {
209
+ code = "SERVE_LOCK_HELD";
210
+ httpStatus = 423;
211
+ };
212
+ function lockPath(projectRoot) {
213
+ return path.join(projectRoot, ".fabric", LOCK_FILENAME);
653
214
  }
654
- function assertNodeData(data, message) {
655
- if (data === void 0) {
656
- throw new Error(message);
215
+ function isAlive(pid) {
216
+ try {
217
+ process.kill(pid, 0);
218
+ return true;
219
+ } catch (e) {
220
+ const err = e;
221
+ if (err.code === "ESRCH") return false;
222
+ if (err.code === "EPERM") return true;
223
+ throw e;
657
224
  }
658
- return agentsMetaNodeSchema.parse(data);
659
225
  }
660
- function applyRegistryOperation(meta, op, nodeId, data) {
661
- const nextNodes = { ...meta.nodes };
662
- if (op === "remove-node") {
663
- delete nextNodes[nodeId];
664
- return {
665
- ...meta,
666
- nodes: nextNodes
667
- };
226
+ function acquireLock(projectRoot, opts) {
227
+ const p = lockPath(projectRoot);
228
+ if (fs.existsSync(p)) {
229
+ let state = null;
230
+ try {
231
+ state = JSON.parse(fs.readFileSync(p, "utf8"));
232
+ } catch {
233
+ }
234
+ if (state && state.pid && state.pid !== process.pid && isAlive(state.pid) && !opts?.force) {
235
+ throw new ServeLockHeldError(
236
+ `serve lock held by live PID ${state.pid}`,
237
+ {
238
+ actionHint: `Stop the other process (PID ${state.pid}) or run with --force to override`,
239
+ details: state
240
+ }
241
+ );
242
+ }
243
+ if (state && state.pid && !isAlive(state.pid)) {
244
+ process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 overwriting
245
+ `);
246
+ }
668
247
  }
669
- if (op === "add-node") {
670
- nextNodes[nodeId] = assertNodeData(data, `fab_update_registry requires data for ${op}`);
671
- return {
672
- ...meta,
673
- nodes: nextNodes
674
- };
248
+ fs.mkdirSync(path.dirname(p), { recursive: true });
249
+ fs.writeFileSync(
250
+ p,
251
+ JSON.stringify({ pid: process.pid, acquiredAt: Date.now(), host: process.env.HOSTNAME })
252
+ );
253
+ }
254
+ function releaseLock(projectRoot) {
255
+ const p = lockPath(projectRoot);
256
+ try {
257
+ if (fs.existsSync(p)) {
258
+ const state = JSON.parse(fs.readFileSync(p, "utf8"));
259
+ if (state.pid === process.pid) {
260
+ fs.unlinkSync(p);
261
+ }
262
+ }
263
+ } catch {
675
264
  }
676
- const currentNode = nextNodes[nodeId];
677
- if (currentNode === void 0) {
678
- throw new Error(`Cannot update missing Fabric registry node: ${nodeId}`);
265
+ }
266
+ function readLockState(projectRoot) {
267
+ const p = lockPath(projectRoot);
268
+ if (!fs.existsSync(p)) return null;
269
+ try {
270
+ return JSON.parse(fs.readFileSync(p, "utf8"));
271
+ } catch {
272
+ return null;
679
273
  }
680
- nextNodes[nodeId] = agentsMetaNodeSchema.parse({
681
- ...currentNode,
682
- ...data
683
- });
684
- return {
685
- ...meta,
686
- nodes: nextNodes
687
- };
688
274
  }
689
-
690
- // src/tools/update-registry.ts
691
- var nodeInputSchema = z4.object({
692
- file: z4.string().optional(),
693
- scope_glob: z4.string().optional(),
694
- deps: z4.array(z4.string()).optional(),
695
- priority: z4.enum(["high", "medium", "low"]).optional(),
696
- layer: agentsLayerSchema.optional(),
697
- topology_type: agentsTopologyTypeSchema.optional(),
698
- hash: z4.string().optional()
699
- });
700
- var inputSchema4 = {
701
- op: z4.enum(["add-node", "remove-node", "update-node"]),
702
- node_id: z4.string(),
703
- data: nodeInputSchema.optional()
704
- };
705
- var outputSchema4 = z4.object({
706
- success: z4.boolean(),
707
- revision_hash: z4.string()
708
- });
709
- function registerUpdateRegistry(server) {
710
- server.registerTool(
711
- "fab_update_registry",
275
+ function checkLockOrThrow(projectRoot, opts) {
276
+ const state = readLockState(projectRoot);
277
+ if (state === null) return;
278
+ if (state.pid === process.pid) return;
279
+ if (!isAlive(state.pid)) {
280
+ process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 ignoring
281
+ `);
282
+ return;
283
+ }
284
+ if (opts?.force) return;
285
+ throw new ServeLockHeldError(
286
+ `serve lock held by live PID ${state.pid}`,
712
287
  {
713
- description: "Call to add, remove, or update Fabric registry nodes. Use instead of editing .fabric/agents.meta.json directly.",
714
- inputSchema: inputSchema4,
715
- outputSchema: outputSchema4,
716
- annotations: { destructiveHint: true }
717
- },
718
- async ({ op, node_id, data }) => {
719
- const projectRoot = resolveProjectRoot();
720
- const result = await updateRegistry(projectRoot, { op, node_id, data });
721
- return {
722
- content: [{ type: "text", text: JSON.stringify(result) }],
723
- structuredContent: result
724
- };
288
+ actionHint: `Stop the other serve process (PID ${state.pid}) before running this command, or pass --force to override`,
289
+ details: state
725
290
  }
726
291
  );
727
292
  }
@@ -737,15 +302,20 @@ function formatError(error) {
737
302
  }
738
303
  return `Unknown error: ${String(error)}`;
739
304
  }
740
- function createFabricServer() {
305
+ function formatPreexistingRootMessage(projectRoot) {
306
+ const preexisting = [];
307
+ if (existsSync2(join2(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
308
+ if (existsSync2(join2(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
309
+ if (preexisting.length === 0) return null;
310
+ return `[startup] info: detected ${preexisting.join(", ")} at project root. Note: Fabric serves rules from .fabric/rules/ via MCP \u2014 root markdown files are not auto-loaded into the AI context.`;
311
+ }
312
+ function createFabricServer(tracker) {
741
313
  const server = new McpServer({
742
314
  name: "fabric-context-server",
743
- version: "1.6.0"
315
+ version: "1.8.0-rc.1"
744
316
  });
745
- registerPlanContext(server);
746
- registerRuleSections(server);
747
- registerAppendIntent(server);
748
- registerUpdateRegistry(server);
317
+ registerPlanContext(server, tracker);
318
+ registerRuleSections(server, tracker);
749
319
  server.registerResource(
750
320
  "bootstrap README",
751
321
  AGENTS_MD_RESOURCE_URI,
@@ -755,7 +325,7 @@ function createFabricServer() {
755
325
  },
756
326
  async (_uri) => {
757
327
  const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
758
- const content = await readFile2(join3(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
328
+ const content = await readFile(join2(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
759
329
  return {
760
330
  contents: [
761
331
  {
@@ -770,12 +340,73 @@ function createFabricServer() {
770
340
  return server;
771
341
  }
772
342
  async function startStdioServer() {
773
- const server = createFabricServer();
343
+ const tracker = createInFlightTracker();
344
+ const projectRoot = resolveProjectRoot();
345
+ const syncStart = Date.now();
346
+ const reconcileResult = await reconcileRules(projectRoot, { trigger: "startup" });
347
+ const syncDurationMs = Date.now() - syncStart;
348
+ process.stderr.write(
349
+ `[startup] rule sync: status=${reconcileResult.status}, events=${reconcileResult.events.length}, ${syncDurationMs}ms
350
+ `
351
+ );
352
+ const rootMsg = formatPreexistingRootMessage(projectRoot);
353
+ if (rootMsg !== null) {
354
+ process.stderr.write(`${rootMsg}
355
+ `);
356
+ }
357
+ const server = createFabricServer(tracker);
774
358
  const transport = new StdioServerTransport();
775
359
  await server.connect(transport);
360
+ const closeServer = async () => {
361
+ await server.close();
362
+ };
363
+ process.on(
364
+ "SIGINT",
365
+ createShutdownHandler({ signal: "SIGINT", tracker, projectRoot, closeServer })
366
+ );
367
+ process.on(
368
+ "SIGTERM",
369
+ createShutdownHandler({ signal: "SIGTERM", tracker, projectRoot, closeServer })
370
+ );
371
+ process.on(
372
+ "SIGHUP",
373
+ createShutdownHandler({ signal: "SIGHUP", tracker, projectRoot, closeServer })
374
+ );
375
+ }
376
+ function createShutdownHandler(deps) {
377
+ const exit = deps.exit ?? ((code) => process.exit(code));
378
+ const deadlineMs = deps.drainDeadlineMs ?? 5e3;
379
+ let invoked = false;
380
+ return () => {
381
+ void (async () => {
382
+ if (invoked) {
383
+ process.stderr.write(`
384
+ [shutdown] ${deps.signal} repeated \u2014 forcing exit(1)
385
+ `);
386
+ exit(1);
387
+ return;
388
+ }
389
+ invoked = true;
390
+ process.stderr.write(
391
+ `
392
+ [shutdown] ${deps.signal} received \u2014 draining ${deps.tracker.size()} requests (${deadlineMs / 1e3}s deadline)
393
+ `
394
+ );
395
+ const result = await deps.tracker.drain(deadlineMs);
396
+ process.stderr.write(`[shutdown] drained ${result.drained}, timed_out ${result.timed_out}
397
+ `);
398
+ flushAndSyncEventLedger(deps.projectRoot);
399
+ process.stderr.write("[shutdown] ledger fsynced; closing server\n");
400
+ try {
401
+ await deps.closeServer();
402
+ } catch {
403
+ }
404
+ exit(0);
405
+ })();
406
+ };
776
407
  }
777
408
  async function startHttpServer(options) {
778
- const { createFabricHttpApp } = await import("./http-DJCTLGF4.js");
409
+ const { createFabricHttpApp } = await import("./http-MEFXOG3L.js");
779
410
  const { port, projectRoot, host = "127.0.0.1", authToken, dashboardDistPath, dev } = options;
780
411
  const app = createFabricHttpApp({ projectRoot, host, authToken, dashboardDistPath, dev });
781
412
  return await new Promise((resolveServer, rejectServer) => {
@@ -802,17 +433,34 @@ if (isMainModule) {
802
433
  }
803
434
  export {
804
435
  AGENTS_MD_RESOURCE_URI,
436
+ EVENT_LEDGER_PATH,
805
437
  LEDGER_PATH,
806
438
  LEGACY_LEDGER_PATH,
807
- approveHumanLock,
439
+ ServeLockHeldError,
440
+ acquireLock,
441
+ buildRuleMeta,
442
+ checkLockOrThrow,
443
+ computeRuleTestIndex,
444
+ computeRulesBasedAgentsMeta,
808
445
  createFabricServer,
446
+ createInFlightTracker,
447
+ createShutdownHandler,
448
+ deriveRuleMetaLayer,
449
+ deriveRuleMetaTopologyType,
450
+ ensureRulesFresh,
451
+ flushAndSyncEventLedger,
452
+ formatPreexistingRootMessage,
453
+ getEventLedgerPath,
809
454
  getLedgerPath,
810
455
  getLegacyLedgerPath,
811
- readHumanLock,
812
- readHumanLockEntry,
813
- runDoctorAuditReport,
456
+ isSameRuleTestIndex,
457
+ readLockState,
458
+ reconcileRules,
459
+ releaseLock,
814
460
  runDoctorFix,
815
461
  runDoctorReport,
462
+ stableStringify,
816
463
  startHttpServer,
817
- startStdioServer
464
+ startStdioServer,
465
+ writeRuleMeta
818
466
  };