@fenglimg/fabric-server 2.0.0-rc.22 → 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-7N3FW5LX.js → chunk-IRB77C6E.js} +476 -57
- package/dist/{http-FF5NZCJK.js → http-ZBV6YUHD.js} +1 -1
- package/dist/index.d.ts +35 -5
- package/dist/index.js +208 -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,
|
|
@@ -20,7 +24,9 @@ import {
|
|
|
20
24
|
loadActiveMeta,
|
|
21
25
|
loadActiveMetaOrStale,
|
|
22
26
|
normalizeKnowledgePath,
|
|
27
|
+
readLockState,
|
|
23
28
|
reconcileKnowledge,
|
|
29
|
+
releaseLock,
|
|
24
30
|
resolveProjectRoot,
|
|
25
31
|
runDoctorApplyLint,
|
|
26
32
|
runDoctorCiteCoverage,
|
|
@@ -29,7 +35,7 @@ import {
|
|
|
29
35
|
sha256,
|
|
30
36
|
stableStringify,
|
|
31
37
|
writeKnowledgeMeta
|
|
32
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-IRB77C6E.js";
|
|
33
39
|
|
|
34
40
|
// src/index.ts
|
|
35
41
|
import { existsSync as existsSync4 } from "fs";
|
|
@@ -42,6 +48,66 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
42
48
|
// src/constants.ts
|
|
43
49
|
var AGENTS_MD_RESOURCE_URI = "fabric://bootstrap-readme";
|
|
44
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
|
+
|
|
45
111
|
// src/services/in-flight-tracker.ts
|
|
46
112
|
function createInFlightTracker() {
|
|
47
113
|
const active = /* @__PURE__ */ new Map();
|
|
@@ -133,7 +199,7 @@ async function extractKnowledge(projectRoot, input) {
|
|
|
133
199
|
}
|
|
134
200
|
}
|
|
135
201
|
const sanitizedSlug = sanitizeSlug(input.slug);
|
|
136
|
-
const sourceSessions =
|
|
202
|
+
const sourceSessions = input.source_sessions ?? [];
|
|
137
203
|
const primarySession = sourceSessions[0] ?? "";
|
|
138
204
|
const idempotencyKey = sha256(
|
|
139
205
|
JSON.stringify({
|
|
@@ -217,7 +283,20 @@ async function extractKnowledge(projectRoot, input) {
|
|
|
217
283
|
proposedReason: input.proposed_reason,
|
|
218
284
|
sessionContext: input.session_context,
|
|
219
285
|
relevanceScope,
|
|
220
|
-
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
|
|
221
300
|
});
|
|
222
301
|
await atomicWriteText(absolutePath, fresh);
|
|
223
302
|
await emitEventBestEffort(projectRoot, {
|
|
@@ -260,6 +339,24 @@ function renderFreshEntry(args) {
|
|
|
260
339
|
const pathsBody = args.relevancePaths.map((p) => quoteRelevancePath(p)).join(", ");
|
|
261
340
|
frontmatterLines.push(`relevance_paths: [${pathsBody}]`);
|
|
262
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
|
+
}
|
|
263
360
|
frontmatterLines.push(
|
|
264
361
|
`x-fabric-idempotency-key: ${args.idempotencyKey}`,
|
|
265
362
|
"---"
|
|
@@ -328,9 +425,9 @@ ${renderEvidenceBlock(newSummary, newRecentPaths)}
|
|
|
328
425
|
const pathSection = /Recent paths:\s*\n([\s\S]*?)(?:\n\s*Notes:|$)/u.exec(block);
|
|
329
426
|
if (pathSection !== null) {
|
|
330
427
|
for (const rawLine of (pathSection[1] ?? "").split(/\r?\n/u)) {
|
|
331
|
-
const
|
|
332
|
-
if (
|
|
333
|
-
existingPaths.push(
|
|
428
|
+
const t = rawLine.trim();
|
|
429
|
+
if (t.startsWith("- ")) {
|
|
430
|
+
existingPaths.push(t.slice(2).trim());
|
|
334
431
|
}
|
|
335
432
|
}
|
|
336
433
|
}
|
|
@@ -339,16 +436,16 @@ ${renderEvidenceBlock(newSummary, newRecentPaths)}
|
|
|
339
436
|
const bulletLines = [];
|
|
340
437
|
let prose = [];
|
|
341
438
|
for (const rawLine of noteBody.split(/\r?\n/u)) {
|
|
342
|
-
const
|
|
343
|
-
if (
|
|
344
|
-
if (
|
|
439
|
+
const t = rawLine.trim();
|
|
440
|
+
if (t.length === 0) continue;
|
|
441
|
+
if (t.startsWith("- ")) {
|
|
345
442
|
if (prose.length > 0) {
|
|
346
443
|
existingNotes.push(prose.join(" ").trim());
|
|
347
444
|
prose = [];
|
|
348
445
|
}
|
|
349
|
-
bulletLines.push(
|
|
446
|
+
bulletLines.push(t.slice(2).trim());
|
|
350
447
|
} else {
|
|
351
|
-
prose.push(
|
|
448
|
+
prose.push(t);
|
|
352
449
|
}
|
|
353
450
|
}
|
|
354
451
|
if (prose.length > 0) existingNotes.push(prose.join(" ").trim());
|
|
@@ -423,7 +520,7 @@ function registerExtractKnowledge(server, tracker) {
|
|
|
423
520
|
server.registerTool(
|
|
424
521
|
"fab_extract_knowledge",
|
|
425
522
|
{
|
|
426
|
-
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.",
|
|
427
524
|
inputSchema: FabExtractKnowledgeInputShape,
|
|
428
525
|
outputSchema: FabExtractKnowledgeOutputSchema.shape,
|
|
429
526
|
annotations: fabExtractKnowledgeAnnotations
|
|
@@ -432,9 +529,14 @@ function registerExtractKnowledge(server, tracker) {
|
|
|
432
529
|
const requestId = randomUUID();
|
|
433
530
|
tracker?.enter(requestId);
|
|
434
531
|
try {
|
|
532
|
+
const gateResult = await awaitFirstReconcileGate();
|
|
533
|
+
const gateWarn = gateWarning(gateResult);
|
|
435
534
|
const projectRoot = resolveProjectRoot();
|
|
436
535
|
const result = await extractKnowledge(projectRoot, input);
|
|
437
536
|
const response = { ...result };
|
|
537
|
+
if (gateWarn) {
|
|
538
|
+
response.warnings = [gateWarn];
|
|
539
|
+
}
|
|
438
540
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
439
541
|
const serialized = JSON.stringify(response);
|
|
440
542
|
enforcePayloadLimit(serialized, payloadLimits);
|
|
@@ -464,17 +566,30 @@ import { deriveAgentsMetaLayer } from "@fenglimg/fabric-shared";
|
|
|
464
566
|
var SELECTION_TOKEN_TTL_MS = 5 * 60 * 1e3;
|
|
465
567
|
var selectionTokenCache = /* @__PURE__ */ new Map();
|
|
466
568
|
async function planContext(projectRoot, input) {
|
|
467
|
-
|
|
468
|
-
|
|
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
|
+
}
|
|
469
584
|
const stale = metaResult.degraded === true || input.client_hash !== void 0 && input.client_hash !== meta.revision;
|
|
470
585
|
const uniquePaths = dedupePaths(input.paths);
|
|
471
586
|
const allDescriptions = buildDescriptionIndex(meta);
|
|
472
587
|
const relevanceTargetPaths = input.target_paths ?? uniquePaths;
|
|
473
|
-
const entries = uniquePaths.map((
|
|
474
|
-
const profile = buildRequirementProfile(
|
|
475
|
-
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));
|
|
476
591
|
return {
|
|
477
|
-
path
|
|
592
|
+
path,
|
|
478
593
|
requirement_profile: profile,
|
|
479
594
|
description_index: descriptionIndex
|
|
480
595
|
};
|
|
@@ -491,12 +606,14 @@ async function planContext(projectRoot, input) {
|
|
|
491
606
|
description_index: sharedDescriptionIndex,
|
|
492
607
|
preflight_diagnostics: buildPreflightDiagnostics(meta)
|
|
493
608
|
},
|
|
494
|
-
// v2.0.0-rc.22 Scope D T-D2: surface auto-heal pair
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
|
|
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 ? {
|
|
498
615
|
auto_healed: true,
|
|
499
|
-
previous_revision_hash:
|
|
616
|
+
previous_revision_hash: firstSeenPreviousRevision
|
|
500
617
|
} : {}
|
|
501
618
|
};
|
|
502
619
|
try {
|
|
@@ -519,15 +636,15 @@ async function planContext(projectRoot, input) {
|
|
|
519
636
|
return result;
|
|
520
637
|
}
|
|
521
638
|
function readSelectionToken(token, now = Date.now()) {
|
|
522
|
-
const
|
|
523
|
-
if (
|
|
639
|
+
const state2 = selectionTokenCache.get(token);
|
|
640
|
+
if (state2 === void 0) {
|
|
524
641
|
return void 0;
|
|
525
642
|
}
|
|
526
|
-
if (
|
|
643
|
+
if (state2.expires_at <= now) {
|
|
527
644
|
selectionTokenCache.delete(token);
|
|
528
645
|
return void 0;
|
|
529
646
|
}
|
|
530
|
-
return
|
|
647
|
+
return state2;
|
|
531
648
|
}
|
|
532
649
|
function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSelectableStableIds, now = Date.now()) {
|
|
533
650
|
const token = `selection:${revisionHash}:${now.toString(36)}:${Math.random().toString(36).slice(2)}`;
|
|
@@ -544,8 +661,8 @@ function createSelectionToken(revisionHash, targetPaths, requiredStableIds, aiSe
|
|
|
544
661
|
}
|
|
545
662
|
function dedupePaths(paths) {
|
|
546
663
|
const seenPaths = /* @__PURE__ */ new Set();
|
|
547
|
-
return paths.flatMap((
|
|
548
|
-
const normalizedPath = normalizeKnowledgePath(
|
|
664
|
+
return paths.flatMap((path) => {
|
|
665
|
+
const normalizedPath = normalizeKnowledgePath(path);
|
|
549
666
|
if (seenPaths.has(normalizedPath)) {
|
|
550
667
|
return [];
|
|
551
668
|
}
|
|
@@ -553,8 +670,8 @@ function dedupePaths(paths) {
|
|
|
553
670
|
return [normalizedPath];
|
|
554
671
|
});
|
|
555
672
|
}
|
|
556
|
-
function buildRequirementProfile(
|
|
557
|
-
const normalizedPath = normalizeKnowledgePath(
|
|
673
|
+
function buildRequirementProfile(path, input) {
|
|
674
|
+
const normalizedPath = normalizeKnowledgePath(path);
|
|
558
675
|
const extensionMatch = /(\.[^./\\]+)$/u.exec(normalizedPath);
|
|
559
676
|
const knownTech = dedupeStableIds([
|
|
560
677
|
...input.known_tech ?? [],
|
|
@@ -566,7 +683,7 @@ function buildRequirementProfile(path2, input) {
|
|
|
566
683
|
extension: extensionMatch?.[1] ?? "",
|
|
567
684
|
known_tech: knownTech,
|
|
568
685
|
user_intent: input.intent ?? "",
|
|
569
|
-
detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[
|
|
686
|
+
detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path] ?? []
|
|
570
687
|
};
|
|
571
688
|
}
|
|
572
689
|
function buildDescriptionIndex(meta) {
|
|
@@ -647,6 +764,11 @@ function descriptionFromLegacyActivation(summary) {
|
|
|
647
764
|
function shouldIncludeIndexItemForPath(_item, _meta, _path) {
|
|
648
765
|
return true;
|
|
649
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
|
+
}
|
|
650
772
|
function buildPreflightDiagnostics(meta) {
|
|
651
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();
|
|
652
774
|
if (missingDescriptionStableIds.length === 0) {
|
|
@@ -690,6 +812,8 @@ function registerPlanContext(server, tracker) {
|
|
|
690
812
|
const requestId = randomUUID2();
|
|
691
813
|
tracker?.enter(requestId);
|
|
692
814
|
try {
|
|
815
|
+
const gateResult = await awaitFirstReconcileGate();
|
|
816
|
+
const gateWarn = gateWarning(gateResult);
|
|
693
817
|
const projectRoot = resolveProjectRoot();
|
|
694
818
|
const syncReport = await ensureKnowledgeFresh(projectRoot);
|
|
695
819
|
const result = await planContext(projectRoot, {
|
|
@@ -703,7 +827,10 @@ function registerPlanContext(server, tracker) {
|
|
|
703
827
|
});
|
|
704
828
|
const response = {
|
|
705
829
|
...result,
|
|
706
|
-
warnings: [
|
|
830
|
+
warnings: [
|
|
831
|
+
...gateWarn ? [gateWarn] : [],
|
|
832
|
+
...syncReport.warnings
|
|
833
|
+
]
|
|
707
834
|
};
|
|
708
835
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
709
836
|
const serialized = JSON.stringify(response);
|
|
@@ -952,7 +1079,7 @@ async function listPending(projectRoot, filters) {
|
|
|
952
1079
|
}
|
|
953
1080
|
if (filters?.tags !== void 0 && filters.tags.length > 0) {
|
|
954
1081
|
const itemTags = fm.tags ?? [];
|
|
955
|
-
const hasAll = filters.tags.every((
|
|
1082
|
+
const hasAll = filters.tags.every((t) => itemTags.includes(t));
|
|
956
1083
|
if (!hasAll) continue;
|
|
957
1084
|
}
|
|
958
1085
|
if (filters?.created_after !== void 0) {
|
|
@@ -1125,8 +1252,8 @@ function resolveModifyTarget(projectRoot, pendingPath) {
|
|
|
1125
1252
|
}
|
|
1126
1253
|
return null;
|
|
1127
1254
|
}
|
|
1128
|
-
function inferTypeFromPath(
|
|
1129
|
-
const match = /knowledge\/(?:pending\/)?([^/]+)\/[^/]+\.md$/u.exec(
|
|
1255
|
+
function inferTypeFromPath(path) {
|
|
1256
|
+
const match = /knowledge\/(?:pending\/)?([^/]+)\/[^/]+\.md$/u.exec(path);
|
|
1130
1257
|
if (match === null) return null;
|
|
1131
1258
|
const seg = match[1];
|
|
1132
1259
|
if (seg !== void 0 && PLURAL_TYPES.includes(seg)) {
|
|
@@ -1134,8 +1261,8 @@ function inferTypeFromPath(path2) {
|
|
|
1134
1261
|
}
|
|
1135
1262
|
return null;
|
|
1136
1263
|
}
|
|
1137
|
-
function extractSlug(
|
|
1138
|
-
const file = basename(
|
|
1264
|
+
function extractSlug(path) {
|
|
1265
|
+
const file = basename(path).replace(/\.md$/u, "");
|
|
1139
1266
|
return file.replace(/^K[PT]-(MOD|DEC|GLD|PIT|PRO)-\d+--/u, "");
|
|
1140
1267
|
}
|
|
1141
1268
|
async function modifyLayerFlip(projectRoot, target, content, fm, changes) {
|
|
@@ -1252,7 +1379,7 @@ async function searchEntries(projectRoot, query, filters) {
|
|
|
1252
1379
|
}
|
|
1253
1380
|
if (filters?.tags !== void 0 && filters.tags.length > 0) {
|
|
1254
1381
|
const itemTags = fm.tags ?? [];
|
|
1255
|
-
const hasAll = filters.tags.every((
|
|
1382
|
+
const hasAll = filters.tags.every((t) => itemTags.includes(t));
|
|
1256
1383
|
if (!hasAll) continue;
|
|
1257
1384
|
}
|
|
1258
1385
|
if (filters?.created_after !== void 0) {
|
|
@@ -1491,10 +1618,15 @@ function registerReview(server, tracker) {
|
|
|
1491
1618
|
const requestId = randomUUID3();
|
|
1492
1619
|
tracker?.enter(requestId);
|
|
1493
1620
|
try {
|
|
1621
|
+
const gateResult = await awaitFirstReconcileGate();
|
|
1622
|
+
const gateWarn = gateWarning(gateResult);
|
|
1494
1623
|
const narrowed = FabReviewInputSchema.parse(input);
|
|
1495
1624
|
const projectRoot = resolveProjectRoot();
|
|
1496
1625
|
const result = await reviewKnowledge(projectRoot, narrowed);
|
|
1497
|
-
const response = result;
|
|
1626
|
+
const response = { ...result };
|
|
1627
|
+
if (gateWarn) {
|
|
1628
|
+
response.warnings = [gateWarn];
|
|
1629
|
+
}
|
|
1498
1630
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
1499
1631
|
const serialized = JSON.stringify(response);
|
|
1500
1632
|
enforcePayloadLimit3(serialized, payloadLimits);
|
|
@@ -1522,62 +1654,17 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
|
|
|
1522
1654
|
import { readFile as readFile4 } from "fs/promises";
|
|
1523
1655
|
import { homedir as homedir3 } from "os";
|
|
1524
1656
|
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
1657
|
var PRIORITY_ORDER = {
|
|
1532
1658
|
high: 0,
|
|
1533
1659
|
medium: 1,
|
|
1534
1660
|
low: 2
|
|
1535
1661
|
};
|
|
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
|
-
}
|
|
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, "");
|
|
1576
1666
|
}
|
|
1577
|
-
|
|
1578
|
-
return new Map(
|
|
1579
|
-
Array.from(sections.entries()).map(([section, values]) => [section, values.join("\n\n")])
|
|
1580
|
-
);
|
|
1667
|
+
return content.slice(match[0].length);
|
|
1581
1668
|
}
|
|
1582
1669
|
async function getKnowledgeSections(projectRoot, input) {
|
|
1583
1670
|
const token = readSelectionToken(input.selection_token);
|
|
@@ -1592,21 +1679,7 @@ async function getKnowledgeSections(projectRoot, input) {
|
|
|
1592
1679
|
const rules = [];
|
|
1593
1680
|
for (const rule of selectedRules) {
|
|
1594
1681
|
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
|
-
}
|
|
1682
|
+
const body = extractBody(content);
|
|
1610
1683
|
const description = rule.node.description;
|
|
1611
1684
|
if (description !== void 0 && description.knowledge_type === void 0 && description.knowledge_layer === void 0) {
|
|
1612
1685
|
diagnostics.push({
|
|
@@ -1620,7 +1693,7 @@ async function getKnowledgeSections(projectRoot, input) {
|
|
|
1620
1693
|
stable_id: rule.stable_id,
|
|
1621
1694
|
level: rule.level,
|
|
1622
1695
|
path: rule.path,
|
|
1623
|
-
|
|
1696
|
+
body
|
|
1624
1697
|
});
|
|
1625
1698
|
}
|
|
1626
1699
|
const result = {
|
|
@@ -1652,7 +1725,7 @@ async function getKnowledgeSections(projectRoot, input) {
|
|
|
1652
1725
|
event_type: "knowledge_sections_fetched",
|
|
1653
1726
|
selection_token: input.selection_token,
|
|
1654
1727
|
target_paths: token.target_paths,
|
|
1655
|
-
requested_sections:
|
|
1728
|
+
requested_sections: [],
|
|
1656
1729
|
final_stable_ids: result.selected_stable_ids,
|
|
1657
1730
|
ai_selected_stable_ids: input.ai_selected_stable_ids,
|
|
1658
1731
|
diagnostics,
|
|
@@ -1737,9 +1810,6 @@ function outputLevelOrder(level) {
|
|
|
1737
1810
|
return 2;
|
|
1738
1811
|
}
|
|
1739
1812
|
}
|
|
1740
|
-
function isKnowledgeSectionName(value) {
|
|
1741
|
-
return KNOWLEDGE_SECTION_NAMES.includes(value);
|
|
1742
|
-
}
|
|
1743
1813
|
function resolveRuleSourcePath(projectRoot, contentRef) {
|
|
1744
1814
|
if (contentRef.startsWith("~/.fabric/knowledge/")) {
|
|
1745
1815
|
const home = process.env.FABRIC_HOME ?? homedir3();
|
|
@@ -1756,7 +1826,7 @@ function registerKnowledgeSections(server, tracker) {
|
|
|
1756
1826
|
server.registerTool(
|
|
1757
1827
|
"fab_get_knowledge_sections",
|
|
1758
1828
|
{
|
|
1759
|
-
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.).",
|
|
1760
1830
|
inputSchema: knowledgeSectionsInputSchema,
|
|
1761
1831
|
outputSchema: knowledgeSectionsOutputSchema,
|
|
1762
1832
|
annotations: knowledgeSectionsAnnotations
|
|
@@ -1765,12 +1835,17 @@ function registerKnowledgeSections(server, tracker) {
|
|
|
1765
1835
|
const requestId = randomUUID4();
|
|
1766
1836
|
tracker?.enter(requestId);
|
|
1767
1837
|
try {
|
|
1838
|
+
const gateResult = await awaitFirstReconcileGate();
|
|
1839
|
+
const gateWarn = gateWarning(gateResult);
|
|
1768
1840
|
const projectRoot = resolveProjectRoot();
|
|
1769
1841
|
const syncReport = await ensureKnowledgeFresh(projectRoot);
|
|
1770
1842
|
const result = await getKnowledgeSections(projectRoot, input);
|
|
1771
1843
|
const response = {
|
|
1772
1844
|
...result,
|
|
1773
|
-
warnings: [
|
|
1845
|
+
warnings: [
|
|
1846
|
+
...gateWarn ? [gateWarn] : [],
|
|
1847
|
+
...syncReport.warnings
|
|
1848
|
+
]
|
|
1774
1849
|
};
|
|
1775
1850
|
const payloadLimits = readPayloadLimits(projectRoot);
|
|
1776
1851
|
const serialized = JSON.stringify(response);
|
|
@@ -1796,99 +1871,6 @@ function registerKnowledgeSections(server, tracker) {
|
|
|
1796
1871
|
);
|
|
1797
1872
|
}
|
|
1798
1873
|
|
|
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
1874
|
// src/index.ts
|
|
1893
1875
|
function writeStderr(message) {
|
|
1894
1876
|
process.stderr.write(`${message}
|
|
@@ -1910,7 +1892,7 @@ function formatPreexistingRootMessage(projectRoot) {
|
|
|
1910
1892
|
function createFabricServer(tracker) {
|
|
1911
1893
|
const server = new McpServer({
|
|
1912
1894
|
name: "fabric-knowledge-server",
|
|
1913
|
-
version: "2.0.0-rc.
|
|
1895
|
+
version: "2.0.0-rc.23"
|
|
1914
1896
|
});
|
|
1915
1897
|
registerPlanContext(server, tracker);
|
|
1916
1898
|
registerKnowledgeSections(server, tracker);
|
|
@@ -1925,10 +1907,10 @@ function createFabricServer(tracker) {
|
|
|
1925
1907
|
},
|
|
1926
1908
|
async (_uri) => {
|
|
1927
1909
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
1928
|
-
const
|
|
1910
|
+
const path = join5(projectRoot, ".fabric", "bootstrap", "README.md");
|
|
1929
1911
|
let text = "";
|
|
1930
|
-
if (existsSync4(
|
|
1931
|
-
text = await readFile5(
|
|
1912
|
+
if (existsSync4(path)) {
|
|
1913
|
+
text = await readFile5(path, "utf8");
|
|
1932
1914
|
}
|
|
1933
1915
|
return {
|
|
1934
1916
|
contents: [
|
|
@@ -1946,13 +1928,6 @@ function createFabricServer(tracker) {
|
|
|
1946
1928
|
async function startStdioServer() {
|
|
1947
1929
|
const tracker = createInFlightTracker();
|
|
1948
1930
|
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
1931
|
const rootMsg = formatPreexistingRootMessage(projectRoot);
|
|
1957
1932
|
if (rootMsg !== null) {
|
|
1958
1933
|
process.stderr.write(`${rootMsg}
|
|
@@ -1961,6 +1936,21 @@ async function startStdioServer() {
|
|
|
1961
1936
|
const server = createFabricServer(tracker);
|
|
1962
1937
|
const transport = new StdioServerTransport();
|
|
1963
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);
|
|
1964
1954
|
const closeServer = async () => {
|
|
1965
1955
|
await server.close();
|
|
1966
1956
|
};
|
|
@@ -2010,7 +2000,7 @@ function createShutdownHandler(deps) {
|
|
|
2010
2000
|
};
|
|
2011
2001
|
}
|
|
2012
2002
|
async function startHttpServer(options) {
|
|
2013
|
-
const { createFabricHttpApp } = await import("./http-
|
|
2003
|
+
const { createFabricHttpApp } = await import("./http-ZBV6YUHD.js");
|
|
2014
2004
|
const { port, projectRoot, host = "127.0.0.1", authToken } = options;
|
|
2015
2005
|
const app = createFabricHttpApp({ projectRoot, host, authToken });
|
|
2016
2006
|
return await new Promise((resolveServer, rejectServer) => {
|
|
@@ -2053,6 +2043,7 @@ export {
|
|
|
2053
2043
|
createShutdownHandler,
|
|
2054
2044
|
deriveKnowledgeMetaLayer,
|
|
2055
2045
|
deriveKnowledgeMetaTopologyType,
|
|
2046
|
+
enrichDescriptions,
|
|
2056
2047
|
ensureKnowledgeFresh,
|
|
2057
2048
|
extractKnowledge,
|
|
2058
2049
|
flushAndSyncEventLedger,
|