@fenglimg/fabric-server 2.0.0-rc.21 → 2.0.0-rc.23
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-7R6MFA7Y.js → chunk-IRB77C6E.js} +1041 -243
- package/dist/{http-JGWQGUZS.js → http-ZBV6YUHD.js} +1 -1
- package/dist/index.d.ts +37 -5
- package/dist/index.js +223 -215
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AgentsMetaFileMissingError,
|
|
2
3
|
EVENT_LEDGER_PATH,
|
|
3
4
|
LEDGER_PATH,
|
|
4
5
|
LEGACY_LEDGER_PATH,
|
|
6
|
+
ServeLockHeldError,
|
|
7
|
+
acquireLock,
|
|
5
8
|
appendEventLedgerEvent,
|
|
6
9
|
atomicWriteText,
|
|
7
10
|
buildKnowledgeMeta,
|
|
11
|
+
checkLockOrThrow,
|
|
8
12
|
computeKnowledgeBasedAgentsMeta,
|
|
9
13
|
computeKnowledgeTestIndex,
|
|
10
14
|
deriveKnowledgeMetaLayer,
|
|
11
15
|
deriveKnowledgeMetaTopologyType,
|
|
16
|
+
enrichDescriptions,
|
|
12
17
|
ensureKnowledgeFresh,
|
|
13
18
|
ensureParentDirectory,
|
|
14
19
|
flushAndSyncEventLedger,
|
|
@@ -16,9 +21,12 @@ import {
|
|
|
16
21
|
getLedgerPath,
|
|
17
22
|
getLegacyLedgerPath,
|
|
18
23
|
isSameKnowledgeTestIndex,
|
|
24
|
+
loadActiveMeta,
|
|
25
|
+
loadActiveMetaOrStale,
|
|
19
26
|
normalizeKnowledgePath,
|
|
20
|
-
|
|
27
|
+
readLockState,
|
|
21
28
|
reconcileKnowledge,
|
|
29
|
+
releaseLock,
|
|
22
30
|
resolveProjectRoot,
|
|
23
31
|
runDoctorApplyLint,
|
|
24
32
|
runDoctorCiteCoverage,
|
|
@@ -27,7 +35,7 @@ import {
|
|
|
27
35
|
sha256,
|
|
28
36
|
stableStringify,
|
|
29
37
|
writeKnowledgeMeta
|
|
30
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-IRB77C6E.js";
|
|
31
39
|
|
|
32
40
|
// src/index.ts
|
|
33
41
|
import { existsSync as existsSync4 } from "fs";
|
|
@@ -40,6 +48,66 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
40
48
|
// src/constants.ts
|
|
41
49
|
var AGENTS_MD_RESOURCE_URI = "fabric://bootstrap-readme";
|
|
42
50
|
|
|
51
|
+
// src/services/first-reconcile-gate.ts
|
|
52
|
+
function gateWarning(result) {
|
|
53
|
+
if (result.status === "ready") return null;
|
|
54
|
+
if (result.status === "stale") {
|
|
55
|
+
return {
|
|
56
|
+
code: "meta_stale",
|
|
57
|
+
file: "<response>",
|
|
58
|
+
action_hint: "Initial reconcile still pending; results may use cached meta. Retry shortly or run `fab doctor --fix`."
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
code: "reconcile_failed",
|
|
63
|
+
file: "<response>",
|
|
64
|
+
action_hint: "Reconcile failed at startup; run `fab doctor --fix` and restart the MCP server."
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
var state = {
|
|
68
|
+
firstReconcilePromise: null,
|
|
69
|
+
reconcileFailure: null,
|
|
70
|
+
settled: false
|
|
71
|
+
};
|
|
72
|
+
function setFirstReconcile(reconcilePromise) {
|
|
73
|
+
state.firstReconcilePromise = reconcilePromise.then(
|
|
74
|
+
() => {
|
|
75
|
+
state.settled = true;
|
|
76
|
+
},
|
|
77
|
+
(error) => {
|
|
78
|
+
state.reconcileFailure = error;
|
|
79
|
+
state.settled = true;
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
async function awaitFirstReconcileGate(timeoutMs = 5e3) {
|
|
84
|
+
if (state.reconcileFailure !== null) {
|
|
85
|
+
return { status: "failed", error: state.reconcileFailure };
|
|
86
|
+
}
|
|
87
|
+
if (state.firstReconcilePromise === null || state.settled) {
|
|
88
|
+
return { status: "ready" };
|
|
89
|
+
}
|
|
90
|
+
let timer = null;
|
|
91
|
+
const timeoutPromise = new Promise((resolveTimeout) => {
|
|
92
|
+
timer = setTimeout(() => resolveTimeout("timeout"), timeoutMs);
|
|
93
|
+
});
|
|
94
|
+
try {
|
|
95
|
+
const winner = await Promise.race([
|
|
96
|
+
state.firstReconcilePromise.then(() => "reconcile"),
|
|
97
|
+
timeoutPromise
|
|
98
|
+
]);
|
|
99
|
+
if (winner === "timeout") {
|
|
100
|
+
return { status: "stale" };
|
|
101
|
+
}
|
|
102
|
+
if (state.reconcileFailure !== null) {
|
|
103
|
+
return { status: "failed", error: state.reconcileFailure };
|
|
104
|
+
}
|
|
105
|
+
return { status: "ready" };
|
|
106
|
+
} finally {
|
|
107
|
+
if (timer !== null) clearTimeout(timer);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
43
111
|
// src/services/in-flight-tracker.ts
|
|
44
112
|
function createInFlightTracker() {
|
|
45
113
|
const active = /* @__PURE__ */ new Map();
|
|
@@ -123,8 +191,15 @@ function resolvePersonalRoot() {
|
|
|
123
191
|
return process.env.FABRIC_HOME ?? homedir();
|
|
124
192
|
}
|
|
125
193
|
async function extractKnowledge(projectRoot, input) {
|
|
194
|
+
try {
|
|
195
|
+
await loadActiveMeta(projectRoot, { caller: "extractKnowledge" });
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (!(error instanceof AgentsMetaFileMissingError)) {
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
126
201
|
const sanitizedSlug = sanitizeSlug(input.slug);
|
|
127
|
-
const sourceSessions =
|
|
202
|
+
const sourceSessions = input.source_sessions ?? [];
|
|
128
203
|
const primarySession = sourceSessions[0] ?? "";
|
|
129
204
|
const idempotencyKey = sha256(
|
|
130
205
|
JSON.stringify({
|
|
@@ -208,7 +283,20 @@ async function extractKnowledge(projectRoot, input) {
|
|
|
208
283
|
proposedReason: input.proposed_reason,
|
|
209
284
|
sessionContext: input.session_context,
|
|
210
285
|
relevanceScope,
|
|
211
|
-
relevancePaths
|
|
286
|
+
relevancePaths,
|
|
287
|
+
// v2.0.0-rc.23 TASK-006 (a-C1): optional structured triage fields. Each is
|
|
288
|
+
// emitted as a YAML line only when caller-supplied; omitted lines preserve
|
|
289
|
+
// the historical pending-file shape.
|
|
290
|
+
intentClues: input.intent_clues,
|
|
291
|
+
techStack: input.tech_stack,
|
|
292
|
+
impact: input.impact,
|
|
293
|
+
mustReadIf: input.must_read_if,
|
|
294
|
+
// v2.0.0-rc.23 TASK-014 (F8c): optional S5 onboard-slot tag. Same emit
|
|
295
|
+
// discipline as the four a-C1 fields — bare YAML line iff caller-supplied,
|
|
296
|
+
// never in the idempotency_key hash. fabric-archive's first-run phase is
|
|
297
|
+
// the only producer; downstream `fab onboard-coverage` walks frontmatter
|
|
298
|
+
// looking for this exact key.
|
|
299
|
+
onboardSlot: input.onboard_slot
|
|
212
300
|
});
|
|
213
301
|
await atomicWriteText(absolutePath, fresh);
|
|
214
302
|
await emitEventBestEffort(projectRoot, {
|
|
@@ -251,6 +339,24 @@ function renderFreshEntry(args) {
|
|
|
251
339
|
const pathsBody = args.relevancePaths.map((p) => quoteRelevancePath(p)).join(", ");
|
|
252
340
|
frontmatterLines.push(`relevance_paths: [${pathsBody}]`);
|
|
253
341
|
}
|
|
342
|
+
if (args.intentClues !== void 0) {
|
|
343
|
+
const body2 = args.intentClues.map((s) => quoteRelevancePath(s)).join(", ");
|
|
344
|
+
frontmatterLines.push(`intent_clues: [${body2}]`);
|
|
345
|
+
}
|
|
346
|
+
if (args.techStack !== void 0) {
|
|
347
|
+
const body2 = args.techStack.map((s) => quoteRelevancePath(s)).join(", ");
|
|
348
|
+
frontmatterLines.push(`tech_stack: [${body2}]`);
|
|
349
|
+
}
|
|
350
|
+
if (args.impact !== void 0) {
|
|
351
|
+
const body2 = args.impact.map((s) => quoteRelevancePath(s)).join(", ");
|
|
352
|
+
frontmatterLines.push(`impact: [${body2}]`);
|
|
353
|
+
}
|
|
354
|
+
if (args.mustReadIf !== void 0) {
|
|
355
|
+
frontmatterLines.push(`must_read_if: ${quoteRelevancePath(args.mustReadIf)}`);
|
|
356
|
+
}
|
|
357
|
+
if (args.onboardSlot !== void 0) {
|
|
358
|
+
frontmatterLines.push(`onboard_slot: ${args.onboardSlot}`);
|
|
359
|
+
}
|
|
254
360
|
frontmatterLines.push(
|
|
255
361
|
`x-fabric-idempotency-key: ${args.idempotencyKey}`,
|
|
256
362
|
"---"
|
|
@@ -319,9 +425,9 @@ ${renderEvidenceBlock(newSummary, newRecentPaths)}
|
|
|
319
425
|
const pathSection = /Recent paths:\s*\n([\s\S]*?)(?:\n\s*Notes:|$)/u.exec(block);
|
|
320
426
|
if (pathSection !== null) {
|
|
321
427
|
for (const rawLine of (pathSection[1] ?? "").split(/\r?\n/u)) {
|
|
322
|
-
const
|
|
323
|
-
if (
|
|
324
|
-
existingPaths.push(
|
|
428
|
+
const t = rawLine.trim();
|
|
429
|
+
if (t.startsWith("- ")) {
|
|
430
|
+
existingPaths.push(t.slice(2).trim());
|
|
325
431
|
}
|
|
326
432
|
}
|
|
327
433
|
}
|
|
@@ -330,16 +436,16 @@ ${renderEvidenceBlock(newSummary, newRecentPaths)}
|
|
|
330
436
|
const bulletLines = [];
|
|
331
437
|
let prose = [];
|
|
332
438
|
for (const rawLine of noteBody.split(/\r?\n/u)) {
|
|
333
|
-
const
|
|
334
|
-
if (
|
|
335
|
-
if (
|
|
439
|
+
const t = rawLine.trim();
|
|
440
|
+
if (t.length === 0) continue;
|
|
441
|
+
if (t.startsWith("- ")) {
|
|
336
442
|
if (prose.length > 0) {
|
|
337
443
|
existingNotes.push(prose.join(" ").trim());
|
|
338
444
|
prose = [];
|
|
339
445
|
}
|
|
340
|
-
bulletLines.push(
|
|
446
|
+
bulletLines.push(t.slice(2).trim());
|
|
341
447
|
} else {
|
|
342
|
-
prose.push(
|
|
448
|
+
prose.push(t);
|
|
343
449
|
}
|
|
344
450
|
}
|
|
345
451
|
if (prose.length > 0) existingNotes.push(prose.join(" ").trim());
|
|
@@ -414,7 +520,7 @@ function registerExtractKnowledge(server, tracker) {
|
|
|
414
520
|
server.registerTool(
|
|
415
521
|
"fab_extract_knowledge",
|
|
416
522
|
{
|
|
417
|
-
description: "Persist a proposed pending knowledge entry under .fabric/knowledge/pending/<type>/<slug>.md. Idempotent on (
|
|
523
|
+
description: "Persist a proposed pending knowledge entry under .fabric/knowledge/pending/<type>/<slug>.md. Idempotent on (source_sessions[0], type, slug); repeat calls append evidence rather than overwrite. Skill-side tool \u2014 invoked at session-stop.",
|
|
418
524
|
inputSchema: FabExtractKnowledgeInputShape,
|
|
419
525
|
outputSchema: FabExtractKnowledgeOutputSchema.shape,
|
|
420
526
|
annotations: fabExtractKnowledgeAnnotations
|
|
@@ -423,9 +529,14 @@ function registerExtractKnowledge(server, tracker) {
|
|
|
423
529
|
const requestId = randomUUID();
|
|
424
530
|
tracker?.enter(requestId);
|
|
425
531
|
try {
|
|
532
|
+
const gateResult = await awaitFirstReconcileGate();
|
|
533
|
+
const gateWarn = gateWarning(gateResult);
|
|
426
534
|
const projectRoot = resolveProjectRoot();
|
|
427
535
|
const result = await extractKnowledge(projectRoot, input);
|
|
428
536
|
const response = { ...result };
|
|
537
|
+
if (gateWarn) {
|
|
538
|
+
response.warnings = [gateWarn];
|
|
539
|
+
}
|
|
429
540
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
430
541
|
const serialized = JSON.stringify(response);
|
|
431
542
|
enforcePayloadLimit(serialized, payloadLimits);
|
|
@@ -455,16 +566,30 @@ import { deriveAgentsMetaLayer } from "@fenglimg/fabric-shared";
|
|
|
455
566
|
var SELECTION_TOKEN_TTL_MS = 5 * 60 * 1e3;
|
|
456
567
|
var selectionTokenCache = /* @__PURE__ */ new Map();
|
|
457
568
|
async function planContext(projectRoot, input) {
|
|
458
|
-
|
|
459
|
-
|
|
569
|
+
let metaResult = await loadActiveMetaOrStale(projectRoot, { caller: "planContext" });
|
|
570
|
+
let meta = metaResult.meta;
|
|
571
|
+
let firstSeenPreviousRevision = metaResult.previous_revision_hash;
|
|
572
|
+
let autoHealedAccumulated = metaResult.auto_healed;
|
|
573
|
+
if (metaResult.auto_healed !== true && hasUndefinedDescription(meta)) {
|
|
574
|
+
try {
|
|
575
|
+
await reconcileKnowledge(projectRoot, { trigger: "auto-heal-description" });
|
|
576
|
+
const healedResult = await loadActiveMetaOrStale(projectRoot, { caller: "planContext" });
|
|
577
|
+
meta = healedResult.meta;
|
|
578
|
+
autoHealedAccumulated = true;
|
|
579
|
+
firstSeenPreviousRevision = metaResult.previous_revision_hash;
|
|
580
|
+
metaResult = healedResult;
|
|
581
|
+
} catch {
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const stale = metaResult.degraded === true || input.client_hash !== void 0 && input.client_hash !== meta.revision;
|
|
460
585
|
const uniquePaths = dedupePaths(input.paths);
|
|
461
586
|
const allDescriptions = buildDescriptionIndex(meta);
|
|
462
587
|
const relevanceTargetPaths = input.target_paths ?? uniquePaths;
|
|
463
|
-
const entries = uniquePaths.map((
|
|
464
|
-
const profile = buildRequirementProfile(
|
|
465
|
-
const descriptionIndex = allDescriptions.filter((item) => shouldIncludeIndexItemForPath(item, meta,
|
|
588
|
+
const entries = uniquePaths.map((path) => {
|
|
589
|
+
const profile = buildRequirementProfile(path, input);
|
|
590
|
+
const descriptionIndex = allDescriptions.filter((item) => shouldIncludeIndexItemForPath(item, meta, path)).filter((item) => shouldIncludeByRelevance(item, relevanceTargetPaths));
|
|
466
591
|
return {
|
|
467
|
-
path
|
|
592
|
+
path,
|
|
468
593
|
requirement_profile: profile,
|
|
469
594
|
description_index: descriptionIndex
|
|
470
595
|
};
|
|
@@ -480,7 +605,16 @@ async function planContext(projectRoot, input) {
|
|
|
480
605
|
shared: {
|
|
481
606
|
description_index: sharedDescriptionIndex,
|
|
482
607
|
preflight_diagnostics: buildPreflightDiagnostics(meta)
|
|
483
|
-
}
|
|
608
|
+
},
|
|
609
|
+
// v2.0.0-rc.22 Scope D T-D2 + rc.23 TASK-005 (a-B): surface auto-heal pair
|
|
610
|
+
// only when a heal actually fired (either revision-drift heal in
|
|
611
|
+
// loadActiveMetaOrStale or description-undefined heal driven from here).
|
|
612
|
+
// Keeping these fields absent on the steady-state path means existing
|
|
613
|
+
// consumers see the same wire shape they always have.
|
|
614
|
+
...autoHealedAccumulated ? {
|
|
615
|
+
auto_healed: true,
|
|
616
|
+
previous_revision_hash: firstSeenPreviousRevision
|
|
617
|
+
} : {}
|
|
484
618
|
};
|
|
485
619
|
try {
|
|
486
620
|
await appendEventLedgerEvent(projectRoot, {
|
|
@@ -502,15 +636,15 @@ async function planContext(projectRoot, input) {
|
|
|
502
636
|
return result;
|
|
503
637
|
}
|
|
504
638
|
function readSelectionToken(token, now = Date.now()) {
|
|
505
|
-
const
|
|
506
|
-
if (
|
|
639
|
+
const state2 = selectionTokenCache.get(token);
|
|
640
|
+
if (state2 === void 0) {
|
|
507
641
|
return void 0;
|
|
508
642
|
}
|
|
509
|
-
if (
|
|
643
|
+
if (state2.expires_at <= now) {
|
|
510
644
|
selectionTokenCache.delete(token);
|
|
511
645
|
return void 0;
|
|
512
646
|
}
|
|
513
|
-
return
|
|
647
|
+
return state2;
|
|
514
648
|
}
|
|
515
649
|
function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSelectableStableIds, now = Date.now()) {
|
|
516
650
|
const token = `selection:${revisionHash}:${now.toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
@@ -527,8 +661,8 @@ function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSe
|
|
|
527
661
|
}
|
|
528
662
|
function dedupePaths(paths) {
|
|
529
663
|
const seenPaths = /* @__PURE__ */ new Set();
|
|
530
|
-
return paths.flatMap((
|
|
531
|
-
const normalizedPath = normalizeKnowledgePath(
|
|
664
|
+
return paths.flatMap((path) => {
|
|
665
|
+
const normalizedPath = normalizeKnowledgePath(path);
|
|
532
666
|
if (seenPaths.has(normalizedPath)) {
|
|
533
667
|
return [];
|
|
534
668
|
}
|
|
@@ -536,8 +670,8 @@ function dedupePaths(paths) {
|
|
|
536
670
|
return [normalizedPath];
|
|
537
671
|
});
|
|
538
672
|
}
|
|
539
|
-
function buildRequirementProfile(
|
|
540
|
-
const normalizedPath = normalizeKnowledgePath(
|
|
673
|
+
function buildRequirementProfile(path, input) {
|
|
674
|
+
const normalizedPath = normalizeKnowledgePath(path);
|
|
541
675
|
const extensionMatch = /(\.[^./\\]+)$/u.exec(normalizedPath);
|
|
542
676
|
const knownTech = dedupeStableIds([
|
|
543
677
|
...input.known_tech ?? [],
|
|
@@ -549,7 +683,7 @@ function buildRequirementProfile(path2, input) {
|
|
|
549
683
|
extension: extensionMatch?.[1] ?? "",
|
|
550
684
|
known_tech: knownTech,
|
|
551
685
|
user_intent: input.intent ?? "",
|
|
552
|
-
detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[
|
|
686
|
+
detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path] ?? []
|
|
553
687
|
};
|
|
554
688
|
}
|
|
555
689
|
function buildDescriptionIndex(meta) {
|
|
@@ -630,6 +764,11 @@ function descriptionFromLegacyActivation(summary) {
|
|
|
630
764
|
function shouldIncludeIndexItemForPath(_item, _meta, _path) {
|
|
631
765
|
return true;
|
|
632
766
|
}
|
|
767
|
+
function hasUndefinedDescription(meta) {
|
|
768
|
+
return Object.values(meta.nodes).some(
|
|
769
|
+
(node) => node.description === void 0 && node.activation?.description === void 0
|
|
770
|
+
);
|
|
771
|
+
}
|
|
633
772
|
function buildPreflightDiagnostics(meta) {
|
|
634
773
|
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();
|
|
635
774
|
if (missingDescriptionStableIds.length === 0) {
|
|
@@ -673,6 +812,8 @@ function registerPlanContext(server, tracker) {
|
|
|
673
812
|
const requestId = randomUUID2();
|
|
674
813
|
tracker?.enter(requestId);
|
|
675
814
|
try {
|
|
815
|
+
const gateResult = await awaitFirstReconcileGate();
|
|
816
|
+
const gateWarn = gateWarning(gateResult);
|
|
676
817
|
const projectRoot = resolveProjectRoot();
|
|
677
818
|
const syncReport = await ensureKnowledgeFresh(projectRoot);
|
|
678
819
|
const result = await planContext(projectRoot, {
|
|
@@ -686,7 +827,10 @@ function registerPlanContext(server, tracker) {
|
|
|
686
827
|
});
|
|
687
828
|
const response = {
|
|
688
829
|
...result,
|
|
689
|
-
warnings: [
|
|
830
|
+
warnings: [
|
|
831
|
+
...gateWarn ? [gateWarn] : [],
|
|
832
|
+
...syncReport.warnings
|
|
833
|
+
]
|
|
690
834
|
};
|
|
691
835
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
692
836
|
const serialized = JSON.stringify(response);
|
|
@@ -935,7 +1079,7 @@ async function listPending(projectRoot, filters) {
|
|
|
935
1079
|
}
|
|
936
1080
|
if (filters?.tags !== void 0 && filters.tags.length > 0) {
|
|
937
1081
|
const itemTags = fm.tags ?? [];
|
|
938
|
-
const hasAll = filters.tags.every((
|
|
1082
|
+
const hasAll = filters.tags.every((t) => itemTags.includes(t));
|
|
939
1083
|
if (!hasAll) continue;
|
|
940
1084
|
}
|
|
941
1085
|
if (filters?.created_after !== void 0) {
|
|
@@ -1108,8 +1252,8 @@ function resolveModifyTarget(projectRoot, pendingPath) {
|
|
|
1108
1252
|
}
|
|
1109
1253
|
return null;
|
|
1110
1254
|
}
|
|
1111
|
-
function inferTypeFromPath(
|
|
1112
|
-
const match = /knowledge\/(?:pending\/)?([^/]+)\/[^/]+\.md$/u.exec(
|
|
1255
|
+
function inferTypeFromPath(path) {
|
|
1256
|
+
const match = /knowledge\/(?:pending\/)?([^/]+)\/[^/]+\.md$/u.exec(path);
|
|
1113
1257
|
if (match === null) return null;
|
|
1114
1258
|
const seg = match[1];
|
|
1115
1259
|
if (seg !== void 0 && PLURAL_TYPES.includes(seg)) {
|
|
@@ -1117,8 +1261,8 @@ function inferTypeFromPath(path2) {
|
|
|
1117
1261
|
}
|
|
1118
1262
|
return null;
|
|
1119
1263
|
}
|
|
1120
|
-
function extractSlug(
|
|
1121
|
-
const file = basename(
|
|
1264
|
+
function extractSlug(path) {
|
|
1265
|
+
const file = basename(path).replace(/\.md$/u, "");
|
|
1122
1266
|
return file.replace(/^K[PT]-(MOD|DEC|GLD|PIT|PRO)-\d+--/u, "");
|
|
1123
1267
|
}
|
|
1124
1268
|
async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
@@ -1235,7 +1379,7 @@ async function searchEntries(projectRoot, query, filters) {
|
|
|
1235
1379
|
}
|
|
1236
1380
|
if (filters?.tags !== void 0 && filters.tags.length > 0) {
|
|
1237
1381
|
const itemTags = fm.tags ?? [];
|
|
1238
|
-
const hasAll = filters.tags.every((
|
|
1382
|
+
const hasAll = filters.tags.every((t) => itemTags.includes(t));
|
|
1239
1383
|
if (!hasAll) continue;
|
|
1240
1384
|
}
|
|
1241
1385
|
if (filters?.created_after !== void 0) {
|
|
@@ -1474,10 +1618,15 @@ function registerReview(server, tracker) {
|
|
|
1474
1618
|
const requestId = randomUUID3();
|
|
1475
1619
|
tracker?.enter(requestId);
|
|
1476
1620
|
try {
|
|
1621
|
+
const gateResult = await awaitFirstReconcileGate();
|
|
1622
|
+
const gateWarn = gateWarning(gateResult);
|
|
1477
1623
|
const narrowed = FabReviewInputSchema.parse(input);
|
|
1478
1624
|
const projectRoot = resolveProjectRoot();
|
|
1479
1625
|
const result = await reviewKnowledge(projectRoot, narrowed);
|
|
1480
|
-
const response = result;
|
|
1626
|
+
const response = { ...result };
|
|
1627
|
+
if (gateWarn) {
|
|
1628
|
+
response.warnings = [gateWarn];
|
|
1629
|
+
}
|
|
1481
1630
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
1482
1631
|
const serialized = JSON.stringify(response);
|
|
1483
1632
|
enforcePayloadLimit3(serialized, payloadLimits);
|
|
@@ -1505,62 +1654,17 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
|
|
|
1505
1654
|
import { readFile as readFile4 } from "fs/promises";
|
|
1506
1655
|
import { homedir as homedir3 } from "os";
|
|
1507
1656
|
import { join as join4 } from "path";
|
|
1508
|
-
var KNOWLEDGE_SECTION_NAMES = [
|
|
1509
|
-
"MISSION_STATEMENT",
|
|
1510
|
-
"MANDATORY_INJECTION",
|
|
1511
|
-
"BUSINESS_LOGIC_CHUNKS",
|
|
1512
|
-
"CONTEXT_INFO"
|
|
1513
|
-
];
|
|
1514
1657
|
var PRIORITY_ORDER = {
|
|
1515
1658
|
high: 0,
|
|
1516
1659
|
medium: 1,
|
|
1517
1660
|
low: 2
|
|
1518
1661
|
};
|
|
1519
|
-
function
|
|
1520
|
-
const
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
let activeSectionDepth = 0;
|
|
1524
|
-
let buffer = [];
|
|
1525
|
-
const flush = () => {
|
|
1526
|
-
if (activeSection === void 0) {
|
|
1527
|
-
return;
|
|
1528
|
-
}
|
|
1529
|
-
const text = buffer.join("\n").trim();
|
|
1530
|
-
if (text.length === 0) {
|
|
1531
|
-
buffer = [];
|
|
1532
|
-
return;
|
|
1533
|
-
}
|
|
1534
|
-
sections.set(activeSection, [...sections.get(activeSection) ?? [], text]);
|
|
1535
|
-
buffer = [];
|
|
1536
|
-
};
|
|
1537
|
-
for (const line of lines) {
|
|
1538
|
-
const heading = /^(#{2,6})\s+\[([A-Z_]+)\]\s*$/u.exec(line.trim());
|
|
1539
|
-
if (heading !== null) {
|
|
1540
|
-
flush();
|
|
1541
|
-
activeSection = isKnowledgeSectionName(heading[2]) ? heading[2] : void 0;
|
|
1542
|
-
activeSectionDepth = activeSection === void 0 ? 0 : heading[1].length;
|
|
1543
|
-
continue;
|
|
1544
|
-
}
|
|
1545
|
-
const ordinaryHeading = /^(#{1,6})\s+/u.exec(line.trim());
|
|
1546
|
-
if (ordinaryHeading !== null) {
|
|
1547
|
-
if (activeSection !== void 0 && ordinaryHeading[1].length > activeSectionDepth) {
|
|
1548
|
-
buffer.push(line);
|
|
1549
|
-
continue;
|
|
1550
|
-
}
|
|
1551
|
-
flush();
|
|
1552
|
-
activeSection = void 0;
|
|
1553
|
-
activeSectionDepth = 0;
|
|
1554
|
-
continue;
|
|
1555
|
-
}
|
|
1556
|
-
if (activeSection !== void 0) {
|
|
1557
|
-
buffer.push(line);
|
|
1558
|
-
}
|
|
1662
|
+
function extractBody(content) {
|
|
1663
|
+
const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/u.exec(content);
|
|
1664
|
+
if (match === null) {
|
|
1665
|
+
return content.replace(/^\uFEFF/u, "");
|
|
1559
1666
|
}
|
|
1560
|
-
|
|
1561
|
-
return new Map(
|
|
1562
|
-
Array.from(sections.entries()).map(([section, values]) => [section, values.join("\n\n")])
|
|
1563
|
-
);
|
|
1667
|
+
return content.slice(match[0].length);
|
|
1564
1668
|
}
|
|
1565
1669
|
async function getKnowledgeSections(projectRoot, input) {
|
|
1566
1670
|
const token = readSelectionToken(input.selection_token);
|
|
@@ -1568,28 +1672,14 @@ async function getKnowledgeSections(projectRoot, input) {
|
|
|
1568
1672
|
throw new Error("selection_token is missing or expired");
|
|
1569
1673
|
}
|
|
1570
1674
|
validateAiSelections(token.ai_selectable_stable_ids, input.ai_selected_stable_ids, input.ai_selection_reasons);
|
|
1571
|
-
const meta = await
|
|
1675
|
+
const { meta } = await loadActiveMeta(projectRoot, { caller: "getKnowledgeSections" });
|
|
1572
1676
|
const selectedStableIds = [...token.required_stable_ids, ...input.ai_selected_stable_ids];
|
|
1573
1677
|
const selectedRules = sortRuleNodes(selectedStableIds.map((stableId) => findRuleNode(meta, stableId)));
|
|
1574
1678
|
const diagnostics = [];
|
|
1575
1679
|
const rules = [];
|
|
1576
1680
|
for (const rule of selectedRules) {
|
|
1577
1681
|
const content = await readFile4(resolveRuleSourcePath(projectRoot, rule.path), "utf8");
|
|
1578
|
-
const
|
|
1579
|
-
const sections = {};
|
|
1580
|
-
for (const section of input.sections) {
|
|
1581
|
-
const sectionContent = parsedSections.get(section);
|
|
1582
|
-
sections[section] = sectionContent ?? "";
|
|
1583
|
-
if (sectionContent === void 0) {
|
|
1584
|
-
diagnostics.push({
|
|
1585
|
-
code: "missing_section",
|
|
1586
|
-
severity: "warn",
|
|
1587
|
-
stable_id: rule.stable_id,
|
|
1588
|
-
section,
|
|
1589
|
-
message: `Rule ${rule.stable_id} does not define section ${section}.`
|
|
1590
|
-
});
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1682
|
+
const body = extractBody(content);
|
|
1593
1683
|
const description = rule.node.description;
|
|
1594
1684
|
if (description !== void 0 && description.knowledge_type === void 0 && description.knowledge_layer === void 0) {
|
|
1595
1685
|
diagnostics.push({
|
|
@@ -1603,7 +1693,7 @@ async function getKnowledgeSections(projectRoot, input) {
|
|
|
1603
1693
|
stable_id: rule.stable_id,
|
|
1604
1694
|
level: rule.level,
|
|
1605
1695
|
path: rule.path,
|
|
1606
|
-
|
|
1696
|
+
body
|
|
1607
1697
|
});
|
|
1608
1698
|
}
|
|
1609
1699
|
const result = {
|
|
@@ -1635,7 +1725,7 @@ async function getKnowledgeSections(projectRoot, input) {
|
|
|
1635
1725
|
event_type: "knowledge_sections_fetched",
|
|
1636
1726
|
selection_token: input.selection_token,
|
|
1637
1727
|
target_paths: token.target_paths,
|
|
1638
|
-
requested_sections:
|
|
1728
|
+
requested_sections: [],
|
|
1639
1729
|
final_stable_ids: result.selected_stable_ids,
|
|
1640
1730
|
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
1641
1731
|
diagnostics,
|
|
@@ -1720,9 +1810,6 @@ function outputLevelOrder(level) {
|
|
|
1720
1810
|
return 2;
|
|
1721
1811
|
}
|
|
1722
1812
|
}
|
|
1723
|
-
function isKnowledgeSectionName(value) {
|
|
1724
|
-
return KNOWLEDGE_SECTION_NAMES.includes(value);
|
|
1725
|
-
}
|
|
1726
1813
|
function resolveRuleSourcePath(projectRoot, contentRef) {
|
|
1727
1814
|
if (contentRef.startsWith("~/.fabric/knowledge/")) {
|
|
1728
1815
|
const home = process.env.FABRIC_HOME ?? homedir3();
|
|
@@ -1739,7 +1826,7 @@ function registerKnowledgeSections(server, tracker) {
|
|
|
1739
1826
|
server.registerTool(
|
|
1740
1827
|
"fab_get_knowledge_sections",
|
|
1741
1828
|
{
|
|
1742
|
-
description: "Fetch
|
|
1829
|
+
description: "Fetch the full markdown body of one or more Fabric rules picked from fab_plan_context. Returns body strings keyed by stable_id (frontmatter stripped). Use after fab_plan_context returned selectable entries to load full rule content for LLM context injection \u2014 scan the body for whatever headings the rule defines (Summary / Why proposed / Session context / Evidence, etc.).",
|
|
1743
1830
|
inputSchema: knowledgeSectionsInputSchema,
|
|
1744
1831
|
outputSchema: knowledgeSectionsOutputSchema,
|
|
1745
1832
|
annotations: knowledgeSectionsAnnotations
|
|
@@ -1748,12 +1835,17 @@ function registerKnowledgeSections(server, tracker) {
|
|
|
1748
1835
|
const requestId = randomUUID4();
|
|
1749
1836
|
tracker?.enter(requestId);
|
|
1750
1837
|
try {
|
|
1838
|
+
const gateResult = await awaitFirstReconcileGate();
|
|
1839
|
+
const gateWarn = gateWarning(gateResult);
|
|
1751
1840
|
const projectRoot = resolveProjectRoot();
|
|
1752
1841
|
const syncReport = await ensureKnowledgeFresh(projectRoot);
|
|
1753
1842
|
const result = await getKnowledgeSections(projectRoot, input);
|
|
1754
1843
|
const response = {
|
|
1755
1844
|
...result,
|
|
1756
|
-
warnings: [
|
|
1845
|
+
warnings: [
|
|
1846
|
+
...gateWarn ? [gateWarn] : [],
|
|
1847
|
+
...syncReport.warnings
|
|
1848
|
+
]
|
|
1757
1849
|
};
|
|
1758
1850
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
1759
1851
|
const serialized = JSON.stringify(response);
|
|
@@ -1779,99 +1871,6 @@ function registerKnowledgeSections(server, tracker) {
|
|
|
1779
1871
|
);
|
|
1780
1872
|
}
|
|
1781
1873
|
|
|
1782
|
-
// src/services/serve-lock.ts
|
|
1783
|
-
import fs from "fs";
|
|
1784
|
-
import path from "path";
|
|
1785
|
-
import { createTranslator, detectNodeLocale } from "@fenglimg/fabric-shared";
|
|
1786
|
-
import { IOFabricError } from "@fenglimg/fabric-shared/errors";
|
|
1787
|
-
var LOCK_FILENAME = ".serve.lock";
|
|
1788
|
-
var t = createTranslator(detectNodeLocale());
|
|
1789
|
-
var ServeLockHeldError = class extends IOFabricError {
|
|
1790
|
-
code = "SERVE_LOCK_HELD";
|
|
1791
|
-
httpStatus = 423;
|
|
1792
|
-
};
|
|
1793
|
-
function lockPath(projectRoot) {
|
|
1794
|
-
return path.join(projectRoot, ".fabric", LOCK_FILENAME);
|
|
1795
|
-
}
|
|
1796
|
-
function isAlive(pid) {
|
|
1797
|
-
try {
|
|
1798
|
-
process.kill(pid, 0);
|
|
1799
|
-
return true;
|
|
1800
|
-
} catch (e) {
|
|
1801
|
-
const err = e;
|
|
1802
|
-
if (err.code === "ESRCH") return false;
|
|
1803
|
-
if (err.code === "EPERM") return true;
|
|
1804
|
-
throw e;
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
function acquireLock(projectRoot, opts) {
|
|
1808
|
-
const p = lockPath(projectRoot);
|
|
1809
|
-
if (fs.existsSync(p)) {
|
|
1810
|
-
let state = null;
|
|
1811
|
-
try {
|
|
1812
|
-
state = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1813
|
-
} catch {
|
|
1814
|
-
}
|
|
1815
|
-
if (state && state.pid && state.pid !== process.pid && isAlive(state.pid) && !opts?.force) {
|
|
1816
|
-
throw new ServeLockHeldError(
|
|
1817
|
-
`serve lock held by live PID ${state.pid}`,
|
|
1818
|
-
{
|
|
1819
|
-
actionHint: t("cli.serve.lock-held.action-hint", { pid: String(state.pid) }),
|
|
1820
|
-
details: state
|
|
1821
|
-
}
|
|
1822
|
-
);
|
|
1823
|
-
}
|
|
1824
|
-
if (state && state.pid && !isAlive(state.pid)) {
|
|
1825
|
-
process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 overwriting
|
|
1826
|
-
`);
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
1830
|
-
fs.writeFileSync(
|
|
1831
|
-
p,
|
|
1832
|
-
JSON.stringify({ pid: process.pid, acquiredAt: Date.now(), host: process.env.HOSTNAME })
|
|
1833
|
-
);
|
|
1834
|
-
}
|
|
1835
|
-
function releaseLock(projectRoot) {
|
|
1836
|
-
const p = lockPath(projectRoot);
|
|
1837
|
-
try {
|
|
1838
|
-
if (fs.existsSync(p)) {
|
|
1839
|
-
const state = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1840
|
-
if (state.pid === process.pid) {
|
|
1841
|
-
fs.unlinkSync(p);
|
|
1842
|
-
}
|
|
1843
|
-
}
|
|
1844
|
-
} catch {
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
function readLockState(projectRoot) {
|
|
1848
|
-
const p = lockPath(projectRoot);
|
|
1849
|
-
if (!fs.existsSync(p)) return null;
|
|
1850
|
-
try {
|
|
1851
|
-
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1852
|
-
} catch {
|
|
1853
|
-
return null;
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
function checkLockOrThrow(projectRoot, opts) {
|
|
1857
|
-
const state = readLockState(projectRoot);
|
|
1858
|
-
if (state === null) return;
|
|
1859
|
-
if (state.pid === process.pid) return;
|
|
1860
|
-
if (!isAlive(state.pid)) {
|
|
1861
|
-
process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 ignoring
|
|
1862
|
-
`);
|
|
1863
|
-
return;
|
|
1864
|
-
}
|
|
1865
|
-
if (opts?.force) return;
|
|
1866
|
-
throw new ServeLockHeldError(
|
|
1867
|
-
`serve lock held by live PID ${state.pid}`,
|
|
1868
|
-
{
|
|
1869
|
-
actionHint: t("cli.serve.lock-held.action-hint", { pid: String(state.pid) }),
|
|
1870
|
-
details: state
|
|
1871
|
-
}
|
|
1872
|
-
);
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
1874
|
// src/index.ts
|
|
1876
1875
|
function writeStderr(message) {
|
|
1877
1876
|
process.stderr.write(`${message}
|
|
@@ -1893,7 +1892,7 @@ function formatPreexistingRootMessage(projectRoot) {
|
|
|
1893
1892
|
function createFabricServer(tracker) {
|
|
1894
1893
|
const server = new McpServer({
|
|
1895
1894
|
name: "fabric-knowledge-server",
|
|
1896
|
-
version: "2.0.0-rc.
|
|
1895
|
+
version: "2.0.0-rc.23"
|
|
1897
1896
|
});
|
|
1898
1897
|
registerPlanContext(server, tracker);
|
|
1899
1898
|
registerKnowledgeSections(server, tracker);
|
|
@@ -1908,10 +1907,10 @@ function createFabricServer(tracker) {
|
|
|
1908
1907
|
},
|
|
1909
1908
|
async (_uri) => {
|
|
1910
1909
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
1911
|
-
const
|
|
1910
|
+
const path = join5(projectRoot, ".fabric", "bootstrap", "README.md");
|
|
1912
1911
|
let text = "";
|
|
1913
|
-
if (existsSync4(
|
|
1914
|
-
text = await readFile5(
|
|
1912
|
+
if (existsSync4(path)) {
|
|
1913
|
+
text = await readFile5(path, "utf8");
|
|
1915
1914
|
}
|
|
1916
1915
|
return {
|
|
1917
1916
|
contents: [
|
|
@@ -1929,13 +1928,6 @@ function createFabricServer(tracker) {
|
|
|
1929
1928
|
async function startStdioServer() {
|
|
1930
1929
|
const tracker = createInFlightTracker();
|
|
1931
1930
|
const projectRoot = resolveProjectRoot();
|
|
1932
|
-
const syncStart = Date.now();
|
|
1933
|
-
const reconcileResult = await reconcileKnowledge(projectRoot, { trigger: "startup" });
|
|
1934
|
-
const syncDurationMs = Date.now() - syncStart;
|
|
1935
|
-
process.stderr.write(
|
|
1936
|
-
`[startup] rule sync: status=${reconcileResult.status}, events=${reconcileResult.events.length}, ${syncDurationMs}ms
|
|
1937
|
-
`
|
|
1938
|
-
);
|
|
1939
1931
|
const rootMsg = formatPreexistingRootMessage(projectRoot);
|
|
1940
1932
|
if (rootMsg !== null) {
|
|
1941
1933
|
process.stderr.write(`${rootMsg}
|
|
@@ -1944,6 +1936,21 @@ async function startStdioServer() {
|
|
|
1944
1936
|
const server = createFabricServer(tracker);
|
|
1945
1937
|
const transport = new StdioServerTransport();
|
|
1946
1938
|
await server.connect(transport);
|
|
1939
|
+
const syncStart = Date.now();
|
|
1940
|
+
const backgroundReconcile = (async () => {
|
|
1941
|
+
const reconcileResult = await reconcileKnowledge(projectRoot, { trigger: "startup" });
|
|
1942
|
+
const syncDurationMs = Date.now() - syncStart;
|
|
1943
|
+
process.stderr.write(
|
|
1944
|
+
`[startup] rule sync: status=${reconcileResult.status}, events=${reconcileResult.events.length}, ${syncDurationMs}ms
|
|
1945
|
+
`
|
|
1946
|
+
);
|
|
1947
|
+
})().catch((error) => {
|
|
1948
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
1949
|
+
process.stderr.write(`[startup] rule sync FAILED: ${message}
|
|
1950
|
+
`);
|
|
1951
|
+
throw error;
|
|
1952
|
+
});
|
|
1953
|
+
setFirstReconcile(backgroundReconcile);
|
|
1947
1954
|
const closeServer = async () => {
|
|
1948
1955
|
await server.close();
|
|
1949
1956
|
};
|
|
@@ -1993,7 +2000,7 @@ function createShutdownHandler(deps) {
|
|
|
1993
2000
|
};
|
|
1994
2001
|
}
|
|
1995
2002
|
async function startHttpServer(options) {
|
|
1996
|
-
const { createFabricHttpApp } = await import("./http-
|
|
2003
|
+
const { createFabricHttpApp } = await import("./http-ZBV6YUHD.js");
|
|
1997
2004
|
const { port, projectRoot, host = "127.0.0.1", authToken } = options;
|
|
1998
2005
|
const app = createFabricHttpApp({ projectRoot, host, authToken });
|
|
1999
2006
|
return await new Promise((resolveServer, rejectServer) => {
|
|
@@ -2036,6 +2043,7 @@ export {
|
|
|
2036
2043
|
createShutdownHandler,
|
|
2037
2044
|
deriveKnowledgeMetaLayer,
|
|
2038
2045
|
deriveKnowledgeMetaTopologyType,
|
|
2046
|
+
enrichDescriptions,
|
|
2039
2047
|
ensureKnowledgeFresh,
|
|
2040
2048
|
extractKnowledge,
|
|
2041
2049
|
flushAndSyncEventLedger,
|