@fenglimg/fabric-server 1.8.0-rc.2 → 2.0.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-EGGZFXMO.js → chunk-NRWDWAVO.js} +945 -1341
- package/dist/{http-Q7GIL23Y.js → http-CHCOF6DJ.js} +8 -19
- package/dist/index.d.ts +45 -7
- package/dist/index.js +564 -16
- package/dist/static/assets/index-DNSKn-El.js +10 -0
- package/dist/static/index.html +1 -1
- package/package.json +3 -3
- package/dist/static/assets/index-DeTFBeTM.js +0 -10
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AGENTS_MD_RESOURCE_URI,
|
|
3
2
|
EVENT_LEDGER_PATH,
|
|
4
3
|
LEDGER_PATH,
|
|
5
4
|
LEGACY_LEDGER_PATH,
|
|
5
|
+
appendEventLedgerEvent,
|
|
6
|
+
appendRuleSelectionAuditEvent,
|
|
6
7
|
buildRuleMeta,
|
|
7
8
|
computeRuleTestIndex,
|
|
8
9
|
computeRulesBasedAgentsMeta,
|
|
@@ -13,25 +14,28 @@ import {
|
|
|
13
14
|
getEventLedgerPath,
|
|
14
15
|
getLedgerPath,
|
|
15
16
|
getLegacyLedgerPath,
|
|
16
|
-
getRuleSections,
|
|
17
17
|
isSameRuleTestIndex,
|
|
18
|
-
|
|
18
|
+
normalizeRulesPath,
|
|
19
|
+
readAgentsMeta,
|
|
19
20
|
reconcileRules,
|
|
20
21
|
resolveProjectRoot,
|
|
21
22
|
runDoctorFix,
|
|
22
23
|
runDoctorReport,
|
|
23
24
|
stableStringify,
|
|
24
25
|
writeRuleMeta
|
|
25
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-NRWDWAVO.js";
|
|
26
27
|
|
|
27
28
|
// src/index.ts
|
|
28
29
|
import { existsSync as existsSync2 } from "fs";
|
|
29
|
-
import { readFile } from "fs/promises";
|
|
30
|
-
import { join as
|
|
30
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
31
|
+
import { join as join3, resolve } from "path";
|
|
31
32
|
import { fileURLToPath } from "url";
|
|
32
33
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
33
34
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
34
35
|
|
|
36
|
+
// src/constants.ts
|
|
37
|
+
var AGENTS_MD_RESOURCE_URI = "fabric://bootstrap-readme";
|
|
38
|
+
|
|
35
39
|
// src/services/in-flight-tracker.ts
|
|
36
40
|
function createInFlightTracker() {
|
|
37
41
|
const active = /* @__PURE__ */ new Map();
|
|
@@ -95,6 +99,254 @@ function readPayloadLimits(projectRoot) {
|
|
|
95
99
|
return readFabricConfig(projectRoot).mcpPayloadLimits;
|
|
96
100
|
}
|
|
97
101
|
|
|
102
|
+
// src/services/plan-context.ts
|
|
103
|
+
var SELECTION_TOKEN_TTL_MS = 5 * 60 * 1e3;
|
|
104
|
+
var selectionTokenCache = /* @__PURE__ */ new Map();
|
|
105
|
+
async function planContext(projectRoot, input) {
|
|
106
|
+
const meta = await readAgentsMeta(projectRoot);
|
|
107
|
+
const stale = input.client_hash !== void 0 && input.client_hash !== meta.revision;
|
|
108
|
+
const uniquePaths = dedupePaths(input.paths);
|
|
109
|
+
const includeDeprecated = input.include_deprecated === true;
|
|
110
|
+
const allDescriptions = buildDescriptionIndex(meta).filter(
|
|
111
|
+
(item) => includeDeprecated ? true : !isDeprecatedMaturity(item)
|
|
112
|
+
);
|
|
113
|
+
const entries = uniquePaths.map((path2) => {
|
|
114
|
+
const profile = buildRequirementProfile(path2, input);
|
|
115
|
+
const descriptionIndex = allDescriptions.filter((item) => shouldIncludeIndexItemForPath(item, meta, path2));
|
|
116
|
+
const requiredStableIds2 = descriptionIndex.filter((item) => item.required).map((item) => item.stable_id);
|
|
117
|
+
const aiSelectableStableIds2 = descriptionIndex.filter((item) => item.selectable).map((item) => item.stable_id);
|
|
118
|
+
return {
|
|
119
|
+
path: path2,
|
|
120
|
+
requirement_profile: profile,
|
|
121
|
+
description_index: descriptionIndex,
|
|
122
|
+
required_stable_ids: requiredStableIds2,
|
|
123
|
+
ai_selectable_stable_ids: aiSelectableStableIds2,
|
|
124
|
+
initial_selected_stable_ids: requiredStableIds2,
|
|
125
|
+
selection_policy: {
|
|
126
|
+
required_levels: ["L0", "L2"],
|
|
127
|
+
ai_selectable_levels: ["L1"],
|
|
128
|
+
final_fetch_rule: "required_stable_ids + ai_selected_l1_stable_ids"
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
const requiredStableIds = dedupeStableIds(entries.flatMap((entry) => entry.required_stable_ids));
|
|
133
|
+
const aiSelectableStableIds = dedupeStableIds(entries.flatMap((entry) => entry.ai_selectable_stable_ids));
|
|
134
|
+
const sharedDescriptionIndex = dedupeDescriptionIndex(entries.flatMap((entry) => entry.description_index));
|
|
135
|
+
const selectionToken = createSelectionToken(meta.revision, uniquePaths, requiredStableIds, aiSelectableStableIds);
|
|
136
|
+
const result = {
|
|
137
|
+
revision_hash: meta.revision,
|
|
138
|
+
stale,
|
|
139
|
+
selection_token: selectionToken,
|
|
140
|
+
entries,
|
|
141
|
+
shared: {
|
|
142
|
+
required_stable_ids: requiredStableIds,
|
|
143
|
+
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
144
|
+
description_index: sharedDescriptionIndex,
|
|
145
|
+
preflight_diagnostics: buildPreflightDiagnostics(meta)
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
try {
|
|
149
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
150
|
+
event_type: "knowledge_context_planned",
|
|
151
|
+
target_paths: uniquePaths,
|
|
152
|
+
required_stable_ids: requiredStableIds,
|
|
153
|
+
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
154
|
+
final_stable_ids: requiredStableIds,
|
|
155
|
+
selection_token: selectionToken,
|
|
156
|
+
client_hash: input.client_hash,
|
|
157
|
+
intent: input.intent,
|
|
158
|
+
known_tech: input.known_tech,
|
|
159
|
+
diagnostics: result.shared.preflight_diagnostics,
|
|
160
|
+
correlation_id: input.correlation_id,
|
|
161
|
+
session_id: input.session_id
|
|
162
|
+
});
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
function readSelectionToken(token, now = Date.now()) {
|
|
168
|
+
const state = selectionTokenCache.get(token);
|
|
169
|
+
if (state === void 0) {
|
|
170
|
+
return void 0;
|
|
171
|
+
}
|
|
172
|
+
if (state.expires_at <= now) {
|
|
173
|
+
selectionTokenCache.delete(token);
|
|
174
|
+
return void 0;
|
|
175
|
+
}
|
|
176
|
+
return state;
|
|
177
|
+
}
|
|
178
|
+
function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSelectableStableIds, now = Date.now()) {
|
|
179
|
+
const token = `selection:${revisionHash}:${now.toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
180
|
+
selectionTokenCache.set(token, {
|
|
181
|
+
token,
|
|
182
|
+
revision_hash: revisionHash,
|
|
183
|
+
target_paths: targetPaths,
|
|
184
|
+
required_stable_ids: requiredStableIds,
|
|
185
|
+
ai_selectable_stable_ids: aiSelectableStableIds,
|
|
186
|
+
created_at: now,
|
|
187
|
+
expires_at: now + SELECTION_TOKEN_TTL_MS
|
|
188
|
+
});
|
|
189
|
+
return token;
|
|
190
|
+
}
|
|
191
|
+
function dedupePaths(paths) {
|
|
192
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
193
|
+
return paths.flatMap((path2) => {
|
|
194
|
+
const normalizedPath = normalizeRulesPath(path2);
|
|
195
|
+
if (seenPaths.has(normalizedPath)) {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
seenPaths.add(normalizedPath);
|
|
199
|
+
return [normalizedPath];
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function buildRequirementProfile(path2, input) {
|
|
203
|
+
const normalizedPath = normalizeRulesPath(path2);
|
|
204
|
+
const extensionMatch = /(\.[^./\\]+)$/u.exec(normalizedPath);
|
|
205
|
+
const knownTech = dedupeStableIds([
|
|
206
|
+
...input.known_tech ?? [],
|
|
207
|
+
...extensionMatch?.[1] === ".ts" ? ["TypeScript"] : []
|
|
208
|
+
]);
|
|
209
|
+
return {
|
|
210
|
+
target_path: normalizedPath,
|
|
211
|
+
path_segments: normalizedPath.split("/").filter(Boolean),
|
|
212
|
+
extension: extensionMatch?.[1] ?? "",
|
|
213
|
+
inferred_domain: inferDomains(normalizedPath),
|
|
214
|
+
known_tech: knownTech,
|
|
215
|
+
user_intent: input.intent ?? "",
|
|
216
|
+
intent_tokens: tokenizeIntent(input.intent ?? ""),
|
|
217
|
+
impact_hints: inferImpactHints(input.intent ?? ""),
|
|
218
|
+
detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path2] ?? []
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function buildDescriptionIndex(meta) {
|
|
222
|
+
return Object.entries(meta.nodes).flatMap(([nodeId, node]) => {
|
|
223
|
+
const level = node.level ?? node.layer;
|
|
224
|
+
const description = node.description ?? descriptionFromLegacyActivation(node.activation?.description);
|
|
225
|
+
if (description === void 0) {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
const inferredLayer = inferKnowledgeLayerFromContentRef(node.content_ref ?? node.file);
|
|
229
|
+
return [{
|
|
230
|
+
stable_id: node.stable_id ?? nodeId,
|
|
231
|
+
level,
|
|
232
|
+
required: level === "L0" || level === "L2",
|
|
233
|
+
selectable: level === "L1",
|
|
234
|
+
description,
|
|
235
|
+
type: description.knowledge_type,
|
|
236
|
+
maturity: description.maturity,
|
|
237
|
+
layer: description.knowledge_layer ?? inferredLayer,
|
|
238
|
+
layer_reason: description.layer_reason
|
|
239
|
+
}];
|
|
240
|
+
}).sort(compareDescriptionIndexItems);
|
|
241
|
+
}
|
|
242
|
+
function inferKnowledgeLayerFromContentRef(contentRef) {
|
|
243
|
+
if (contentRef === void 0) {
|
|
244
|
+
return void 0;
|
|
245
|
+
}
|
|
246
|
+
if (contentRef.startsWith("~/.fabric/knowledge/")) {
|
|
247
|
+
return "personal";
|
|
248
|
+
}
|
|
249
|
+
if (contentRef.startsWith(".fabric/knowledge/")) {
|
|
250
|
+
return "team";
|
|
251
|
+
}
|
|
252
|
+
return void 0;
|
|
253
|
+
}
|
|
254
|
+
function isDeprecatedMaturity(item) {
|
|
255
|
+
const a = item.maturity;
|
|
256
|
+
const b = item.description.maturity;
|
|
257
|
+
return a === "deprecated" || b === "deprecated";
|
|
258
|
+
}
|
|
259
|
+
function descriptionFromLegacyActivation(summary) {
|
|
260
|
+
if (summary === void 0) {
|
|
261
|
+
return void 0;
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
summary,
|
|
265
|
+
intent_clues: [],
|
|
266
|
+
tech_stack: [],
|
|
267
|
+
impact: [],
|
|
268
|
+
must_read_if: summary
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function shouldIncludeIndexItemForPath(item, meta, path2) {
|
|
272
|
+
if (item.level === "L0" || item.level === "L1") {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
const node = Object.values(meta.nodes).find((candidate) => candidate.stable_id === item.stable_id);
|
|
276
|
+
if (node === void 0) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
return node.scope_glob === path2 || minimatchSimple(path2, node.scope_glob);
|
|
280
|
+
}
|
|
281
|
+
function minimatchSimple(path2, glob) {
|
|
282
|
+
if (glob === "**") {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
if (glob.endsWith("/**")) {
|
|
286
|
+
return path2.startsWith(glob.slice(0, -3));
|
|
287
|
+
}
|
|
288
|
+
return path2 === glob;
|
|
289
|
+
}
|
|
290
|
+
function buildPreflightDiagnostics(meta) {
|
|
291
|
+
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();
|
|
292
|
+
if (missingDescriptionStableIds.length === 0) {
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
return [{
|
|
296
|
+
code: "missing_description",
|
|
297
|
+
severity: "warn",
|
|
298
|
+
stable_ids: missingDescriptionStableIds,
|
|
299
|
+
message: `Resolved registry includes ${missingDescriptionStableIds.length} node(s) without structured descriptions.`
|
|
300
|
+
}];
|
|
301
|
+
}
|
|
302
|
+
function inferDomains(path2) {
|
|
303
|
+
const domains = [];
|
|
304
|
+
if (path2.includes("/ui/") || path2.toLowerCase().includes("ui")) {
|
|
305
|
+
domains.push("UI");
|
|
306
|
+
}
|
|
307
|
+
if (path2.includes("assets/scripts")) {
|
|
308
|
+
domains.push("Gameplay");
|
|
309
|
+
}
|
|
310
|
+
if (path2.includes("resources") || path2.includes("assets/resources")) {
|
|
311
|
+
domains.push("Asset");
|
|
312
|
+
}
|
|
313
|
+
return domains;
|
|
314
|
+
}
|
|
315
|
+
function tokenizeIntent(intent) {
|
|
316
|
+
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()));
|
|
317
|
+
return dedupeStableIds(tokens);
|
|
318
|
+
}
|
|
319
|
+
function inferImpactHints(intent) {
|
|
320
|
+
return /性能|优化|drawcall|渲染|卡顿|闪烁/iu.test(intent) ? ["Performance"] : [];
|
|
321
|
+
}
|
|
322
|
+
function dedupeStableIds(stableIds) {
|
|
323
|
+
return Array.from(new Set(stableIds));
|
|
324
|
+
}
|
|
325
|
+
function dedupeDescriptionIndex(items) {
|
|
326
|
+
const seenStableIds = /* @__PURE__ */ new Set();
|
|
327
|
+
return items.filter((item) => {
|
|
328
|
+
if (seenStableIds.has(item.stable_id)) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
seenStableIds.add(item.stable_id);
|
|
332
|
+
return true;
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
function compareDescriptionIndexItems(left, right) {
|
|
336
|
+
const levelDelta = levelOrder(left.level) - levelOrder(right.level);
|
|
337
|
+
return levelDelta !== 0 ? levelDelta : left.stable_id.localeCompare(right.stable_id);
|
|
338
|
+
}
|
|
339
|
+
function levelOrder(level) {
|
|
340
|
+
switch (level) {
|
|
341
|
+
case "L0":
|
|
342
|
+
return 0;
|
|
343
|
+
case "L1":
|
|
344
|
+
return 1;
|
|
345
|
+
case "L2":
|
|
346
|
+
return 2;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
98
350
|
// src/tools/plan-context.ts
|
|
99
351
|
function registerPlanContext(server, tracker) {
|
|
100
352
|
server.registerTool(
|
|
@@ -105,7 +357,7 @@ function registerPlanContext(server, tracker) {
|
|
|
105
357
|
outputSchema: planContextOutputSchema,
|
|
106
358
|
annotations: planContextAnnotations
|
|
107
359
|
},
|
|
108
|
-
async ({ paths, intent, known_tech, detected_entities, client_hash, correlation_id, session_id }) => {
|
|
360
|
+
async ({ paths, intent, known_tech, detected_entities, client_hash, correlation_id, session_id, include_deprecated }) => {
|
|
109
361
|
const requestId = randomUUID();
|
|
110
362
|
tracker?.enter(requestId);
|
|
111
363
|
try {
|
|
@@ -118,7 +370,8 @@ function registerPlanContext(server, tracker) {
|
|
|
118
370
|
detected_entities,
|
|
119
371
|
client_hash,
|
|
120
372
|
correlation_id,
|
|
121
|
-
session_id
|
|
373
|
+
session_id,
|
|
374
|
+
include_deprecated
|
|
122
375
|
});
|
|
123
376
|
const response = {
|
|
124
377
|
...result,
|
|
@@ -156,6 +409,218 @@ import {
|
|
|
156
409
|
ruleSectionsOutputSchema
|
|
157
410
|
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
158
411
|
import { enforcePayloadLimit as enforcePayloadLimit2 } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
412
|
+
|
|
413
|
+
// src/services/rule-sections.ts
|
|
414
|
+
import { readFile } from "fs/promises";
|
|
415
|
+
import { homedir } from "os";
|
|
416
|
+
import { join as join2 } from "path";
|
|
417
|
+
var RULE_SECTION_NAMES = [
|
|
418
|
+
"MISSION_STATEMENT",
|
|
419
|
+
"MANDATORY_INJECTION",
|
|
420
|
+
"BUSINESS_LOGIC_CHUNKS",
|
|
421
|
+
"CONTEXT_INFO"
|
|
422
|
+
];
|
|
423
|
+
var PRIORITY_ORDER = {
|
|
424
|
+
high: 0,
|
|
425
|
+
medium: 1,
|
|
426
|
+
low: 2
|
|
427
|
+
};
|
|
428
|
+
function parseRuleSections(content) {
|
|
429
|
+
const sections = /* @__PURE__ */ new Map();
|
|
430
|
+
const lines = content.split(/\r?\n/u);
|
|
431
|
+
let activeSection;
|
|
432
|
+
let activeSectionDepth = 0;
|
|
433
|
+
let buffer = [];
|
|
434
|
+
const flush = () => {
|
|
435
|
+
if (activeSection === void 0) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const text = buffer.join("\n").trim();
|
|
439
|
+
if (text.length === 0) {
|
|
440
|
+
buffer = [];
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
sections.set(activeSection, [...sections.get(activeSection) ?? [], text]);
|
|
444
|
+
buffer = [];
|
|
445
|
+
};
|
|
446
|
+
for (const line of lines) {
|
|
447
|
+
const heading = /^(#{2,6})\s+\[([A-Z_]+)\]\s*$/u.exec(line.trim());
|
|
448
|
+
if (heading !== null) {
|
|
449
|
+
flush();
|
|
450
|
+
activeSection = isRuleSectionName(heading[2]) ? heading[2] : void 0;
|
|
451
|
+
activeSectionDepth = activeSection === void 0 ? 0 : heading[1].length;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const ordinaryHeading = /^(#{1,6})\s+/u.exec(line.trim());
|
|
455
|
+
if (ordinaryHeading !== null) {
|
|
456
|
+
if (activeSection !== void 0 && ordinaryHeading[1].length > activeSectionDepth) {
|
|
457
|
+
buffer.push(line);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
flush();
|
|
461
|
+
activeSection = void 0;
|
|
462
|
+
activeSectionDepth = 0;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (activeSection !== void 0) {
|
|
466
|
+
buffer.push(line);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
flush();
|
|
470
|
+
return new Map(
|
|
471
|
+
Array.from(sections.entries()).map(([section, values]) => [section, values.join("\n\n")])
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
async function getRuleSections(projectRoot, input) {
|
|
475
|
+
const token = readSelectionToken(input.selection_token);
|
|
476
|
+
if (token === void 0) {
|
|
477
|
+
throw new Error("selection_token is missing or expired");
|
|
478
|
+
}
|
|
479
|
+
validateAiSelections(token.ai_selectable_stable_ids, input.ai_selected_stable_ids, input.ai_selection_reasons);
|
|
480
|
+
const meta = await readAgentsMeta(projectRoot);
|
|
481
|
+
const selectedStableIds = [...token.required_stable_ids, ...input.ai_selected_stable_ids];
|
|
482
|
+
const selectedRules = sortRuleNodes(selectedStableIds.map((stableId) => findRuleNode(meta, stableId)));
|
|
483
|
+
const diagnostics = [];
|
|
484
|
+
const rules = [];
|
|
485
|
+
for (const rule of selectedRules) {
|
|
486
|
+
const content = await readFile(resolveRuleSourcePath(projectRoot, rule.path), "utf8");
|
|
487
|
+
const parsedSections = parseRuleSections(content);
|
|
488
|
+
const sections = {};
|
|
489
|
+
for (const section of input.sections) {
|
|
490
|
+
const sectionContent = parsedSections.get(section);
|
|
491
|
+
sections[section] = sectionContent ?? "";
|
|
492
|
+
if (sectionContent === void 0) {
|
|
493
|
+
diagnostics.push({
|
|
494
|
+
code: "missing_section",
|
|
495
|
+
severity: "warn",
|
|
496
|
+
stable_id: rule.stable_id,
|
|
497
|
+
section,
|
|
498
|
+
message: `Rule ${rule.stable_id} does not define section ${section}.`
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const description = rule.node.description;
|
|
503
|
+
if (description !== void 0 && description.knowledge_type === void 0 && description.knowledge_layer === void 0) {
|
|
504
|
+
diagnostics.push({
|
|
505
|
+
code: "missing_knowledge_metadata",
|
|
506
|
+
severity: "warn",
|
|
507
|
+
stable_id: rule.stable_id,
|
|
508
|
+
message: `Rule ${rule.stable_id} has no knowledge metadata (type/layer) \u2014 likely an un-migrated v1.x entry.`
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
rules.push({
|
|
512
|
+
stable_id: rule.stable_id,
|
|
513
|
+
level: rule.level,
|
|
514
|
+
path: rule.path,
|
|
515
|
+
sections
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
const result = {
|
|
519
|
+
revision_hash: meta.revision,
|
|
520
|
+
precedence: ["L2", "L1", "L0"],
|
|
521
|
+
selected_stable_ids: rules.map((rule) => rule.stable_id),
|
|
522
|
+
rules,
|
|
523
|
+
diagnostics
|
|
524
|
+
};
|
|
525
|
+
await appendRuleSelectionAuditEvent(projectRoot, {
|
|
526
|
+
path: token.target_paths[0] ?? "",
|
|
527
|
+
selection_token: input.selection_token,
|
|
528
|
+
target_paths: token.target_paths,
|
|
529
|
+
required_stable_ids: token.required_stable_ids,
|
|
530
|
+
ai_selectable_stable_ids: token.ai_selectable_stable_ids,
|
|
531
|
+
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
532
|
+
final_stable_ids: result.selected_stable_ids,
|
|
533
|
+
ai_selection_reasons: pickSelectionReasons(input.ai_selected_stable_ids, input.ai_selection_reasons),
|
|
534
|
+
rejected_stable_ids: [],
|
|
535
|
+
ignored_stable_ids: [],
|
|
536
|
+
correlation_id: input.correlation_id,
|
|
537
|
+
session_id: input.session_id
|
|
538
|
+
});
|
|
539
|
+
try {
|
|
540
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
541
|
+
event_type: "knowledge_sections_fetched",
|
|
542
|
+
selection_token: input.selection_token,
|
|
543
|
+
target_paths: token.target_paths,
|
|
544
|
+
requested_sections: input.sections,
|
|
545
|
+
final_stable_ids: result.selected_stable_ids,
|
|
546
|
+
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
547
|
+
diagnostics,
|
|
548
|
+
correlation_id: input.correlation_id,
|
|
549
|
+
session_id: input.session_id
|
|
550
|
+
});
|
|
551
|
+
} catch {
|
|
552
|
+
}
|
|
553
|
+
return result;
|
|
554
|
+
}
|
|
555
|
+
function validateAiSelections(aiSelectableStableIds, aiSelectedStableIds, aiSelectionReasons) {
|
|
556
|
+
const selectable = new Set(aiSelectableStableIds);
|
|
557
|
+
for (const stableId of aiSelectedStableIds) {
|
|
558
|
+
if (!selectable.has(stableId)) {
|
|
559
|
+
throw new Error(`Invalid L1 rule selection: ${stableId}`);
|
|
560
|
+
}
|
|
561
|
+
if (aiSelectionReasons[stableId]?.trim() === "") {
|
|
562
|
+
throw new Error(`Missing AI selection reason for ${stableId}`);
|
|
563
|
+
}
|
|
564
|
+
if (aiSelectionReasons[stableId] === void 0) {
|
|
565
|
+
throw new Error(`Missing AI selection reason for ${stableId}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function findRuleNode(meta, stableId) {
|
|
570
|
+
for (const [nodeId, node] of Object.entries(meta.nodes)) {
|
|
571
|
+
const nodeStableId = node.stable_id ?? nodeId;
|
|
572
|
+
if (nodeStableId !== stableId) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
const level = node.level ?? node.layer;
|
|
576
|
+
return {
|
|
577
|
+
stable_id: nodeStableId,
|
|
578
|
+
level,
|
|
579
|
+
path: normalizeRulesPath(node.content_ref ?? node.file),
|
|
580
|
+
priority: node.priority,
|
|
581
|
+
node
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
throw new Error(`Selected rule is not present in agents.meta.json: ${stableId}`);
|
|
585
|
+
}
|
|
586
|
+
function sortRuleNodes(rules) {
|
|
587
|
+
return [...rules].sort((left, right) => {
|
|
588
|
+
const levelDelta = outputLevelOrder(left.level) - outputLevelOrder(right.level);
|
|
589
|
+
if (levelDelta !== 0) {
|
|
590
|
+
return levelDelta;
|
|
591
|
+
}
|
|
592
|
+
const priorityDelta = PRIORITY_ORDER[left.priority] - PRIORITY_ORDER[right.priority];
|
|
593
|
+
if (priorityDelta !== 0) {
|
|
594
|
+
return priorityDelta;
|
|
595
|
+
}
|
|
596
|
+
return left.stable_id.localeCompare(right.stable_id);
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
function outputLevelOrder(level) {
|
|
600
|
+
switch (level) {
|
|
601
|
+
case "L0":
|
|
602
|
+
return 0;
|
|
603
|
+
case "L1":
|
|
604
|
+
return 1;
|
|
605
|
+
case "L2":
|
|
606
|
+
return 2;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function isRuleSectionName(value) {
|
|
610
|
+
return RULE_SECTION_NAMES.includes(value);
|
|
611
|
+
}
|
|
612
|
+
function resolveRuleSourcePath(projectRoot, contentRef) {
|
|
613
|
+
if (contentRef.startsWith("~/.fabric/knowledge/")) {
|
|
614
|
+
const home = process.env.FABRIC_HOME ?? homedir();
|
|
615
|
+
return join2(home, ".fabric", "knowledge", contentRef.slice("~/.fabric/knowledge/".length));
|
|
616
|
+
}
|
|
617
|
+
return join2(projectRoot, contentRef);
|
|
618
|
+
}
|
|
619
|
+
function pickSelectionReasons(selectedStableIds, reasons) {
|
|
620
|
+
return Object.fromEntries(selectedStableIds.map((stableId) => [stableId, reasons[stableId] ?? ""]));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/tools/rule-sections.ts
|
|
159
624
|
function registerRuleSections(server, tracker) {
|
|
160
625
|
server.registerTool(
|
|
161
626
|
"fab_get_rule_sections",
|
|
@@ -200,6 +665,83 @@ function registerRuleSections(server, tracker) {
|
|
|
200
665
|
);
|
|
201
666
|
}
|
|
202
667
|
|
|
668
|
+
// src/services/knowledge-id-allocator.ts
|
|
669
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
670
|
+
import { dirname } from "path";
|
|
671
|
+
import { mkdir } from "fs/promises";
|
|
672
|
+
import {
|
|
673
|
+
AgentsMetaCountersSchema,
|
|
674
|
+
agentsMetaSchema,
|
|
675
|
+
allocateKnowledgeId,
|
|
676
|
+
defaultAgentsMetaCounters
|
|
677
|
+
} from "@fenglimg/fabric-shared";
|
|
678
|
+
import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
679
|
+
var KnowledgeIdAllocator = class {
|
|
680
|
+
constructor(metaPath) {
|
|
681
|
+
this.metaPath = metaPath;
|
|
682
|
+
}
|
|
683
|
+
metaPath;
|
|
684
|
+
/**
|
|
685
|
+
* Allocate the next stable_id for the given (layer, type) pair and persist
|
|
686
|
+
* the advanced counter to `agents.meta.json`.
|
|
687
|
+
*/
|
|
688
|
+
async allocate(layer, type) {
|
|
689
|
+
const meta = await this.readMeta();
|
|
690
|
+
const counters = this.normalizeCounters(meta.counters);
|
|
691
|
+
const { id, nextCounters } = allocateKnowledgeId(layer, type, counters);
|
|
692
|
+
await this.writeMetaAtomic({ ...meta, counters: nextCounters });
|
|
693
|
+
return id;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Returns the current counters envelope, defaulting to all-zero slots when
|
|
697
|
+
* the meta file is absent or pre-v2.0 (counters key missing).
|
|
698
|
+
*/
|
|
699
|
+
async getCounters() {
|
|
700
|
+
const meta = await this.readMeta();
|
|
701
|
+
return this.normalizeCounters(meta.counters);
|
|
702
|
+
}
|
|
703
|
+
// ---- internal helpers ------------------------------------------------
|
|
704
|
+
async readMeta() {
|
|
705
|
+
let raw;
|
|
706
|
+
try {
|
|
707
|
+
raw = await readFile2(this.metaPath, "utf8");
|
|
708
|
+
} catch (err) {
|
|
709
|
+
if (isNodeError(err) && err.code === "ENOENT") {
|
|
710
|
+
return { revision: "", nodes: {} };
|
|
711
|
+
}
|
|
712
|
+
throw err;
|
|
713
|
+
}
|
|
714
|
+
let parsed;
|
|
715
|
+
try {
|
|
716
|
+
parsed = JSON.parse(raw);
|
|
717
|
+
} catch {
|
|
718
|
+
return { revision: "", nodes: {} };
|
|
719
|
+
}
|
|
720
|
+
const validation = agentsMetaSchema.safeParse(parsed);
|
|
721
|
+
if (validation.success) {
|
|
722
|
+
return validation.data;
|
|
723
|
+
}
|
|
724
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
725
|
+
}
|
|
726
|
+
normalizeCounters(input) {
|
|
727
|
+
if (input === void 0 || input === null) {
|
|
728
|
+
return defaultAgentsMetaCounters();
|
|
729
|
+
}
|
|
730
|
+
const parsed = AgentsMetaCountersSchema.safeParse(input);
|
|
731
|
+
return parsed.success ? parsed.data : defaultAgentsMetaCounters();
|
|
732
|
+
}
|
|
733
|
+
async writeMetaAtomic(meta) {
|
|
734
|
+
await ensureParentDirectory(this.metaPath);
|
|
735
|
+
await atomicWriteJson(this.metaPath, meta, { indent: 2 });
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
async function ensureParentDirectory(filePath) {
|
|
739
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
740
|
+
}
|
|
741
|
+
function isNodeError(err) {
|
|
742
|
+
return err instanceof Error && typeof err.code === "string";
|
|
743
|
+
}
|
|
744
|
+
|
|
203
745
|
// src/services/serve-lock.ts
|
|
204
746
|
import fs from "fs";
|
|
205
747
|
import path from "path";
|
|
@@ -304,15 +846,15 @@ function formatError(error) {
|
|
|
304
846
|
}
|
|
305
847
|
function formatPreexistingRootMessage(projectRoot) {
|
|
306
848
|
const preexisting = [];
|
|
307
|
-
if (existsSync2(
|
|
308
|
-
if (existsSync2(
|
|
849
|
+
if (existsSync2(join3(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
|
|
850
|
+
if (existsSync2(join3(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
|
|
309
851
|
if (preexisting.length === 0) return null;
|
|
310
|
-
return `[startup] info: detected ${preexisting.join(", ")} at project root. Note: Fabric serves
|
|
852
|
+
return `[startup] info: detected ${preexisting.join(", ")} at project root. Note: Fabric serves knowledge from .fabric/knowledge/ via MCP \u2014 root markdown files are not auto-loaded into the AI context.`;
|
|
311
853
|
}
|
|
312
854
|
function createFabricServer(tracker) {
|
|
313
855
|
const server = new McpServer({
|
|
314
856
|
name: "fabric-context-server",
|
|
315
|
-
version: "
|
|
857
|
+
version: "2.0.0-rc.1"
|
|
316
858
|
});
|
|
317
859
|
registerPlanContext(server, tracker);
|
|
318
860
|
registerRuleSections(server, tracker);
|
|
@@ -320,18 +862,22 @@ function createFabricServer(tracker) {
|
|
|
320
862
|
"bootstrap README",
|
|
321
863
|
AGENTS_MD_RESOURCE_URI,
|
|
322
864
|
{
|
|
323
|
-
description: "
|
|
865
|
+
description: "Legacy v1.x bootstrap anchor (deprecated in v2.0; kept as MCP contract shim)",
|
|
324
866
|
mimeType: "text/markdown"
|
|
325
867
|
},
|
|
326
868
|
async (_uri) => {
|
|
327
869
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
328
|
-
const
|
|
870
|
+
const path2 = join3(projectRoot, ".fabric", "bootstrap", "README.md");
|
|
871
|
+
let text = "";
|
|
872
|
+
if (existsSync2(path2)) {
|
|
873
|
+
text = await readFile3(path2, "utf8");
|
|
874
|
+
}
|
|
329
875
|
return {
|
|
330
876
|
contents: [
|
|
331
877
|
{
|
|
332
878
|
uri: AGENTS_MD_RESOURCE_URI,
|
|
333
879
|
mimeType: "text/markdown",
|
|
334
|
-
text
|
|
880
|
+
text
|
|
335
881
|
}
|
|
336
882
|
]
|
|
337
883
|
};
|
|
@@ -406,7 +952,7 @@ function createShutdownHandler(deps) {
|
|
|
406
952
|
};
|
|
407
953
|
}
|
|
408
954
|
async function startHttpServer(options) {
|
|
409
|
-
const { createFabricHttpApp } = await import("./http-
|
|
955
|
+
const { createFabricHttpApp } = await import("./http-CHCOF6DJ.js");
|
|
410
956
|
const { port, projectRoot, host = "127.0.0.1", authToken, dashboardDistPath, dev } = options;
|
|
411
957
|
const app = createFabricHttpApp({ projectRoot, host, authToken, dashboardDistPath, dev });
|
|
412
958
|
return await new Promise((resolveServer, rejectServer) => {
|
|
@@ -434,10 +980,12 @@ if (isMainModule) {
|
|
|
434
980
|
export {
|
|
435
981
|
AGENTS_MD_RESOURCE_URI,
|
|
436
982
|
EVENT_LEDGER_PATH,
|
|
983
|
+
KnowledgeIdAllocator,
|
|
437
984
|
LEDGER_PATH,
|
|
438
985
|
LEGACY_LEDGER_PATH,
|
|
439
986
|
ServeLockHeldError,
|
|
440
987
|
acquireLock,
|
|
988
|
+
appendEventLedgerEvent,
|
|
441
989
|
buildRuleMeta,
|
|
442
990
|
checkLockOrThrow,
|
|
443
991
|
computeRuleTestIndex,
|