@fenglimg/fabric-server 2.0.0-rc.22 → 2.0.0-rc.25
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-7N3FW5LX.js → chunk-HAXROPQM.js} +746 -57
- package/dist/{http-FF5NZCJK.js → http-QBGLHCHA.js} +1 -1
- package/dist/index.d.ts +97 -6
- package/dist/index.js +212 -217
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3,13 +3,17 @@ import {
|
|
|
3
3
|
EVENT_LEDGER_PATH,
|
|
4
4
|
LEDGER_PATH,
|
|
5
5
|
LEGACY_LEDGER_PATH,
|
|
6
|
+
ServeLockHeldError,
|
|
7
|
+
acquireLock,
|
|
6
8
|
appendEventLedgerEvent,
|
|
7
9
|
atomicWriteText,
|
|
8
10
|
buildKnowledgeMeta,
|
|
11
|
+
checkLockOrThrow,
|
|
9
12
|
computeKnowledgeBasedAgentsMeta,
|
|
10
13
|
computeKnowledgeTestIndex,
|
|
11
14
|
deriveKnowledgeMetaLayer,
|
|
12
15
|
deriveKnowledgeMetaTopologyType,
|
|
16
|
+
enrichDescriptions,
|
|
13
17
|
ensureKnowledgeFresh,
|
|
14
18
|
ensureParentDirectory,
|
|
15
19
|
flushAndSyncEventLedger,
|
|
@@ -19,17 +23,21 @@ import {
|
|
|
19
23
|
isSameKnowledgeTestIndex,
|
|
20
24
|
loadActiveMeta,
|
|
21
25
|
loadActiveMetaOrStale,
|
|
26
|
+
loadKbIdTypeMap,
|
|
22
27
|
normalizeKnowledgePath,
|
|
28
|
+
readLockState,
|
|
23
29
|
reconcileKnowledge,
|
|
30
|
+
releaseLock,
|
|
24
31
|
resolveProjectRoot,
|
|
25
32
|
runDoctorApplyLint,
|
|
33
|
+
runDoctorArchiveHistory,
|
|
26
34
|
runDoctorCiteCoverage,
|
|
27
35
|
runDoctorFix,
|
|
28
36
|
runDoctorReport,
|
|
29
37
|
sha256,
|
|
30
38
|
stableStringify,
|
|
31
39
|
writeKnowledgeMeta
|
|
32
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-HAXROPQM.js";
|
|
33
41
|
|
|
34
42
|
// src/index.ts
|
|
35
43
|
import { existsSync as existsSync4 } from "fs";
|
|
@@ -42,6 +50,66 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
42
50
|
// src/constants.ts
|
|
43
51
|
var AGENTS_MD_RESOURCE_URI = "fabric://bootstrap-readme";
|
|
44
52
|
|
|
53
|
+
// src/services/first-reconcile-gate.ts
|
|
54
|
+
function gateWarning(result) {
|
|
55
|
+
if (result.status === "ready") return null;
|
|
56
|
+
if (result.status === "stale") {
|
|
57
|
+
return {
|
|
58
|
+
code: "meta_stale",
|
|
59
|
+
file: "<response>",
|
|
60
|
+
action_hint: "Initial reconcile still pending; results may use cached meta. Retry shortly or run `fab doctor --fix`."
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
code: "reconcile_failed",
|
|
65
|
+
file: "<response>",
|
|
66
|
+
action_hint: "Reconcile failed at startup; run `fab doctor --fix` and restart the MCP server."
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
var state = {
|
|
70
|
+
firstReconcilePromise: null,
|
|
71
|
+
reconcileFailure: null,
|
|
72
|
+
settled: false
|
|
73
|
+
};
|
|
74
|
+
function setFirstReconcile(reconcilePromise) {
|
|
75
|
+
state.firstReconcilePromise = reconcilePromise.then(
|
|
76
|
+
() => {
|
|
77
|
+
state.settled = true;
|
|
78
|
+
},
|
|
79
|
+
(error) => {
|
|
80
|
+
state.reconcileFailure = error;
|
|
81
|
+
state.settled = true;
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
async function awaitFirstReconcileGate(timeoutMs = 5e3) {
|
|
86
|
+
if (state.reconcileFailure !== null) {
|
|
87
|
+
return { status: "failed", error: state.reconcileFailure };
|
|
88
|
+
}
|
|
89
|
+
if (state.firstReconcilePromise === null || state.settled) {
|
|
90
|
+
return { status: "ready" };
|
|
91
|
+
}
|
|
92
|
+
let timer = null;
|
|
93
|
+
const timeoutPromise = new Promise((resolveTimeout) => {
|
|
94
|
+
timer = setTimeout(() => resolveTimeout("timeout"), timeoutMs);
|
|
95
|
+
});
|
|
96
|
+
try {
|
|
97
|
+
const winner = await Promise.race([
|
|
98
|
+
state.firstReconcilePromise.then(() => "reconcile"),
|
|
99
|
+
timeoutPromise
|
|
100
|
+
]);
|
|
101
|
+
if (winner === "timeout") {
|
|
102
|
+
return { status: "stale" };
|
|
103
|
+
}
|
|
104
|
+
if (state.reconcileFailure !== null) {
|
|
105
|
+
return { status: "failed", error: state.reconcileFailure };
|
|
106
|
+
}
|
|
107
|
+
return { status: "ready" };
|
|
108
|
+
} finally {
|
|
109
|
+
if (timer !== null) clearTimeout(timer);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
45
113
|
// src/services/in-flight-tracker.ts
|
|
46
114
|
function createInFlightTracker() {
|
|
47
115
|
const active = /* @__PURE__ */ new Map();
|
|
@@ -133,7 +201,7 @@ async function extractKnowledge(projectRoot, input) {
|
|
|
133
201
|
}
|
|
134
202
|
}
|
|
135
203
|
const sanitizedSlug = sanitizeSlug(input.slug);
|
|
136
|
-
const sourceSessions =
|
|
204
|
+
const sourceSessions = input.source_sessions ?? [];
|
|
137
205
|
const primarySession = sourceSessions[0] ?? "";
|
|
138
206
|
const idempotencyKey = sha256(
|
|
139
207
|
JSON.stringify({
|
|
@@ -217,7 +285,20 @@ async function extractKnowledge(projectRoot, input) {
|
|
|
217
285
|
proposedReason: input.proposed_reason,
|
|
218
286
|
sessionContext: input.session_context,
|
|
219
287
|
relevanceScope,
|
|
220
|
-
relevancePaths
|
|
288
|
+
relevancePaths,
|
|
289
|
+
// v2.0.0-rc.23 TASK-006 (a-C1): optional structured triage fields. Each is
|
|
290
|
+
// emitted as a YAML line only when caller-supplied; omitted lines preserve
|
|
291
|
+
// the historical pending-file shape.
|
|
292
|
+
intentClues: input.intent_clues,
|
|
293
|
+
techStack: input.tech_stack,
|
|
294
|
+
impact: input.impact,
|
|
295
|
+
mustReadIf: input.must_read_if,
|
|
296
|
+
// v2.0.0-rc.23 TASK-014 (F8c): optional S5 onboard-slot tag. Same emit
|
|
297
|
+
// discipline as the four a-C1 fields — bare YAML line iff caller-supplied,
|
|
298
|
+
// never in the idempotency_key hash. fabric-archive's first-run phase is
|
|
299
|
+
// the only producer; downstream `fab onboard-coverage` walks frontmatter
|
|
300
|
+
// looking for this exact key.
|
|
301
|
+
onboardSlot: input.onboard_slot
|
|
221
302
|
});
|
|
222
303
|
await atomicWriteText(absolutePath, fresh);
|
|
223
304
|
await emitEventBestEffort(projectRoot, {
|
|
@@ -260,6 +341,24 @@ function renderFreshEntry(args) {
|
|
|
260
341
|
const pathsBody = args.relevancePaths.map((p) => quoteRelevancePath(p)).join(", ");
|
|
261
342
|
frontmatterLines.push(`relevance_paths: [${pathsBody}]`);
|
|
262
343
|
}
|
|
344
|
+
if (args.intentClues !== void 0) {
|
|
345
|
+
const body2 = args.intentClues.map((s) => quoteRelevancePath(s)).join(", ");
|
|
346
|
+
frontmatterLines.push(`intent_clues: [${body2}]`);
|
|
347
|
+
}
|
|
348
|
+
if (args.techStack !== void 0) {
|
|
349
|
+
const body2 = args.techStack.map((s) => quoteRelevancePath(s)).join(", ");
|
|
350
|
+
frontmatterLines.push(`tech_stack: [${body2}]`);
|
|
351
|
+
}
|
|
352
|
+
if (args.impact !== void 0) {
|
|
353
|
+
const body2 = args.impact.map((s) => quoteRelevancePath(s)).join(", ");
|
|
354
|
+
frontmatterLines.push(`impact: [${body2}]`);
|
|
355
|
+
}
|
|
356
|
+
if (args.mustReadIf !== void 0) {
|
|
357
|
+
frontmatterLines.push(`must_read_if: ${quoteRelevancePath(args.mustReadIf)}`);
|
|
358
|
+
}
|
|
359
|
+
if (args.onboardSlot !== void 0) {
|
|
360
|
+
frontmatterLines.push(`onboard_slot: ${args.onboardSlot}`);
|
|
361
|
+
}
|
|
263
362
|
frontmatterLines.push(
|
|
264
363
|
`x-fabric-idempotency-key: ${args.idempotencyKey}`,
|
|
265
364
|
"---"
|
|
@@ -328,9 +427,9 @@ ${renderEvidenceBlock(newSummary, newRecentPaths)}
|
|
|
328
427
|
const pathSection = /Recent paths:\s*\n([\s\S]*?)(?:\n\s*Notes:|$)/u.exec(block);
|
|
329
428
|
if (pathSection !== null) {
|
|
330
429
|
for (const rawLine of (pathSection[1] ?? "").split(/\r?\n/u)) {
|
|
331
|
-
const
|
|
332
|
-
if (
|
|
333
|
-
existingPaths.push(
|
|
430
|
+
const t = rawLine.trim();
|
|
431
|
+
if (t.startsWith("- ")) {
|
|
432
|
+
existingPaths.push(t.slice(2).trim());
|
|
334
433
|
}
|
|
335
434
|
}
|
|
336
435
|
}
|
|
@@ -339,16 +438,16 @@ ${renderEvidenceBlock(newSummary, newRecentPaths)}
|
|
|
339
438
|
const bulletLines = [];
|
|
340
439
|
let prose = [];
|
|
341
440
|
for (const rawLine of noteBody.split(/\r?\n/u)) {
|
|
342
|
-
const
|
|
343
|
-
if (
|
|
344
|
-
if (
|
|
441
|
+
const t = rawLine.trim();
|
|
442
|
+
if (t.length === 0) continue;
|
|
443
|
+
if (t.startsWith("- ")) {
|
|
345
444
|
if (prose.length > 0) {
|
|
346
445
|
existingNotes.push(prose.join(" ").trim());
|
|
347
446
|
prose = [];
|
|
348
447
|
}
|
|
349
|
-
bulletLines.push(
|
|
448
|
+
bulletLines.push(t.slice(2).trim());
|
|
350
449
|
} else {
|
|
351
|
-
prose.push(
|
|
450
|
+
prose.push(t);
|
|
352
451
|
}
|
|
353
452
|
}
|
|
354
453
|
if (prose.length > 0) existingNotes.push(prose.join(" ").trim());
|
|
@@ -423,7 +522,7 @@ function registerExtractKnowledge(server, tracker) {
|
|
|
423
522
|
server.registerTool(
|
|
424
523
|
"fab_extract_knowledge",
|
|
425
524
|
{
|
|
426
|
-
description: "Persist a proposed pending knowledge entry under .fabric/knowledge/pending/<type>/<slug>.md. Idempotent on (
|
|
525
|
+
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.",
|
|
427
526
|
inputSchema: FabExtractKnowledgeInputShape,
|
|
428
527
|
outputSchema: FabExtractKnowledgeOutputSchema.shape,
|
|
429
528
|
annotations: fabExtractKnowledgeAnnotations
|
|
@@ -432,9 +531,14 @@ function registerExtractKnowledge(server, tracker) {
|
|
|
432
531
|
const requestId = randomUUID();
|
|
433
532
|
tracker?.enter(requestId);
|
|
434
533
|
try {
|
|
534
|
+
const gateResult = await awaitFirstReconcileGate();
|
|
535
|
+
const gateWarn = gateWarning(gateResult);
|
|
435
536
|
const projectRoot = resolveProjectRoot();
|
|
436
537
|
const result = await extractKnowledge(projectRoot, input);
|
|
437
538
|
const response = { ...result };
|
|
539
|
+
if (gateWarn) {
|
|
540
|
+
response.warnings = [gateWarn];
|
|
541
|
+
}
|
|
438
542
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
439
543
|
const serialized = JSON.stringify(response);
|
|
440
544
|
enforcePayloadLimit(serialized, payloadLimits);
|
|
@@ -464,17 +568,30 @@ import { deriveAgentsMetaLayer } from "@fenglimg/fabric-shared";
|
|
|
464
568
|
var SELECTION_TOKEN_TTL_MS = 5 * 60 * 1e3;
|
|
465
569
|
var selectionTokenCache = /* @__PURE__ */ new Map();
|
|
466
570
|
async function planContext(projectRoot, input) {
|
|
467
|
-
|
|
468
|
-
|
|
571
|
+
let metaResult = await loadActiveMetaOrStale(projectRoot, { caller: "planContext" });
|
|
572
|
+
let meta = metaResult.meta;
|
|
573
|
+
let firstSeenPreviousRevision = metaResult.previous_revision_hash;
|
|
574
|
+
let autoHealedAccumulated = metaResult.auto_healed;
|
|
575
|
+
if (metaResult.auto_healed !== true && hasUndefinedDescription(meta)) {
|
|
576
|
+
try {
|
|
577
|
+
await reconcileKnowledge(projectRoot, { trigger: "auto-heal-description" });
|
|
578
|
+
const healedResult = await loadActiveMetaOrStale(projectRoot, { caller: "planContext" });
|
|
579
|
+
meta = healedResult.meta;
|
|
580
|
+
autoHealedAccumulated = true;
|
|
581
|
+
firstSeenPreviousRevision = metaResult.previous_revision_hash;
|
|
582
|
+
metaResult = healedResult;
|
|
583
|
+
} catch {
|
|
584
|
+
}
|
|
585
|
+
}
|
|
469
586
|
const stale = metaResult.degraded === true || input.client_hash !== void 0 && input.client_hash !== meta.revision;
|
|
470
587
|
const uniquePaths = dedupePaths(input.paths);
|
|
471
588
|
const allDescriptions = buildDescriptionIndex(meta);
|
|
472
589
|
const relevanceTargetPaths = input.target_paths ?? uniquePaths;
|
|
473
|
-
const entries = uniquePaths.map((
|
|
474
|
-
const profile = buildRequirementProfile(
|
|
475
|
-
const descriptionIndex = allDescriptions.filter((item) => shouldIncludeIndexItemForPath(item, meta,
|
|
590
|
+
const entries = uniquePaths.map((path) => {
|
|
591
|
+
const profile = buildRequirementProfile(path, input);
|
|
592
|
+
const descriptionIndex = allDescriptions.filter((item) => shouldIncludeIndexItemForPath(item, meta, path)).filter((item) => shouldIncludeByRelevance(item, relevanceTargetPaths));
|
|
476
593
|
return {
|
|
477
|
-
path
|
|
594
|
+
path,
|
|
478
595
|
requirement_profile: profile,
|
|
479
596
|
description_index: descriptionIndex
|
|
480
597
|
};
|
|
@@ -491,12 +608,14 @@ async function planContext(projectRoot, input) {
|
|
|
491
608
|
description_index: sharedDescriptionIndex,
|
|
492
609
|
preflight_diagnostics: buildPreflightDiagnostics(meta)
|
|
493
610
|
},
|
|
494
|
-
// v2.0.0-rc.22 Scope D T-D2: surface auto-heal pair
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
|
|
611
|
+
// v2.0.0-rc.22 Scope D T-D2 + rc.23 TASK-005 (a-B): surface auto-heal pair
|
|
612
|
+
// only when a heal actually fired (either revision-drift heal in
|
|
613
|
+
// loadActiveMetaOrStale or description-undefined heal driven from here).
|
|
614
|
+
// Keeping these fields absent on the steady-state path means existing
|
|
615
|
+
// consumers see the same wire shape they always have.
|
|
616
|
+
...autoHealedAccumulated ? {
|
|
498
617
|
auto_healed: true,
|
|
499
|
-
previous_revision_hash:
|
|
618
|
+
previous_revision_hash: firstSeenPreviousRevision
|
|
500
619
|
} : {}
|
|
501
620
|
};
|
|
502
621
|
try {
|
|
@@ -519,15 +638,15 @@ async function planContext(projectRoot, input) {
|
|
|
519
638
|
return result;
|
|
520
639
|
}
|
|
521
640
|
function readSelectionToken(token, now = Date.now()) {
|
|
522
|
-
const
|
|
523
|
-
if (
|
|
641
|
+
const state2 = selectionTokenCache.get(token);
|
|
642
|
+
if (state2 === void 0) {
|
|
524
643
|
return void 0;
|
|
525
644
|
}
|
|
526
|
-
if (
|
|
645
|
+
if (state2.expires_at <= now) {
|
|
527
646
|
selectionTokenCache.delete(token);
|
|
528
647
|
return void 0;
|
|
529
648
|
}
|
|
530
|
-
return
|
|
649
|
+
return state2;
|
|
531
650
|
}
|
|
532
651
|
function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSelectableStableIds, now = Date.now()) {
|
|
533
652
|
const token = `selection:${revisionHash}:${now.toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
@@ -544,8 +663,8 @@ function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSe
|
|
|
544
663
|
}
|
|
545
664
|
function dedupePaths(paths) {
|
|
546
665
|
const seenPaths = /* @__PURE__ */ new Set();
|
|
547
|
-
return paths.flatMap((
|
|
548
|
-
const normalizedPath = normalizeKnowledgePath(
|
|
666
|
+
return paths.flatMap((path) => {
|
|
667
|
+
const normalizedPath = normalizeKnowledgePath(path);
|
|
549
668
|
if (seenPaths.has(normalizedPath)) {
|
|
550
669
|
return [];
|
|
551
670
|
}
|
|
@@ -553,8 +672,8 @@ function dedupePaths(paths) {
|
|
|
553
672
|
return [normalizedPath];
|
|
554
673
|
});
|
|
555
674
|
}
|
|
556
|
-
function buildRequirementProfile(
|
|
557
|
-
const normalizedPath = normalizeKnowledgePath(
|
|
675
|
+
function buildRequirementProfile(path, input) {
|
|
676
|
+
const normalizedPath = normalizeKnowledgePath(path);
|
|
558
677
|
const extensionMatch = /(\.[^./\\]+)$/u.exec(normalizedPath);
|
|
559
678
|
const knownTech = dedupeStableIds([
|
|
560
679
|
...input.known_tech ?? [],
|
|
@@ -566,7 +685,7 @@ function buildRequirementProfile(path2, input) {
|
|
|
566
685
|
extension: extensionMatch?.[1] ?? "",
|
|
567
686
|
known_tech: knownTech,
|
|
568
687
|
user_intent: input.intent ?? "",
|
|
569
|
-
detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[
|
|
688
|
+
detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path] ?? []
|
|
570
689
|
};
|
|
571
690
|
}
|
|
572
691
|
function buildDescriptionIndex(meta) {
|
|
@@ -647,6 +766,11 @@ function descriptionFromLegacyActivation(summary) {
|
|
|
647
766
|
function shouldIncludeIndexItemForPath(_item, _meta, _path) {
|
|
648
767
|
return true;
|
|
649
768
|
}
|
|
769
|
+
function hasUndefinedDescription(meta) {
|
|
770
|
+
return Object.values(meta.nodes).some(
|
|
771
|
+
(node) => node.description === void 0 && node.activation?.description === void 0
|
|
772
|
+
);
|
|
773
|
+
}
|
|
650
774
|
function buildPreflightDiagnostics(meta) {
|
|
651
775
|
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();
|
|
652
776
|
if (missingDescriptionStableIds.length === 0) {
|
|
@@ -690,6 +814,8 @@ function registerPlanContext(server, tracker) {
|
|
|
690
814
|
const requestId = randomUUID2();
|
|
691
815
|
tracker?.enter(requestId);
|
|
692
816
|
try {
|
|
817
|
+
const gateResult = await awaitFirstReconcileGate();
|
|
818
|
+
const gateWarn = gateWarning(gateResult);
|
|
693
819
|
const projectRoot = resolveProjectRoot();
|
|
694
820
|
const syncReport = await ensureKnowledgeFresh(projectRoot);
|
|
695
821
|
const result = await planContext(projectRoot, {
|
|
@@ -703,7 +829,10 @@ function registerPlanContext(server, tracker) {
|
|
|
703
829
|
});
|
|
704
830
|
const response = {
|
|
705
831
|
...result,
|
|
706
|
-
warnings: [
|
|
832
|
+
warnings: [
|
|
833
|
+
...gateWarn ? [gateWarn] : [],
|
|
834
|
+
...syncReport.warnings
|
|
835
|
+
]
|
|
707
836
|
};
|
|
708
837
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
709
838
|
const serialized = JSON.stringify(response);
|
|
@@ -952,7 +1081,7 @@ async function listPending(projectRoot, filters) {
|
|
|
952
1081
|
}
|
|
953
1082
|
if (filters?.tags !== void 0 && filters.tags.length > 0) {
|
|
954
1083
|
const itemTags = fm.tags ?? [];
|
|
955
|
-
const hasAll = filters.tags.every((
|
|
1084
|
+
const hasAll = filters.tags.every((t) => itemTags.includes(t));
|
|
956
1085
|
if (!hasAll) continue;
|
|
957
1086
|
}
|
|
958
1087
|
if (filters?.created_after !== void 0) {
|
|
@@ -1125,8 +1254,8 @@ function resolveModifyTarget(projectRoot, pendingPath) {
|
|
|
1125
1254
|
}
|
|
1126
1255
|
return null;
|
|
1127
1256
|
}
|
|
1128
|
-
function inferTypeFromPath(
|
|
1129
|
-
const match = /knowledge\/(?:pending\/)?([^/]+)\/[^/]+\.md$/u.exec(
|
|
1257
|
+
function inferTypeFromPath(path) {
|
|
1258
|
+
const match = /knowledge\/(?:pending\/)?([^/]+)\/[^/]+\.md$/u.exec(path);
|
|
1130
1259
|
if (match === null) return null;
|
|
1131
1260
|
const seg = match[1];
|
|
1132
1261
|
if (seg !== void 0 && PLURAL_TYPES.includes(seg)) {
|
|
@@ -1134,8 +1263,8 @@ function inferTypeFromPath(path2) {
|
|
|
1134
1263
|
}
|
|
1135
1264
|
return null;
|
|
1136
1265
|
}
|
|
1137
|
-
function extractSlug(
|
|
1138
|
-
const file = basename(
|
|
1266
|
+
function extractSlug(path) {
|
|
1267
|
+
const file = basename(path).replace(/\.md$/u, "");
|
|
1139
1268
|
return file.replace(/^K[PT]-(MOD|DEC|GLD|PIT|PRO)-\d+--/u, "");
|
|
1140
1269
|
}
|
|
1141
1270
|
async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
@@ -1252,7 +1381,7 @@ async function searchEntries(projectRoot, query, filters) {
|
|
|
1252
1381
|
}
|
|
1253
1382
|
if (filters?.tags !== void 0 && filters.tags.length > 0) {
|
|
1254
1383
|
const itemTags = fm.tags ?? [];
|
|
1255
|
-
const hasAll = filters.tags.every((
|
|
1384
|
+
const hasAll = filters.tags.every((t) => itemTags.includes(t));
|
|
1256
1385
|
if (!hasAll) continue;
|
|
1257
1386
|
}
|
|
1258
1387
|
if (filters?.created_after !== void 0) {
|
|
@@ -1491,10 +1620,15 @@ function registerReview(server, tracker) {
|
|
|
1491
1620
|
const requestId = randomUUID3();
|
|
1492
1621
|
tracker?.enter(requestId);
|
|
1493
1622
|
try {
|
|
1623
|
+
const gateResult = await awaitFirstReconcileGate();
|
|
1624
|
+
const gateWarn = gateWarning(gateResult);
|
|
1494
1625
|
const narrowed = FabReviewInputSchema.parse(input);
|
|
1495
1626
|
const projectRoot = resolveProjectRoot();
|
|
1496
1627
|
const result = await reviewKnowledge(projectRoot, narrowed);
|
|
1497
|
-
const response = result;
|
|
1628
|
+
const response = { ...result };
|
|
1629
|
+
if (gateWarn) {
|
|
1630
|
+
response.warnings = [gateWarn];
|
|
1631
|
+
}
|
|
1498
1632
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
1499
1633
|
const serialized = JSON.stringify(response);
|
|
1500
1634
|
enforcePayloadLimit3(serialized, payloadLimits);
|
|
@@ -1522,62 +1656,17 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
|
|
|
1522
1656
|
import { readFile as readFile4 } from "fs/promises";
|
|
1523
1657
|
import { homedir as homedir3 } from "os";
|
|
1524
1658
|
import { join as join4 } from "path";
|
|
1525
|
-
var KNOWLEDGE_SECTION_NAMES = [
|
|
1526
|
-
"MISSION_STATEMENT",
|
|
1527
|
-
"MANDATORY_INJECTION",
|
|
1528
|
-
"BUSINESS_LOGIC_CHUNKS",
|
|
1529
|
-
"CONTEXT_INFO"
|
|
1530
|
-
];
|
|
1531
1659
|
var PRIORITY_ORDER = {
|
|
1532
1660
|
high: 0,
|
|
1533
1661
|
medium: 1,
|
|
1534
1662
|
low: 2
|
|
1535
1663
|
};
|
|
1536
|
-
function
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
let activeSectionDepth = 0;
|
|
1541
|
-
let buffer = [];
|
|
1542
|
-
const flush = () => {
|
|
1543
|
-
if (activeSection === void 0) {
|
|
1544
|
-
return;
|
|
1545
|
-
}
|
|
1546
|
-
const text = buffer.join("\n").trim();
|
|
1547
|
-
if (text.length === 0) {
|
|
1548
|
-
buffer = [];
|
|
1549
|
-
return;
|
|
1550
|
-
}
|
|
1551
|
-
sections.set(activeSection, [...sections.get(activeSection) ?? [], text]);
|
|
1552
|
-
buffer = [];
|
|
1553
|
-
};
|
|
1554
|
-
for (const line of lines) {
|
|
1555
|
-
const heading = /^(#{2,6})\s+\[([A-Z_]+)\]\s*$/u.exec(line.trim());
|
|
1556
|
-
if (heading !== null) {
|
|
1557
|
-
flush();
|
|
1558
|
-
activeSection = isKnowledgeSectionName(heading[2]) ? heading[2] : void 0;
|
|
1559
|
-
activeSectionDepth = activeSection === void 0 ? 0 : heading[1].length;
|
|
1560
|
-
continue;
|
|
1561
|
-
}
|
|
1562
|
-
const ordinaryHeading = /^(#{1,6})\s+/u.exec(line.trim());
|
|
1563
|
-
if (ordinaryHeading !== null) {
|
|
1564
|
-
if (activeSection !== void 0 && ordinaryHeading[1].length > activeSectionDepth) {
|
|
1565
|
-
buffer.push(line);
|
|
1566
|
-
continue;
|
|
1567
|
-
}
|
|
1568
|
-
flush();
|
|
1569
|
-
activeSection = void 0;
|
|
1570
|
-
activeSectionDepth = 0;
|
|
1571
|
-
continue;
|
|
1572
|
-
}
|
|
1573
|
-
if (activeSection !== void 0) {
|
|
1574
|
-
buffer.push(line);
|
|
1575
|
-
}
|
|
1664
|
+
function extractBody(content) {
|
|
1665
|
+
const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/u.exec(content);
|
|
1666
|
+
if (match === null) {
|
|
1667
|
+
return content.replace(/^\uFEFF/u, "");
|
|
1576
1668
|
}
|
|
1577
|
-
|
|
1578
|
-
return new Map(
|
|
1579
|
-
Array.from(sections.entries()).map(([section, values]) => [section, values.join("\n\n")])
|
|
1580
|
-
);
|
|
1669
|
+
return content.slice(match[0].length);
|
|
1581
1670
|
}
|
|
1582
1671
|
async function getKnowledgeSections(projectRoot, input) {
|
|
1583
1672
|
const token = readSelectionToken(input.selection_token);
|
|
@@ -1592,21 +1681,7 @@ async function getKnowledgeSections(projectRoot, input) {
|
|
|
1592
1681
|
const rules = [];
|
|
1593
1682
|
for (const rule of selectedRules) {
|
|
1594
1683
|
const content = await readFile4(resolveRuleSourcePath(projectRoot, rule.path), "utf8");
|
|
1595
|
-
const
|
|
1596
|
-
const sections = {};
|
|
1597
|
-
for (const section of input.sections) {
|
|
1598
|
-
const sectionContent = parsedSections.get(section);
|
|
1599
|
-
sections[section] = sectionContent ?? "";
|
|
1600
|
-
if (sectionContent === void 0) {
|
|
1601
|
-
diagnostics.push({
|
|
1602
|
-
code: "missing_section",
|
|
1603
|
-
severity: "warn",
|
|
1604
|
-
stable_id: rule.stable_id,
|
|
1605
|
-
section,
|
|
1606
|
-
message: `Rule ${rule.stable_id} does not define section ${section}.`
|
|
1607
|
-
});
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1684
|
+
const body = extractBody(content);
|
|
1610
1685
|
const description = rule.node.description;
|
|
1611
1686
|
if (description !== void 0 && description.knowledge_type === void 0 && description.knowledge_layer === void 0) {
|
|
1612
1687
|
diagnostics.push({
|
|
@@ -1620,7 +1695,7 @@ async function getKnowledgeSections(projectRoot, input) {
|
|
|
1620
1695
|
stable_id: rule.stable_id,
|
|
1621
1696
|
level: rule.level,
|
|
1622
1697
|
path: rule.path,
|
|
1623
|
-
|
|
1698
|
+
body
|
|
1624
1699
|
});
|
|
1625
1700
|
}
|
|
1626
1701
|
const result = {
|
|
@@ -1652,7 +1727,7 @@ async function getKnowledgeSections(projectRoot, input) {
|
|
|
1652
1727
|
event_type: "knowledge_sections_fetched",
|
|
1653
1728
|
selection_token: input.selection_token,
|
|
1654
1729
|
target_paths: token.target_paths,
|
|
1655
|
-
requested_sections:
|
|
1730
|
+
requested_sections: [],
|
|
1656
1731
|
final_stable_ids: result.selected_stable_ids,
|
|
1657
1732
|
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
1658
1733
|
diagnostics,
|
|
@@ -1737,9 +1812,6 @@ function outputLevelOrder(level) {
|
|
|
1737
1812
|
return 2;
|
|
1738
1813
|
}
|
|
1739
1814
|
}
|
|
1740
|
-
function isKnowledgeSectionName(value) {
|
|
1741
|
-
return KNOWLEDGE_SECTION_NAMES.includes(value);
|
|
1742
|
-
}
|
|
1743
1815
|
function resolveRuleSourcePath(projectRoot, contentRef) {
|
|
1744
1816
|
if (contentRef.startsWith("~/.fabric/knowledge/")) {
|
|
1745
1817
|
const home = process.env.FABRIC_HOME ?? homedir3();
|
|
@@ -1756,7 +1828,7 @@ function registerKnowledgeSections(server, tracker) {
|
|
|
1756
1828
|
server.registerTool(
|
|
1757
1829
|
"fab_get_knowledge_sections",
|
|
1758
1830
|
{
|
|
1759
|
-
description: "Fetch
|
|
1831
|
+
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.).",
|
|
1760
1832
|
inputSchema: knowledgeSectionsInputSchema,
|
|
1761
1833
|
outputSchema: knowledgeSectionsOutputSchema,
|
|
1762
1834
|
annotations: knowledgeSectionsAnnotations
|
|
@@ -1765,12 +1837,17 @@ function registerKnowledgeSections(server, tracker) {
|
|
|
1765
1837
|
const requestId = randomUUID4();
|
|
1766
1838
|
tracker?.enter(requestId);
|
|
1767
1839
|
try {
|
|
1840
|
+
const gateResult = await awaitFirstReconcileGate();
|
|
1841
|
+
const gateWarn = gateWarning(gateResult);
|
|
1768
1842
|
const projectRoot = resolveProjectRoot();
|
|
1769
1843
|
const syncReport = await ensureKnowledgeFresh(projectRoot);
|
|
1770
1844
|
const result = await getKnowledgeSections(projectRoot, input);
|
|
1771
1845
|
const response = {
|
|
1772
1846
|
...result,
|
|
1773
|
-
warnings: [
|
|
1847
|
+
warnings: [
|
|
1848
|
+
...gateWarn ? [gateWarn] : [],
|
|
1849
|
+
...syncReport.warnings
|
|
1850
|
+
]
|
|
1774
1851
|
};
|
|
1775
1852
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
1776
1853
|
const serialized = JSON.stringify(response);
|
|
@@ -1796,99 +1873,6 @@ function registerKnowledgeSections(server, tracker) {
|
|
|
1796
1873
|
);
|
|
1797
1874
|
}
|
|
1798
1875
|
|
|
1799
|
-
// src/services/serve-lock.ts
|
|
1800
|
-
import fs from "fs";
|
|
1801
|
-
import path from "path";
|
|
1802
|
-
import { createTranslator, detectNodeLocale } from "@fenglimg/fabric-shared";
|
|
1803
|
-
import { IOFabricError } from "@fenglimg/fabric-shared/errors";
|
|
1804
|
-
var LOCK_FILENAME = ".serve.lock";
|
|
1805
|
-
var t = createTranslator(detectNodeLocale());
|
|
1806
|
-
var ServeLockHeldError = class extends IOFabricError {
|
|
1807
|
-
code = "SERVE_LOCK_HELD";
|
|
1808
|
-
httpStatus = 423;
|
|
1809
|
-
};
|
|
1810
|
-
function lockPath(projectRoot) {
|
|
1811
|
-
return path.join(projectRoot, ".fabric", LOCK_FILENAME);
|
|
1812
|
-
}
|
|
1813
|
-
function isAlive(pid) {
|
|
1814
|
-
try {
|
|
1815
|
-
process.kill(pid, 0);
|
|
1816
|
-
return true;
|
|
1817
|
-
} catch (e) {
|
|
1818
|
-
const err = e;
|
|
1819
|
-
if (err.code === "ESRCH") return false;
|
|
1820
|
-
if (err.code === "EPERM") return true;
|
|
1821
|
-
throw e;
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
function acquireLock(projectRoot, opts) {
|
|
1825
|
-
const p = lockPath(projectRoot);
|
|
1826
|
-
if (fs.existsSync(p)) {
|
|
1827
|
-
let state = null;
|
|
1828
|
-
try {
|
|
1829
|
-
state = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1830
|
-
} catch {
|
|
1831
|
-
}
|
|
1832
|
-
if (state && state.pid && state.pid !== process.pid && isAlive(state.pid) && !opts?.force) {
|
|
1833
|
-
throw new ServeLockHeldError(
|
|
1834
|
-
`serve lock held by live PID ${state.pid}`,
|
|
1835
|
-
{
|
|
1836
|
-
actionHint: t("cli.serve.lock-held.action-hint", { pid: String(state.pid) }),
|
|
1837
|
-
details: state
|
|
1838
|
-
}
|
|
1839
|
-
);
|
|
1840
|
-
}
|
|
1841
|
-
if (state && state.pid && !isAlive(state.pid)) {
|
|
1842
|
-
process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 overwriting
|
|
1843
|
-
`);
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
1847
|
-
fs.writeFileSync(
|
|
1848
|
-
p,
|
|
1849
|
-
JSON.stringify({ pid: process.pid, acquiredAt: Date.now(), host: process.env.HOSTNAME })
|
|
1850
|
-
);
|
|
1851
|
-
}
|
|
1852
|
-
function releaseLock(projectRoot) {
|
|
1853
|
-
const p = lockPath(projectRoot);
|
|
1854
|
-
try {
|
|
1855
|
-
if (fs.existsSync(p)) {
|
|
1856
|
-
const state = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1857
|
-
if (state.pid === process.pid) {
|
|
1858
|
-
fs.unlinkSync(p);
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
1861
|
-
} catch {
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
function readLockState(projectRoot) {
|
|
1865
|
-
const p = lockPath(projectRoot);
|
|
1866
|
-
if (!fs.existsSync(p)) return null;
|
|
1867
|
-
try {
|
|
1868
|
-
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
1869
|
-
} catch {
|
|
1870
|
-
return null;
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
function checkLockOrThrow(projectRoot, opts) {
|
|
1874
|
-
const state = readLockState(projectRoot);
|
|
1875
|
-
if (state === null) return;
|
|
1876
|
-
if (state.pid === process.pid) return;
|
|
1877
|
-
if (!isAlive(state.pid)) {
|
|
1878
|
-
process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 ignoring
|
|
1879
|
-
`);
|
|
1880
|
-
return;
|
|
1881
|
-
}
|
|
1882
|
-
if (opts?.force) return;
|
|
1883
|
-
throw new ServeLockHeldError(
|
|
1884
|
-
`serve lock held by live PID ${state.pid}`,
|
|
1885
|
-
{
|
|
1886
|
-
actionHint: t("cli.serve.lock-held.action-hint", { pid: String(state.pid) }),
|
|
1887
|
-
details: state
|
|
1888
|
-
}
|
|
1889
|
-
);
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
1876
|
// src/index.ts
|
|
1893
1877
|
function writeStderr(message) {
|
|
1894
1878
|
process.stderr.write(`${message}
|
|
@@ -1910,7 +1894,7 @@ function formatPreexistingRootMessage(projectRoot) {
|
|
|
1910
1894
|
function createFabricServer(tracker) {
|
|
1911
1895
|
const server = new McpServer({
|
|
1912
1896
|
name: "fabric-knowledge-server",
|
|
1913
|
-
version: "2.0.0-rc.
|
|
1897
|
+
version: "2.0.0-rc.25"
|
|
1914
1898
|
});
|
|
1915
1899
|
registerPlanContext(server, tracker);
|
|
1916
1900
|
registerKnowledgeSections(server, tracker);
|
|
@@ -1925,10 +1909,10 @@ function createFabricServer(tracker) {
|
|
|
1925
1909
|
},
|
|
1926
1910
|
async (_uri) => {
|
|
1927
1911
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
1928
|
-
const
|
|
1912
|
+
const path = join5(projectRoot, ".fabric", "bootstrap", "README.md");
|
|
1929
1913
|
let text = "";
|
|
1930
|
-
if (existsSync4(
|
|
1931
|
-
text = await readFile5(
|
|
1914
|
+
if (existsSync4(path)) {
|
|
1915
|
+
text = await readFile5(path, "utf8");
|
|
1932
1916
|
}
|
|
1933
1917
|
return {
|
|
1934
1918
|
contents: [
|
|
@@ -1946,13 +1930,6 @@ function createFabricServer(tracker) {
|
|
|
1946
1930
|
async function startStdioServer() {
|
|
1947
1931
|
const tracker = createInFlightTracker();
|
|
1948
1932
|
const projectRoot = resolveProjectRoot();
|
|
1949
|
-
const syncStart = Date.now();
|
|
1950
|
-
const reconcileResult = await reconcileKnowledge(projectRoot, { trigger: "startup" });
|
|
1951
|
-
const syncDurationMs = Date.now() - syncStart;
|
|
1952
|
-
process.stderr.write(
|
|
1953
|
-
`[startup] rule sync: status=${reconcileResult.status}, events=${reconcileResult.events.length}, ${syncDurationMs}ms
|
|
1954
|
-
`
|
|
1955
|
-
);
|
|
1956
1933
|
const rootMsg = formatPreexistingRootMessage(projectRoot);
|
|
1957
1934
|
if (rootMsg !== null) {
|
|
1958
1935
|
process.stderr.write(`${rootMsg}
|
|
@@ -1961,6 +1938,21 @@ async function startStdioServer() {
|
|
|
1961
1938
|
const server = createFabricServer(tracker);
|
|
1962
1939
|
const transport = new StdioServerTransport();
|
|
1963
1940
|
await server.connect(transport);
|
|
1941
|
+
const syncStart = Date.now();
|
|
1942
|
+
const backgroundReconcile = (async () => {
|
|
1943
|
+
const reconcileResult = await reconcileKnowledge(projectRoot, { trigger: "startup" });
|
|
1944
|
+
const syncDurationMs = Date.now() - syncStart;
|
|
1945
|
+
process.stderr.write(
|
|
1946
|
+
`[startup] rule sync: status=${reconcileResult.status}, events=${reconcileResult.events.length}, ${syncDurationMs}ms
|
|
1947
|
+
`
|
|
1948
|
+
);
|
|
1949
|
+
})().catch((error) => {
|
|
1950
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
1951
|
+
process.stderr.write(`[startup] rule sync FAILED: ${message}
|
|
1952
|
+
`);
|
|
1953
|
+
throw error;
|
|
1954
|
+
});
|
|
1955
|
+
setFirstReconcile(backgroundReconcile);
|
|
1964
1956
|
const closeServer = async () => {
|
|
1965
1957
|
await server.close();
|
|
1966
1958
|
};
|
|
@@ -2010,7 +2002,7 @@ function createShutdownHandler(deps) {
|
|
|
2010
2002
|
};
|
|
2011
2003
|
}
|
|
2012
2004
|
async function startHttpServer(options) {
|
|
2013
|
-
const { createFabricHttpApp } = await import("./http-
|
|
2005
|
+
const { createFabricHttpApp } = await import("./http-QBGLHCHA.js");
|
|
2014
2006
|
const { port, projectRoot, host = "127.0.0.1", authToken } = options;
|
|
2015
2007
|
const app = createFabricHttpApp({ projectRoot, host, authToken });
|
|
2016
2008
|
return await new Promise((resolveServer, rejectServer) => {
|
|
@@ -2053,6 +2045,7 @@ export {
|
|
|
2053
2045
|
createShutdownHandler,
|
|
2054
2046
|
deriveKnowledgeMetaLayer,
|
|
2055
2047
|
deriveKnowledgeMetaTopologyType,
|
|
2048
|
+
enrichDescriptions,
|
|
2056
2049
|
ensureKnowledgeFresh,
|
|
2057
2050
|
extractKnowledge,
|
|
2058
2051
|
flushAndSyncEventLedger,
|
|
@@ -2061,6 +2054,7 @@ export {
|
|
|
2061
2054
|
getLedgerPath,
|
|
2062
2055
|
getLegacyLedgerPath,
|
|
2063
2056
|
isSameKnowledgeTestIndex,
|
|
2057
|
+
loadKbIdTypeMap,
|
|
2064
2058
|
planContext,
|
|
2065
2059
|
readLockState,
|
|
2066
2060
|
readSelectionToken,
|
|
@@ -2068,6 +2062,7 @@ export {
|
|
|
2068
2062
|
releaseLock,
|
|
2069
2063
|
reviewKnowledge,
|
|
2070
2064
|
runDoctorApplyLint,
|
|
2065
|
+
runDoctorArchiveHistory,
|
|
2071
2066
|
runDoctorCiteCoverage,
|
|
2072
2067
|
runDoctorFix,
|
|
2073
2068
|
runDoctorReport,
|