@fenglimg/fabric-server 2.0.0-rc.37 → 2.0.0-rc.38

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/index.d.ts CHANGED
@@ -132,6 +132,10 @@ type CiteCoverageReport = {
132
132
  recalled_unverified: number;
133
133
  expected_but_missed: number;
134
134
  total_turns: number;
135
+ cite_compliance_rate?: number | null;
136
+ compliant_cites?: number;
137
+ noncompliant_cites?: number;
138
+ uncorrelatable_edits?: number;
135
139
  };
136
140
  per_client?: Record<string, Partial<CiteCoverageReport["metrics"]>>;
137
141
  dismissed_reason_histogram?: Record<string, number>;
@@ -341,8 +345,6 @@ type PlanContextInput = {
341
345
  };
342
346
  type RequirementProfile = {
343
347
  target_path: string;
344
- path_segments: string[];
345
- extension: string;
346
348
  known_tech: string[];
347
349
  user_intent: string;
348
350
  detected_entities: string[];
@@ -350,23 +352,21 @@ type RequirementProfile = {
350
352
  type PlanContextEntry = {
351
353
  path: string;
352
354
  requirement_profile: RequirementProfile;
353
- description_index: RuleDescriptionIndexItem[];
355
+ };
356
+ type PreflightDiagnostic = {
357
+ code: "missing_description" | "empty_shell_suppressed";
358
+ severity: "warn";
359
+ message: string;
360
+ stable_ids?: string[];
361
+ path?: string;
354
362
  };
355
363
  type PlanContextResult = {
356
364
  revision_hash: string;
357
365
  stale: boolean;
358
366
  selection_token: string;
359
367
  entries: PlanContextEntry[];
360
- shared: {
361
- description_index: RuleDescriptionIndexItem[];
362
- preflight_diagnostics: Array<{
363
- code: "missing_description";
364
- severity: "warn";
365
- message: string;
366
- stable_ids?: string[];
367
- path?: string;
368
- }>;
369
- };
368
+ candidates: RuleDescriptionIndexItem[];
369
+ preflight_diagnostics: PreflightDiagnostic[];
370
370
  auto_healed?: boolean;
371
371
  previous_revision_hash?: string;
372
372
  redirects?: Record<string, string>;
package/dist/index.js CHANGED
@@ -2395,10 +2395,6 @@ import {
2395
2395
  } from "@fenglimg/fabric-shared/schemas/api-contracts";
2396
2396
  import { enforcePayloadLimit as enforcePayloadLimit2 } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
2397
2397
 
2398
- // src/services/plan-context.ts
2399
- import { minimatch as minimatch2 } from "minimatch";
2400
- import { deriveAgentsMetaLayer as deriveAgentsMetaLayer3 } from "@fenglimg/fabric-shared";
2401
-
2402
2398
  // src/services/get-knowledge.ts
2403
2399
  import { readFile as readFile6 } from "fs/promises";
2404
2400
  import { join as join8 } from "path";
@@ -2613,19 +2609,13 @@ async function planContext(projectRoot, input) {
2613
2609
  nowMs: Date.now(),
2614
2610
  targetPaths: input.target_paths ?? dedupePaths(input.paths)
2615
2611
  };
2616
- const allDescriptions = buildDescriptionIndex(meta, scoringContext);
2617
- const relevanceTargetPaths = input.target_paths ?? uniquePaths;
2618
- const entries = uniquePaths.map((path2) => {
2619
- const profile = buildRequirementProfile(path2, input);
2620
- const descriptionIndex = allDescriptions.filter((item) => shouldIncludeIndexItemForPath(item, meta, path2)).filter((item) => shouldIncludeByRelevance(item, relevanceTargetPaths));
2621
- return {
2622
- path: path2,
2623
- requirement_profile: profile,
2624
- description_index: descriptionIndex
2625
- };
2626
- });
2627
- const sharedDescriptionIndex = dedupeDescriptionIndex(entries.flatMap((entry) => entry.description_index));
2628
- const sharedStableIds = sharedDescriptionIndex.map((item) => item.stable_id);
2612
+ const { items: builtItems, suppressedStableIds } = buildDescriptionIndex(meta, scoringContext);
2613
+ const candidates = dedupeDescriptionIndex(builtItems);
2614
+ const entries = uniquePaths.map((path2) => ({
2615
+ path: path2,
2616
+ requirement_profile: buildRequirementProfile(path2, input)
2617
+ }));
2618
+ const sharedStableIds = candidates.map((item) => item.stable_id);
2629
2619
  const ttlMs = readSelectionTokenTtlMs(projectRoot) ?? SELECTION_TOKEN_TTL_DEFAULT_MS;
2630
2620
  const selectionToken = createSelectionToken(meta.revision, uniquePaths, [], sharedStableIds, Date.now(), ttlMs);
2631
2621
  let redirects;
@@ -2642,10 +2632,8 @@ async function planContext(projectRoot, input) {
2642
2632
  stale,
2643
2633
  selection_token: selectionToken,
2644
2634
  entries,
2645
- shared: {
2646
- description_index: sharedDescriptionIndex,
2647
- preflight_diagnostics: buildPreflightDiagnostics(meta)
2648
- },
2635
+ candidates,
2636
+ preflight_diagnostics: buildPreflightDiagnostics(meta, suppressedStableIds),
2649
2637
  // v2.0.0-rc.22 Scope D T-D2 + rc.23 TASK-005 (a-B): surface auto-heal pair
2650
2638
  // only when a heal actually fired (either revision-drift heal in
2651
2639
  // loadActiveMetaOrStale or description-undefined heal driven from here).
@@ -2663,13 +2651,13 @@ async function planContext(projectRoot, input) {
2663
2651
  event_type: "knowledge_context_planned",
2664
2652
  target_paths: uniquePaths,
2665
2653
  required_stable_ids: [],
2666
- ai_selectable_stable_ids: sharedDescriptionIndex.map((item) => item.stable_id),
2654
+ ai_selectable_stable_ids: sharedStableIds,
2667
2655
  final_stable_ids: [],
2668
2656
  selection_token: selectionToken,
2669
2657
  client_hash: input.client_hash,
2670
2658
  intent: input.intent,
2671
2659
  known_tech: input.known_tech,
2672
- diagnostics: result.shared.preflight_diagnostics,
2660
+ diagnostics: result.preflight_diagnostics,
2673
2661
  correlation_id: input.correlation_id,
2674
2662
  session_id: input.session_id
2675
2663
  });
@@ -2721,42 +2709,31 @@ function buildRequirementProfile(path2, input) {
2721
2709
  ]);
2722
2710
  return {
2723
2711
  target_path: normalizedPath,
2724
- path_segments: normalizedPath.split("/").filter(Boolean),
2725
- extension: extensionMatch?.[1] ?? "",
2726
2712
  known_tech: knownTech,
2727
2713
  user_intent: input.intent ?? "",
2728
2714
  detected_entities: input.detected_entities?.[normalizedPath] ?? input.detected_entities?.[path2] ?? []
2729
2715
  };
2730
2716
  }
2731
2717
  function buildDescriptionIndex(meta, scoringContext) {
2732
- return Object.entries(meta.nodes).flatMap(([nodeId, node]) => {
2733
- const level = deriveAgentsMetaLayer3(node.file);
2734
- const description = node.description ?? descriptionFromLegacyActivation(node.activation?.description);
2735
- if (description === void 0) {
2718
+ const suppressedStableIds = [];
2719
+ const items = Object.entries(meta.nodes).flatMap(([nodeId, node]) => {
2720
+ const baseDescription = node.description ?? descriptionFromLegacyActivation(node.activation?.description);
2721
+ if (baseDescription === void 0) {
2722
+ return [];
2723
+ }
2724
+ const stableId = node.stable_id ?? nodeId;
2725
+ if (isEmptyShellDescription(baseDescription, stableId)) {
2726
+ suppressedStableIds.push(stableId);
2736
2727
  return [];
2737
2728
  }
2738
2729
  const inferredLayer = inferKnowledgeLayerFromContentRef(node.content_ref ?? node.file);
2739
- return [{
2740
- stable_id: node.stable_id ?? nodeId,
2741
- level,
2742
- required: false,
2743
- selectable: false,
2744
- description,
2745
- type: description.knowledge_type,
2746
- maturity: description.maturity,
2747
- layer: description.knowledge_layer ?? inferredLayer,
2748
- layer_reason: description.layer_reason,
2749
- // v2.0-rc.5 C3 (TASK-012): surface relevance fields at the top level
2750
- // so the per-entry filter + downstream MCP clients can read them
2751
- // without reaching into description.*. Defaults (broad + []) are
2752
- // applied at the meta-builder layer; we just pass them through here.
2753
- relevance_scope: description.relevance_scope,
2754
- relevance_paths: description.relevance_paths
2755
- }];
2730
+ const description = baseDescription.knowledge_layer === void 0 && inferredLayer !== void 0 ? { ...baseDescription, knowledge_layer: inferredLayer } : baseDescription;
2731
+ return [{ stable_id: stableId, description }];
2756
2732
  }).sort((left, right) => compareDescriptionIndexItems(left, right, scoringContext));
2733
+ return { items, suppressedStableIds };
2757
2734
  }
2758
- function shouldIncludeByRelevance(_item, _targetPaths) {
2759
- return true;
2735
+ function isEmptyShellDescription(description, stableId) {
2736
+ return description.summary === stableId && description.intent_clues.length === 0 && description.tech_stack.length === 0 && description.impact.length === 0;
2760
2737
  }
2761
2738
  function inferKnowledgeLayerFromContentRef(contentRef) {
2762
2739
  if (contentRef === void 0) {
@@ -2782,25 +2759,31 @@ function descriptionFromLegacyActivation(summary) {
2782
2759
  must_read_if: summary
2783
2760
  };
2784
2761
  }
2785
- function shouldIncludeIndexItemForPath(_item, _meta, _path) {
2786
- return true;
2787
- }
2788
2762
  function hasUndefinedDescription(meta) {
2789
2763
  return Object.values(meta.nodes).some(
2790
2764
  (node) => node.description === void 0 && node.activation?.description === void 0
2791
2765
  );
2792
2766
  }
2793
- function buildPreflightDiagnostics(meta) {
2767
+ function buildPreflightDiagnostics(meta, suppressedStableIds) {
2768
+ const diagnostics = [];
2794
2769
  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();
2795
- if (missingDescriptionStableIds.length === 0) {
2796
- return [];
2770
+ if (missingDescriptionStableIds.length > 0) {
2771
+ diagnostics.push({
2772
+ code: "missing_description",
2773
+ severity: "warn",
2774
+ stable_ids: missingDescriptionStableIds,
2775
+ message: `Resolved registry includes ${missingDescriptionStableIds.length} node(s) without structured descriptions.`
2776
+ });
2797
2777
  }
2798
- return [{
2799
- code: "missing_description",
2800
- severity: "warn",
2801
- stable_ids: missingDescriptionStableIds,
2802
- message: `Resolved registry includes ${missingDescriptionStableIds.length} node(s) without structured descriptions.`
2803
- }];
2778
+ if (suppressedStableIds.length > 0) {
2779
+ diagnostics.push({
2780
+ code: "empty_shell_suppressed",
2781
+ severity: "warn",
2782
+ stable_ids: [...suppressedStableIds].sort(),
2783
+ message: `${suppressedStableIds.length} draft entr${suppressedStableIds.length === 1 ? "y" : "ies"} hidden from candidates (summary === stable_id, no signal). Run \`fabric doctor --enrich-descriptions\` to populate them.`
2784
+ });
2785
+ }
2786
+ return diagnostics;
2804
2787
  }
2805
2788
  function dedupeStableIds(stableIds) {
2806
2789
  return Array.from(new Set(stableIds));
@@ -2840,7 +2823,7 @@ function scoreDescriptionItem(item, nowMs, targetPaths) {
2840
2823
  }
2841
2824
  }
2842
2825
  if (targetPaths.length > 0) {
2843
- const relevancePaths = item.relevance_paths ?? item.description?.relevance_paths ?? [];
2826
+ const relevancePaths = item.description?.relevance_paths ?? [];
2844
2827
  let best = 0;
2845
2828
  for (const rp of relevancePaths) {
2846
2829
  for (const tp of targetPaths) {
@@ -2949,7 +2932,7 @@ import { enforcePayloadLimit as enforcePayloadLimit3 } from "@fenglimg/fabric-sh
2949
2932
  import { readFile as readFile8 } from "fs/promises";
2950
2933
  import { homedir as homedir4 } from "os";
2951
2934
  import { join as join9 } from "path";
2952
- import { deriveAgentsMetaLayer as deriveAgentsMetaLayer4 } from "@fenglimg/fabric-shared";
2935
+ import { deriveAgentsMetaLayer as deriveAgentsMetaLayer3 } from "@fenglimg/fabric-shared";
2953
2936
  var PRIORITY_ORDER = {
2954
2937
  high: 0,
2955
2938
  medium: 1,
@@ -3014,7 +2997,6 @@ async function getKnowledgeSections(projectRoot, input) {
3014
2997
  }
3015
2998
  const result = {
3016
2999
  revision_hash: meta.revision,
3017
- precedence: ["L2", "L1", "L0"],
3018
3000
  selected_stable_ids: rules.map((rule) => rule.stable_id),
3019
3001
  rules,
3020
3002
  diagnostics,
@@ -3085,7 +3067,9 @@ function validateAiSelections(aiSelectableStableIds, aiSelectedStableIds, aiSele
3085
3067
  const selectable = new Set(aiSelectableStableIds);
3086
3068
  for (const stableId of aiSelectedStableIds) {
3087
3069
  if (!selectable.has(stableId)) {
3088
- throw new Error(`Invalid L1 rule selection: ${stableId}`);
3070
+ throw new Error(
3071
+ `Invalid rule selection "${stableId}": not in this token's plan-context candidates. Pass only stable_ids from fab_plan_context candidates[].stable_id.`
3072
+ );
3089
3073
  }
3090
3074
  if (aiSelectionReasons[stableId]?.trim() === "") {
3091
3075
  throw new Error(`Missing AI selection reason for ${stableId}`);
@@ -3101,7 +3085,7 @@ function findRuleNode(meta, stableId) {
3101
3085
  if (nodeStableId !== stableId) {
3102
3086
  continue;
3103
3087
  }
3104
- const level = node.level ?? deriveAgentsMetaLayer4(node.file);
3088
+ const level = node.level ?? deriveAgentsMetaLayer3(node.file);
3105
3089
  return {
3106
3090
  stable_id: nodeStableId,
3107
3091
  level,
@@ -3150,7 +3134,7 @@ function pickSelectionReasons(selectedStableIds, reasons) {
3150
3134
  var RECALL_REASON_MARKER = "fab_recall: combined-call auto-selection";
3151
3135
  async function recall(projectRoot, input) {
3152
3136
  const planResult = await planContext(projectRoot, input);
3153
- const candidateIds = planResult.shared.description_index.map((item) => item.stable_id);
3137
+ const candidateIds = planResult.candidates.map((item) => item.stable_id);
3154
3138
  let rewrittenIds;
3155
3139
  if (input.ids !== void 0) {
3156
3140
  try {
@@ -4423,7 +4407,7 @@ import { constants } from "fs";
4423
4407
  import { homedir as homedir6 } from "os";
4424
4408
  import { isAbsolute as isAbsolute2, join as join11, posix, relative as nodeRelative, resolve as resolve4, sep as sep3 } from "path";
4425
4409
  import { Script } from "vm";
4426
- import { minimatch as minimatch3 } from "minimatch";
4410
+ import { minimatch as minimatch2 } from "minimatch";
4427
4411
  import { ZodError } from "zod";
4428
4412
  import {
4429
4413
  agentsMetaSchema as agentsMetaSchema5,
@@ -8542,7 +8526,7 @@ function inspectRelevancePathsDangling(projectRoot) {
8542
8526
  const glob = rawGlob.endsWith("/") ? `${rawGlob}**` : rawGlob;
8543
8527
  let matched = false;
8544
8528
  for (const target of workspacePaths) {
8545
- if (minimatch3(target, glob, { dot: true, matchBase: false })) {
8529
+ if (minimatch2(target, glob, { dot: true, matchBase: false })) {
8546
8530
  matched = true;
8547
8531
  break;
8548
8532
  }
@@ -8626,7 +8610,7 @@ function inspectRelevancePathsDrift(projectRoot) {
8626
8610
  for (const rawGlob of paths) {
8627
8611
  const glob = rawGlob.endsWith("/") ? `${rawGlob}**` : rawGlob;
8628
8612
  for (const target of recentPaths) {
8629
- if (minimatch3(target, glob, { dot: true, matchBase: false })) {
8613
+ if (minimatch2(target, glob, { dot: true, matchBase: false })) {
8630
8614
  anyMatch = true;
8631
8615
  break;
8632
8616
  }
@@ -8761,7 +8745,7 @@ function inspectPersonalLayerPathMisclassify(projectRoot) {
8761
8745
  }
8762
8746
  const glob = rawGlob.endsWith("/") ? `${rawGlob}**` : rawGlob;
8763
8747
  for (const target of workspacePaths) {
8764
- if (minimatch3(target, glob, { dot: true, matchBase: false })) {
8748
+ if (minimatch2(target, glob, { dot: true, matchBase: false })) {
8765
8749
  matched.push(rawGlob);
8766
8750
  break;
8767
8751
  }
@@ -9760,7 +9744,7 @@ function matchesRelevancePath(editPath, relevancePaths) {
9760
9744
  }
9761
9745
  const normalized = normalizePath(editPath);
9762
9746
  for (const glob of relevancePaths) {
9763
- if (minimatch3(normalized, glob, { dot: true, matchBase: false })) {
9747
+ if (minimatch2(normalized, glob, { dot: true, matchBase: false })) {
9764
9748
  return true;
9765
9749
  }
9766
9750
  }
@@ -9953,7 +9937,7 @@ async function runDoctorCiteCoverage(projectRoot, options) {
9953
9937
  case "edit": {
9954
9938
  let matched = false;
9955
9939
  for (const p of editPaths) {
9956
- if (minimatch3(p, op.target, { dot: true, matchBase: false })) {
9940
+ if (minimatch2(p, op.target, { dot: true, matchBase: false })) {
9957
9941
  matched = true;
9958
9942
  break;
9959
9943
  }
@@ -9963,7 +9947,7 @@ async function runDoctorCiteCoverage(projectRoot, options) {
9963
9947
  }
9964
9948
  case "not_edit": {
9965
9949
  for (const p of editPaths) {
9966
- if (minimatch3(p, op.target, { dot: true, matchBase: false })) {
9950
+ if (minimatch2(p, op.target, { dot: true, matchBase: false })) {
9967
9951
  return true;
9968
9952
  }
9969
9953
  }
@@ -10080,14 +10064,17 @@ async function runDoctorCiteCoverage(projectRoot, options) {
10080
10064
  }
10081
10065
  let editsTouched = 0;
10082
10066
  let expectedButMissed = 0;
10067
+ let uncorrelatableEdits = 0;
10083
10068
  for (const edit of editEvents) {
10084
10069
  const sid = edit.session_id;
10070
+ const hasSid = typeof sid === "string" && sid.length > 0;
10071
+ if (!hasSid) uncorrelatableEdits += 1;
10085
10072
  if (clientSessionIds !== null) {
10086
- if (typeof sid !== "string" || sid.length === 0) continue;
10073
+ if (!hasSid) continue;
10087
10074
  if (!clientSessionIds.has(sid)) continue;
10088
10075
  }
10089
10076
  editsTouched += 1;
10090
- if (typeof sid !== "string" || sid.length === 0) continue;
10077
+ if (!hasSid) continue;
10091
10078
  const citedSet = sessionCitedKbs.get(sid) ?? /* @__PURE__ */ new Set();
10092
10079
  for (const [kbId, kb] of kbIndex) {
10093
10080
  if (kb.relevance_scope !== "narrow") continue;
@@ -10097,12 +10084,21 @@ async function runDoctorCiteCoverage(projectRoot, options) {
10097
10084
  }
10098
10085
  }
10099
10086
  }
10087
+ const noneTotal = Object.values(noneHistogram).reduce((a, b) => a + b, 0);
10088
+ const compliantCites = qualifyingCites + noneTotal;
10089
+ const noncompliantCites = expectedButMissed;
10090
+ const complianceDenom = compliantCites + noncompliantCites;
10091
+ const citeComplianceRate = complianceDenom > 0 ? compliantCites / complianceDenom : null;
10100
10092
  const metrics = {
10101
10093
  edits_touched: editsTouched,
10102
10094
  qualifying_cites: qualifyingCites,
10103
10095
  recalled_unverified: recalledUnverified,
10104
10096
  expected_but_missed: expectedButMissed,
10105
- total_turns: totalTurns
10097
+ total_turns: totalTurns,
10098
+ cite_compliance_rate: citeComplianceRate,
10099
+ compliant_cites: compliantCites,
10100
+ noncompliant_cites: noncompliantCites,
10101
+ uncorrelatable_edits: uncorrelatableEdits
10106
10102
  };
10107
10103
  let perClient;
10108
10104
  if (options.client === "all" && perClientAccum.size > 0) {
@@ -10512,7 +10508,7 @@ function formatPreexistingRootMessage(projectRoot) {
10512
10508
  function createFabricServer(tracker) {
10513
10509
  const server = new McpServer({
10514
10510
  name: "fabric-knowledge-server",
10515
- version: "2.0.0-rc.37"
10511
+ version: "2.0.0-rc.38"
10516
10512
  });
10517
10513
  registerPlanContext(server, tracker);
10518
10514
  registerKnowledgeSections(server, tracker);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fenglimg/fabric-server",
3
- "version": "2.0.0-rc.37",
3
+ "version": "2.0.0-rc.38",
4
4
  "description": "Fabric MCP knowledge server — stdio transport for Claude Code / Cursor / Codex CLI, manages .fabric/ knowledge base + agents.meta.json + event ledger.",
5
5
  "license": "MIT",
6
6
  "author": "wangzhichao <fenglimg90@gmail.com>",
@@ -38,7 +38,7 @@
38
38
  "@modelcontextprotocol/sdk": "^1.29.0",
39
39
  "minimatch": "^10.0.1",
40
40
  "zod": "^3.25.0",
41
- "@fenglimg/fabric-shared": "2.0.0-rc.37"
41
+ "@fenglimg/fabric-shared": "2.0.0-rc.38"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^22.15.0",