@fenglimg/fabric-shared 2.2.0 → 2.3.0-rc.10

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.js CHANGED
@@ -7,7 +7,8 @@ import {
7
7
  BOOTSTRAP_REGEX,
8
8
  matchBootstrapCanonicalLocale,
9
9
  resolveBootstrapCanonical
10
- } from "./chunk-BDJQIOQO.js";
10
+ } from "./chunk-KFFBQRL5.js";
11
+ import "./chunk-LXNCAKJZ.js";
11
12
  import {
12
13
  PROTECTED_TOKENS,
13
14
  createTranslator,
@@ -15,7 +16,7 @@ import {
15
16
  enMessages,
16
17
  resolveFabricLocale,
17
18
  zhCNMessages
18
- } from "./chunk-AQMDXC6J.js";
19
+ } from "./chunk-XRX6RVZY.js";
19
20
  import {
20
21
  GLOBAL_BINDINGS_DIR,
21
22
  GLOBAL_STATE_DIR,
@@ -50,7 +51,7 @@ import {
50
51
  storeRelativePath,
51
52
  storeRelativePathForMount,
52
53
  storeUuidSchema
53
- } from "./chunk-2GLIAZ5M.js";
54
+ } from "./chunk-ASS2KBB7.js";
54
55
  import {
55
56
  atomicWriteJson,
56
57
  withFileLock
@@ -59,6 +60,10 @@ import {
59
60
  FabExtractKnowledgeInputSchema,
60
61
  FabExtractKnowledgeInputShape,
61
62
  FabExtractKnowledgeOutputSchema,
63
+ FabPendingInputSchema,
64
+ FabPendingInputShape,
65
+ FabPendingOutputSchema,
66
+ FabPendingOutputShape,
62
67
  FabReviewInputSchema,
63
68
  FabReviewInputShape,
64
69
  FabReviewOutputSchema,
@@ -74,6 +79,7 @@ import {
74
79
  PERSONAL_SCOPE,
75
80
  PROPOSED_REASON_DESCRIPTIONS_BY_LOCALE,
76
81
  ProposedReasonSchema,
82
+ SCOPE_COORDINATE_HINT,
77
83
  SCOPE_COORDINATE_PATTERN,
78
84
  StableIdSchema,
79
85
  annotateIntentRequestSchema,
@@ -85,6 +91,7 @@ import {
85
91
  citeLayerTypeBreakdownSchema,
86
92
  entryScopeMetadataSchema,
87
93
  fabExtractKnowledgeAnnotations,
94
+ fabPendingAnnotations,
88
95
  fabReviewAnnotations,
89
96
  formatKnowledgeId,
90
97
  historyStateQuerySchema,
@@ -109,8 +116,7 @@ import {
109
116
  scopeCoordinateSchema,
110
117
  scopeRoot,
111
118
  structuredWarningSchema
112
- } from "./chunk-5AKCRBKJ.js";
113
- import "./chunk-LXNCAKJZ.js";
119
+ } from "./chunk-TV264D7E.js";
114
120
 
115
121
  // src/schemas/agents-meta.ts
116
122
  import { z } from "zod";
@@ -132,7 +138,6 @@ var ruleDescriptionSchema = z.object({
132
138
  tech_stack: z.array(z.string()),
133
139
  impact: z.array(z.string()),
134
140
  must_read_if: z.string(),
135
- entities: z.array(z.string()).optional(),
136
141
  // v2.0 knowledge entry fields (TASK-002 schemas). All optional for backward compat.
137
142
  id: z.string().optional(),
138
143
  // rc.31 NEW-1: forward-compat for legacy on-disk agents.meta.json carrying
@@ -145,8 +150,8 @@ var ruleDescriptionSchema = z.object({
145
150
  z.enum(["models", "decisions", "guidelines", "pitfalls", "processes"])
146
151
  ).optional(),
147
152
  maturity: z.enum(["draft", "verified", "proven"]).optional(),
148
- knowledge_layer: z.enum(["personal", "team"]).optional(),
149
- layer_reason: z.string().optional(),
153
+ // W4/Track1: `knowledge_layer` removed — an entry's layer is derived from its
154
+ // stable_id prefix (KP-→personal, else team; KT-DEC-0004), never a field.
150
155
  created_at: z.string().optional(),
151
156
  // v2/rc.2: flat flow-style YAML array; populated by init-scan from forensic tech stack and editable by user. Used by rc.3 review skill for tag-filter search.
152
157
  tags: z.array(z.string()).default([]).optional(),
@@ -349,6 +354,32 @@ var humanLockFileSchema = z4.object({
349
354
 
350
355
  // src/schemas/fabric-config.ts
351
356
  import { z as z5 } from "zod";
357
+ function isNonPersonalRequiredStore(entry) {
358
+ return entry.id !== PERSONAL_STORE_SENTINEL;
359
+ }
360
+ function refineMaxOneTeamStore(entries, ctx) {
361
+ const teamCount = entries.filter(isNonPersonalRequiredStore).length;
362
+ if (teamCount > 1) {
363
+ ctx.addIssue({
364
+ code: z5.ZodIssueCode.custom,
365
+ message: `a project may bind at most one team store (found ${teamCount}); run \`fabric install\` to migrate to the single team slot`
366
+ });
367
+ }
368
+ }
369
+ function migrateRequiredStores(config) {
370
+ const declared = config.required_stores;
371
+ if (declared === void 0) {
372
+ return config;
373
+ }
374
+ const team = declared.filter(isNonPersonalRequiredStore);
375
+ if (team.length <= 1) {
376
+ return config;
377
+ }
378
+ const personal = declared.filter((entry) => !isNonPersonalRequiredStore(entry));
379
+ const active = config.active_write_store;
380
+ const kept = (active !== void 0 ? team.find((entry) => entry.id === active) : void 0) ?? team[0];
381
+ return { ...config, required_stores: [...personal, kept] };
382
+ }
352
383
  var auditModeSchema = z5.enum(["strict", "warn", "off"]);
353
384
  var clientPathsSchema = z5.object({
354
385
  claudeCodeCLI: z5.string().optional(),
@@ -395,7 +426,9 @@ var fabricConfigSchema = z5.object({
395
426
  // `$personal` sentinel). Drives the read-set (required_stores ∪ implicit
396
427
  // personal, S11/S54) and `clone`'s missing-store onboarding (S51). Optional
397
428
  // + absent → read-set is just the implicit personal store.
398
- required_stores: z5.array(requiredStoreEntrySchema).optional(),
429
+ // W2 dual-slot (TASK-002 / R6): at most ONE non-personal (team-type) store —
430
+ // the two-slot model. >1 fails parse and is migrated by `migrateRequiredStores`.
431
+ required_stores: z5.array(requiredStoreEntrySchema).superRefine(refineMaxOneTeamStore).optional(),
399
432
  // v2.1.0-rc.1 P3 (S60 / `store switch-write`): alias of the store that
400
433
  // non-personal-scope writes land in for this project. Set by
401
434
  // `fabric store switch-write <alias>`; consumed as the resolver's
@@ -494,92 +527,28 @@ var fabricConfigSchema = z5.object({
494
527
  // worst — pairing 14d trigger + 7d cooldown means at most ~2 reminders
495
528
  // per month for a workspace that ignores them.
496
529
  maintenance_hint_cooldown_days: z5.number().int().positive().optional().default(7),
497
- // rc.9+ (skill-contract-fix B1): first-run import window in months. The
498
- // `fabric-import` skill scans this many months of git history on the very
499
- // first invocation (when no prior `import_run_completed` event exists).
500
- // Default 60 (~5 years) captures the bulk of a mature repo's signal in
501
- // one pass; small / fresh repos can lower to 12-24 with no loss.
502
- import_window_first_run_months: z5.number().int().min(1).optional().default(60),
503
- // rc.9+ (skill-contract-fix B1): rerun import window in months. After
504
- // the first successful import, subsequent runs only scan this many
505
- // recent months — assumed everything older has already been crystallized
506
- // into pending or canonical knowledge. Default 2 keeps incremental cost
507
- // low; raise to 6 if the workspace pauses fabric-import for long stretches.
508
- import_window_rerun_months: z5.number().int().min(1).optional().default(2),
509
- // rc.9+ (skill-contract-fix B1): hard cap on pending entries produced
510
- // per fabric-import invocation. Prevents one run from dumping hundreds
511
- // of proposals when a backfill window is wide open. Default 10 matches
512
- // the rule-of-thumb "human can triage ~10 pending entries in one
513
- // review pass." Range 1-50.
514
- import_max_pending_per_run: z5.number().int().min(1).max(50).optional().default(10),
515
- // rc.9+ (skill-contract-fix B1): hard cap on commits scanned per
516
- // fabric-import invocation. Bounds runtime on monorepos with high
517
- // commit velocity. Default 500 covers ~2 months of typical churn;
518
- // range 50-2000. Hitting the cap mid-window is logged but non-fatal.
519
- import_max_commits_scan: z5.number().int().min(50).max(2e3).optional().default(500),
520
- // rc.9+ (skill-contract-fix B1): canonical-node count above which
521
- // fabric-import's pre-flight should warn / suggest review instead of
522
- // proceeding. A workspace with 50+ canonical entries usually benefits
523
- // more from `fabric-review` to consolidate than from importing more.
524
- // Default 50; raise to 100+ for large polyglot repos.
525
- import_skip_canonical_threshold: z5.number().int().positive().optional().default(50),
526
- // rc.9+ (skill-contract-fix B1): max candidate entries surfaced per
527
- // fabric-archive batch (one invocation of the skill). Pagination knob
528
- // for the archive UI flow. Default 8 keeps each batch reviewable in
529
- // one sitting; raise for large repos with high archive throughput.
530
- archive_max_candidates_per_batch: z5.number().int().positive().optional().default(8),
531
- // rc.9+ (skill-contract-fix B1): max recently-touched paths included
532
- // in fabric-archive's "relevant context" lookup. Limits the size of
533
- // the path-relevance digest the skill emits when ranking candidates.
534
- // Default 20; large repos with deep directory fan-out can raise to
535
- // 50+ if archive candidates feel under-contextualized.
536
- archive_max_recent_paths: z5.number().int().positive().optional().default(20),
537
- // rc.9+ (skill-contract-fix B1): max prior fabric-archive sessions
538
- // summarised in the digest the skill loads on start. Prevents the
539
- // digest from ballooning past the model context budget on workspaces
540
- // that have archived repeatedly. Default 10; lower if context pressure
541
- // bites, raise if you want longer-range archive trend visibility.
542
- archive_digest_max_sessions: z5.number().int().positive().optional().default(10),
543
- // rc.9+ (skill-contract-fix B1): max review results returned per
544
- // topic when `fabric-review` clusters pending entries. Pagination
545
- // knob analogous to archive_max_candidates_per_batch but scoped to
546
- // each topic cluster. Default 8; raise to 15-20 for large repos
547
- // where each topic legitimately groups many pending entries.
548
- review_topic_result_cap: z5.number().int().positive().optional().default(8),
530
+ // ux-w2-3: import_*/archive_max_*/review_topic_result_cap skill thresholds
531
+ // were hardcoded (✂ per census Table 1) never tuned, pure skill-internal
532
+ // pagination/window caps. Removed from the schema; the skills/services read
533
+ // raw config with a built-in default, so an absent key falls to that default
534
+ // (60/2/10/500/50/8/20/10/8 respectively). Lenient parser drops any stale
535
+ // on-disk value.
549
536
  // rc.9+ (skill-contract-fix B1): age threshold (in days) above which
550
537
  // a pending entry is considered "stale" by fabric-review and surfaced
551
538
  // for explicit resolve-or-drop decision. Default 14; tighter than the
552
539
  // 7d Signal-B trigger because review specifically targets the long
553
540
  // tail. Large repos with slower cadence can raise to 30.
554
541
  review_stale_pending_days: z5.number().int().positive().optional().default(14),
555
- // v2.0.0-rc.34 TASK-05: reverse-unarchive opt-in. When true, callers of the
556
- // `unarchiveKnowledge` primitive (and any future doctor auto-detect lint built
557
- // on top) will execute the file move + ledger emit. When false (default),
558
- // the same callers MUST short-circuit before any mutation — the primitive is
559
- // shipped but inert until explicitly enabled. Opt-in posture mirrors the
560
- // archive-flow precedent: destructive-ish file moves stay behind a flag.
561
- reverse_unarchive_enabled: z5.boolean().optional().default(false),
562
- // v2.0.0-rc.34 TASK-05: forces `unarchiveKnowledge` into dry-run mode even
563
- // when called with `options.dryRun=false`. Lets operators preview a
564
- // restoration pass before flipping `reverse_unarchive_enabled` to true.
565
- reverse_unarchive_dry_run: z5.boolean().optional().default(false),
566
- // v2.0.0-rc.34 TASK-06: long-session cite-policy evict window in user-prompt
567
- // turns. UserPromptSubmit hook (Claude Code only) maintains a per-session
568
- // counter and re-injects the cite contract reminder via
569
- // hookSpecificOutput.additionalContext when `turn_count % interval === 0`.
570
- // Default 0 = OFF (opt-in). Recommend 10-20 for active sessions; 5 for
571
- // high-contract-criticality projects. Other strategies (time-based,
572
- // token-budget) deferred to rc.35 per plan locked-decisions 2026-05-26.
573
- cite_evict_interval: z5.number().int().min(0).optional().default(0),
574
542
  // v2.1 ⑤ cite-redesign (P5): recall-based cite-accounting hook config. The
575
- // rc.34 cite_evict_interval turn-counter above is superseded by the
576
- // PreToolUse(Edit/Write) recall-aware nudge in cite-policy-evict.cjs; the old
577
- // key is retained for back-compat (inert now that the hook moved off
578
- // UserPromptSubmit). `cite_recall_nudge` is the master switch (default true =
579
- // ON); set false to silence the "改前先 fab_recall" nudge entirely (mirrors
580
- // the cite_evict_interval=0 opt-out convention). `cite_recall_window_minutes`
581
- // bounds how far back an in-session fab_recall counts as "informing" the edit
582
- // (default 30; 0 = unbounded).
543
+ // PreToolUse(Edit/Write) recall-aware nudge in cite-policy-evict.cjs replaced
544
+ // the retired rc.34 `cite_evict_interval` turn-counter. ux-w1-5: that inert
545
+ // key plus the never-wired `reverse_unarchive_enabled`/`reverse_unarchive_dry_run`
546
+ // opt-in flags (the unarchiveKnowledge primitive takes dryRun from its caller,
547
+ // not from config) were deleted; the lenient root parser drops any stale
548
+ // value left in an on-disk config. `cite_recall_nudge` is the master switch
549
+ // (default true = ON); set false to silence the "改前先 fab_recall" nudge
550
+ // entirely. `cite_recall_window_minutes` bounds how far back an in-session
551
+ // fab_recall counts as "informing" the edit (default 30; 0 = unbounded).
583
552
  cite_recall_nudge: z5.boolean().optional().default(true),
584
553
  cite_recall_window_minutes: z5.number().int().min(0).optional().default(30),
585
554
  // F2: glob exemptions for the cite nudge (cite-policy-evict.cjs). Edit paths
@@ -625,18 +594,11 @@ var fabricConfigSchema = z5.object({
625
594
  // Default `[]` keeps the field optional on existing configs — fresh
626
595
  // installs land with no opt-outs.
627
596
  onboard_slots_opted_out: z5.array(z5.string()).optional().default([]),
628
- // v2.0.0-rc.33 W2-1 (P0-9): TopK upper bound for the broad SessionStart hint
629
- // banner emitted by knowledge-hint-broad.cjs. After plan-context-hint returns
630
- // its full broad-scoped index, the hook slices the entries to this many
631
- // before grouping/truncation rendering keeps the banner from scrolling off
632
- // screen on well-seeded repos (Werewolf-class projects routinely surface 40+
633
- // broad entries which buried the actually-relevant top hits). Default 8 is
634
- // calibrated against the rc.32 eval baseline (cite-coverage 3.1%): the
635
- // banner needs to fit in ~1 screenful so the agent actually reads it.
636
- // Range 1..50; values above 20 effectively disable the cap because the
637
- // TRUNCATION_THRESHOLD=12 grouped-render kicks in. Mirrors the rc.7 T7 +
638
- // archive_max_* pattern of externalizing previously-hardcoded thresholds.
639
- hint_broad_top_k: z5.number().int().min(1).max(50).optional().default(8),
597
+ // ux-w3-j (W3-J): `hint_broad_top_k` was deleted. W2-1 (KT-DEC-0028) retired its
598
+ // hard-cap function the SessionStart broad banner now shows EVERY broad entry
599
+ // and `broad_index_backstop` (below) is the sole scale guard; the field had been
600
+ // inert ever since (its only remaining refs were retirement comments). The lenient
601
+ // root parser drops any stale on-disk value (zero migration).
640
602
  // KT-DEC-0036: the SessionStart broad-menu is now index-only (title + summary
641
603
  // per always-active entry, no eager body), so the former `hint_broad_budget_chars`
642
604
  // body char-budget knob was retired — there is no rendered body left to bound.
@@ -649,12 +611,37 @@ var fabricConfigSchema = z5.object({
649
611
  // banner silently truncates. Default 50; range 20..500 (read inline by
650
612
  // knowledge-hint-broad.cjs#readBroadIndexBackstop with the same bounds).
651
613
  broad_index_backstop: z5.number().int().min(20).max(500).optional().default(50),
652
- // v2.0.0-rc.37 NEW-16: durable per-signal dismiss for the fabric-hint Stop
653
- // hook nudges. Any signal type listed here is suppressed at emit time across
614
+ // v2.0.0-rc.37 NEW-16: durable per-signal dismiss for the fabric-hint hook
615
+ // nudges. Any signal type listed here is suppressed at emit time across
654
616
  // all sessions (the session-scoped sibling lives in a .fabric/.cache sidecar
655
617
  // written on request). Mirrors the cite_evict_interval=0 opt-out convention —
656
618
  // a knob for an existing surface, not a new feature. Unknown types ignored.
657
- hint_dismiss_signals: z5.array(z5.enum(["archive", "review", "import", "maintenance"])).optional(),
619
+ //
620
+ // TASK-005 (grill G5 / C-004 "全 nudge MUST 可 dismiss"): the enum now spans
621
+ // ALL nudge surfaces, not just the fabric-hint Stop signals:
622
+ // - Stop (fabric-hint): archive / review / import / maintenance
623
+ // - SessionStart (broad): review / import / maintenance now surface as
624
+ // the SessionStart summary line (see
625
+ // buildSessionStartSinks H4 ladder) — the same
626
+ // dismiss key silences them there too.
627
+ // - PreToolUse (per-edit): "narrow" (knowledge-hint-narrow) and
628
+ // "cite-evict" (cite-before-edit nudge).
629
+ // C-004 semantics are enforced at the trigger sites, NOT here: "narrow"
630
+ // defaults ON (impact-bearing) and "cite-evict" defaults OFF-able; listing
631
+ // either key here is the durable opt-out. Adding enum values is backward
632
+ // compatible — legacy configs that omit the new keys parse unchanged, and
633
+ // unknown on-disk values are dropped by the lenient root parser.
634
+ hint_dismiss_signals: z5.array(
635
+ z5.enum([
636
+ "archive",
637
+ "review",
638
+ "import",
639
+ "maintenance",
640
+ // per-edit (PreToolUse) nudge surfaces — TASK-005
641
+ "narrow",
642
+ "cite-evict"
643
+ ])
644
+ ).optional(),
658
645
  // v2.1 ADJ-NEWN-4: user-override escape hatches for the two strong behavioral
659
646
  // policies (cite-before-edit + self-archive). The strong policies can make an
660
647
  // agent feel like a "stubborn parrot" (D2 user-in-control red line); these
@@ -714,6 +701,36 @@ var fabricConfigSchema = z5.object({
714
701
  orphan_demote_proven_days: z5.number().int().min(1).max(3650).optional(),
715
702
  orphan_demote_verified_days: z5.number().int().min(1).max(3650).optional(),
716
703
  orphan_demote_draft_days: z5.number().int().min(1).max(3650).optional(),
704
+ // PLN-004 F1 (credibility content-age decay): per-knowledge-type half-lives
705
+ // (days) for the recall-scoring credibility MULTIPLIER. Orthogonal to the
706
+ // orphan_demote usage-inactivity ladder above (that is last-activity age; this
707
+ // is content age off created_at) and to recencyBoost (a 7-day additive
708
+ // freshness bump). Team-curated knowledge decays slower than transient work, so
709
+ // defaults run longer than upstream: decisions 180 / guidelines 150 / models
710
+ // 150 / pitfalls 120 / processes 120 (applied in config-loader). Each optional;
711
+ // absent → the loader default. Range 1..3650 mirrors orphan_demote.
712
+ credibility_half_life_decisions_days: z5.number().int().min(1).max(3650).optional(),
713
+ credibility_half_life_guidelines_days: z5.number().int().min(1).max(3650).optional(),
714
+ credibility_half_life_models_days: z5.number().int().min(1).max(3650).optional(),
715
+ credibility_half_life_pitfalls_days: z5.number().int().min(1).max(3650).optional(),
716
+ credibility_half_life_processes_days: z5.number().int().min(1).max(3650).optional(),
717
+ // PLN-004 F1: per-maturity floor the credibility multiplier never decays below
718
+ // (a stale-but-endorsed entry keeps a minimum weight). Higher maturity → higher
719
+ // floor: draft 0.4 / verified 0.55 / proven 0.7 (config-loader defaults). Range
720
+ // [0,1]. This is the softened form of the "content leads structural" invariant —
721
+ // credibility may sink a stale match but never zero it.
722
+ credibility_floor_draft: z5.number().min(0).max(1).optional(),
723
+ credibility_floor_verified: z5.number().min(0).max(1).optional(),
724
+ credibility_floor_proven: z5.number().min(0).max(1).optional(),
725
+ // v2.2 C1 (processes/maturity-promotion-rubric-v1): days a `broad` entry may
726
+ // go without a fab-review re-confirmation before doctor surfaces a RECHECK
727
+ // nudge. `broad` is EXEMPT from usage-age decay (it is SessionStart-pushed,
728
+ // never pull-recalled → usage-blind, KT-DEC; see doctor-knowledge-age.ts), so
729
+ // its continued validity is instead checked against the review-confirmation
730
+ // clock (`last_review_confirmed_at`, stamped at approve/modify). This is a
731
+ // non-blocking INFO nudge ("re-confirm"), NEVER an auto-demote. Absent → the
732
+ // config-loader default (180d) applies. Range 1..3650 mirrors orphan_demote.
733
+ broad_review_recheck_days: z5.number().int().min(1).max(3650).optional(),
717
734
  // v2.0.0-rc.33 W4-A3 (T4 P2): per-entry summary truncation length used by
718
735
  // knowledge-hint-{broad,narrow}.cjs. Hard-coded at 80 chars in rc.32 — too
719
736
  // short for entries with parameterized summaries (e.g. "Use bcrypt with
@@ -762,10 +779,13 @@ var fabricConfigSchema = z5.object({
762
779
  // sole retrieval knob (plan_context_top_k above); payload limits pass through
763
780
  // explicit `mcpPayloadLimits`, else the fixed PAYLOAD_LIMIT_DEFAULT_* guardrail.
764
781
  // v2.2 C2-vector (W2-T7): OPTIONAL dense-embedding semantic retrieval, layered
765
- // as a recall supplement after BM25. Default OFF (`--no-embed` is the baseline);
766
- // requires the operator to install the optional `fastembed` package absent →
767
- // text-only fallback. Never grows the default install footprint.
768
- embed_enabled: z5.boolean().optional().default(false),
782
+ // as a recall supplement after BM25. P1 recall-engine-refactor (TASK-004):
783
+ // Default ON `fastembed` is now an optionalDependency (auto-installed; absent →
784
+ // degrade-safe text-only fallback), so CJK semantic recall is on out of the box.
785
+ // OFF only when set explicitly to false. This default mirrors the runtime read in
786
+ // config-loader.ts (`embed_enabled !== false`); keep the two in sync so config
787
+ // introspection never reports a default that contradicts runtime behavior.
788
+ embed_enabled: z5.boolean().optional().default(true),
769
789
  // Weight applied to the 0..1 cosine similarity before it joins the additive
770
790
  // score. Capped at 49 — strictly BELOW BM25_WEIGHT (50) — so a perfect vector
771
791
  // match (weight × 1) can never outscore a single strong BM25 term match. This
@@ -790,8 +810,25 @@ var fabricConfigSchema = z5.object({
790
810
  "fast-bge-base-en-v1.5",
791
811
  "fast-bge-base-en",
792
812
  "fast-all-MiniLM-L6-v2"
793
- ]).optional().default("fast-bge-small-zh-v1.5")
794
- });
813
+ ]).optional().default("fast-bge-small-zh-v1.5"),
814
+ // P1 recall-engine-refactor (TASK-003 + follow-up): content-channel fusion
815
+ // strategy. 'additive' = the weighted-sum path (BM25_WEIGHT·bm25 + vectorWeight·
816
+ // vector + structural); the vector term is structurally minor (cosine·30 vs an
817
+ // unbounded BM25), so additive is effectively BM25-led. 'rrf' = Reciprocal Rank
818
+ // Fusion over the two CONTENT channels (bm25_rank, vector_rank, equal footing)
819
+ // + a re-scaled structural tiebreaker — lets semantic recall actually matter.
820
+ // 'auto' (DEFAULT) = adaptive: use 'rrf' WHEN the vector channel is actually
821
+ // producing scores (embeddings installed + model warm), else fall back to
822
+ // 'additive'. This is the safe default — real-store shadow showed single-channel
823
+ // rrf (no vectors) is strictly worse than additive, so auto never lets that
824
+ // happen. no-query ranking is byte-identical under every value.
825
+ fusion: z5.enum(["additive", "rrf", "auto"]).optional().default("auto")
826
+ });
827
+ var fabricConfigLoadSchema = fabricConfigSchema.merge(
828
+ z5.object({
829
+ required_stores: z5.array(requiredStoreEntrySchema).optional()
830
+ })
831
+ );
795
832
 
796
833
  // src/schemas/fabric-config-introspect.ts
797
834
  import { z as z6 } from "zod";
@@ -956,9 +993,15 @@ var PANEL_FIELDS = [
956
993
  // nudge_mode — the master switch for the human-visible nudge experience
957
994
  // (the most user-facing runtime knob, previously JSON-only). embed_enabled —
958
995
  // vector semantic recall, panel-editable now that config lives in `.fabric`
959
- // (A1); enabling also needs the host-side `fabric install --enable-embed`.
996
+ // (A1). TASK-004: default ON (fastembed is an optionalDependency, degrade-safe
997
+ // when absent) — this introspection default mirrors the runtime read in
998
+ // config-loader.ts so the panel never shows a default that contradicts behavior.
960
999
  makeEnumField("nudge_mode", "D_behavior", nudgeModeSchema.options, "normal"),
961
- makeBooleanField("embed_enabled", false)
1000
+ makeBooleanField("embed_enabled", true),
1001
+ // P1 recall-engine-refactor (follow-up): the content-channel fusion strategy,
1002
+ // panel-editable so it sits next to embed_enabled (the two go together — rrf
1003
+ // only pays off when embeddings are on). 'auto' is the safe adaptive default.
1004
+ makeEnumField("fusion", "D_behavior", ["additive", "rrf", "auto"], "auto")
962
1005
  ];
963
1006
 
964
1007
  // src/schemas/store-stable-id.ts
@@ -1125,6 +1168,13 @@ var storeResolveInputSchema = z9.object({
1125
1168
  ),
1126
1169
  // Alias selected as the active write store for non-personal scopes, if any.
1127
1170
  activeWriteAlias: z9.string().min(1).optional(),
1171
+ // Alias/UUID of the ACTIVE personal store among possibly-many mounted
1172
+ // `personal:true` stores (语义 A: singleton-at-a-time). Drives the SINGLE
1173
+ // personal choke point (findPersonal) → both read-set inclusion and the
1174
+ // personal-scope write-target. Absent or dangling ⇒ the resolver falls back
1175
+ // to the first mounted personal, so legacy single-personal configs are
1176
+ // unchanged. Sourced from `~/.fabric/fabric-global.json` → active_personal_store.
1177
+ activePersonalAlias: z9.string().min(1).optional(),
1128
1178
  // Scope-aware write routes. Exact scope wins first, then longest prefix route.
1129
1179
  writeRoutes: z9.array(
1130
1180
  z9.object({
@@ -1187,7 +1237,16 @@ function createProjectRootResolver() {
1187
1237
 
1188
1238
  // src/resolver/store-resolver.ts
1189
1239
  function findPersonal(input) {
1190
- return input.mountedStores.find((s) => s.personal);
1240
+ const actives = input.mountedStores.filter((s) => s.personal);
1241
+ if (input.activePersonalAlias !== void 0) {
1242
+ const picked = actives.find(
1243
+ (s) => s.alias === input.activePersonalAlias || s.store_uuid === input.activePersonalAlias
1244
+ );
1245
+ if (picked !== void 0) {
1246
+ return picked;
1247
+ }
1248
+ }
1249
+ return actives[0];
1191
1250
  }
1192
1251
  function personalEntry(input) {
1193
1252
  const p = findPersonal(input);
@@ -1254,7 +1313,7 @@ function createStoreResolver() {
1254
1313
  warnings.push({
1255
1314
  code: "missing_store",
1256
1315
  ref: req.id,
1257
- message: `required store '${req.id}' is not mounted; run \`fabric store add\` (suggested remote: $personal)`
1316
+ message: `required store '${req.id}' is not mounted; run \`fabric store mount\` (suggested remote: $personal)`
1258
1317
  });
1259
1318
  }
1260
1319
  continue;
@@ -1267,7 +1326,7 @@ function createStoreResolver() {
1267
1326
  warnings.push({
1268
1327
  code: "missing_store",
1269
1328
  ref: req.id,
1270
- message: `required store '${req.id}' is not mounted; run \`fabric store add\`${suffix}`
1329
+ message: `required store '${req.id}' is not mounted; run \`fabric store mount\`${suffix}`
1271
1330
  });
1272
1331
  continue;
1273
1332
  }
@@ -1313,7 +1372,7 @@ function createStoreResolver() {
1313
1372
  {
1314
1373
  code: "missing_write_route",
1315
1374
  ref: scope,
1316
- message: `scope '${scope}' has no explicit write route; set \`fabric store route-write ${scope} <alias>\``
1375
+ message: `scope '${scope}' has no explicit write route; set \`fabric store switch-write <alias> --scope ${scope}\``
1317
1376
  }
1318
1377
  ]
1319
1378
  };
@@ -1555,6 +1614,7 @@ import { execFile } from "child_process";
1555
1614
  import { access, mkdir, readdir, readFile as readFile2, writeFile } from "fs/promises";
1556
1615
  import { join as join2 } from "path";
1557
1616
  import { promisify } from "util";
1617
+ var STORE_PROJECTS_DIR = "projects";
1558
1618
  var STORE_PENDING_DIR = "pending";
1559
1619
  var STORE_GITIGNORE = [
1560
1620
  "# v2.1 store \u2014 volatile / derived data is never committed",
@@ -1608,12 +1668,28 @@ async function listMarkdown(dir) {
1608
1668
  return entries.filter((name) => name.endsWith(".md")).sort().map((name) => join2(dir, name));
1609
1669
  }
1610
1670
  async function listStoreKnowledge(store) {
1671
+ const knowledgeDir = join2(store.dir, STORE_LAYOUT.knowledgeDir);
1611
1672
  const refs = [];
1612
1673
  for (const type of STORE_KNOWLEDGE_TYPE_DIRS) {
1613
- for (const file of await listMarkdown(join2(store.dir, STORE_LAYOUT.knowledgeDir, type))) {
1674
+ for (const file of await listMarkdown(join2(knowledgeDir, type))) {
1614
1675
  refs.push({ store_uuid: store.store_uuid, alias: store.alias, type, file });
1615
1676
  }
1616
1677
  }
1678
+ let projectEntries;
1679
+ try {
1680
+ projectEntries = await readdir(join2(knowledgeDir, STORE_PROJECTS_DIR));
1681
+ } catch {
1682
+ return refs;
1683
+ }
1684
+ const reserved = /* @__PURE__ */ new Set([STORE_PROJECTS_DIR, ...STORE_KNOWLEDGE_TYPE_DIRS]);
1685
+ const projectIds = projectEntries.filter((id) => STORE_PROJECT_ID_PATTERN.test(id) && !reserved.has(id)).sort();
1686
+ for (const project of projectIds) {
1687
+ for (const type of STORE_KNOWLEDGE_TYPE_DIRS) {
1688
+ for (const file of await listMarkdown(join2(knowledgeDir, STORE_PROJECTS_DIR, project, type))) {
1689
+ refs.push({ store_uuid: store.store_uuid, alias: store.alias, type, file, project });
1690
+ }
1691
+ }
1692
+ }
1617
1693
  return refs;
1618
1694
  }
1619
1695
  async function readKnowledgeAcrossStores(stores) {
@@ -1776,7 +1852,12 @@ function loadProjectConfig(projectRoot) {
1776
1852
  if (!existsSync3(path)) {
1777
1853
  return null;
1778
1854
  }
1779
- return fabricConfigSchema.parse(JSON.parse(readFileSync3(path, "utf8")));
1855
+ const raw = JSON.parse(readFileSync3(path, "utf8"));
1856
+ const parsed = fabricConfigSchema.safeParse(raw);
1857
+ if (parsed.success) {
1858
+ return parsed.data;
1859
+ }
1860
+ return fabricConfigLoadSchema.parse(raw);
1780
1861
  }
1781
1862
  function saveProjectConfig(config, projectRoot) {
1782
1863
  const validated = fabricConfigSchema.parse(config);
@@ -1809,6 +1890,7 @@ function buildStoreResolveInput(projectRoot, globalRoot = resolveGlobalRoot()) {
1809
1890
  })
1810
1891
  ),
1811
1892
  ...project?.active_write_store === void 0 ? {} : { activeWriteAlias: project.active_write_store },
1893
+ ...global.active_personal_store === void 0 ? {} : { activePersonalAlias: global.active_personal_store },
1812
1894
  writeRoutes: project?.write_routes ?? [],
1813
1895
  ...project?.default_write_store === void 0 ? {} : { defaultWriteAlias: project.default_write_store }
1814
1896
  };
@@ -2018,7 +2100,7 @@ var MCP_STORE_AWARE_TOOLS = [
2018
2100
  "fab_plan_context",
2019
2101
  "fab_get_knowledge_sections",
2020
2102
  "fab_archive_scan",
2021
- "fab_extract_knowledge",
2103
+ "fab_propose",
2022
2104
  "fab_review"
2023
2105
  ];
2024
2106
  var storeAwareEntrySchema = z11.object({
@@ -2043,8 +2125,8 @@ var MCP_STORE_AWARE_CONTRACTS = {
2043
2125
  surfacesEntries: false,
2044
2126
  echoesWrittenStore: false
2045
2127
  },
2046
- fab_extract_knowledge: {
2047
- tool: "fab_extract_knowledge",
2128
+ fab_propose: {
2129
+ tool: "fab_propose",
2048
2130
  surfacesEntries: false,
2049
2131
  echoesWrittenStore: true
2050
2132
  },
@@ -3217,10 +3299,81 @@ var eventLedgerEventSchema = z16.discriminatedUnion("event_type", [
3217
3299
  graphEdgeCandidateRequestedEventSchema
3218
3300
  ]);
3219
3301
 
3302
+ // src/high-value-predicate.ts
3303
+ var HIGH_VALUE_ARCHIVE_EVENT_TYPES = /* @__PURE__ */ new Set([
3304
+ "knowledge_context_planned",
3305
+ "edit_paths_recorded",
3306
+ "edit_intent_checked"
3307
+ // the real high-freq edit signal (rc.37 NEW-14/B3)
3308
+ ]);
3309
+ var NORMATIVE_KEYWORDS = [
3310
+ "\u4EE5\u540E",
3311
+ "always",
3312
+ "never",
3313
+ "from now on",
3314
+ "\u4E0B\u6B21",
3315
+ "\u8BB0\u4E00\u4E0B",
3316
+ "\u6C38\u8FDC\u4E0D\u8981"
3317
+ ];
3318
+ function isHighValueArchiveCandidate(events, sessionId, watermarkTs) {
3319
+ if (!Array.isArray(events)) return false;
3320
+ if (typeof sessionId !== "string" || sessionId.length === 0) return false;
3321
+ const wm = typeof watermarkTs === "number" ? watermarkTs : 0;
3322
+ let latestTurn = null;
3323
+ for (const raw of events) {
3324
+ const e = raw;
3325
+ if (!e || typeof e.ts !== "number" || e.ts <= wm) continue;
3326
+ if (e.session_id !== sessionId) continue;
3327
+ if (typeof e.event_type === "string" && HIGH_VALUE_ARCHIVE_EVENT_TYPES.has(e.event_type)) {
3328
+ return true;
3329
+ }
3330
+ if (e.event_type === "assistant_turn_observed") {
3331
+ if (latestTurn === null || typeof latestTurn.ts === "number" && e.ts > latestTurn.ts) {
3332
+ latestTurn = e;
3333
+ }
3334
+ }
3335
+ }
3336
+ if (latestTurn !== null) {
3337
+ const haystack = JSON.stringify(latestTurn).toLowerCase();
3338
+ for (const kw of NORMATIVE_KEYWORDS) {
3339
+ if (haystack.includes(kw.toLowerCase())) return true;
3340
+ }
3341
+ }
3342
+ return false;
3343
+ }
3344
+
3220
3345
  // src/text-tokenize.ts
3221
3346
  var CJK_CLASS = "\\u3400-\\u4dbf\\u4e00-\\u9fff\\uf900-\\ufaff\\u3040-\\u30ff\\uac00-\\ud7af";
3222
3347
  var RUN_RE = new RegExp(`[a-z0-9]+|[${CJK_CLASS}]+`, "gu");
3223
3348
  var CJK_FIRST_RE = new RegExp(`[${CJK_CLASS}]`, "u");
3349
+ var STOP_WORDS = /* @__PURE__ */ new Set([
3350
+ "the",
3351
+ "a",
3352
+ "an",
3353
+ "and",
3354
+ "or",
3355
+ "of",
3356
+ "to",
3357
+ "in",
3358
+ "on",
3359
+ "for",
3360
+ "is",
3361
+ "it",
3362
+ "with",
3363
+ "as",
3364
+ "at",
3365
+ "by",
3366
+ "be",
3367
+ "are",
3368
+ "was",
3369
+ "were",
3370
+ "this",
3371
+ "that",
3372
+ "from",
3373
+ "but",
3374
+ "not"
3375
+ ]);
3376
+ var MAX_CJK_NGRAM = 3;
3224
3377
  function tokenize(text) {
3225
3378
  if (text.length === 0) {
3226
3379
  return [];
@@ -3234,12 +3387,17 @@ function tokenize(text) {
3234
3387
  if (CJK_FIRST_RE.test(run[0])) {
3235
3388
  if (run.length === 1) {
3236
3389
  tokens.push(run);
3237
- } else {
3238
- for (let i = 0; i < run.length - 1; i += 1) {
3239
- tokens.push(run.slice(i, i + 2));
3390
+ continue;
3391
+ }
3392
+ for (let n = 2; n <= MAX_CJK_NGRAM; n += 1) {
3393
+ if (run.length < n) {
3394
+ break;
3395
+ }
3396
+ for (let i = 0; i + n <= run.length; i += 1) {
3397
+ tokens.push(run.slice(i, i + n));
3240
3398
  }
3241
3399
  }
3242
- } else {
3400
+ } else if (run.length >= 2 && !STOP_WORDS.has(run)) {
3243
3401
  tokens.push(run);
3244
3402
  }
3245
3403
  }
@@ -3258,6 +3416,10 @@ export {
3258
3416
  FabExtractKnowledgeInputSchema,
3259
3417
  FabExtractKnowledgeInputShape,
3260
3418
  FabExtractKnowledgeOutputSchema,
3419
+ FabPendingInputSchema,
3420
+ FabPendingInputShape,
3421
+ FabPendingOutputSchema,
3422
+ FabPendingOutputShape,
3261
3423
  FabReviewInputSchema,
3262
3424
  FabReviewInputShape,
3263
3425
  FabReviewOutputSchema,
@@ -3285,6 +3447,7 @@ export {
3285
3447
  PROTECTED_TOKENS,
3286
3448
  ProposedReasonSchema,
3287
3449
  REDACTION_PLACEHOLDER_PREFIX,
3450
+ SCOPE_COORDINATE_HINT,
3288
3451
  SCOPE_COORDINATE_PATTERN,
3289
3452
  STORES_ROOT_DIR,
3290
3453
  STORE_ALIAS_PATTERN,
@@ -3355,7 +3518,9 @@ export {
3355
3518
  eventsRotatedEventSchema,
3356
3519
  explainStore,
3357
3520
  fabExtractKnowledgeAnnotations,
3521
+ fabPendingAnnotations,
3358
3522
  fabReviewAnnotations,
3523
+ fabricConfigLoadSchema,
3359
3524
  fabricConfigSchema,
3360
3525
  fabricEventSchema,
3361
3526
  fabricLanguageSchema,
@@ -3399,7 +3564,9 @@ export {
3399
3564
  initScanCompletedEventSchema,
3400
3565
  initStore,
3401
3566
  installDiffAppliedEventSchema,
3567
+ isHighValueArchiveCandidate,
3402
3568
  isKnowledgeStableId,
3569
+ isNonPersonalRequiredStore,
3403
3570
  isPersonalLeakIntoSharedStore,
3404
3571
  isPersonalScope,
3405
3572
  knowledgeArchiveAttemptedEventSchema,
@@ -3452,6 +3619,7 @@ export {
3452
3619
  metaReconciledEventSchema,
3453
3620
  metaReconciledOnStartupEventSchema,
3454
3621
  metaUpdatedEventSchema,
3622
+ migrateRequiredStores,
3455
3623
  mountedStoreSchema,
3456
3624
  normalizeCiteTag,
3457
3625
  normalizeLocale,