@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) {
@@ -14,7 +14,7 @@ import {
14
14
  readEventLedger,
15
15
  runDoctorReport,
16
16
  sha256
17
- } from "./chunk-AR2HV5JT.js";
17
+ } from "./chunk-MMGHD42I.js";
18
18
 
19
19
  // src/http.ts
20
20
  import { randomUUID as randomUUID2 } from "crypto";
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-AR2HV5JT.js";
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 frontmatter = [
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
- ].join("\n");
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.10"
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-7HJQGQSN.js");
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.10",
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.10"
16
+ "@fenglimg/fabric-shared": "2.0.0-rc.11"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/express": "^5.0.6",