@fenglimg/fabric-server 2.3.0-rc.6 → 2.3.0-rc.7

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
@@ -745,6 +745,36 @@ interface RelatedGraphNode {
745
745
  * id index carries both forms for every node.
746
746
  */
747
747
  declare function buildRelatedGraph(nodes: RelatedGraphNode[]): RelatedGraphInspection;
748
+ /** A richer node carrying the text/metadata signals the suggestion heuristic reads. */
749
+ interface RelatedGraphNodeRich {
750
+ /** Store-qualified id (`<alias>:<stableId>`). */
751
+ qualifiedId: string;
752
+ summary: string;
753
+ /** RuleDescription.intent_clues (NOT `keywords` — RuleDescription has no such field). */
754
+ intentClues: string[];
755
+ tags: string[];
756
+ relevancePaths: string[];
757
+ /** Existing declared `related` edges — a pair already connected here is never re-proposed. */
758
+ related: string[];
759
+ }
760
+ /** A proposed related edge: an ordered id pair, a confidence, and the signals that fired. */
761
+ interface SuggestedRelatedEdge {
762
+ source: string;
763
+ target: string;
764
+ confidence: number;
765
+ provenance: string[];
766
+ }
767
+ /**
768
+ * Propose related edges the corpus is missing. For every unordered pair NOT already
769
+ * connected via existing `related`, score:
770
+ * - token Jaccard over tokenize(summary + intent_clues) — the DOMINANT signal
771
+ * - tag-set intersection — an INDEPENDENT boolean bonus (never folded into tokens)
772
+ * - shared relevance_paths — an INDEPENDENT boolean bonus
773
+ * confidence = jaccard + bonuses, clamped [0,1]. Only pairs >= 0.6 are returned, each
774
+ * with a non-empty provenance[] naming the firing signals. Pure + deterministic
775
+ * (same input → same output; sorted confidence desc, then source, then target).
776
+ */
777
+ declare function suggestRelatedEdges(nodes: RelatedGraphNodeRich[]): SuggestedRelatedEdge[];
748
778
  /**
749
779
  * Walk the store canonical corpus and build the related graph. Never throws —
750
780
  * collectStoreCanonicalEntries degrades to [] when no store is in the read-set.
@@ -756,6 +786,13 @@ declare function buildRelatedGraph(nodes: RelatedGraphNode[]): RelatedGraphInspe
756
786
  * read `description.related` directly (the single source the recall path uses).
757
787
  */
758
788
  declare function inspectRelatedGraph(projectRoot: string): Promise<RelatedGraphInspection>;
789
+ /**
790
+ * Walk the store canonical corpus and PROPOSE the top-N missing related edges via the
791
+ * pure suggestRelatedEdges heuristic. Never throws — collectStoreCanonicalEntries
792
+ * degrades to [] when no store is in the read-set. READ-ONLY: it computes suggestions
793
+ * and writes nothing; edge creation stays on the fabric-review modify path.
794
+ */
795
+ declare function inspectSuggestedRelatedEdges(projectRoot: string): Promise<SuggestedRelatedEdge[]>;
759
796
 
760
797
  interface StoreReachability {
761
798
  uuid: string;
@@ -988,10 +1025,12 @@ type RecallInput = PlanContextInput & {
988
1025
  */
989
1026
  include_related?: boolean;
990
1027
  };
1028
+ type FullRuleDescription = PlanContextResult["candidates"][number]["description"];
1029
+ type RecallEntryDescription = Pick<FullRuleDescription, "summary" | "must_read_if" | "intent_clues"> & Partial<Pick<FullRuleDescription, "knowledge_type">>;
991
1030
  type RecallEntry = {
992
1031
  stable_id: string;
993
1032
  rank: number;
994
- description: PlanContextResult["candidates"][number]["description"];
1033
+ description: RecallEntryDescription;
995
1034
  read_path?: string;
996
1035
  store?: {
997
1036
  alias: string;
@@ -1147,4 +1186,4 @@ interface ShutdownHandlerDeps {
1147
1186
  */
1148
1187
  declare function createShutdownHandler(deps: ShutdownHandlerDeps): () => void;
1149
1188
 
1150
- export { AGENTS_MD_RESOURCE_URI, type AlwaysActiveBody, type ArchiveHistoryEntry, type ArchiveHistoryReport, COLD_EVAL_RUBRIC, type CiteCoverageReport, type ColdEvalBatch, type ColdEvalCandidate, type ColdEvalVerdict, type ConflictEntry, type ConflictJudge, type ConflictLintReport, type ConflictPair, type ConflictVerdict, type ConsumptionEntry, type ConsumptionInspection, type ConsumptionLintConfig, DEFAULT_CONFLICT_SIMILARITY_THRESHOLD, type DoctorApplyLintMutation, type DoctorApplyLintMutationKind, type DoctorApplyLintReport, type DoctorFixReport, type DoctorIssue, type DoctorReport, EVENT_LEDGER_PATH, type EnrichDescriptionsCandidate, type EnrichDescriptionsMode, type EnrichDescriptionsReport, FABRIC_SERVER_INSTRUCTIONS, type HistoryAllReport, type HistoryDayRow, type InFlightTracker, type KnowledgeCensus, LEDGER_PATH, LEGACY_LEDGER_PATH, METRICS_LEDGER_PATH, METRIC_COUNTER_NAMES, type MetricCounterName, type MetricsRow, OPTIONAL_EMBED_PACKAGE, type PlanContextInput, type PlanContextResult, type PrecheckResult, RETIRED_TOKENS, type RecallInput, type RecallResult, type RelatedBrokenLink, type RelatedGraphInspection, type RelatedGraphNode, type RelatedHubEntry, type RequirementProfile, type RetiredReferenceHit, type RetiredReferenceInspection, type RetiredToken, type SelectionTokenState, type ShutdownHandlerDeps, type StoreCanonicalEntry, type StoreReachability, type SurfaceVerdict, type UnboundProjectViolation, type WhyNotSurfacedResult, aggregateConsumption, appendEventLedgerEvent, buildAlwaysActiveBodies, buildColdEvalBatch, buildKnowledgeCensus, buildRelatedGraph, bumpCounter, clearPrecheckCache, collectStoreCanonicalEntries, contextCache, createFabricServer, createInFlightTracker, createShutdownHandler, defaultEmbedCacheDir, detectUnboundProject, drainCounters, enrichDescriptions, evaluateStoreDir, explainWhyNotSurfaced, extractKnowledge, findConflictCandidates, flushAndSyncEventLedger, flushMetrics, formatPreexistingRootMessage, getEventLedgerPath, getLedgerPath, getLegacyLedgerPath, getMetricsLedgerPath, inspectConsumption, inspectRelatedGraph, inspectRetiredReferences, isEmbedderResolvable, lintConflicts, loadConflictEntries, loadEmbedder, pairSimilarity, planContext, precheckStoreReachability, readEmbedConfig, readEventLedger, readFusion, readLedger, readMetrics, readSelectionToken, recall, rehydrateAgentsMetaAt, resolveLedgerPaths, reviewKnowledge, reviewPending, runDoctorApplyLint, runDoctorArchiveHistory, runDoctorCiteCoverage, runDoctorConflictLint, runDoctorFix, runDoctorHistoryAll, runDoctorReport, startMetricsFlush, startRotationTick, startStdioServer, stopMetricsFlush, stopRotationTick };
1189
+ export { AGENTS_MD_RESOURCE_URI, type AlwaysActiveBody, type ArchiveHistoryEntry, type ArchiveHistoryReport, COLD_EVAL_RUBRIC, type CiteCoverageReport, type ColdEvalBatch, type ColdEvalCandidate, type ColdEvalVerdict, type ConflictEntry, type ConflictJudge, type ConflictLintReport, type ConflictPair, type ConflictVerdict, type ConsumptionEntry, type ConsumptionInspection, type ConsumptionLintConfig, DEFAULT_CONFLICT_SIMILARITY_THRESHOLD, type DoctorApplyLintMutation, type DoctorApplyLintMutationKind, type DoctorApplyLintReport, type DoctorFixReport, type DoctorIssue, type DoctorReport, EVENT_LEDGER_PATH, type EnrichDescriptionsCandidate, type EnrichDescriptionsMode, type EnrichDescriptionsReport, FABRIC_SERVER_INSTRUCTIONS, type HistoryAllReport, type HistoryDayRow, type InFlightTracker, type KnowledgeCensus, LEDGER_PATH, LEGACY_LEDGER_PATH, METRICS_LEDGER_PATH, METRIC_COUNTER_NAMES, type MetricCounterName, type MetricsRow, OPTIONAL_EMBED_PACKAGE, type PlanContextInput, type PlanContextResult, type PrecheckResult, RETIRED_TOKENS, type RecallInput, type RecallResult, type RelatedBrokenLink, type RelatedGraphInspection, type RelatedGraphNode, type RelatedGraphNodeRich, type RelatedHubEntry, type RequirementProfile, type RetiredReferenceHit, type RetiredReferenceInspection, type RetiredToken, type SelectionTokenState, type ShutdownHandlerDeps, type StoreCanonicalEntry, type StoreReachability, type SuggestedRelatedEdge, type SurfaceVerdict, type UnboundProjectViolation, type WhyNotSurfacedResult, aggregateConsumption, appendEventLedgerEvent, buildAlwaysActiveBodies, buildColdEvalBatch, buildKnowledgeCensus, buildRelatedGraph, bumpCounter, clearPrecheckCache, collectStoreCanonicalEntries, contextCache, createFabricServer, createInFlightTracker, createShutdownHandler, defaultEmbedCacheDir, detectUnboundProject, drainCounters, enrichDescriptions, evaluateStoreDir, explainWhyNotSurfaced, extractKnowledge, findConflictCandidates, flushAndSyncEventLedger, flushMetrics, formatPreexistingRootMessage, getEventLedgerPath, getLedgerPath, getLegacyLedgerPath, getMetricsLedgerPath, inspectConsumption, inspectRelatedGraph, inspectRetiredReferences, inspectSuggestedRelatedEdges, isEmbedderResolvable, lintConflicts, loadConflictEntries, loadEmbedder, pairSimilarity, planContext, precheckStoreReachability, readEmbedConfig, readEventLedger, readFusion, readLedger, readMetrics, readSelectionToken, recall, rehydrateAgentsMetaAt, resolveLedgerPaths, reviewKnowledge, reviewPending, runDoctorApplyLint, runDoctorArchiveHistory, runDoctorCiteCoverage, runDoctorConflictLint, runDoctorFix, runDoctorHistoryAll, runDoctorReport, startMetricsFlush, startRotationTick, startStdioServer, stopMetricsFlush, stopRotationTick, suggestRelatedEdges };
package/dist/index.js CHANGED
@@ -817,6 +817,64 @@ function readOrphanDemoteThresholdDays(projectRoot) {
817
817
  return {};
818
818
  }
819
819
  }
820
+ function readCredibilityHalfLives(projectRoot) {
821
+ const defaults = {
822
+ decisions: 180,
823
+ guidelines: 150,
824
+ models: 150,
825
+ pitfalls: 120,
826
+ processes: 120
827
+ };
828
+ try {
829
+ const cfg = readFabricConfig(projectRoot);
830
+ const validate = (v) => {
831
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 1 || v > 3650 || !Number.isInteger(v)) {
832
+ return void 0;
833
+ }
834
+ return v;
835
+ };
836
+ const out = { ...defaults };
837
+ const dec = validate(cfg.credibility_half_life_decisions_days);
838
+ if (dec !== void 0) out.decisions = dec;
839
+ const gui = validate(cfg.credibility_half_life_guidelines_days);
840
+ if (gui !== void 0) out.guidelines = gui;
841
+ const mod = validate(cfg.credibility_half_life_models_days);
842
+ if (mod !== void 0) out.models = mod;
843
+ const pit = validate(cfg.credibility_half_life_pitfalls_days);
844
+ if (pit !== void 0) out.pitfalls = pit;
845
+ const pro = validate(cfg.credibility_half_life_processes_days);
846
+ if (pro !== void 0) out.processes = pro;
847
+ return out;
848
+ } catch {
849
+ return { ...defaults };
850
+ }
851
+ }
852
+ function readCredibilityFloors(projectRoot) {
853
+ const defaults = {
854
+ draft: 0.4,
855
+ verified: 0.55,
856
+ proven: 0.7
857
+ };
858
+ try {
859
+ const cfg = readFabricConfig(projectRoot);
860
+ const validate = (v) => {
861
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0 || v > 1) {
862
+ return void 0;
863
+ }
864
+ return v;
865
+ };
866
+ const out = { ...defaults };
867
+ const draft = validate(cfg.credibility_floor_draft);
868
+ if (draft !== void 0) out.draft = draft;
869
+ const verified = validate(cfg.credibility_floor_verified);
870
+ if (verified !== void 0) out.verified = verified;
871
+ const proven = validate(cfg.credibility_floor_proven);
872
+ if (proven !== void 0) out.proven = proven;
873
+ return out;
874
+ } catch {
875
+ return { ...defaults };
876
+ }
877
+ }
820
878
  function readFusion(projectRoot) {
821
879
  try {
822
880
  const raw = readFabricConfig(projectRoot).fusion;
@@ -1109,7 +1167,9 @@ function extractRuleDescription(source) {
1109
1167
  relevance_scope: knowledge?.relevance_scope ?? "broad",
1110
1168
  relevance_paths: knowledge?.relevance_paths ?? [],
1111
1169
  // v2.2 H2-related (W1-T7): graph edges, undefined when absent.
1112
- related: knowledge?.related
1170
+ related: knowledge?.related,
1171
+ // v2.2 glossary aliases FIELD (C-002): synonym terms, undefined when absent.
1172
+ aliases: knowledge?.aliases
1113
1173
  };
1114
1174
  }
1115
1175
  function extractDescriptionFromFrontmatter(frontmatter) {
@@ -1133,7 +1193,9 @@ function extractDescriptionFromFrontmatter(frontmatter) {
1133
1193
  relevance_scope: knowledge.relevance_scope,
1134
1194
  relevance_paths: knowledge.relevance_paths,
1135
1195
  // v2.2 H2-related (W1-T7): graph edges parsed from frontmatter.
1136
- related: knowledge.related
1196
+ related: knowledge.related,
1197
+ // v2.2 glossary aliases FIELD (C-002): synonym terms for the BM25 body.
1198
+ aliases: knowledge.aliases
1137
1199
  };
1138
1200
  }
1139
1201
  function isForbiddenCrossLayerEdge(sourceLayer, targetId) {
@@ -1222,6 +1284,7 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
1222
1284
  }
1223
1285
  return true;
1224
1286
  });
1287
+ const aliases = extractInlineArray(frontmatter, "aliases");
1225
1288
  return {
1226
1289
  id,
1227
1290
  knowledge_type,
@@ -1230,7 +1293,8 @@ function extractKnowledgeFieldsFromFrontmatter(frontmatter) {
1230
1293
  tags: tags.length > 0 ? tags : void 0,
1231
1294
  relevance_scope,
1232
1295
  relevance_paths,
1233
- related: related.length > 0 ? related : void 0
1296
+ related: related.length > 0 ? related : void 0,
1297
+ aliases: aliases.length > 0 ? aliases : void 0
1234
1298
  };
1235
1299
  }
1236
1300
  function extractScalar(frontmatter, key) {
@@ -3491,7 +3555,11 @@ async function buildScoringContext(projectRoot, revision, rawItems, opts) {
3491
3555
  const scoringContext = {
3492
3556
  nowMs: Date.now(),
3493
3557
  targetPaths: opts.targetPaths,
3494
- queryTerms: buildQueryTerms(opts.queryText)
3558
+ queryTerms: buildQueryTerms(opts.queryText),
3559
+ // PLN-004 F1: resolve the credibility half-lives + floors ONCE here (never per
3560
+ // candidate) so credibilityFactor is a pure lookup on the ranking hot path.
3561
+ credibilityHalfLives: readCredibilityHalfLives(projectRoot),
3562
+ credibilityFloors: readCredibilityFloors(projectRoot)
3495
3563
  };
3496
3564
  const docTexts = /* @__PURE__ */ new Map();
3497
3565
  for (const item of rawItems) {
@@ -3620,14 +3688,21 @@ function documentTextForItem(description) {
3620
3688
  ...description.intent_clues,
3621
3689
  ...description.tech_stack,
3622
3690
  ...description.impact,
3623
- ...description.tags ?? []
3691
+ ...description.tags ?? [],
3692
+ // v2.2 glossary aliases (C-002): long-tail synonym terms feed the flat
3693
+ // vector-embedding document verbatim, same as the BM25F summary slot below.
3694
+ ...description.aliases ?? []
3624
3695
  ].join(" ");
3625
3696
  }
3626
3697
  function documentFieldsForItem(description) {
3627
3698
  return {
3628
3699
  title: tokenize2(description.summary),
3629
3700
  tags: tokenize2([...description.tags ?? [], ...description.tech_stack].join(" ")),
3630
- summary: tokenize2([description.must_read_if, ...description.intent_clues].join(" ")),
3701
+ summary: tokenize2(
3702
+ [description.must_read_if, ...description.intent_clues, ...description.aliases ?? []].join(
3703
+ " "
3704
+ )
3705
+ ),
3631
3706
  body: tokenize2(description.impact.join(" "))
3632
3707
  };
3633
3708
  }
@@ -3716,11 +3791,28 @@ function localityBoost(item, context) {
3716
3791
  function structuralScaleFor(context) {
3717
3792
  return context.fusion === "rrf" && context.queryTerms.length > 0 ? RRF_STRUCTURAL_SCALE : 1;
3718
3793
  }
3794
+ function credibilityFactor(item, context) {
3795
+ const halfLives = context.credibilityHalfLives;
3796
+ const floors = context.credibilityFloors;
3797
+ if (halfLives === void 0 || floors === void 0) return 1;
3798
+ const createdAtRaw = item.description?.created_at;
3799
+ if (typeof createdAtRaw !== "string" || createdAtRaw.length === 0) return 1;
3800
+ const createdMs = Date.parse(createdAtRaw);
3801
+ if (!Number.isFinite(createdMs)) return 1;
3802
+ const ageDays = (context.nowMs - createdMs) / (24 * 60 * 60 * 1e3);
3803
+ if (ageDays <= 0) return 1;
3804
+ const type = item.description?.knowledge_type;
3805
+ const halfLife = type !== void 0 ? halfLives[type] : halfLives.decisions;
3806
+ const factor = Math.pow(2, -ageDays / halfLife);
3807
+ const maturity = item.description?.maturity;
3808
+ const floor = maturity !== void 0 ? floors[maturity] : floors.draft;
3809
+ return Math.max(floor, Math.min(1, factor));
3810
+ }
3719
3811
  function scoreDescriptionItem(item, context) {
3720
3812
  const content = contentScore(item, context);
3721
3813
  const structural = salienceScore(item) + recencyBoost(item, context) + localityBoost(item, context);
3722
3814
  const proximity = proximityBoost(item, context, content);
3723
- return content + structuralScaleFor(context) * structural + proximity;
3815
+ return (content + structuralScaleFor(context) * structural + proximity) * credibilityFactor(item, context);
3724
3816
  }
3725
3817
  function scoreBreakdownForItem(item, context) {
3726
3818
  const hasQuery = context.queryTerms.length > 0;
@@ -3742,7 +3834,9 @@ function scoreBreakdownForItem(item, context) {
3742
3834
  const salience = salienceScore(item) * scale;
3743
3835
  const recency = recencyBoost(item, context) * scale;
3744
3836
  const locality = localityBoost(item, context) * scale;
3745
- const final = bm25 + vector + salience + recency + locality;
3837
+ const proximity = proximityBoost(item, context, bm25 + vector);
3838
+ const credibility = credibilityFactor(item, context);
3839
+ const final = (bm25 + vector + salience + recency + locality + proximity) * credibility;
3746
3840
  return {
3747
3841
  final,
3748
3842
  ...bm25 !== 0 ? { bm25 } : {},
@@ -3751,7 +3845,9 @@ function scoreBreakdownForItem(item, context) {
3751
3845
  ...vectorRank !== void 0 ? { vector_rank: vectorRank } : {},
3752
3846
  salience,
3753
3847
  recency,
3754
- locality
3848
+ locality,
3849
+ proximity,
3850
+ credibility
3755
3851
  };
3756
3852
  }
3757
3853
  function localityTier(relevancePath, targetPath) {
@@ -3848,7 +3944,7 @@ async function recall(projectRoot, input) {
3848
3944
  return {
3849
3945
  stable_id: c.stable_id,
3850
3946
  rank: index + 1,
3851
- description: c.description,
3947
+ description: slimDescription(c.description),
3852
3948
  ...readPath ? { read_path: readPath.path } : {},
3853
3949
  ...readPath?.store ? { store: readPath.store } : {},
3854
3950
  ...isAlwaysActive(c) ? { body_in_context: true } : {},
@@ -3868,6 +3964,14 @@ function isAlwaysActive(candidate) {
3868
3964
  const { relevance_scope, knowledge_type } = candidate.description;
3869
3965
  return (relevance_scope ?? "broad") !== "narrow" && ALWAYS_ACTIVE_TYPES2.has(knowledge_type ?? "");
3870
3966
  }
3967
+ function slimDescription(d) {
3968
+ return {
3969
+ summary: d.summary,
3970
+ must_read_if: d.must_read_if,
3971
+ intent_clues: d.intent_clues,
3972
+ ...d.knowledge_type !== void 0 ? { knowledge_type: d.knowledge_type } : {}
3973
+ };
3974
+ }
3871
3975
  function buildNextSteps(planResult, paths, candidateById, candidateLookup) {
3872
3976
  const nextSteps = [];
3873
3977
  const omitted = (planResult.dropped ?? []).filter((d) => d.reason === "retrieval_budget").length;
@@ -10983,6 +11087,7 @@ function buildColdEvalBatch(candidates) {
10983
11087
  }
10984
11088
 
10985
11089
  // src/services/doctor-related-graph.ts
11090
+ import { tokenize as tokenize3 } from "@fenglimg/fabric-shared";
10986
11091
  function buildRelatedGraph(nodes) {
10987
11092
  const brokenLinks = [];
10988
11093
  const inDegree = /* @__PURE__ */ new Map();
@@ -11023,6 +11128,66 @@ function extractBareStableId(qualifiedId) {
11023
11128
  }
11024
11129
  return null;
11025
11130
  }
11131
+ var SUGGEST_CONFIDENCE_THRESHOLD = 0.6;
11132
+ var TAG_OVERLAP_BONUS = 0.15;
11133
+ var PATH_OVERLAP_BONUS = 0.15;
11134
+ function suggestRelatedEdges(nodes) {
11135
+ const pairKey = (a, b) => a < b ? `${a}|${b}` : `${b}|${a}`;
11136
+ const prepared = nodes.map((node) => ({
11137
+ qualifiedId: node.qualifiedId,
11138
+ bareId: extractBareStableId(node.qualifiedId) ?? node.qualifiedId,
11139
+ tokens: new Set(tokenize3(`${node.summary} ${node.intentClues.join(" ")}`)),
11140
+ tags: new Set(node.tags),
11141
+ paths: new Set(node.relevancePaths)
11142
+ }));
11143
+ const existing = /* @__PURE__ */ new Set();
11144
+ for (const node of nodes) {
11145
+ const aBare = extractBareStableId(node.qualifiedId) ?? node.qualifiedId;
11146
+ for (const rel of node.related) {
11147
+ existing.add(pairKey(aBare, extractBareStableId(rel) ?? rel));
11148
+ }
11149
+ }
11150
+ const out = [];
11151
+ for (let i = 0; i < prepared.length; i++) {
11152
+ for (let j = i + 1; j < prepared.length; j++) {
11153
+ const a = prepared[i];
11154
+ const b = prepared[j];
11155
+ if (a.bareId === b.bareId) continue;
11156
+ if (existing.has(pairKey(a.bareId, b.bareId))) continue;
11157
+ let intersection = 0;
11158
+ for (const t of a.tokens) if (b.tokens.has(t)) intersection++;
11159
+ const union = a.tokens.size + b.tokens.size - intersection;
11160
+ const jaccard = union === 0 ? 0 : intersection / union;
11161
+ let tagOverlap = false;
11162
+ for (const t of a.tags)
11163
+ if (b.tags.has(t)) {
11164
+ tagOverlap = true;
11165
+ break;
11166
+ }
11167
+ let pathOverlap = false;
11168
+ for (const p of a.paths)
11169
+ if (b.paths.has(p)) {
11170
+ pathOverlap = true;
11171
+ break;
11172
+ }
11173
+ let confidence = jaccard;
11174
+ if (tagOverlap) confidence += TAG_OVERLAP_BONUS;
11175
+ if (pathOverlap) confidence += PATH_OVERLAP_BONUS;
11176
+ confidence = Math.min(1, confidence);
11177
+ if (confidence < SUGGEST_CONFIDENCE_THRESHOLD) continue;
11178
+ const provenance = [];
11179
+ if (jaccard > 0) provenance.push("token-jaccard");
11180
+ if (tagOverlap) provenance.push("tag-overlap");
11181
+ if (pathOverlap) provenance.push("shared-path");
11182
+ const [source, target] = a.qualifiedId < b.qualifiedId ? [a.qualifiedId, b.qualifiedId] : [b.qualifiedId, a.qualifiedId];
11183
+ out.push({ source, target, confidence, provenance });
11184
+ }
11185
+ }
11186
+ out.sort(
11187
+ (x, y) => y.confidence - x.confidence || x.source.localeCompare(y.source) || x.target.localeCompare(y.target)
11188
+ );
11189
+ return out;
11190
+ }
11026
11191
  async function inspectRelatedGraph(projectRoot) {
11027
11192
  const entries = await collectStoreCanonicalEntries(projectRoot);
11028
11193
  return buildRelatedGraph(
@@ -11032,6 +11197,24 @@ async function inspectRelatedGraph(projectRoot) {
11032
11197
  }))
11033
11198
  );
11034
11199
  }
11200
+ var SUGGESTED_EDGES_TOP_N = 20;
11201
+ async function inspectSuggestedRelatedEdges(projectRoot) {
11202
+ let entries;
11203
+ try {
11204
+ entries = await collectStoreCanonicalEntries(projectRoot);
11205
+ } catch {
11206
+ return [];
11207
+ }
11208
+ const nodes = entries.map((entry) => ({
11209
+ qualifiedId: entry.qualifiedId,
11210
+ summary: entry.description.summary ?? "",
11211
+ intentClues: entry.description.intent_clues ?? [],
11212
+ tags: entry.description.tags ?? [],
11213
+ relevancePaths: entry.description.relevance_paths ?? [],
11214
+ related: entry.description.related ?? []
11215
+ }));
11216
+ return suggestRelatedEdges(nodes).slice(0, SUGGESTED_EDGES_TOP_N);
11217
+ }
11035
11218
 
11036
11219
  // src/services/store-precheck.ts
11037
11220
  import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
@@ -11513,7 +11696,7 @@ function createFabricServer(tracker) {
11513
11696
  const server = new McpServer(
11514
11697
  {
11515
11698
  name: "fabric-knowledge-server",
11516
- version: "2.3.0-rc.6"
11699
+ version: "2.3.0-rc.7"
11517
11700
  },
11518
11701
  {
11519
11702
  instructions: FABRIC_SERVER_INSTRUCTIONS
@@ -11667,6 +11850,7 @@ export {
11667
11850
  inspectConsumption,
11668
11851
  inspectRelatedGraph,
11669
11852
  inspectRetiredReferences,
11853
+ inspectSuggestedRelatedEdges,
11670
11854
  isEmbedderResolvable,
11671
11855
  lintConflicts,
11672
11856
  loadConflictEntries,
@@ -11696,5 +11880,6 @@ export {
11696
11880
  startRotationTick,
11697
11881
  startStdioServer,
11698
11882
  stopMetricsFlush,
11699
- stopRotationTick
11883
+ stopRotationTick,
11884
+ suggestRelatedEdges
11700
11885
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fenglimg/fabric-server",
3
- "version": "2.3.0-rc.6",
3
+ "version": "2.3.0-rc.7",
4
4
  "description": "Fabric MCP knowledge server — stdio transport for Claude Code / Codex CLI, manages .fabric/ knowledge base + agents.meta.json + event ledger.",
5
5
  "license": "MIT",
6
6
  "author": "wangzhichao <fenglimg90@gmail.com>",
@@ -37,7 +37,7 @@
37
37
  "@modelcontextprotocol/sdk": "^1.29.0",
38
38
  "minimatch": "^10.0.1",
39
39
  "zod": "^3.25.0",
40
- "@fenglimg/fabric-shared": "2.3.0-rc.6"
40
+ "@fenglimg/fabric-shared": "2.3.0-rc.7"
41
41
  },
42
42
  "optionalDependencies": {
43
43
  "fastembed": "^2.0.0"