@fenglimg/fabric-server 2.3.0-rc.3 → 2.3.0-rc.4
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 +175 -2
- package/dist/index.js +477 -68
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { FabExtractKnowledgeInput, FabExtractKnowledgeOutput, FabReviewInput, FabReviewOutput, FabPendingInput, FabPendingOutput } from '@fenglimg/fabric-shared/schemas/api-contracts';
|
|
3
|
-
import { EventLedgerEventInput, EventLedgerEvent, RuleDescriptionIndexItem, RecallScore, RecallScoreBreakdown, LedgerEntry, AgentsMeta } from '@fenglimg/fabric-shared';
|
|
3
|
+
import { RuleDescription, EventLedgerEventInput, EventLedgerEvent, RuleDescriptionIndexItem, RecallScore, RecallScoreBreakdown, LedgerEntry, AgentsMeta } from '@fenglimg/fabric-shared';
|
|
4
4
|
import { PayloadGuardOptions } from '@fenglimg/fabric-shared/node/mcp-payload-guard';
|
|
5
5
|
|
|
6
6
|
interface InFlightTracker {
|
|
@@ -33,6 +33,8 @@ type DoctorHealth = {
|
|
|
33
33
|
};
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
declare function extractRuleDescription(source: string): RuleDescription | undefined;
|
|
37
|
+
|
|
36
38
|
interface AlwaysActiveBody {
|
|
37
39
|
/** store-qualified id (`<alias>:<stableId>`). */
|
|
38
40
|
stable_id: string;
|
|
@@ -78,6 +80,16 @@ interface KnowledgeCensus {
|
|
|
78
80
|
*/
|
|
79
81
|
declare function buildKnowledgeCensus(projectRoot: string): Promise<KnowledgeCensus>;
|
|
80
82
|
declare function buildAlwaysActiveBodies(projectRoot: string): Promise<AlwaysActiveBody[]>;
|
|
83
|
+
interface StoreCanonicalEntry {
|
|
84
|
+
stableId: string;
|
|
85
|
+
qualifiedId: string;
|
|
86
|
+
file: string;
|
|
87
|
+
type: string;
|
|
88
|
+
layer: "team" | "personal";
|
|
89
|
+
body: string;
|
|
90
|
+
description: NonNullable<ReturnType<typeof extractRuleDescription>>;
|
|
91
|
+
}
|
|
92
|
+
declare function collectStoreCanonicalEntries(projectRoot: string): Promise<StoreCanonicalEntry[]>;
|
|
81
93
|
|
|
82
94
|
interface UnboundProjectViolation {
|
|
83
95
|
/** The store already bound as the active write target. */
|
|
@@ -196,6 +208,11 @@ type CiteCoverageReport = {
|
|
|
196
208
|
uncorrelatable_edits?: number;
|
|
197
209
|
recall_backed_edits?: number;
|
|
198
210
|
recall_coverage_rate?: number | null;
|
|
211
|
+
recall_diagnostics?: {
|
|
212
|
+
recalls_in_window: number;
|
|
213
|
+
recall_sessions: number;
|
|
214
|
+
recall_sessions_correlated: number;
|
|
215
|
+
};
|
|
199
216
|
exposed_and_mutated?: {
|
|
200
217
|
count: number;
|
|
201
218
|
ids?: string[];
|
|
@@ -541,6 +558,20 @@ interface Embedder {
|
|
|
541
558
|
embed(texts: string[]): Promise<number[][]>;
|
|
542
559
|
}
|
|
543
560
|
declare const OPTIONAL_EMBED_PACKAGE = "fastembed";
|
|
561
|
+
/**
|
|
562
|
+
* Is the optional embedder package resolvable FROM THE SERVER'S module location —
|
|
563
|
+
* i.e. from exactly where `loadEmbedder`'s dynamic `import()` will look for it?
|
|
564
|
+
*
|
|
565
|
+
* This is the honest "is fastembed installed?" probe. The naive alternative — a
|
|
566
|
+
* `createRequire(import.meta.url)` check run inside the CLI package — answers from
|
|
567
|
+
* the WRONG base: in a pnpm / non-hoisted layout (or a dev-linked global install)
|
|
568
|
+
* fastembed lives under the SERVER's `node_modules` (it is the server's
|
|
569
|
+
* optionalDependency), so the CLI cannot resolve it even though the server — the
|
|
570
|
+
* only code that actually imports it — can. That mismatch made `fabric info
|
|
571
|
+
* --recall` report "not installed" on a perfectly working setup. Callers that
|
|
572
|
+
* surface embedder availability MUST use this server-anchored probe.
|
|
573
|
+
*/
|
|
574
|
+
declare function isEmbedderResolvable(): boolean;
|
|
544
575
|
declare function defaultEmbedCacheDir(): string;
|
|
545
576
|
/**
|
|
546
577
|
* Lazy-load the optional fastembed embedder, pinned to CPU + cache-only. Returns
|
|
@@ -680,6 +711,148 @@ declare const COLD_EVAL_RUBRIC: string;
|
|
|
680
711
|
*/
|
|
681
712
|
declare function buildColdEvalBatch(candidates: ColdEvalCandidate[]): ColdEvalBatch;
|
|
682
713
|
|
|
714
|
+
interface RelatedBrokenLink {
|
|
715
|
+
/** The entry (store-qualified id) that declared the `related` edge. */
|
|
716
|
+
source: string;
|
|
717
|
+
/** The unresolvable related value. */
|
|
718
|
+
target: string;
|
|
719
|
+
}
|
|
720
|
+
interface RelatedHubEntry {
|
|
721
|
+
stableId: string;
|
|
722
|
+
inDegree: number;
|
|
723
|
+
}
|
|
724
|
+
interface RelatedGraphInspection {
|
|
725
|
+
brokenLinks: RelatedBrokenLink[];
|
|
726
|
+
hubEntries: RelatedHubEntry[];
|
|
727
|
+
/** Total canonical entries scanned. */
|
|
728
|
+
totalEntries: number;
|
|
729
|
+
}
|
|
730
|
+
/** A single node fed into the pure graph builder. */
|
|
731
|
+
interface RelatedGraphNode {
|
|
732
|
+
/** Store-qualified id (`<alias>:<stableId>`). */
|
|
733
|
+
qualifiedId: string;
|
|
734
|
+
/** The entry's declared `related` edges (frontmatter), if any. */
|
|
735
|
+
related?: string[];
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Build the related-edge graph from a list of nodes and report broken links +
|
|
739
|
+
* hub ranking. Pure — no I/O — so the broken-link / in-degree logic can be
|
|
740
|
+
* tested with hand-built fixtures.
|
|
741
|
+
*
|
|
742
|
+
* A "broken link" is a `related` value that does not appear as any node's
|
|
743
|
+
* store-qualified id NOR bare stable_id in the corpus. Store-qualified
|
|
744
|
+
* references (`alias:id`) and bare references (`id`) both resolve, because the
|
|
745
|
+
* id index carries both forms for every node.
|
|
746
|
+
*/
|
|
747
|
+
declare function buildRelatedGraph(nodes: RelatedGraphNode[]): RelatedGraphInspection;
|
|
748
|
+
/**
|
|
749
|
+
* Walk the store canonical corpus and build the related graph. Never throws —
|
|
750
|
+
* collectStoreCanonicalEntries degrades to [] when no store is in the read-set.
|
|
751
|
+
*
|
|
752
|
+
* Note: the previous (dead, never-wired) draft of this module re-parsed each
|
|
753
|
+
* entry's frontmatter via extractRuleDescription(entry.file) — passing a FILE
|
|
754
|
+
* PATH where a raw-markdown SOURCE was expected, which silently produced no
|
|
755
|
+
* edges. StoreCanonicalEntry already carries the parsed `description`, so we
|
|
756
|
+
* read `description.related` directly (the single source the recall path uses).
|
|
757
|
+
*/
|
|
758
|
+
declare function inspectRelatedGraph(projectRoot: string): Promise<RelatedGraphInspection>;
|
|
759
|
+
|
|
760
|
+
interface StoreReachability {
|
|
761
|
+
uuid: string;
|
|
762
|
+
alias: string;
|
|
763
|
+
reachable: boolean;
|
|
764
|
+
reason?: string;
|
|
765
|
+
}
|
|
766
|
+
interface PrecheckResult {
|
|
767
|
+
stores: StoreReachability[];
|
|
768
|
+
allReachable: boolean;
|
|
769
|
+
}
|
|
770
|
+
declare function clearPrecheckCache(): void;
|
|
771
|
+
/**
|
|
772
|
+
* Evaluate a single store directory's reachability. A store is reachable when
|
|
773
|
+
* BOTH hold:
|
|
774
|
+
* 1. The store directory exists on disk.
|
|
775
|
+
* 2. The directory contains either a `store.json` (valid JSON, local-only
|
|
776
|
+
* store) OR a `.git/` subdirectory (git-cloned store).
|
|
777
|
+
*
|
|
778
|
+
* Pure (filesystem-only, no config resolution) so the rule is testable with a
|
|
779
|
+
* temp directory regardless of the machine's ~/.fabric layout.
|
|
780
|
+
*/
|
|
781
|
+
declare function evaluateStoreDir(storeDir: string, identity: {
|
|
782
|
+
uuid: string;
|
|
783
|
+
alias: string;
|
|
784
|
+
}): StoreReachability;
|
|
785
|
+
/**
|
|
786
|
+
* Check that every store in the project's read-set is reachable on disk.
|
|
787
|
+
* Results are cached for 60s (CACHE_TTL_MS). Call `clearPrecheckCache()` when a
|
|
788
|
+
* store mount / unmount operation invalidates the cache.
|
|
789
|
+
*
|
|
790
|
+
* `globalRoot` is injectable for hermetic tests; production callers use the
|
|
791
|
+
* resolved ~/.fabric root.
|
|
792
|
+
*/
|
|
793
|
+
declare function precheckStoreReachability(projectRoot: string, globalRoot?: string, now?: number): Promise<PrecheckResult>;
|
|
794
|
+
|
|
795
|
+
interface ConsumptionLintConfig {
|
|
796
|
+
/** Rolling window in days (default 30). */
|
|
797
|
+
windowDays: number;
|
|
798
|
+
/** How many top-consumed entries to report (default 10). */
|
|
799
|
+
topN: number;
|
|
800
|
+
/** Minimum CORPUS size before the lint runs at all (fresh-install guard). */
|
|
801
|
+
minTotalEntries: number;
|
|
802
|
+
/**
|
|
803
|
+
* Data-maturity gate axis 1: minimum number of metrics windows (rows) that
|
|
804
|
+
* recorded ≥1 consumption counter before zero-consumed is trustworthy.
|
|
805
|
+
*/
|
|
806
|
+
minConsumedWindows: number;
|
|
807
|
+
/**
|
|
808
|
+
* Data-maturity gate axis 2: minimum total consumption events in the window
|
|
809
|
+
* before zero-consumed is trustworthy.
|
|
810
|
+
*/
|
|
811
|
+
minConsumedEvents: number;
|
|
812
|
+
}
|
|
813
|
+
interface ConsumptionEntry {
|
|
814
|
+
/** Store-qualified id (`<alias>:<stableId>`). */
|
|
815
|
+
stableId: string;
|
|
816
|
+
count: number;
|
|
817
|
+
}
|
|
818
|
+
interface ConsumptionInspection {
|
|
819
|
+
topConsumed: ConsumptionEntry[];
|
|
820
|
+
/**
|
|
821
|
+
* Corpus entries never consumed in the window. ALWAYS [] when the data is not
|
|
822
|
+
* mature (dataMature === false) — the gate suppresses the noisy signal.
|
|
823
|
+
*/
|
|
824
|
+
zeroConsumed: string[];
|
|
825
|
+
/** Total corpus entries considered for the zero-consumed denominator. */
|
|
826
|
+
totalEntries: number;
|
|
827
|
+
/** Distinct entries with ≥1 consumption in the window. */
|
|
828
|
+
consumedEntries: number;
|
|
829
|
+
/** Number of metrics windows (rows) that carried ≥1 consumption counter. */
|
|
830
|
+
consumedWindows: number;
|
|
831
|
+
/** Sum of all consumption counts in the window. */
|
|
832
|
+
totalConsumedEvents: number;
|
|
833
|
+
/** True when both maturity axes clear their thresholds. */
|
|
834
|
+
dataMature: boolean;
|
|
835
|
+
windowDays: number;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Aggregate consumption from metrics rows against the canonical corpus id list.
|
|
839
|
+
* Pure — no I/O — so the windowing, prefix parsing, top-N ranking, zero-consumed
|
|
840
|
+
* computation, and the maturity gate are all testable with fixtures.
|
|
841
|
+
*
|
|
842
|
+
* @param rows metrics.jsonl rows (any window/order)
|
|
843
|
+
* @param allQualifiedIds store-qualified ids of the canonical corpus
|
|
844
|
+
* @param now current epoch ms (injected for deterministic windowing)
|
|
845
|
+
*/
|
|
846
|
+
declare function aggregateConsumption(rows: MetricsRow[], allQualifiedIds: string[], now: number, config?: Partial<ConsumptionLintConfig>): ConsumptionInspection;
|
|
847
|
+
/**
|
|
848
|
+
* Read metrics.jsonl + the canonical corpus and aggregate consumption. Never
|
|
849
|
+
* throws — readMetrics returns [] on a missing ledger and collectStoreCanonical
|
|
850
|
+
* Entries returns [] when no store is in the read-set.
|
|
851
|
+
*
|
|
852
|
+
* `now` is injectable for deterministic tests.
|
|
853
|
+
*/
|
|
854
|
+
declare function inspectConsumption(projectRoot: string, config?: Partial<ConsumptionLintConfig>, now?: number): Promise<ConsumptionInspection>;
|
|
855
|
+
|
|
683
856
|
type StoredEventLedgerEvent = EventLedgerEvent;
|
|
684
857
|
type ReadEventLedgerOptions = {
|
|
685
858
|
event_type?: EventLedgerEvent["event_type"];
|
|
@@ -974,4 +1147,4 @@ interface ShutdownHandlerDeps {
|
|
|
974
1147
|
*/
|
|
975
1148
|
declare function createShutdownHandler(deps: ShutdownHandlerDeps): () => void;
|
|
976
1149
|
|
|
977
|
-
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, 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, RETIRED_TOKENS, type RecallInput, type RecallResult, type RequirementProfile, type RetiredReferenceHit, type RetiredReferenceInspection, type RetiredToken, type SelectionTokenState, type ShutdownHandlerDeps, type SurfaceVerdict, type UnboundProjectViolation, type WhyNotSurfacedResult, appendEventLedgerEvent, buildAlwaysActiveBodies, buildColdEvalBatch, buildKnowledgeCensus, bumpCounter, contextCache, createFabricServer, createInFlightTracker, createShutdownHandler, defaultEmbedCacheDir, detectUnboundProject, drainCounters, enrichDescriptions, explainWhyNotSurfaced, extractKnowledge, findConflictCandidates, flushAndSyncEventLedger, flushMetrics, formatPreexistingRootMessage, getEventLedgerPath, getLedgerPath, getLegacyLedgerPath, getMetricsLedgerPath, inspectRetiredReferences, lintConflicts, loadConflictEntries, loadEmbedder, pairSimilarity, planContext, readEmbedConfig, readEventLedger, readFusion, readLedger, readMetrics, readSelectionToken, recall, rehydrateAgentsMetaAt, resolveLedgerPaths, reviewKnowledge, reviewPending, runDoctorApplyLint, runDoctorArchiveHistory, runDoctorCiteCoverage, runDoctorConflictLint, runDoctorFix, runDoctorHistoryAll, runDoctorReport, startMetricsFlush, startRotationTick, startStdioServer, stopMetricsFlush, stopRotationTick };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { existsSync as
|
|
3
|
-
import { readFile as
|
|
4
|
-
import { join as
|
|
2
|
+
import { existsSync as existsSync10 } from "fs";
|
|
3
|
+
import { readFile as readFile21 } from "fs/promises";
|
|
4
|
+
import { join as join26, resolve as resolve4 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -893,7 +893,7 @@ async function awaitFirstReconcileGate(timeoutMs = 5e3) {
|
|
|
893
893
|
|
|
894
894
|
// src/services/extract-knowledge.ts
|
|
895
895
|
import { existsSync as existsSync3 } from "fs";
|
|
896
|
-
import { readFile as
|
|
896
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
897
897
|
import { join as join7 } from "path";
|
|
898
898
|
import {
|
|
899
899
|
PROPOSED_REASON_DESCRIPTIONS_BY_LOCALE
|
|
@@ -901,6 +901,8 @@ import {
|
|
|
901
901
|
import { hasSecrets, isPersonalScope, redactPii, resolveGlobalLocale } from "@fenglimg/fabric-shared";
|
|
902
902
|
|
|
903
903
|
// src/services/cross-store-write.ts
|
|
904
|
+
import { createHash as createHash2 } from "crypto";
|
|
905
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
904
906
|
import { join as join5 } from "path";
|
|
905
907
|
import {
|
|
906
908
|
STORE_LAYOUT,
|
|
@@ -916,6 +918,7 @@ import {
|
|
|
916
918
|
PersonalScopeLeakError,
|
|
917
919
|
StoreWriteTargetUnresolvedError
|
|
918
920
|
} from "@fenglimg/fabric-shared/errors";
|
|
921
|
+
import { withFileLock as withFileLock2 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
919
922
|
function writeTargetUnresolved(scope, layer) {
|
|
920
923
|
const actionHint = layer === "personal" ? "run `fabric install --global` to mint your personal store, then retry" : `mount + bind a shared store, then set an explicit route: \`fabric store switch-write <alias> --scope ${scope}\``;
|
|
921
924
|
return new StoreWriteTargetUnresolvedError(
|
|
@@ -978,7 +981,7 @@ function resolveWriteScopeMeta(layer, projectRoot, semanticScope) {
|
|
|
978
981
|
}
|
|
979
982
|
|
|
980
983
|
// src/services/cross-store-recall.ts
|
|
981
|
-
import { readFile as
|
|
984
|
+
import { readFile as readFile4, stat } from "fs/promises";
|
|
982
985
|
import { join as join6 } from "path";
|
|
983
986
|
import {
|
|
984
987
|
buildStoreResolveInput as buildStoreResolveInput2,
|
|
@@ -1350,7 +1353,7 @@ async function walkReadSetStoresUncached(snapshot) {
|
|
|
1350
1353
|
const entries = await Promise.all(snapshot.refs.map(async (ref) => {
|
|
1351
1354
|
let source;
|
|
1352
1355
|
try {
|
|
1353
|
-
source = await
|
|
1356
|
+
source = await readFile4(ref.file, "utf8");
|
|
1354
1357
|
} catch {
|
|
1355
1358
|
return null;
|
|
1356
1359
|
}
|
|
@@ -1622,6 +1625,144 @@ function serializeBm25Model(model) {
|
|
|
1622
1625
|
function rehydrateBm25Model(serialized) {
|
|
1623
1626
|
return modelFromStats(serialized);
|
|
1624
1627
|
}
|
|
1628
|
+
var SYNONYM_PAIRS = {
|
|
1629
|
+
// Code / engineering actions
|
|
1630
|
+
refactor: ["restructure", "rewrite", "redesign", "reorganize", "clean", "rework"],
|
|
1631
|
+
optimize: ["improve", "speed-up", "tune", "accelerate", "perf", "performance"],
|
|
1632
|
+
debug: ["fix", "troubleshoot", "diagnose", "investigate", "resolve", "correct"],
|
|
1633
|
+
migrate: ["port", "move", "transfer", "upgrade", "transition", "convert"],
|
|
1634
|
+
"set up": ["init", "initialize", "configure", "bootstrap", "install", "onboard"],
|
|
1635
|
+
implement: ["add", "build", "create", "develop", "write", "introduce", "introduce"],
|
|
1636
|
+
// Architecture / design
|
|
1637
|
+
architecture: ["design", "structure", "layout", "organization", "pattern", "system"],
|
|
1638
|
+
"data model": ["schema", "entity", "type", "structure", "contract", "shape"],
|
|
1639
|
+
// Quality / correctness
|
|
1640
|
+
test: ["verify", "validate", "assert", "check", "spec", "coverage"],
|
|
1641
|
+
lint: ["check", "validate", "audit", "inspect", "analyze"],
|
|
1642
|
+
// Communication / impact
|
|
1643
|
+
documentation: ["docs", "readme", "guide", "spec", "explanation", "reference"],
|
|
1644
|
+
decision: ["adr", "rationale", "why", "motivation", "reason", "trade-off"],
|
|
1645
|
+
// Change management
|
|
1646
|
+
release: ["deploy", "ship", "publish", "cut", "version", "tag"],
|
|
1647
|
+
rollback: ["revert", "undo", "back-out", "backout", "restore"],
|
|
1648
|
+
// Containers / infra
|
|
1649
|
+
container: ["docker", "image", "oci", "cri-o"],
|
|
1650
|
+
deploy: ["release", "rollout", "ship", "publish", "promote"],
|
|
1651
|
+
// Project / process
|
|
1652
|
+
on_boarding: ["getting-started", "quickstart", "newcomer", "new-hire", "first-time"],
|
|
1653
|
+
best_practice: ["convention", "guideline", "standard", "rule", "recommendation"],
|
|
1654
|
+
// Tech stacks
|
|
1655
|
+
api: ["endpoint", "route", "service", "interface", "rpc", "rest"],
|
|
1656
|
+
typescript: ["ts", "types", "type-safe"],
|
|
1657
|
+
react: ["jsx", "tsx", "component", "ui"],
|
|
1658
|
+
node: ["nodejs", "runtime", "backend", "server"],
|
|
1659
|
+
database: ["db", "sql", "nosql", "storage", "persistence"]
|
|
1660
|
+
};
|
|
1661
|
+
var STEMMING_PATTERNS = [
|
|
1662
|
+
{ suffix: "e", alternatives: ["es", "ed", "ing", "ation"] },
|
|
1663
|
+
{ suffix: "y", alternatives: ["ies", "ied", "ying"] }
|
|
1664
|
+
// Catch-all for non-verb / already-stemmed query terms: no-op by default.
|
|
1665
|
+
];
|
|
1666
|
+
var DEFAULT_IDF_WEIGHT = 1;
|
|
1667
|
+
var COMMON_TERMS = /* @__PURE__ */ new Set([
|
|
1668
|
+
"a",
|
|
1669
|
+
"an",
|
|
1670
|
+
"the",
|
|
1671
|
+
"and",
|
|
1672
|
+
"or",
|
|
1673
|
+
"but",
|
|
1674
|
+
"in",
|
|
1675
|
+
"on",
|
|
1676
|
+
"at",
|
|
1677
|
+
"to",
|
|
1678
|
+
"for",
|
|
1679
|
+
"of",
|
|
1680
|
+
"with",
|
|
1681
|
+
"by",
|
|
1682
|
+
"from",
|
|
1683
|
+
"as",
|
|
1684
|
+
"is",
|
|
1685
|
+
"it",
|
|
1686
|
+
"at",
|
|
1687
|
+
"be",
|
|
1688
|
+
"do",
|
|
1689
|
+
"has",
|
|
1690
|
+
"have",
|
|
1691
|
+
"was",
|
|
1692
|
+
"are",
|
|
1693
|
+
"been",
|
|
1694
|
+
"this",
|
|
1695
|
+
"that",
|
|
1696
|
+
"these",
|
|
1697
|
+
"those",
|
|
1698
|
+
"will",
|
|
1699
|
+
"can",
|
|
1700
|
+
"may",
|
|
1701
|
+
"would",
|
|
1702
|
+
"could",
|
|
1703
|
+
"should",
|
|
1704
|
+
"does",
|
|
1705
|
+
"not",
|
|
1706
|
+
"no",
|
|
1707
|
+
"if",
|
|
1708
|
+
"so",
|
|
1709
|
+
"up",
|
|
1710
|
+
"out",
|
|
1711
|
+
"all",
|
|
1712
|
+
"each",
|
|
1713
|
+
"every",
|
|
1714
|
+
"both",
|
|
1715
|
+
"some",
|
|
1716
|
+
"any",
|
|
1717
|
+
"such",
|
|
1718
|
+
"only",
|
|
1719
|
+
"own",
|
|
1720
|
+
"same",
|
|
1721
|
+
"too",
|
|
1722
|
+
"very",
|
|
1723
|
+
"just",
|
|
1724
|
+
"about",
|
|
1725
|
+
"over",
|
|
1726
|
+
"than",
|
|
1727
|
+
"then",
|
|
1728
|
+
"also"
|
|
1729
|
+
]);
|
|
1730
|
+
function idfWeight(term) {
|
|
1731
|
+
return COMMON_TERMS.has(term) ? 0.3 : DEFAULT_IDF_WEIGHT;
|
|
1732
|
+
}
|
|
1733
|
+
function expandQueryTerms(text) {
|
|
1734
|
+
const baseTerms = tokenize(text);
|
|
1735
|
+
const weighted = /* @__PURE__ */ new Map();
|
|
1736
|
+
for (const term of baseTerms) {
|
|
1737
|
+
const lower = term.toLowerCase();
|
|
1738
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
1739
|
+
const syns = SYNONYM_PAIRS[lower];
|
|
1740
|
+
if (syns !== void 0) {
|
|
1741
|
+
for (const s of syns) aliases.add(s);
|
|
1742
|
+
}
|
|
1743
|
+
for (const [key, values] of Object.entries(SYNONYM_PAIRS)) {
|
|
1744
|
+
if (values.includes(lower)) {
|
|
1745
|
+
aliases.add(key);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
for (const { suffix, alternatives } of STEMMING_PATTERNS) {
|
|
1749
|
+
if (lower.endsWith(suffix) && lower.length > suffix.length) {
|
|
1750
|
+
const stem = lower.slice(0, -suffix.length);
|
|
1751
|
+
for (const alt of alternatives) {
|
|
1752
|
+
aliases.add(stem + alt);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
const originalWeight = idfWeight(lower);
|
|
1757
|
+
weighted.set(term, originalWeight);
|
|
1758
|
+
for (const alias of aliases) {
|
|
1759
|
+
if (!weighted.has(alias)) {
|
|
1760
|
+
weighted.set(alias, originalWeight * 0.5);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
return weighted;
|
|
1765
|
+
}
|
|
1625
1766
|
function buildQueryTerms(text) {
|
|
1626
1767
|
return tokenize(text);
|
|
1627
1768
|
}
|
|
@@ -2021,7 +2162,7 @@ async function extractKnowledge(projectRoot, input) {
|
|
|
2021
2162
|
const writeScopeMeta = resolveWriteScopeMeta(layer, projectRoot, semanticScope);
|
|
2022
2163
|
await ensureParentDirectory(absolutePath);
|
|
2023
2164
|
if (existsSync3(absolutePath)) {
|
|
2024
|
-
const existing = await
|
|
2165
|
+
const existing = await readFile5(absolutePath, "utf8");
|
|
2025
2166
|
const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
|
|
2026
2167
|
if (existingKey === effectiveIdempotencyKey) {
|
|
2027
2168
|
const fresh2 = renderFreshEntry({
|
|
@@ -2154,7 +2295,7 @@ async function resolveDisambiguatedSlugPath(args) {
|
|
|
2154
2295
|
idempotencyKey: candidateKey
|
|
2155
2296
|
};
|
|
2156
2297
|
}
|
|
2157
|
-
const existing = await
|
|
2298
|
+
const existing = await readFile5(candidatePath, "utf8");
|
|
2158
2299
|
const existingKey = readFrontmatterKey(existing, "x-fabric-idempotency-key");
|
|
2159
2300
|
if (existingKey === candidateKey) {
|
|
2160
2301
|
return {
|
|
@@ -2752,15 +2893,24 @@ var LEDGER_DUAL_WRITE_METRIC_NAMES = {
|
|
|
2752
2893
|
};
|
|
2753
2894
|
|
|
2754
2895
|
// src/services/plan-context.ts
|
|
2755
|
-
import { mkdir as mkdir4, readFile as
|
|
2896
|
+
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
|
|
2756
2897
|
import { join as join10 } from "path";
|
|
2757
2898
|
|
|
2758
2899
|
// src/services/vector-retrieval.ts
|
|
2759
2900
|
import { mkdirSync } from "fs";
|
|
2901
|
+
import { createRequire } from "module";
|
|
2760
2902
|
import { join as join9 } from "path";
|
|
2761
2903
|
import { resolveGlobalRoot as resolveGlobalRoot3 } from "@fenglimg/fabric-shared";
|
|
2762
2904
|
var embedderLoad;
|
|
2763
2905
|
var OPTIONAL_EMBED_PACKAGE = "fastembed";
|
|
2906
|
+
function isEmbedderResolvable() {
|
|
2907
|
+
try {
|
|
2908
|
+
createRequire(import.meta.url).resolve(OPTIONAL_EMBED_PACKAGE);
|
|
2909
|
+
return true;
|
|
2910
|
+
} catch {
|
|
2911
|
+
return false;
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2764
2914
|
var embedderModuleLoader = (name) => import(name);
|
|
2765
2915
|
var MISSING_EMBEDDER_HINT = "[fabric] vector semantic recall is enabled but the optional 'fastembed' package is unavailable \u2014 falling back to text-only ranking. Install it where the server resolves modules (e.g. `npm i -g fastembed`) to enable embeddings, or set embed_enabled:false to silence this.\n";
|
|
2766
2916
|
var defaultMissingEmbedderHint = () => {
|
|
@@ -3170,7 +3320,7 @@ function bm25CachePath(projectRoot, revision) {
|
|
|
3170
3320
|
}
|
|
3171
3321
|
async function loadBm25ModelFromDisk(projectRoot, revision) {
|
|
3172
3322
|
try {
|
|
3173
|
-
const raw = await
|
|
3323
|
+
const raw = await readFile6(bm25CachePath(projectRoot, revision), "utf8");
|
|
3174
3324
|
const parsed = JSON.parse(raw);
|
|
3175
3325
|
if (parsed.version !== 1) return null;
|
|
3176
3326
|
return rehydrateBm25Model(parsed);
|
|
@@ -3283,6 +3433,10 @@ async function buildScoringContext(projectRoot, revision, rawItems, opts) {
|
|
|
3283
3433
|
const configuredFusion = readFusion(projectRoot);
|
|
3284
3434
|
const vectorActive = scoringContext.vectorScores !== void 0 && scoringContext.vectorScores.size > 0;
|
|
3285
3435
|
scoringContext.fusion = configuredFusion === "auto" ? vectorActive ? "rrf" : "additive" : configuredFusion;
|
|
3436
|
+
let queryTermWeights;
|
|
3437
|
+
if (scoringContext.queryTerms.length > 0) {
|
|
3438
|
+
queryTermWeights = expandQueryTerms(opts.queryText);
|
|
3439
|
+
}
|
|
3286
3440
|
if (scoringContext.fusion === "rrf" && scoringContext.queryTerms.length > 0 && rawItems.length > 0) {
|
|
3287
3441
|
const rankIds = rawItems.map((item) => item.stable_id).sort((a, b) => compareStableIds(a, b));
|
|
3288
3442
|
if (scoringContext.bm25 !== void 0) {
|
|
@@ -3294,6 +3448,49 @@ async function buildScoringContext(projectRoot, revision, rawItems, opts) {
|
|
|
3294
3448
|
}
|
|
3295
3449
|
return scoringContext;
|
|
3296
3450
|
}
|
|
3451
|
+
var PROXIMITY_WINDOW = 6;
|
|
3452
|
+
var PROXIMITY_BOOST_CAP = 0.15;
|
|
3453
|
+
function proximityBoost(item, context, contentScore2) {
|
|
3454
|
+
if (contentScore2 <= 0) return 0;
|
|
3455
|
+
const text = context.docTexts?.get(item.stable_id);
|
|
3456
|
+
if (text === void 0 || text.length === 0) return 0;
|
|
3457
|
+
const tokens = text.toLowerCase().split(/[^a-z0-9_$#]+/u).filter(Boolean);
|
|
3458
|
+
const queryTerms = context.queryTerms;
|
|
3459
|
+
if (queryTerms.length < 2) return 0;
|
|
3460
|
+
const positions = /* @__PURE__ */ new Map();
|
|
3461
|
+
for (const qt of queryTerms) {
|
|
3462
|
+
const qtLower = qt.toLowerCase();
|
|
3463
|
+
const pos = [];
|
|
3464
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
3465
|
+
if (tokens[i] === qtLower) {
|
|
3466
|
+
pos.push(i);
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
if (pos.length > 0) {
|
|
3470
|
+
positions.set(qtLower, pos);
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
if (positions.size < 2) return 0;
|
|
3474
|
+
const termList = [...positions.entries()];
|
|
3475
|
+
let minDist = Infinity;
|
|
3476
|
+
for (let i = 0; i < termList.length; i++) {
|
|
3477
|
+
const [, posI] = termList[i];
|
|
3478
|
+
for (let j = i + 1; j < termList.length; j++) {
|
|
3479
|
+
const [, posJ] = termList[j];
|
|
3480
|
+
for (const pi of posI) {
|
|
3481
|
+
for (const pj of posJ) {
|
|
3482
|
+
const dist = Math.abs(pi - pj);
|
|
3483
|
+
if (dist < minDist) minDist = dist;
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
if (!Number.isFinite(minDist)) return 0;
|
|
3489
|
+
if (minDist >= PROXIMITY_WINDOW) return 0;
|
|
3490
|
+
const ratio = 1 - minDist / PROXIMITY_WINDOW;
|
|
3491
|
+
const boost = contentScore2 * PROXIMITY_BOOST_CAP * ratio;
|
|
3492
|
+
return boost;
|
|
3493
|
+
}
|
|
3297
3494
|
function sortDescriptionItems(rawItems, scoringContext) {
|
|
3298
3495
|
if (scoringContext === void 0) {
|
|
3299
3496
|
return [...rawItems].sort((left, right) => compareStableIds(left.stable_id, right.stable_id)).map((item) => ({ item, score: 0 }));
|
|
@@ -3436,7 +3633,8 @@ function structuralScaleFor(context) {
|
|
|
3436
3633
|
function scoreDescriptionItem(item, context) {
|
|
3437
3634
|
const content = contentScore(item, context);
|
|
3438
3635
|
const structural = salienceScore(item) + recencyBoost(item, context) + localityBoost(item, context);
|
|
3439
|
-
|
|
3636
|
+
const proximity = proximityBoost(item, context, content);
|
|
3637
|
+
return content + structuralScaleFor(context) * structural + proximity;
|
|
3440
3638
|
}
|
|
3441
3639
|
function scoreBreakdownForItem(item, context) {
|
|
3442
3640
|
const hasQuery = context.queryTerms.length > 0;
|
|
@@ -3918,7 +4116,7 @@ import { enforcePayloadLimit as enforcePayloadLimit4 } from "@fenglimg/fabric-sh
|
|
|
3918
4116
|
// src/services/review.ts
|
|
3919
4117
|
import { execFileSync } from "child_process";
|
|
3920
4118
|
import { existsSync as existsSync5 } from "fs";
|
|
3921
|
-
import { readFile as
|
|
4119
|
+
import { readFile as readFile8, readdir as readdir2, stat as stat2, unlink as unlink2 } from "fs/promises";
|
|
3922
4120
|
import { homedir } from "os";
|
|
3923
4121
|
import { basename, isAbsolute, join as join12, relative, resolve as resolve2, sep as sep2 } from "path";
|
|
3924
4122
|
|
|
@@ -3960,7 +4158,7 @@ import { allocateStoreKnowledgeId, isPersonalScope as isPersonalScope2 } from "@
|
|
|
3960
4158
|
|
|
3961
4159
|
// src/services/pending-dedupe.ts
|
|
3962
4160
|
import { existsSync as existsSync4 } from "fs";
|
|
3963
|
-
import { readdir, readFile as
|
|
4161
|
+
import { readdir, readFile as readFile7, unlink } from "fs/promises";
|
|
3964
4162
|
import { join as join11 } from "path";
|
|
3965
4163
|
var PENDING_TYPES = ["decisions", "pitfalls", "guidelines", "models", "processes"];
|
|
3966
4164
|
var DISAMBIGUATION_SUFFIX = /^(.+)-([2-9])\.md$/u;
|
|
@@ -4058,7 +4256,7 @@ async function mergePendingTwins(projectRoot) {
|
|
|
4058
4256
|
const abs = join11(dir, name);
|
|
4059
4257
|
let content;
|
|
4060
4258
|
try {
|
|
4061
|
-
content = await
|
|
4259
|
+
content = await readFile7(abs, "utf8");
|
|
4062
4260
|
} catch {
|
|
4063
4261
|
continue;
|
|
4064
4262
|
}
|
|
@@ -4253,7 +4451,7 @@ async function listPending(projectRoot, filters) {
|
|
|
4253
4451
|
const absolutePath = join12(dir, name);
|
|
4254
4452
|
let content;
|
|
4255
4453
|
try {
|
|
4256
|
-
content = await
|
|
4454
|
+
content = await readFile8(absolutePath, "utf8");
|
|
4257
4455
|
} catch {
|
|
4258
4456
|
continue;
|
|
4259
4457
|
}
|
|
@@ -4363,7 +4561,7 @@ async function approveOne(projectRoot, pendingPath) {
|
|
|
4363
4561
|
let targetAbs;
|
|
4364
4562
|
let writtenTarget = false;
|
|
4365
4563
|
try {
|
|
4366
|
-
const content = await
|
|
4564
|
+
const content = await readFile8(sourceAbs, "utf8");
|
|
4367
4565
|
const fm = parseFrontmatter(content);
|
|
4368
4566
|
const pluralType = fm.type;
|
|
4369
4567
|
if (pluralType === void 0 || !PLURAL_TYPES.includes(pluralType)) {
|
|
@@ -4435,7 +4633,7 @@ async function rejectAll(projectRoot, pendingPaths, reason) {
|
|
|
4435
4633
|
try {
|
|
4436
4634
|
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
4437
4635
|
if (existsSync5(sandboxed.abs)) {
|
|
4438
|
-
const content = await
|
|
4636
|
+
const content = await readFile8(sandboxed.abs, "utf8");
|
|
4439
4637
|
const merged = rewriteFrontmatterMerge(content, { status: "rejected" });
|
|
4440
4638
|
const rejectedAbs = sandboxed.abs.includes(`${sep2}pending${sep2}`) ? sandboxed.abs.replace(`${sep2}pending${sep2}`, `${sep2}rejected${sep2}`) : null;
|
|
4441
4639
|
if (rejectedAbs !== null) {
|
|
@@ -4462,7 +4660,7 @@ async function modifyEntry(projectRoot, pendingPath, changes) {
|
|
|
4462
4660
|
if (target === null) {
|
|
4463
4661
|
throw new Error(`modify target not found: ${pendingPath}`);
|
|
4464
4662
|
}
|
|
4465
|
-
const content = await
|
|
4663
|
+
const content = await readFile8(target.absPath, "utf8");
|
|
4466
4664
|
const fm = parseFrontmatter(content);
|
|
4467
4665
|
const currentLayer = fm.layer ?? "team";
|
|
4468
4666
|
if (fm.maturity === "verified" && changes.maturity === "proven" && fm.id !== void 0) {
|
|
@@ -4694,7 +4892,7 @@ async function listIndexedSearchEntries(source, type) {
|
|
|
4694
4892
|
}
|
|
4695
4893
|
let content;
|
|
4696
4894
|
try {
|
|
4697
|
-
content = await
|
|
4895
|
+
content = await readFile8(absolutePath, "utf8");
|
|
4698
4896
|
searchEntryIndexContentReads += 1;
|
|
4699
4897
|
} catch {
|
|
4700
4898
|
directoryCache.files.delete(absolutePath);
|
|
@@ -4863,7 +5061,7 @@ async function deferAll(projectRoot, pendingPaths, until, reason) {
|
|
|
4863
5061
|
try {
|
|
4864
5062
|
const sandboxed = resolveSandboxedPath(projectRoot, pendingPath, { allowPersonal: true });
|
|
4865
5063
|
if (existsSync5(sandboxed.abs)) {
|
|
4866
|
-
const content = await
|
|
5064
|
+
const content = await readFile8(sandboxed.abs, "utf8");
|
|
4867
5065
|
stableId = parseFrontmatter(content).id;
|
|
4868
5066
|
const patch = {
|
|
4869
5067
|
status: "deferred",
|
|
@@ -5204,7 +5402,7 @@ function registerPending(server, tracker) {
|
|
|
5204
5402
|
}
|
|
5205
5403
|
|
|
5206
5404
|
// src/services/doctor.ts
|
|
5207
|
-
import { access as access4, readFile as
|
|
5405
|
+
import { access as access4, readFile as readFile18, readdir as readdirAsync, stat as statAsync, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
|
|
5208
5406
|
import { constants as constants2 } from "fs";
|
|
5209
5407
|
import { isAbsolute as isAbsolute2, join as join23, posix as posix5, resolve as resolve3 } from "path";
|
|
5210
5408
|
import {
|
|
@@ -5420,7 +5618,7 @@ function createKnowledgeSummaryOpaqueCheck(t, inspection) {
|
|
|
5420
5618
|
}
|
|
5421
5619
|
|
|
5422
5620
|
// src/services/doctor-stable-id-collision.ts
|
|
5423
|
-
import { readFile as
|
|
5621
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
5424
5622
|
import { basename as basename2, join as join13 } from "path";
|
|
5425
5623
|
import {
|
|
5426
5624
|
buildStoreResolveInput as buildStoreResolveInput4,
|
|
@@ -5467,7 +5665,7 @@ async function inspectStoreStableIdIntegrity(projectRoot) {
|
|
|
5467
5665
|
for (const ref of await readKnowledgeAcrossStores2(resolved.dirs)) {
|
|
5468
5666
|
let source;
|
|
5469
5667
|
try {
|
|
5470
|
-
source = await
|
|
5668
|
+
source = await readFile9(ref.file, "utf8");
|
|
5471
5669
|
} catch {
|
|
5472
5670
|
continue;
|
|
5473
5671
|
}
|
|
@@ -5788,7 +5986,7 @@ function createNarrowNoPathsCheck(t, inspection) {
|
|
|
5788
5986
|
}
|
|
5789
5987
|
|
|
5790
5988
|
// src/services/doctor-broad-index.ts
|
|
5791
|
-
import { readFile as
|
|
5989
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
5792
5990
|
import { join as join15 } from "path";
|
|
5793
5991
|
var DEFAULT_BROAD_INDEX_BACKSTOP = 50;
|
|
5794
5992
|
var BROAD_INDEX_BACKSTOP_MIN = 20;
|
|
@@ -5797,7 +5995,7 @@ var BROAD_INDEX_DRIFT_RATIO = 0.8;
|
|
|
5797
5995
|
async function readBroadIndexBackstop(projectRoot) {
|
|
5798
5996
|
const configPath = join15(projectRoot, ".fabric", "fabric-config.json");
|
|
5799
5997
|
try {
|
|
5800
|
-
const raw = await
|
|
5998
|
+
const raw = await readFile10(configPath, "utf8");
|
|
5801
5999
|
const parsed = JSON.parse(raw);
|
|
5802
6000
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
5803
6001
|
const v = parsed.broad_index_backstop;
|
|
@@ -6114,7 +6312,7 @@ function createBroadReviewRecheckCheck(t, inspection) {
|
|
|
6114
6312
|
}
|
|
6115
6313
|
|
|
6116
6314
|
// src/services/doctor-scope-lint.ts
|
|
6117
|
-
import { readFile as
|
|
6315
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
6118
6316
|
import { join as join16 } from "path";
|
|
6119
6317
|
import {
|
|
6120
6318
|
buildStoreResolveInput as buildStoreResolveInput5,
|
|
@@ -6182,7 +6380,7 @@ async function lintStoreScopes(projectRoot) {
|
|
|
6182
6380
|
}
|
|
6183
6381
|
let source;
|
|
6184
6382
|
try {
|
|
6185
|
-
source = await
|
|
6383
|
+
source = await readFile11(ref.file, "utf8");
|
|
6186
6384
|
} catch {
|
|
6187
6385
|
continue;
|
|
6188
6386
|
}
|
|
@@ -6626,7 +6824,7 @@ async function inspectEventsJsonlGates(projectRoot, options = {}) {
|
|
|
6626
6824
|
}
|
|
6627
6825
|
|
|
6628
6826
|
// src/services/doctor-skill-lints.ts
|
|
6629
|
-
import { readdir as readdir3, readFile as
|
|
6827
|
+
import { readdir as readdir3, readFile as readFile12 } from "fs/promises";
|
|
6630
6828
|
import { join as join19, posix as posix2 } from "path";
|
|
6631
6829
|
var FABRIC_SKILL_SLUGS = ["fabric-archive", "fabric-review"];
|
|
6632
6830
|
var SKILL_MD_FRONTMATTER_ROOTS = [".claude/skills", ".codex/skills"];
|
|
@@ -6675,8 +6873,8 @@ async function inspectSkillRefMirror(projectRoot) {
|
|
|
6675
6873
|
let codexBody;
|
|
6676
6874
|
try {
|
|
6677
6875
|
[claudeBody, codexBody] = await Promise.all([
|
|
6678
|
-
|
|
6679
|
-
|
|
6876
|
+
readFile12(join19(claudeRef, fname), "utf8"),
|
|
6877
|
+
readFile12(join19(codexRef, fname), "utf8")
|
|
6680
6878
|
]);
|
|
6681
6879
|
} catch {
|
|
6682
6880
|
continue;
|
|
@@ -6698,7 +6896,7 @@ async function inspectSkillTokenBudget(projectRoot) {
|
|
|
6698
6896
|
const skillMdPath = join19(projectRoot, ".claude", "skills", slug, "SKILL.md");
|
|
6699
6897
|
let body;
|
|
6700
6898
|
try {
|
|
6701
|
-
body = await
|
|
6899
|
+
body = await readFile12(skillMdPath, "utf8");
|
|
6702
6900
|
} catch {
|
|
6703
6901
|
continue;
|
|
6704
6902
|
}
|
|
@@ -6722,7 +6920,7 @@ async function inspectSkillDescription(projectRoot) {
|
|
|
6722
6920
|
const skillMdPath = join19(projectRoot, ".claude", "skills", slug, "SKILL.md");
|
|
6723
6921
|
let body;
|
|
6724
6922
|
try {
|
|
6725
|
-
body = await
|
|
6923
|
+
body = await readFile12(skillMdPath, "utf8");
|
|
6726
6924
|
} catch {
|
|
6727
6925
|
continue;
|
|
6728
6926
|
}
|
|
@@ -6765,7 +6963,7 @@ async function inspectSkillMdYamlInvalid(projectRoot) {
|
|
|
6765
6963
|
const skillFile = join19(rootAbs, dirEntry.name, "SKILL.md");
|
|
6766
6964
|
let raw;
|
|
6767
6965
|
try {
|
|
6768
|
-
raw = await
|
|
6966
|
+
raw = await readFile12(skillFile, "utf8");
|
|
6769
6967
|
} catch {
|
|
6770
6968
|
continue;
|
|
6771
6969
|
}
|
|
@@ -6888,7 +7086,7 @@ function createSkillMdYamlInvalidCheck(t, inspection) {
|
|
|
6888
7086
|
}
|
|
6889
7087
|
|
|
6890
7088
|
// src/services/doctor-retired-references-lint.ts
|
|
6891
|
-
import { readdir as readdir4, readFile as
|
|
7089
|
+
import { readdir as readdir4, readFile as readFile13, stat as stat3 } from "fs/promises";
|
|
6892
7090
|
import { join as join20, posix as posix3, relative as relative2 } from "path";
|
|
6893
7091
|
var RETIRED_TOKENS = [
|
|
6894
7092
|
{ token: "fab_plan_context", replacement: "fab_recall", reason: "retrieval collapsed to one lean fab_recall (KT-DEC-0026)" },
|
|
@@ -6956,7 +7154,7 @@ async function inspectRetiredReferences(projectRoot) {
|
|
|
6956
7154
|
try {
|
|
6957
7155
|
if ((await stat3(abs)).isFile()) {
|
|
6958
7156
|
scannedFiles += 1;
|
|
6959
|
-
scanText(toRel(abs), await
|
|
7157
|
+
scanText(toRel(abs), await readFile13(abs, "utf8"), false, hits);
|
|
6960
7158
|
}
|
|
6961
7159
|
} catch {
|
|
6962
7160
|
}
|
|
@@ -6965,7 +7163,7 @@ async function inspectRetiredReferences(projectRoot) {
|
|
|
6965
7163
|
for (const file of await walkFiles(join20(projectRoot, dir), [".md"])) {
|
|
6966
7164
|
scannedFiles += 1;
|
|
6967
7165
|
try {
|
|
6968
|
-
scanText(toRel(file), await
|
|
7166
|
+
scanText(toRel(file), await readFile13(file, "utf8"), false, hits);
|
|
6969
7167
|
} catch {
|
|
6970
7168
|
}
|
|
6971
7169
|
}
|
|
@@ -6974,7 +7172,7 @@ async function inspectRetiredReferences(projectRoot) {
|
|
|
6974
7172
|
for (const file of await walkFiles(join20(projectRoot, dir), [".cjs"])) {
|
|
6975
7173
|
scannedFiles += 1;
|
|
6976
7174
|
try {
|
|
6977
|
-
scanText(toRel(file), await
|
|
7175
|
+
scanText(toRel(file), await readFile13(file, "utf8"), true, hits);
|
|
6978
7176
|
} catch {
|
|
6979
7177
|
}
|
|
6980
7178
|
}
|
|
@@ -7009,7 +7207,7 @@ function createRetiredReferenceCheck(t, inspection) {
|
|
|
7009
7207
|
|
|
7010
7208
|
// src/services/doctor-hooks-lints.ts
|
|
7011
7209
|
import { constants } from "fs";
|
|
7012
|
-
import { access, readdir as readdir5, readFile as
|
|
7210
|
+
import { access, readdir as readdir5, readFile as readFile14, stat as stat4 } from "fs/promises";
|
|
7013
7211
|
import { join as join21, posix as posix4 } from "path";
|
|
7014
7212
|
import { Script } from "vm";
|
|
7015
7213
|
var HOOKS_RUNTIME_CLIENT_DIRS = [
|
|
@@ -7079,7 +7277,7 @@ async function inspectHooksWired(projectRoot) {
|
|
|
7079
7277
|
const settingsPath = join21(projectRoot, ".claude", "settings.json");
|
|
7080
7278
|
let parsed;
|
|
7081
7279
|
try {
|
|
7082
|
-
parsed = JSON.parse(await
|
|
7280
|
+
parsed = JSON.parse(await readFile14(settingsPath, "utf8"));
|
|
7083
7281
|
} catch {
|
|
7084
7282
|
return { status: "missing-settings", missingHooks: [] };
|
|
7085
7283
|
}
|
|
@@ -7172,7 +7370,7 @@ async function inspectHooksContentDrift(projectRoot) {
|
|
|
7172
7370
|
const hashes = [];
|
|
7173
7371
|
for (const { client, abs } of copies) {
|
|
7174
7372
|
try {
|
|
7175
|
-
const body = await
|
|
7373
|
+
const body = await readFile14(abs, "utf8");
|
|
7176
7374
|
hashes.push({ client, sha: sha256(body) });
|
|
7177
7375
|
} catch {
|
|
7178
7376
|
}
|
|
@@ -7205,7 +7403,7 @@ async function inspectHooksRuntime(projectRoot) {
|
|
|
7205
7403
|
scanned += 1;
|
|
7206
7404
|
let body;
|
|
7207
7405
|
try {
|
|
7208
|
-
body = await
|
|
7406
|
+
body = await readFile14(abs, "utf8");
|
|
7209
7407
|
} catch (err) {
|
|
7210
7408
|
issues.push({
|
|
7211
7409
|
path: displayPath,
|
|
@@ -7342,7 +7540,7 @@ function createHookCacheWritabilityCheck(t, inspection) {
|
|
|
7342
7540
|
}
|
|
7343
7541
|
|
|
7344
7542
|
// src/services/doctor-bootstrap-lints.ts
|
|
7345
|
-
import { access as access2, readFile as
|
|
7543
|
+
import { access as access2, readFile as readFile15 } from "fs/promises";
|
|
7346
7544
|
import { join as join22 } from "path";
|
|
7347
7545
|
import {
|
|
7348
7546
|
BOOTSTRAP_MARKER_BEGIN,
|
|
@@ -7385,7 +7583,7 @@ async function inspectL1BootstrapSnapshotDrift(target) {
|
|
|
7385
7583
|
const canonical = resolveBootstrapCanonical();
|
|
7386
7584
|
let onDisk;
|
|
7387
7585
|
try {
|
|
7388
|
-
onDisk = await
|
|
7586
|
+
onDisk = await readFile15(abs, "utf8");
|
|
7389
7587
|
} catch {
|
|
7390
7588
|
return { status: "missing", canonical, onDisk: null };
|
|
7391
7589
|
}
|
|
@@ -7414,14 +7612,14 @@ async function inspectL2ManagedBlockDrift(target) {
|
|
|
7414
7612
|
const snapshotPath = join22(target, ".fabric", "AGENTS.md");
|
|
7415
7613
|
let snapshot;
|
|
7416
7614
|
try {
|
|
7417
|
-
snapshot = await
|
|
7615
|
+
snapshot = await readFile15(snapshotPath, "utf8");
|
|
7418
7616
|
} catch {
|
|
7419
7617
|
return { status: "ok", drifted: [] };
|
|
7420
7618
|
}
|
|
7421
7619
|
const projectRulesPath = join22(target, ".fabric", "project-rules.md");
|
|
7422
7620
|
let expectedBody = snapshot;
|
|
7423
7621
|
try {
|
|
7424
|
-
const projectRules = await
|
|
7622
|
+
const projectRules = await readFile15(projectRulesPath, "utf8");
|
|
7425
7623
|
expectedBody = `${snapshot}
|
|
7426
7624
|
---
|
|
7427
7625
|
${projectRules}`;
|
|
@@ -7435,7 +7633,7 @@ ${projectRules}`;
|
|
|
7435
7633
|
for (const abs of blockTargets) {
|
|
7436
7634
|
let content;
|
|
7437
7635
|
try {
|
|
7438
|
-
content = await
|
|
7636
|
+
content = await readFile15(abs, "utf8");
|
|
7439
7637
|
} catch {
|
|
7440
7638
|
continue;
|
|
7441
7639
|
}
|
|
@@ -7460,7 +7658,7 @@ ${projectRules}`;
|
|
|
7460
7658
|
}
|
|
7461
7659
|
const claudeMdPath = join22(target, "CLAUDE.md");
|
|
7462
7660
|
try {
|
|
7463
|
-
const claudeContent = await
|
|
7661
|
+
const claudeContent = await readFile15(claudeMdPath, "utf8");
|
|
7464
7662
|
anyManagedBlockFound = true;
|
|
7465
7663
|
const lines = claudeContent.split(/\r?\n/u);
|
|
7466
7664
|
const hasAtImport = lines.some((line) => line.trim() === "@.fabric/AGENTS.md");
|
|
@@ -7528,7 +7726,7 @@ import { appendFile as appendFile3 } from "fs/promises";
|
|
|
7528
7726
|
import { minimatch as minimatch2 } from "minimatch";
|
|
7529
7727
|
|
|
7530
7728
|
// src/services/cite-rollup.ts
|
|
7531
|
-
import { readFile as
|
|
7729
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
7532
7730
|
import { createLedgerWriteQueue as createLedgerWriteQueue3 } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
7533
7731
|
var citeRollupQueue = createLedgerWriteQueue3();
|
|
7534
7732
|
async function appendCiteRollupRow(projectRoot, row) {
|
|
@@ -7540,7 +7738,7 @@ async function readCiteRollup(projectRoot) {
|
|
|
7540
7738
|
const path = getCiteRollupPath(projectRoot);
|
|
7541
7739
|
let raw;
|
|
7542
7740
|
try {
|
|
7543
|
-
raw = await
|
|
7741
|
+
raw = await readFile16(path, "utf8");
|
|
7544
7742
|
} catch (error) {
|
|
7545
7743
|
if (isNodeError(error) && error.code === "ENOENT") return [];
|
|
7546
7744
|
throw error;
|
|
@@ -8131,6 +8329,18 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
8131
8329
|
}
|
|
8132
8330
|
}
|
|
8133
8331
|
}
|
|
8332
|
+
const editSessionIds = /* @__PURE__ */ new Set();
|
|
8333
|
+
for (const edit of editEvents) {
|
|
8334
|
+
if (typeof edit.session_id === "string" && edit.session_id.length > 0) {
|
|
8335
|
+
editSessionIds.add(edit.session_id);
|
|
8336
|
+
}
|
|
8337
|
+
}
|
|
8338
|
+
let recallsInWindow = 0;
|
|
8339
|
+
for (const list of plannedBySession.values()) recallsInWindow += list.length;
|
|
8340
|
+
let recallSessionsCorrelated = 0;
|
|
8341
|
+
for (const sid of plannedBySession.keys()) {
|
|
8342
|
+
if (editSessionIds.has(sid)) recallSessionsCorrelated += 1;
|
|
8343
|
+
}
|
|
8134
8344
|
const noneTotal = Object.values(noneHistogram).reduce((a, b) => a + b, 0);
|
|
8135
8345
|
const compliantCites = qualifyingCites + noneTotal;
|
|
8136
8346
|
const noncompliantCites = expectedButMissed;
|
|
@@ -8198,6 +8408,11 @@ async function runDoctorCiteCoverage(projectRoot, options) {
|
|
|
8198
8408
|
uncorrelatable_edits: uncorrelatableEdits,
|
|
8199
8409
|
recall_backed_edits: recallBackedEdits,
|
|
8200
8410
|
recall_coverage_rate: recallCoverageRate,
|
|
8411
|
+
recall_diagnostics: {
|
|
8412
|
+
recalls_in_window: recallsInWindow,
|
|
8413
|
+
recall_sessions: plannedBySession.size,
|
|
8414
|
+
recall_sessions_correlated: recallSessionsCorrelated
|
|
8415
|
+
},
|
|
8201
8416
|
exposed_and_mutated: {
|
|
8202
8417
|
count: exposedAndMutated.count,
|
|
8203
8418
|
...exposedAndMutated.ids.length > 0 ? { ids: exposedAndMutated.ids } : {}
|
|
@@ -9091,7 +9306,7 @@ function truncateErrorMessage(error) {
|
|
|
9091
9306
|
async function inspectForensic(projectRoot) {
|
|
9092
9307
|
const path = join23(projectRoot, ".fabric", "forensic.json");
|
|
9093
9308
|
try {
|
|
9094
|
-
const parsed = forensicReportSchema.parse(JSON.parse(await
|
|
9309
|
+
const parsed = forensicReportSchema.parse(JSON.parse(await readFile18(path, "utf8")));
|
|
9095
9310
|
return { present: true, valid: true, report: parsed };
|
|
9096
9311
|
} catch (error) {
|
|
9097
9312
|
if (isMissingFileError(error)) {
|
|
@@ -9121,7 +9336,7 @@ async function inspectEventLedger(projectRoot) {
|
|
|
9121
9336
|
try {
|
|
9122
9337
|
await access4(path, constants2.W_OK);
|
|
9123
9338
|
const { warnings } = await readEventLedger(projectRoot);
|
|
9124
|
-
const raw = await
|
|
9339
|
+
const raw = await readFile18(path, "utf8");
|
|
9125
9340
|
const invalidLine = raw.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).find((line) => !isValidJsonLine(line));
|
|
9126
9341
|
const partialWarning = warnings.find((w) => w.kind === "partial_write_at_tail");
|
|
9127
9342
|
const schemaVersionSamples = [];
|
|
@@ -9797,7 +10012,7 @@ function inspectStaleServeLock(projectRoot, now) {
|
|
|
9797
10012
|
async function readUnderseedThresholdFromConfig(projectRoot) {
|
|
9798
10013
|
const configPath = join23(projectRoot, ".fabric", "fabric-config.json");
|
|
9799
10014
|
try {
|
|
9800
|
-
const raw = await
|
|
10015
|
+
const raw = await readFile18(configPath, "utf8");
|
|
9801
10016
|
const parsed = JSON.parse(raw);
|
|
9802
10017
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
9803
10018
|
const v = parsed.underseed_node_threshold;
|
|
@@ -9916,7 +10131,7 @@ async function readOnboardOptedOut(projectRoot) {
|
|
|
9916
10131
|
const path = join23(projectRoot, ".fabric", "fabric-config.json");
|
|
9917
10132
|
let raw;
|
|
9918
10133
|
try {
|
|
9919
|
-
raw = await
|
|
10134
|
+
raw = await readFile18(path, "utf8");
|
|
9920
10135
|
} catch {
|
|
9921
10136
|
return [];
|
|
9922
10137
|
}
|
|
@@ -10024,7 +10239,7 @@ async function rewriteThreeEndManagedBlocks(projectRoot) {
|
|
|
10024
10239
|
}
|
|
10025
10240
|
let snapshot;
|
|
10026
10241
|
try {
|
|
10027
|
-
snapshot = await
|
|
10242
|
+
snapshot = await readFile18(snapshotPath, "utf8");
|
|
10028
10243
|
} catch {
|
|
10029
10244
|
return;
|
|
10030
10245
|
}
|
|
@@ -10033,7 +10248,7 @@ async function rewriteThreeEndManagedBlocks(projectRoot) {
|
|
|
10033
10248
|
let expectedBody = snapshot;
|
|
10034
10249
|
if (hasProjectRules) {
|
|
10035
10250
|
try {
|
|
10036
|
-
const projectRules = await
|
|
10251
|
+
const projectRules = await readFile18(projectRulesPath, "utf8");
|
|
10037
10252
|
expectedBody = `${snapshot}
|
|
10038
10253
|
---
|
|
10039
10254
|
${projectRules}`;
|
|
@@ -10052,7 +10267,7 @@ ${BOOTSTRAP_MARKER_END2}`;
|
|
|
10052
10267
|
}
|
|
10053
10268
|
let existing;
|
|
10054
10269
|
try {
|
|
10055
|
-
existing = await
|
|
10270
|
+
existing = await readFile18(abs, "utf8");
|
|
10056
10271
|
} catch {
|
|
10057
10272
|
continue;
|
|
10058
10273
|
}
|
|
@@ -10081,7 +10296,7 @@ ${managedBlock}
|
|
|
10081
10296
|
if (await pathExists(claudeMdPath)) {
|
|
10082
10297
|
let claudeContent;
|
|
10083
10298
|
try {
|
|
10084
|
-
claudeContent = await
|
|
10299
|
+
claudeContent = await readFile18(claudeMdPath, "utf8");
|
|
10085
10300
|
} catch {
|
|
10086
10301
|
return;
|
|
10087
10302
|
}
|
|
@@ -10223,7 +10438,7 @@ async function enrichDescriptions(projectRoot, opts = {}) {
|
|
|
10223
10438
|
scanned += 1;
|
|
10224
10439
|
let source;
|
|
10225
10440
|
try {
|
|
10226
|
-
source = await
|
|
10441
|
+
source = await readFile18(absPath, "utf8");
|
|
10227
10442
|
} catch {
|
|
10228
10443
|
continue;
|
|
10229
10444
|
}
|
|
@@ -10361,7 +10576,7 @@ async function runDoctorConflictLint(projectRoot, opts = {}) {
|
|
|
10361
10576
|
}
|
|
10362
10577
|
|
|
10363
10578
|
// src/services/why-not-surfaced.ts
|
|
10364
|
-
import { readFile as
|
|
10579
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
10365
10580
|
import { basename as basename3, join as join24 } from "path";
|
|
10366
10581
|
import {
|
|
10367
10582
|
buildStoreResolveInput as buildStoreResolveInput7,
|
|
@@ -10411,7 +10626,7 @@ async function explainWhyNotSurfaced(projectRoot, query) {
|
|
|
10411
10626
|
}
|
|
10412
10627
|
let source;
|
|
10413
10628
|
try {
|
|
10414
|
-
source = await
|
|
10629
|
+
source = await readFile19(candidate.file, "utf8");
|
|
10415
10630
|
} catch {
|
|
10416
10631
|
return base;
|
|
10417
10632
|
}
|
|
@@ -10464,6 +10679,191 @@ function buildColdEvalBatch(candidates) {
|
|
|
10464
10679
|
return { rubric: COLD_EVAL_RUBRIC, candidates: judgeable };
|
|
10465
10680
|
}
|
|
10466
10681
|
|
|
10682
|
+
// src/services/doctor-related-graph.ts
|
|
10683
|
+
function buildRelatedGraph(nodes) {
|
|
10684
|
+
const brokenLinks = [];
|
|
10685
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
10686
|
+
const allIds = /* @__PURE__ */ new Set();
|
|
10687
|
+
const edgeSources = [];
|
|
10688
|
+
for (const node of nodes) {
|
|
10689
|
+
const bareId = extractBareStableId(node.qualifiedId);
|
|
10690
|
+
allIds.add(node.qualifiedId);
|
|
10691
|
+
if (bareId !== null) {
|
|
10692
|
+
allIds.add(bareId);
|
|
10693
|
+
}
|
|
10694
|
+
const related = node.related;
|
|
10695
|
+
if (related !== void 0 && related.length > 0) {
|
|
10696
|
+
edgeSources.push({ source: node.qualifiedId, related });
|
|
10697
|
+
for (const rel of related) {
|
|
10698
|
+
inDegree.set(rel, (inDegree.get(rel) ?? 0) + 1);
|
|
10699
|
+
}
|
|
10700
|
+
}
|
|
10701
|
+
}
|
|
10702
|
+
for (const { source, related } of edgeSources) {
|
|
10703
|
+
for (const rel of related) {
|
|
10704
|
+
if (!allIds.has(rel)) {
|
|
10705
|
+
brokenLinks.push({ source, target: rel });
|
|
10706
|
+
}
|
|
10707
|
+
}
|
|
10708
|
+
}
|
|
10709
|
+
const hubEntries = [...inDegree.entries()].map(([stableId, degree]) => ({ stableId, inDegree: degree })).sort((a, b) => b.inDegree - a.inDegree || a.stableId.localeCompare(b.stableId));
|
|
10710
|
+
return {
|
|
10711
|
+
brokenLinks,
|
|
10712
|
+
hubEntries,
|
|
10713
|
+
totalEntries: nodes.length
|
|
10714
|
+
};
|
|
10715
|
+
}
|
|
10716
|
+
function extractBareStableId(qualifiedId) {
|
|
10717
|
+
const colonIdx = qualifiedId.indexOf(":");
|
|
10718
|
+
if (colonIdx > 0 && colonIdx < qualifiedId.length - 1) {
|
|
10719
|
+
return qualifiedId.slice(colonIdx + 1);
|
|
10720
|
+
}
|
|
10721
|
+
return null;
|
|
10722
|
+
}
|
|
10723
|
+
async function inspectRelatedGraph(projectRoot) {
|
|
10724
|
+
const entries = await collectStoreCanonicalEntries(projectRoot);
|
|
10725
|
+
return buildRelatedGraph(
|
|
10726
|
+
entries.map((entry) => ({
|
|
10727
|
+
qualifiedId: entry.qualifiedId,
|
|
10728
|
+
related: entry.description.related
|
|
10729
|
+
}))
|
|
10730
|
+
);
|
|
10731
|
+
}
|
|
10732
|
+
|
|
10733
|
+
// src/services/store-precheck.ts
|
|
10734
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4 } from "fs";
|
|
10735
|
+
import { join as join25 } from "path";
|
|
10736
|
+
import {
|
|
10737
|
+
buildStoreResolveInput as buildStoreResolveInput8,
|
|
10738
|
+
createStoreResolver as createStoreResolver8,
|
|
10739
|
+
resolveGlobalRoot as resolveGlobalRoot9,
|
|
10740
|
+
storeRelativePathForMount as storeRelativePathForMount7
|
|
10741
|
+
} from "@fenglimg/fabric-shared";
|
|
10742
|
+
var CACHE_TTL_MS = 6e4;
|
|
10743
|
+
var cache = /* @__PURE__ */ new Map();
|
|
10744
|
+
function clearPrecheckCache() {
|
|
10745
|
+
cache.clear();
|
|
10746
|
+
}
|
|
10747
|
+
function evaluateStoreDir(storeDir, identity) {
|
|
10748
|
+
if (!existsSync9(storeDir)) {
|
|
10749
|
+
return {
|
|
10750
|
+
uuid: identity.uuid,
|
|
10751
|
+
alias: identity.alias,
|
|
10752
|
+
reachable: false,
|
|
10753
|
+
reason: `directory not found at ${storeDir}`
|
|
10754
|
+
};
|
|
10755
|
+
}
|
|
10756
|
+
const storeJsonPath = join25(storeDir, "store.json");
|
|
10757
|
+
const gitDirPath = join25(storeDir, ".git");
|
|
10758
|
+
const hasStoreJson = existsSync9(storeJsonPath) && isValidStoreJson(storeJsonPath);
|
|
10759
|
+
const hasGitDir = existsSync9(gitDirPath);
|
|
10760
|
+
if (!hasStoreJson && !hasGitDir) {
|
|
10761
|
+
return {
|
|
10762
|
+
uuid: identity.uuid,
|
|
10763
|
+
alias: identity.alias,
|
|
10764
|
+
reachable: false,
|
|
10765
|
+
reason: `no store.json or .git found in ${storeDir}`
|
|
10766
|
+
};
|
|
10767
|
+
}
|
|
10768
|
+
return { uuid: identity.uuid, alias: identity.alias, reachable: true };
|
|
10769
|
+
}
|
|
10770
|
+
function isValidStoreJson(path) {
|
|
10771
|
+
try {
|
|
10772
|
+
JSON.parse(readFileSync4(path, "utf8"));
|
|
10773
|
+
return true;
|
|
10774
|
+
} catch {
|
|
10775
|
+
return false;
|
|
10776
|
+
}
|
|
10777
|
+
}
|
|
10778
|
+
async function precheckStoreReachability(projectRoot, globalRoot = resolveGlobalRoot9(), now = Date.now()) {
|
|
10779
|
+
const cached = cache.get(projectRoot);
|
|
10780
|
+
if (cached !== void 0 && cached.expiresAt > now) {
|
|
10781
|
+
return cached.result;
|
|
10782
|
+
}
|
|
10783
|
+
const input = buildStoreResolveInput8(projectRoot, globalRoot);
|
|
10784
|
+
if (input === null) {
|
|
10785
|
+
const result2 = { stores: [], allReachable: true };
|
|
10786
|
+
cache.set(projectRoot, { result: result2, expiresAt: now + CACHE_TTL_MS });
|
|
10787
|
+
return result2;
|
|
10788
|
+
}
|
|
10789
|
+
const readSet = createStoreResolver8().resolveReadSet(input);
|
|
10790
|
+
const stores = readSet.stores.map((entry) => {
|
|
10791
|
+
const mounted = input.mountedStores.find((s) => s.store_uuid === entry.store_uuid);
|
|
10792
|
+
const storeDir = join25(
|
|
10793
|
+
globalRoot,
|
|
10794
|
+
storeRelativePathForMount7(mounted ?? { store_uuid: entry.store_uuid })
|
|
10795
|
+
);
|
|
10796
|
+
return evaluateStoreDir(storeDir, { uuid: entry.store_uuid, alias: entry.alias });
|
|
10797
|
+
});
|
|
10798
|
+
const result = {
|
|
10799
|
+
stores,
|
|
10800
|
+
allReachable: stores.every((s) => s.reachable)
|
|
10801
|
+
};
|
|
10802
|
+
cache.set(projectRoot, { result, expiresAt: now + CACHE_TTL_MS });
|
|
10803
|
+
return result;
|
|
10804
|
+
}
|
|
10805
|
+
|
|
10806
|
+
// src/services/doctor-consumption-lint.ts
|
|
10807
|
+
var DEFAULT_WINDOW_DAYS = 30;
|
|
10808
|
+
var DEFAULT_TOP_N = 10;
|
|
10809
|
+
var DEFAULT_MIN_TOTAL_ENTRIES = 20;
|
|
10810
|
+
var DEFAULT_MIN_CONSUMED_WINDOWS = 30;
|
|
10811
|
+
var DEFAULT_MIN_CONSUMED_EVENTS = 50;
|
|
10812
|
+
var CONSUMED_PREFIX = "knowledge_consumed:";
|
|
10813
|
+
var MS_PER_DAY5 = 24 * 60 * 60 * 1e3;
|
|
10814
|
+
function resolveConfig(config) {
|
|
10815
|
+
return {
|
|
10816
|
+
windowDays: config?.windowDays ?? DEFAULT_WINDOW_DAYS,
|
|
10817
|
+
topN: config?.topN ?? DEFAULT_TOP_N,
|
|
10818
|
+
minTotalEntries: config?.minTotalEntries ?? DEFAULT_MIN_TOTAL_ENTRIES,
|
|
10819
|
+
minConsumedWindows: config?.minConsumedWindows ?? DEFAULT_MIN_CONSUMED_WINDOWS,
|
|
10820
|
+
minConsumedEvents: config?.minConsumedEvents ?? DEFAULT_MIN_CONSUMED_EVENTS
|
|
10821
|
+
};
|
|
10822
|
+
}
|
|
10823
|
+
function aggregateConsumption(rows, allQualifiedIds, now, config) {
|
|
10824
|
+
const cfg = resolveConfig(config);
|
|
10825
|
+
const cutoffMs = now - cfg.windowDays * MS_PER_DAY5;
|
|
10826
|
+
const consumed = /* @__PURE__ */ new Map();
|
|
10827
|
+
let consumedWindows = 0;
|
|
10828
|
+
let totalConsumedEvents = 0;
|
|
10829
|
+
for (const row of rows) {
|
|
10830
|
+
const rowTs = Date.parse(row.timestamp);
|
|
10831
|
+
if (!Number.isFinite(rowTs) || rowTs < cutoffMs) continue;
|
|
10832
|
+
if (row.counters === null || typeof row.counters !== "object") continue;
|
|
10833
|
+
let windowHadConsumption = false;
|
|
10834
|
+
for (const [counterName, rawCount] of Object.entries(row.counters)) {
|
|
10835
|
+
if (!counterName.startsWith(CONSUMED_PREFIX)) continue;
|
|
10836
|
+
const count = typeof rawCount === "number" ? rawCount : 0;
|
|
10837
|
+
if (count <= 0) continue;
|
|
10838
|
+
const id = counterName.slice(CONSUMED_PREFIX.length);
|
|
10839
|
+
if (id.length === 0) continue;
|
|
10840
|
+
consumed.set(id, (consumed.get(id) ?? 0) + count);
|
|
10841
|
+
totalConsumedEvents += count;
|
|
10842
|
+
windowHadConsumption = true;
|
|
10843
|
+
}
|
|
10844
|
+
if (windowHadConsumption) consumedWindows += 1;
|
|
10845
|
+
}
|
|
10846
|
+
const topConsumed = [...consumed.entries()].map(([stableId, count]) => ({ stableId, count })).sort((a, b) => b.count - a.count || a.stableId.localeCompare(b.stableId)).slice(0, cfg.topN);
|
|
10847
|
+
const dataMature = consumedWindows >= cfg.minConsumedWindows && totalConsumedEvents >= cfg.minConsumedEvents && allQualifiedIds.length >= cfg.minTotalEntries;
|
|
10848
|
+
const zeroConsumed = dataMature ? allQualifiedIds.filter((id) => !consumed.has(id)).sort((a, b) => a.localeCompare(b)) : [];
|
|
10849
|
+
return {
|
|
10850
|
+
topConsumed,
|
|
10851
|
+
zeroConsumed,
|
|
10852
|
+
totalEntries: allQualifiedIds.length,
|
|
10853
|
+
consumedEntries: consumed.size,
|
|
10854
|
+
consumedWindows,
|
|
10855
|
+
totalConsumedEvents,
|
|
10856
|
+
dataMature,
|
|
10857
|
+
windowDays: cfg.windowDays
|
|
10858
|
+
};
|
|
10859
|
+
}
|
|
10860
|
+
async function inspectConsumption(projectRoot, config, now = Date.now()) {
|
|
10861
|
+
const rows = await readMetrics(projectRoot);
|
|
10862
|
+
const entries = await collectStoreCanonicalEntries(projectRoot);
|
|
10863
|
+
const allQualifiedIds = entries.map((entry) => entry.qualifiedId);
|
|
10864
|
+
return aggregateConsumption(rows, allQualifiedIds, now, config);
|
|
10865
|
+
}
|
|
10866
|
+
|
|
10467
10867
|
// src/services/rotation-tick.ts
|
|
10468
10868
|
var DEFAULT_TICK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
10469
10869
|
var tickTimers = /* @__PURE__ */ new Map();
|
|
@@ -10499,7 +10899,7 @@ import { IOFabricError as IOFabricError2, RuleError } from "@fenglimg/fabric-sha
|
|
|
10499
10899
|
|
|
10500
10900
|
// src/services/read-ledger.ts
|
|
10501
10901
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
10502
|
-
import { access as access5, copyFile, readFile as
|
|
10902
|
+
import { access as access5, copyFile, readFile as readFile20, rm } from "fs/promises";
|
|
10503
10903
|
import { ledgerEntrySchema } from "@fenglimg/fabric-shared";
|
|
10504
10904
|
async function resolveLedgerPaths(projectRoot) {
|
|
10505
10905
|
const primaryPath = getLedgerPath(projectRoot);
|
|
@@ -10527,7 +10927,7 @@ async function readLegacyLedger(projectRoot) {
|
|
|
10527
10927
|
const { readPath } = await resolveLedgerPaths(projectRoot);
|
|
10528
10928
|
let raw;
|
|
10529
10929
|
try {
|
|
10530
|
-
raw = await
|
|
10930
|
+
raw = await readFile20(readPath, "utf8");
|
|
10531
10931
|
} catch (error) {
|
|
10532
10932
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
10533
10933
|
return [];
|
|
@@ -10782,8 +11182,8 @@ function formatError(error) {
|
|
|
10782
11182
|
}
|
|
10783
11183
|
function formatPreexistingRootMessage(projectRoot) {
|
|
10784
11184
|
const preexisting = [];
|
|
10785
|
-
if (
|
|
10786
|
-
if (
|
|
11185
|
+
if (existsSync10(join26(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
|
|
11186
|
+
if (existsSync10(join26(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
|
|
10787
11187
|
if (preexisting.length === 0) return null;
|
|
10788
11188
|
return `[startup] info: detected ${preexisting.join(", ")} at project root. Note: Fabric serves knowledge from mounted stores via MCP \u2014 root markdown files are not auto-loaded into the AI context.`;
|
|
10789
11189
|
}
|
|
@@ -10810,7 +11210,7 @@ function createFabricServer(tracker) {
|
|
|
10810
11210
|
const server = new McpServer(
|
|
10811
11211
|
{
|
|
10812
11212
|
name: "fabric-knowledge-server",
|
|
10813
|
-
version: "2.3.0-rc.
|
|
11213
|
+
version: "2.3.0-rc.4"
|
|
10814
11214
|
},
|
|
10815
11215
|
{
|
|
10816
11216
|
instructions: FABRIC_SERVER_INSTRUCTIONS
|
|
@@ -10830,10 +11230,10 @@ function createFabricServer(tracker) {
|
|
|
10830
11230
|
},
|
|
10831
11231
|
async (_uri) => {
|
|
10832
11232
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
10833
|
-
const path =
|
|
11233
|
+
const path = join26(projectRoot, ".fabric", "bootstrap", "README.md");
|
|
10834
11234
|
let text = "";
|
|
10835
|
-
if (
|
|
10836
|
-
text = await
|
|
11235
|
+
if (existsSync10(path)) {
|
|
11236
|
+
text = await readFile21(path, "utf8");
|
|
10837
11237
|
}
|
|
10838
11238
|
return {
|
|
10839
11239
|
contents: [
|
|
@@ -10933,11 +11333,15 @@ export {
|
|
|
10933
11333
|
METRIC_COUNTER_NAMES,
|
|
10934
11334
|
OPTIONAL_EMBED_PACKAGE,
|
|
10935
11335
|
RETIRED_TOKENS,
|
|
11336
|
+
aggregateConsumption,
|
|
10936
11337
|
appendEventLedgerEvent,
|
|
10937
11338
|
buildAlwaysActiveBodies,
|
|
10938
11339
|
buildColdEvalBatch,
|
|
10939
11340
|
buildKnowledgeCensus,
|
|
11341
|
+
buildRelatedGraph,
|
|
10940
11342
|
bumpCounter,
|
|
11343
|
+
clearPrecheckCache,
|
|
11344
|
+
collectStoreCanonicalEntries,
|
|
10941
11345
|
contextCache,
|
|
10942
11346
|
createFabricServer,
|
|
10943
11347
|
createInFlightTracker,
|
|
@@ -10946,6 +11350,7 @@ export {
|
|
|
10946
11350
|
detectUnboundProject,
|
|
10947
11351
|
drainCounters,
|
|
10948
11352
|
enrichDescriptions,
|
|
11353
|
+
evaluateStoreDir,
|
|
10949
11354
|
explainWhyNotSurfaced,
|
|
10950
11355
|
extractKnowledge,
|
|
10951
11356
|
findConflictCandidates,
|
|
@@ -10956,12 +11361,16 @@ export {
|
|
|
10956
11361
|
getLedgerPath,
|
|
10957
11362
|
getLegacyLedgerPath,
|
|
10958
11363
|
getMetricsLedgerPath,
|
|
11364
|
+
inspectConsumption,
|
|
11365
|
+
inspectRelatedGraph,
|
|
10959
11366
|
inspectRetiredReferences,
|
|
11367
|
+
isEmbedderResolvable,
|
|
10960
11368
|
lintConflicts,
|
|
10961
11369
|
loadConflictEntries,
|
|
10962
11370
|
loadEmbedder,
|
|
10963
11371
|
pairSimilarity,
|
|
10964
11372
|
planContext,
|
|
11373
|
+
precheckStoreReachability,
|
|
10965
11374
|
readEmbedConfig,
|
|
10966
11375
|
readEventLedger,
|
|
10967
11376
|
readFusion,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fenglimg/fabric-server",
|
|
3
|
-
"version": "2.3.0-rc.
|
|
3
|
+
"version": "2.3.0-rc.4",
|
|
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.
|
|
40
|
+
"@fenglimg/fabric-shared": "2.3.0-rc.4"
|
|
41
41
|
},
|
|
42
42
|
"optionalDependencies": {
|
|
43
43
|
"fastembed": "^2.0.0"
|