@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/chunk-E3BHIUIW.js +2903 -0
- package/dist/{http-DJCTLGF4.js → http-MEFXOG3L.js} +348 -278
- package/dist/index.d.ts +187 -61
- package/dist/index.js +329 -681
- package/dist/static/assets/index-C-ba4ih0.js +10 -0
- package/dist/static/assets/index-FoBU5Kta.css +1 -0
- package/dist/static/index.html +9 -7
- package/package.json +10 -4
- package/dist/chunk-TZCE2K4D.js +0 -1447
- package/dist/static/assets/index-B5hhHHl2.css +0 -1
- package/dist/static/assets/index-LJh6IezM.js +0 -14
package/dist/index.js
CHANGED
|
@@ -1,727 +1,292 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AGENTS_MD_RESOURCE_URI,
|
|
3
|
-
|
|
3
|
+
EVENT_LEDGER_PATH,
|
|
4
4
|
LEDGER_PATH,
|
|
5
5
|
LEGACY_LEDGER_PATH,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
buildRuleMeta,
|
|
7
|
+
computeRuleTestIndex,
|
|
8
|
+
computeRulesBasedAgentsMeta,
|
|
9
|
+
deriveRuleMetaLayer,
|
|
10
|
+
deriveRuleMetaTopologyType,
|
|
11
|
+
ensureRulesFresh,
|
|
12
|
+
flushAndSyncEventLedger,
|
|
13
|
+
getEventLedgerPath,
|
|
12
14
|
getLedgerPath,
|
|
13
15
|
getLegacyLedgerPath,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
getRuleSections,
|
|
17
|
+
isSameRuleTestIndex,
|
|
18
|
+
planContext,
|
|
19
|
+
reconcileRules,
|
|
18
20
|
resolveProjectRoot,
|
|
19
|
-
runDoctorAuditReport,
|
|
20
21
|
runDoctorFix,
|
|
21
22
|
runDoctorReport,
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
stableStringify,
|
|
24
|
+
writeRuleMeta
|
|
25
|
+
} from "./chunk-E3BHIUIW.js";
|
|
24
26
|
|
|
25
27
|
// src/index.ts
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
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/
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
|
|
101
|
-
|
|
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 {
|
|
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/
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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
|
|
92
|
+
return parsed;
|
|
279
93
|
}
|
|
280
|
-
function
|
|
281
|
-
|
|
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
|
-
|
|
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:
|
|
390
|
-
outputSchema:
|
|
391
|
-
annotations:
|
|
104
|
+
inputSchema: planContextInputSchema,
|
|
105
|
+
outputSchema: planContextOutputSchema,
|
|
106
|
+
annotations: planContextAnnotations
|
|
392
107
|
},
|
|
393
|
-
async ({ paths, intent, known_tech, detected_entities, client_hash }) => {
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
};
|
|
578
|
-
|
|
579
|
-
|
|
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:
|
|
606
|
-
outputSchema:
|
|
607
|
-
annotations:
|
|
164
|
+
inputSchema: ruleSectionsInputSchema,
|
|
165
|
+
outputSchema: ruleSectionsOutputSchema,
|
|
166
|
+
annotations: ruleSectionsAnnotations
|
|
608
167
|
},
|
|
609
168
|
async (input) => {
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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/
|
|
621
|
-
import
|
|
622
|
-
import
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
|
655
|
-
|
|
656
|
-
|
|
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
|
|
661
|
-
const
|
|
662
|
-
if (
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
|
|
714
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
};
|