@fenglimg/fabric-server 2.0.0-rc.10 → 2.0.0-rc.11
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.
|
@@ -1381,6 +1381,7 @@ async function runDoctorReport(target) {
|
|
|
1381
1381
|
const relevancePathsDrift = inspectRelevancePathsDrift(projectRoot);
|
|
1382
1382
|
const narrowTooFew = inspectNarrowTooFew(projectRoot, lintNow);
|
|
1383
1383
|
const sessionHintsStale = inspectSessionHintsStale(projectRoot, lintNow);
|
|
1384
|
+
const relevanceFieldsMissing = inspectRelevanceFieldsMissing(projectRoot);
|
|
1384
1385
|
const checks = [
|
|
1385
1386
|
createBootstrapAnchorCheck(bootstrapAnchor),
|
|
1386
1387
|
createKnowledgeDirMissingCheck(knowledgeDirMissing),
|
|
@@ -1436,6 +1437,10 @@ async function runDoctorReport(target) {
|
|
|
1436
1437
|
createNarrowTooFewCheck(narrowTooFew),
|
|
1437
1438
|
// rc.6 TASK-021 (E3): session-hints cache hygiene (lint #27). Info kind.
|
|
1438
1439
|
createSessionHintsStaleCheck(sessionHintsStale),
|
|
1440
|
+
// v2.0.0-rc.9 TASK-003 (A3): relevance fields back-fill (lint #28).
|
|
1441
|
+
// Info kind — applies to pending entries only; canonical entries get
|
|
1442
|
+
// the fields written verbatim by fab_review.approve/modify.
|
|
1443
|
+
createRelevanceFieldsMissingCheck(relevanceFieldsMissing),
|
|
1439
1444
|
createPreexistingRootFilesCheck(preexistingRootFiles)
|
|
1440
1445
|
// v2.0 / rc.2: `createLegacyClientPathCheck` removed. The schema now
|
|
1441
1446
|
// rejects retired clientPaths keys (windsurf/rooCode/geminiCLI) at Zod
|
|
@@ -1592,6 +1597,24 @@ async function runDoctorApplyLint(target) {
|
|
|
1592
1597
|
for (const candidate of sessionHintsStale.candidates) {
|
|
1593
1598
|
mutations.push(await applySessionHintsStaleCleanup(projectRoot, candidate));
|
|
1594
1599
|
}
|
|
1600
|
+
const relevanceFieldsMissing = inspectRelevanceFieldsMissing(projectRoot);
|
|
1601
|
+
let relevanceTouchedCount = 0;
|
|
1602
|
+
for (const candidate of relevanceFieldsMissing.candidates) {
|
|
1603
|
+
const mutation = await applyRelevanceFieldsMissing(candidate);
|
|
1604
|
+
mutations.push(mutation);
|
|
1605
|
+
if (mutation.applied) {
|
|
1606
|
+
relevanceTouchedCount += 1;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
try {
|
|
1610
|
+
await appendEventLedgerEvent(projectRoot, {
|
|
1611
|
+
event_type: "relevance_migration_run",
|
|
1612
|
+
timestamp: new Date(now).toISOString(),
|
|
1613
|
+
scanned_count: relevanceFieldsMissing.scanned_count,
|
|
1614
|
+
touched_count: relevanceTouchedCount
|
|
1615
|
+
});
|
|
1616
|
+
} catch {
|
|
1617
|
+
}
|
|
1595
1618
|
const meta = await inspectMeta(projectRoot);
|
|
1596
1619
|
const indexDrift = inspectIndexDrift(projectRoot, meta);
|
|
1597
1620
|
if (indexDrift.drifts.length > 0) {
|
|
@@ -3458,6 +3481,167 @@ function createRelevancePathsDriftCheck(inspection) {
|
|
|
3458
3481
|
"Review whether the entry is still relevant \u2014 use `fab_review.modify` to refresh the anchors or `fab_review.reject` to archive."
|
|
3459
3482
|
);
|
|
3460
3483
|
}
|
|
3484
|
+
function inspectRelevanceFieldsMissing(projectRoot) {
|
|
3485
|
+
const candidates = [];
|
|
3486
|
+
let scannedCount = 0;
|
|
3487
|
+
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
3488
|
+
const teamRoot = join5(projectRoot, ".fabric", "knowledge", "pending");
|
|
3489
|
+
const personalRoot = join5(
|
|
3490
|
+
resolvePersonalRootForPending(),
|
|
3491
|
+
".fabric",
|
|
3492
|
+
"knowledge",
|
|
3493
|
+
"pending"
|
|
3494
|
+
);
|
|
3495
|
+
for (const [root, displayPrefix] of [
|
|
3496
|
+
[teamRoot, ".fabric/knowledge/pending"],
|
|
3497
|
+
[personalRoot, "~/.fabric/knowledge/pending"]
|
|
3498
|
+
]) {
|
|
3499
|
+
if (!existsSync4(root)) {
|
|
3500
|
+
continue;
|
|
3501
|
+
}
|
|
3502
|
+
let typeDirs = [];
|
|
3503
|
+
try {
|
|
3504
|
+
typeDirs = readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
3505
|
+
} catch {
|
|
3506
|
+
continue;
|
|
3507
|
+
}
|
|
3508
|
+
for (const typeDir of typeDirs) {
|
|
3509
|
+
const dir = join5(root, typeDir);
|
|
3510
|
+
let entries;
|
|
3511
|
+
try {
|
|
3512
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
3513
|
+
} catch {
|
|
3514
|
+
continue;
|
|
3515
|
+
}
|
|
3516
|
+
for (const entry of entries) {
|
|
3517
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
3518
|
+
continue;
|
|
3519
|
+
}
|
|
3520
|
+
const absPath = join5(dir, entry.name);
|
|
3521
|
+
let source;
|
|
3522
|
+
try {
|
|
3523
|
+
source = readFileSync(absPath, "utf8");
|
|
3524
|
+
} catch {
|
|
3525
|
+
continue;
|
|
3526
|
+
}
|
|
3527
|
+
const fm = FM_PATTERN.exec(source);
|
|
3528
|
+
if (fm === null) {
|
|
3529
|
+
continue;
|
|
3530
|
+
}
|
|
3531
|
+
scannedCount += 1;
|
|
3532
|
+
const block = fm[1];
|
|
3533
|
+
const missingScope = !RELEVANCE_SCOPE_LINE_PATTERN.test(block);
|
|
3534
|
+
const missingPaths = !RELEVANCE_PATHS_LINE_PATTERN.test(block);
|
|
3535
|
+
if (!missingScope && !missingPaths) {
|
|
3536
|
+
continue;
|
|
3537
|
+
}
|
|
3538
|
+
candidates.push({
|
|
3539
|
+
pending_path: posix.join(displayPrefix, typeDir, entry.name),
|
|
3540
|
+
pending_path_abs: absPath,
|
|
3541
|
+
missing_scope: missingScope,
|
|
3542
|
+
missing_paths: missingPaths
|
|
3543
|
+
});
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
candidates.sort((a, b) => a.pending_path.localeCompare(b.pending_path));
|
|
3548
|
+
return { candidates, scanned_count: scannedCount };
|
|
3549
|
+
}
|
|
3550
|
+
function appendRelevanceFieldsToFrontmatter(source, needsScope, needsPaths) {
|
|
3551
|
+
const FM_PATTERN = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---/u;
|
|
3552
|
+
const fm = FM_PATTERN.exec(source);
|
|
3553
|
+
if (fm === null) {
|
|
3554
|
+
return null;
|
|
3555
|
+
}
|
|
3556
|
+
const block = fm[1];
|
|
3557
|
+
const actuallyNeedsScope = needsScope && !RELEVANCE_SCOPE_LINE_PATTERN.test(block);
|
|
3558
|
+
const actuallyNeedsPaths = needsPaths && !RELEVANCE_PATHS_LINE_PATTERN.test(block);
|
|
3559
|
+
if (!actuallyNeedsScope && !actuallyNeedsPaths) {
|
|
3560
|
+
return source;
|
|
3561
|
+
}
|
|
3562
|
+
const additions = [];
|
|
3563
|
+
if (actuallyNeedsScope) {
|
|
3564
|
+
additions.push("relevance_scope: broad");
|
|
3565
|
+
}
|
|
3566
|
+
if (actuallyNeedsPaths) {
|
|
3567
|
+
additions.push("relevance_paths: []");
|
|
3568
|
+
}
|
|
3569
|
+
const trailing = block.endsWith("\n") ? "" : "\n";
|
|
3570
|
+
const replacedBlock = `${block}${trailing}${additions.join("\n")}`;
|
|
3571
|
+
const blockStart = source.indexOf(block);
|
|
3572
|
+
if (blockStart < 0) {
|
|
3573
|
+
return null;
|
|
3574
|
+
}
|
|
3575
|
+
return source.slice(0, blockStart) + replacedBlock + source.slice(blockStart + block.length);
|
|
3576
|
+
}
|
|
3577
|
+
async function applyRelevanceFieldsMissing(candidate) {
|
|
3578
|
+
const parts = [];
|
|
3579
|
+
if (candidate.missing_scope) parts.push("relevance_scope: broad");
|
|
3580
|
+
if (candidate.missing_paths) parts.push("relevance_paths: []");
|
|
3581
|
+
const detail = `back-filled: ${parts.join(", ")}`;
|
|
3582
|
+
try {
|
|
3583
|
+
const source = await readFile5(candidate.pending_path_abs, "utf8");
|
|
3584
|
+
const rewritten = appendRelevanceFieldsToFrontmatter(
|
|
3585
|
+
source,
|
|
3586
|
+
candidate.missing_scope,
|
|
3587
|
+
candidate.missing_paths
|
|
3588
|
+
);
|
|
3589
|
+
if (rewritten === null) {
|
|
3590
|
+
return {
|
|
3591
|
+
kind: "knowledge_relevance_fields_missing",
|
|
3592
|
+
path: candidate.pending_path,
|
|
3593
|
+
detail,
|
|
3594
|
+
applied: false,
|
|
3595
|
+
error: "frontmatter not parseable; cannot back-fill"
|
|
3596
|
+
};
|
|
3597
|
+
}
|
|
3598
|
+
if (rewritten === source) {
|
|
3599
|
+
return {
|
|
3600
|
+
kind: "knowledge_relevance_fields_missing",
|
|
3601
|
+
path: candidate.pending_path,
|
|
3602
|
+
detail,
|
|
3603
|
+
applied: false,
|
|
3604
|
+
error: "fields already present at write time (no diff)"
|
|
3605
|
+
};
|
|
3606
|
+
}
|
|
3607
|
+
await atomicWriteText3(candidate.pending_path_abs, rewritten);
|
|
3608
|
+
return {
|
|
3609
|
+
kind: "knowledge_relevance_fields_missing",
|
|
3610
|
+
path: candidate.pending_path,
|
|
3611
|
+
detail,
|
|
3612
|
+
applied: true
|
|
3613
|
+
};
|
|
3614
|
+
} catch (error) {
|
|
3615
|
+
return {
|
|
3616
|
+
kind: "knowledge_relevance_fields_missing",
|
|
3617
|
+
path: candidate.pending_path,
|
|
3618
|
+
detail,
|
|
3619
|
+
applied: false,
|
|
3620
|
+
error: truncateErrorMessage(error)
|
|
3621
|
+
};
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
function createRelevanceFieldsMissingCheck(inspection) {
|
|
3625
|
+
if (inspection.candidates.length === 0) {
|
|
3626
|
+
return okCheck(
|
|
3627
|
+
"Knowledge relevance fields missing",
|
|
3628
|
+
"All pending entries declare both relevance_scope and relevance_paths."
|
|
3629
|
+
);
|
|
3630
|
+
}
|
|
3631
|
+
const first = inspection.candidates[0];
|
|
3632
|
+
const missingParts = [];
|
|
3633
|
+
if (first.missing_scope) missingParts.push("relevance_scope");
|
|
3634
|
+
if (first.missing_paths) missingParts.push("relevance_paths");
|
|
3635
|
+
const detail = `${first.pending_path} (missing: ${missingParts.join(", ")})`;
|
|
3636
|
+
return issueCheck(
|
|
3637
|
+
"Knowledge relevance fields missing",
|
|
3638
|
+
"ok",
|
|
3639
|
+
"info",
|
|
3640
|
+
"knowledge_relevance_fields_missing",
|
|
3641
|
+
`${inspection.candidates.length} pending entr${inspection.candidates.length === 1 ? "y is" : "ies are"} missing relevance_scope and/or relevance_paths in frontmatter. First: ${detail}.`,
|
|
3642
|
+
"Run `fab doctor --apply-lint` to back-fill the schema defaults (relevance_scope: broad, relevance_paths: [])."
|
|
3643
|
+
);
|
|
3644
|
+
}
|
|
3461
3645
|
function createNarrowTooFewCheck(inspection) {
|
|
3462
3646
|
const { structural_flagged, telemetry_flagged } = inspection;
|
|
3463
3647
|
if (!structural_flagged && !telemetry_flagged) {
|
package/dist/index.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ type DoctorFixReport = {
|
|
|
77
77
|
message: string;
|
|
78
78
|
report: DoctorReport;
|
|
79
79
|
};
|
|
80
|
-
type DoctorApplyLintMutationKind = "knowledge_orphan_demote_required" | "knowledge_stale_archive_required" | "knowledge_index_drift" | "knowledge_pending_auto_archive" | "knowledge_session_hints_stale_cleanup";
|
|
80
|
+
type DoctorApplyLintMutationKind = "knowledge_orphan_demote_required" | "knowledge_stale_archive_required" | "knowledge_index_drift" | "knowledge_pending_auto_archive" | "knowledge_session_hints_stale_cleanup" | "knowledge_relevance_fields_missing";
|
|
81
81
|
type DoctorApplyLintMutation = {
|
|
82
82
|
kind: DoctorApplyLintMutationKind;
|
|
83
83
|
path: string;
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
sha256,
|
|
27
27
|
stableStringify,
|
|
28
28
|
writeKnowledgeMeta
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-MMGHD42I.js";
|
|
30
30
|
|
|
31
31
|
// src/index.ts
|
|
32
32
|
import { existsSync as existsSync4 } from "fs";
|
|
@@ -149,6 +149,21 @@ async function extractKnowledge(projectRoot, input) {
|
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
151
|
const layer = input.layer ?? "team";
|
|
152
|
+
let relevanceScope = input.relevance_scope;
|
|
153
|
+
let relevancePaths = input.relevance_paths;
|
|
154
|
+
const shouldAutoDegrade = layer === "personal" && relevanceScope === "narrow";
|
|
155
|
+
if (shouldAutoDegrade) {
|
|
156
|
+
relevanceScope = "broad";
|
|
157
|
+
relevancePaths = [];
|
|
158
|
+
await emitEventBestEffort(projectRoot, {
|
|
159
|
+
event_type: "knowledge_scope_degraded",
|
|
160
|
+
stable_id: `pending:${idempotencyKey}`,
|
|
161
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
162
|
+
from_scope: "narrow",
|
|
163
|
+
to_scope: "broad",
|
|
164
|
+
reason: "personal-implies-broad"
|
|
165
|
+
});
|
|
166
|
+
}
|
|
152
167
|
const baseDir = pendingBase(layer, projectRoot);
|
|
153
168
|
const absolutePath = join2(baseDir, input.type, `${sanitizedSlug}.md`);
|
|
154
169
|
const reportedPath = layer === "personal" ? `~/${relative(resolvePersonalRoot(), absolutePath)}` : relative(projectRoot, absolutePath);
|
|
@@ -190,7 +205,9 @@ async function extractKnowledge(projectRoot, input) {
|
|
|
190
205
|
recentPaths: input.recent_paths,
|
|
191
206
|
layer,
|
|
192
207
|
proposedReason: input.proposed_reason,
|
|
193
|
-
sessionContext: input.session_context
|
|
208
|
+
sessionContext: input.session_context,
|
|
209
|
+
relevanceScope,
|
|
210
|
+
relevancePaths
|
|
194
211
|
});
|
|
195
212
|
await atomicWriteText(absolutePath, fresh);
|
|
196
213
|
await emitEventBestEffort(projectRoot, {
|
|
@@ -216,7 +233,7 @@ function sanitizeSlug(raw) {
|
|
|
216
233
|
}
|
|
217
234
|
function renderFreshEntry(args) {
|
|
218
235
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
219
|
-
const
|
|
236
|
+
const frontmatterLines = [
|
|
220
237
|
"---",
|
|
221
238
|
`type: ${args.type}`,
|
|
222
239
|
"maturity: draft",
|
|
@@ -224,10 +241,20 @@ function renderFreshEntry(args) {
|
|
|
224
241
|
`created_at: ${createdAt}`,
|
|
225
242
|
`source_sessions: [${args.sourceSessions.map((s) => JSON.stringify(s)).join(", ")}]`,
|
|
226
243
|
`proposed_reason: ${args.proposedReason}`,
|
|
227
|
-
"tags: []"
|
|
244
|
+
"tags: []"
|
|
245
|
+
];
|
|
246
|
+
if (args.relevanceScope !== void 0) {
|
|
247
|
+
frontmatterLines.push(`relevance_scope: ${args.relevanceScope}`);
|
|
248
|
+
}
|
|
249
|
+
if (args.relevancePaths !== void 0) {
|
|
250
|
+
const pathsBody = args.relevancePaths.map((p) => quoteRelevancePath(p)).join(", ");
|
|
251
|
+
frontmatterLines.push(`relevance_paths: [${pathsBody}]`);
|
|
252
|
+
}
|
|
253
|
+
frontmatterLines.push(
|
|
228
254
|
`x-fabric-idempotency-key: ${args.idempotencyKey}`,
|
|
229
255
|
"---"
|
|
230
|
-
|
|
256
|
+
);
|
|
257
|
+
const frontmatter = frontmatterLines.join("\n");
|
|
231
258
|
const reasonExplanation = PROPOSED_REASON_DESCRIPTIONS[args.proposedReason];
|
|
232
259
|
const body = [
|
|
233
260
|
"",
|
|
@@ -251,6 +278,9 @@ function renderFreshEntry(args) {
|
|
|
251
278
|
return `${frontmatter}
|
|
252
279
|
${body}`;
|
|
253
280
|
}
|
|
281
|
+
function quoteRelevancePath(value) {
|
|
282
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
283
|
+
}
|
|
254
284
|
function renderEvidenceBlock(summary, recentPaths) {
|
|
255
285
|
const pathLines = recentPaths.length === 0 ? "_(no recent paths reported)_" : recentPaths.map((p) => `- ${p}`).join("\n");
|
|
256
286
|
return [
|
|
@@ -1860,7 +1890,7 @@ function formatPreexistingRootMessage(projectRoot) {
|
|
|
1860
1890
|
function createFabricServer(tracker) {
|
|
1861
1891
|
const server = new McpServer({
|
|
1862
1892
|
name: "fabric-knowledge-server",
|
|
1863
|
-
version: "2.0.0-rc.
|
|
1893
|
+
version: "2.0.0-rc.11"
|
|
1864
1894
|
});
|
|
1865
1895
|
registerPlanContext(server, tracker);
|
|
1866
1896
|
registerKnowledgeSections(server, tracker);
|
|
@@ -1960,7 +1990,7 @@ function createShutdownHandler(deps) {
|
|
|
1960
1990
|
};
|
|
1961
1991
|
}
|
|
1962
1992
|
async function startHttpServer(options) {
|
|
1963
|
-
const { createFabricHttpApp } = await import("./http-
|
|
1993
|
+
const { createFabricHttpApp } = await import("./http-U5RL5Q6P.js");
|
|
1964
1994
|
const { port, projectRoot, host = "127.0.0.1", authToken } = options;
|
|
1965
1995
|
const app = createFabricHttpApp({ projectRoot, host, authToken });
|
|
1966
1996
|
return await new Promise((resolveServer, rejectServer) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fenglimg/fabric-server",
|
|
3
|
-
"version": "2.0.0-rc.
|
|
3
|
+
"version": "2.0.0-rc.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"express": "^5.2.1",
|
|
14
14
|
"minimatch": "^10.0.1",
|
|
15
15
|
"zod": "^3.25.0",
|
|
16
|
-
"@fenglimg/fabric-shared": "2.0.0-rc.
|
|
16
|
+
"@fenglimg/fabric-shared": "2.0.0-rc.11"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@types/express": "^5.0.6",
|