@fenglimg/fabric-server 2.0.0-rc.8 → 2.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wangzhichao (fenglimg)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # @fenglimg/fabric-server
2
+
3
+ Fabric MCP knowledge server. Runs over stdio transport and serves Claude Code, Cursor, and Codex CLI from a single `.fabric/` directory.
4
+
5
+ ## Tools exposed
6
+
7
+ - `fab_plan_context` — neutral rule description index + selection token
8
+ - `fab_get_knowledge_sections` — fetch full markdown bodies by stable_id
9
+ - `fab_recall` — combined one-call recall (plan + sections), the rc.37+ default
10
+ - `fab_extract_knowledge` — persist a pending knowledge entry
11
+ - `fab_review` — list / approve / reject / modify / defer pending entries
12
+
13
+ ## Install
14
+
15
+ Usually installed indirectly via [`@fenglimg/fabric-cli`](https://www.npmjs.com/package/@fenglimg/fabric-cli):
16
+
17
+ ```bash
18
+ npm i -g @fenglimg/fabric-cli
19
+ fabric install
20
+ ```
21
+
22
+ Direct consumption (programmatic):
23
+
24
+ ```bash
25
+ npm i @fenglimg/fabric-server
26
+ ```
27
+
28
+ ## Repo
29
+
30
+ Source + issues + roadmap: <https://github.com/fenglimg/fabric-v2>
31
+
32
+ ## License
33
+
34
+ MIT — see [LICENSE](./LICENSE).
package/dist/index.d.ts CHANGED
@@ -1,8 +1,6 @@
1
- import { Server } from 'node:http';
2
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import { AgentsMeta, KnowledgeTestIndex, AgentsLayer, AgentsTopologyType, Layer, KnowledgeType, StableId, AgentsMetaCounters, EventLedgerEventInput, EventLedgerEvent, RuleDescriptionIndexItem } from '@fenglimg/fabric-shared';
2
+ import { AgentsMeta, KnowledgeTestIndex, AgentsLayer, AgentsTopologyType, KnowledgeType, Layer, StableId, AgentsMetaCounters, EventLedgerEventInput, EventLedgerEvent, RuleDescriptionIndexItem } from '@fenglimg/fabric-shared';
4
3
  import { FabExtractKnowledgeInput, FabExtractKnowledgeOutput, FabReviewInput, FabReviewOutput } from '@fenglimg/fabric-shared/schemas/api-contracts';
5
- import { IOFabricError } from '@fenglimg/fabric-shared/errors';
6
4
 
7
5
  interface InFlightTracker {
8
6
  enter(requestId: string): void;
@@ -18,9 +16,11 @@ declare function createInFlightTracker(): InFlightTracker;
18
16
  declare const LEDGER_PATH = ".fabric/.intent-ledger.jsonl";
19
17
  declare const LEGACY_LEDGER_PATH = ".intent-ledger.jsonl";
20
18
  declare const EVENT_LEDGER_PATH = ".fabric/events.jsonl";
19
+ declare const METRICS_LEDGER_PATH = ".fabric/metrics.jsonl";
21
20
  declare function getLedgerPath(projectRoot: string): string;
22
21
  declare function getLegacyLedgerPath(projectRoot: string): string;
23
22
  declare function getEventLedgerPath(projectRoot: string): string;
23
+ declare function getMetricsLedgerPath(projectRoot: string): string;
24
24
 
25
25
  type DoctorStatus = "ok" | "warn" | "error";
26
26
  type DoctorIssueKind = "fixable_error" | "manual_error" | "warning" | "info";
@@ -32,12 +32,20 @@ type DoctorCheck = {
32
32
  code?: string;
33
33
  fixable?: boolean;
34
34
  actionHint?: string;
35
+ audience?: "user" | "maintainer";
35
36
  };
36
37
  type DoctorIssue = {
37
38
  code: string;
38
39
  name: string;
39
40
  message: string;
40
41
  path?: string;
42
+ actionHint?: string;
43
+ audience?: "user" | "maintainer";
44
+ };
45
+ type DoctorPayloadLimits = {
46
+ warn_bytes: number;
47
+ hard_bytes: number;
48
+ source: "default" | "config";
41
49
  };
42
50
  type DoctorSummary = {
43
51
  target: string;
@@ -59,6 +67,7 @@ type DoctorSummary = {
59
67
  warningCount: number;
60
68
  infoCount: number;
61
69
  targetFiles: Record<string, boolean>;
70
+ payload_limits: DoctorPayloadLimits;
62
71
  };
63
72
  type DoctorReport = {
64
73
  status: DoctorStatus;
@@ -77,7 +86,7 @@ type DoctorFixReport = {
77
86
  message: string;
78
87
  report: DoctorReport;
79
88
  };
80
- type DoctorApplyLintMutationKind = "knowledge_orphan_demote_required" | "knowledge_stale_archive_required" | "knowledge_index_drift" | "knowledge_pending_auto_archive" | "knowledge_session_hints_stale_cleanup";
89
+ type DoctorApplyLintMutationKind = "knowledge_orphan_demote_required" | "knowledge_stale_archive_required" | "knowledge_index_drift" | "knowledge_pending_auto_archive" | "knowledge_session_hints_stale_cleanup" | "knowledge_relevance_fields_missing";
81
90
  type DoctorApplyLintMutation = {
82
91
  kind: DoctorApplyLintMutationKind;
83
92
  path: string;
@@ -97,6 +106,112 @@ type DoctorApplyLintReport = {
97
106
  declare function runDoctorReport(target: string): Promise<DoctorReport>;
98
107
  declare function runDoctorFix(target: string): Promise<DoctorFixReport>;
99
108
  declare function runDoctorApplyLint(target: string): Promise<DoctorApplyLintReport>;
109
+ type CiteContractMetrics = {
110
+ decisions_cited: number;
111
+ pitfalls_cited: number;
112
+ contract_with: number;
113
+ contract_missing: number;
114
+ hard_violated: number;
115
+ cite_id_unresolved: number;
116
+ skip_count: Record<string, number>;
117
+ };
118
+ type CiteLayerTypeBreakdown = {
119
+ team: Record<string, number>;
120
+ personal: Record<string, number>;
121
+ };
122
+ type CiteCoverageReport = {
123
+ status: "ok" | "skipped";
124
+ marker_ts: number;
125
+ marker_emitted_now: boolean;
126
+ since_ts: number;
127
+ client_filter: "cc" | "codex" | "cursor" | "all";
128
+ layer_filter?: "team" | "personal" | "all";
129
+ metrics: {
130
+ edits_touched: number;
131
+ qualifying_cites: number;
132
+ recalled_unverified: number;
133
+ expected_but_missed: number;
134
+ total_turns: number;
135
+ cite_compliance_rate?: number | null;
136
+ compliant_cites?: number;
137
+ noncompliant_cites?: number;
138
+ uncorrelatable_edits?: number;
139
+ };
140
+ per_client?: Record<string, Partial<CiteCoverageReport["metrics"]>>;
141
+ dismissed_reason_histogram?: Record<string, number>;
142
+ none_reason_histogram?: Record<string, number>;
143
+ contract_metrics_status?: "ok" | "skipped:bootstrap_drift" | "awaiting_marker";
144
+ contract_metrics?: CiteContractMetrics;
145
+ per_layer_type?: CiteLayerTypeBreakdown;
146
+ contract_marker_ts?: number;
147
+ generated_at: string;
148
+ rollup_days_merged?: number;
149
+ rollup_trend?: {
150
+ date: string;
151
+ generated_at: string;
152
+ metrics: CiteCoverageReport["metrics"];
153
+ }[];
154
+ };
155
+ declare function runDoctorCiteCoverage(projectRoot: string, options: {
156
+ since: number;
157
+ client: "cc" | "codex" | "cursor" | "all";
158
+ layer?: "team" | "personal" | "all";
159
+ until?: number;
160
+ }): Promise<CiteCoverageReport>;
161
+ type ArchiveHistoryEntry = {
162
+ session_id_short: string;
163
+ last_attempted_at: string;
164
+ outcome: "proposed" | "viability_failed" | "user_dismissed" | "skipped_no_signal";
165
+ candidates_proposed: number;
166
+ covered_through_ts: number;
167
+ age_since_covered_hours: number;
168
+ };
169
+ type ArchiveHistoryReport = {
170
+ entries: ArchiveHistoryEntry[];
171
+ total: number;
172
+ since_ms: number;
173
+ generated_at: string;
174
+ };
175
+ declare function runDoctorArchiveHistory(projectRoot: string, options: {
176
+ since: number;
177
+ }): Promise<ArchiveHistoryReport>;
178
+ type HistoryDayRow = {
179
+ date: string;
180
+ doctor_runs_lint: number;
181
+ doctor_runs_fix: number;
182
+ doctor_total_issues: number;
183
+ doctor_total_mutations: number;
184
+ archive_attempts: number;
185
+ archive_proposed: number;
186
+ };
187
+ type HistoryAllReport = {
188
+ rows: HistoryDayRow[];
189
+ since_ms: number;
190
+ generated_at: string;
191
+ };
192
+ declare function runDoctorHistoryAll(projectRoot: string, options: {
193
+ since: number;
194
+ }): Promise<HistoryAllReport>;
195
+ type EnrichDescriptionsMode = "auto" | "preview" | "readonly" | "interactive";
196
+ type EnrichDescriptionsCandidate = {
197
+ path: string;
198
+ missing: Array<"intent_clues" | "tech_stack" | "impact" | "must_read_if">;
199
+ modified: boolean;
200
+ added_fields: Array<"intent_clues" | "tech_stack" | "impact" | "must_read_if">;
201
+ error?: string;
202
+ };
203
+ type EnrichDescriptionsReport = {
204
+ mode: EnrichDescriptionsMode;
205
+ dryRun: boolean;
206
+ scanned: number;
207
+ modified: number;
208
+ skipped: number;
209
+ candidates: EnrichDescriptionsCandidate[];
210
+ };
211
+ declare function enrichDescriptions(projectRoot: string, opts?: {
212
+ auto?: boolean;
213
+ dryRun?: boolean;
214
+ }): Promise<EnrichDescriptionsReport>;
100
215
 
101
216
  type KnowledgeMetaBuildSource = "doctor_fix" | "sync_meta";
102
217
  type KnowledgeMetaBuildResult = {
@@ -107,6 +222,31 @@ type KnowledgeMetaBuildResult = {
107
222
  type WriteKnowledgeMetaOptions = {
108
223
  source: KnowledgeMetaBuildSource;
109
224
  };
225
+ /**
226
+ * v2.0-rc.24 TASK-07: Load a Map<stable_id, knowledge_type> from the
227
+ * project's `.fabric/agents.meta.json`. Consumed by the doctor cite-coverage
228
+ * routing (TASK-08) to dispatch cites to the correct policy bucket
229
+ * (decision/pitfall = strict contract / model = reference-only /
230
+ * guideline+process = deferred to rc.25 LLM-judge). Cited ids absent from
231
+ * this map fall into the `cite_id_unresolved` bucket.
232
+ *
233
+ * **Plural knowledge_type contract (rc.29 BUG-C1 unification):** the returned
234
+ * map values are the PLURAL `KnowledgeType` enum (`"models" | "decisions" |
235
+ * "guidelines" | "pitfalls" | "processes"`) — matching disk frontmatter,
236
+ * filesystem layout, MCP I/O surface, and the canonical `KnowledgeTypeSchema`
237
+ * exported from `@fenglimg/fabric-shared`. Legacy singular frontmatter is
238
+ * normalized at parse time (see `SINGULAR_TO_PLURAL` in `parseFrontmatter`);
239
+ * downstream callers (TASK-08 doctor) match against the plural enum.
240
+ *
241
+ * Both team (KT-*) and personal (KP-*) entries are included — they live in
242
+ * the same `meta.nodes` map.
243
+ *
244
+ * Graceful on failure: a missing meta file, malformed JSON, or schema
245
+ * validation failure all yield an empty Map (no throw). The doctor will then
246
+ * surface every cite as `cite_id_unresolved`, which is the safe degraded
247
+ * mode.
248
+ */
249
+ declare function loadKbIdTypeMap(projectRootInput: string): Promise<Map<string, KnowledgeType>>;
110
250
  declare function buildKnowledgeMeta(projectRootInput: string): Promise<KnowledgeMetaBuildResult>;
111
251
  declare function writeKnowledgeMeta(projectRootInput: string, options: WriteKnowledgeMetaOptions): Promise<KnowledgeMetaBuildResult>;
112
252
  declare function computeKnowledgeBasedAgentsMeta(projectRootInput: string, existingMeta?: AgentsMeta): Promise<AgentsMeta>;
@@ -155,8 +295,11 @@ declare class KnowledgeIdAllocator {
155
295
  /**
156
296
  * Append-evidence-on-collision service for fab_extract_knowledge.
157
297
  *
158
- * Idempotency_key = sha256({source_session, type, slug}). When the same
159
- * triple hits an existing pending file (verified by frontmatter
298
+ * Idempotency_key = sha256({source_session: source_sessions[0], type, slug}).
299
+ * The `source_session` key inside the hash payload is FROZEN for backward
300
+ * compatibility with on-disk pending entries written before rc.23 — changing
301
+ * it would invalidate every existing `x-fabric-idempotency-key`. When the
302
+ * same triple hits an existing pending file (verified by frontmatter
160
303
  * `x-fabric-idempotency-key`), the body is preserved and a fresh
161
304
  * `## Evidence (call N)` section is appended — LLM-regenerated summaries
162
305
  * stay observable without overwriting prior context.
@@ -209,8 +352,6 @@ type PlanContextInput = {
209
352
  };
210
353
  type RequirementProfile = {
211
354
  target_path: string;
212
- path_segments: string[];
213
- extension: string;
214
355
  known_tech: string[];
215
356
  user_intent: string;
216
357
  detected_entities: string[];
@@ -218,23 +359,24 @@ type RequirementProfile = {
218
359
  type PlanContextEntry = {
219
360
  path: string;
220
361
  requirement_profile: RequirementProfile;
221
- description_index: RuleDescriptionIndexItem[];
362
+ };
363
+ type PreflightDiagnostic = {
364
+ code: "missing_description" | "empty_shell_suppressed";
365
+ severity: "warn";
366
+ message: string;
367
+ stable_ids?: string[];
368
+ path?: string;
222
369
  };
223
370
  type PlanContextResult = {
224
371
  revision_hash: string;
225
372
  stale: boolean;
226
373
  selection_token: string;
227
374
  entries: PlanContextEntry[];
228
- shared: {
229
- description_index: RuleDescriptionIndexItem[];
230
- preflight_diagnostics: Array<{
231
- code: "missing_description";
232
- severity: "warn";
233
- message: string;
234
- stable_ids?: string[];
235
- path?: string;
236
- }>;
237
- };
375
+ candidates: RuleDescriptionIndexItem[];
376
+ preflight_diagnostics: PreflightDiagnostic[];
377
+ auto_healed?: boolean;
378
+ previous_revision_hash?: string;
379
+ redirects?: Record<string, string>;
238
380
  };
239
381
  type SelectionTokenState = {
240
382
  token: string;
@@ -248,31 +390,149 @@ type SelectionTokenState = {
248
390
  declare function planContext(projectRoot: string, input: PlanContextInput): Promise<PlanContextResult>;
249
391
  declare function readSelectionToken(token: string, now?: number): SelectionTokenState | undefined;
250
392
 
251
- /**
252
- * Shared constants used across the server package.
253
- */
254
- /** MCP resource URI for the project's bootstrap README (L0 rules) file. */
255
- declare const AGENTS_MD_RESOURCE_URI = "fabric://bootstrap-readme";
393
+ type RecallInput = PlanContextInput & {
394
+ /**
395
+ * Optional explicit set of stable_ids to fetch bodies for. When omitted,
396
+ * fab_recall picks up every stable_id surfaced in the shared description
397
+ * index (the common case after rc.37 selectable-filter removal). When
398
+ * provided, filters the fetched body set to this intersection.
399
+ */
400
+ ids?: string[];
401
+ };
402
+ type RecallResult = PlanContextResult & {
403
+ rules: Array<{
404
+ stable_id: string;
405
+ level: "L0" | "L1" | "L2";
406
+ path: string;
407
+ body: string;
408
+ }>;
409
+ selected_stable_ids: string[];
410
+ diagnostics: Array<{
411
+ code: "missing_knowledge_metadata";
412
+ severity: "warn";
413
+ stable_id: string;
414
+ message: string;
415
+ }>;
416
+ };
417
+ declare function recall(projectRoot: string, input: RecallInput): Promise<RecallResult>;
256
418
 
257
419
  /**
258
- * knowledge-sync.ts Rule-sync orchestrator framework (R28, TASK-011)
420
+ * O(1) in-memory increment for a named counter. Safe to call from any MCP
421
+ * tool handler; no I/O happens until the next `flushMetrics()` call.
259
422
  *
260
- * Public surface: ensureKnowledgeFresh, reconcileKnowledge + exported types.
261
- * Internal helpers are co-located in this file.
262
- * Does NOT wire any consumers (MCP tools, doctor, watchers).
423
+ * `delta` defaults to 1; callers can pass a positive integer (e.g. fetched
424
+ * N stable_ids in a single sections call) to fold N bumps into one.
425
+ */
426
+ declare function bumpCounter(projectRoot: string, name: string, delta?: number): void;
427
+ /**
428
+ * Snapshot the current counter accumulator and reset it. Returned map is a
429
+ * frozen copy; the live accumulator starts fresh from zero. Exposed so
430
+ * flushMetrics + tests + a future fab_metrics manual-flush CLI hook can all
431
+ * use the same primitive without racing.
432
+ */
433
+ declare function drainCounters(projectRoot: string): Record<string, number>;
434
+ /**
435
+ * Drain the current accumulator and append one JSONL row to
436
+ * `.fabric/metrics.jsonl`. Returns the appended row (or `null` when the
437
+ * accumulator was empty — no spurious zero rows). fs failures degrade
438
+ * silently; the next flush will carry the union of the failed-write
439
+ * interval + the current one.
440
+ */
441
+ declare function flushMetrics(projectRoot: string, options?: {
442
+ windowMs?: number;
443
+ now?: Date;
444
+ }): Promise<MetricsRow | null>;
445
+ /**
446
+ * Start the background flush timer for a project root. Idempotent — calling
447
+ * twice on the same root replaces the prior interval. Returns a stop handle
448
+ * the caller can invoke at shutdown to flush + clear the timer.
263
449
  *
264
- * Distinction between the two public entry points:
450
+ * The flush is fire-and-forget (no await on the setInterval callback) so
451
+ * the timer cadence stays accurate even when fs is slow.
452
+ */
453
+ declare function startMetricsFlush(projectRoot: string, options?: {
454
+ intervalMs?: number;
455
+ }): () => Promise<void>;
456
+ /**
457
+ * Cancel the background flush timer for a project root if one is running.
458
+ * Does NOT drain the accumulator — callers that want a final flush should
459
+ * await flushMetrics(projectRoot) afterward.
460
+ */
461
+ declare function stopMetricsFlush(projectRoot: string): void;
462
+ /**
463
+ * Read accumulated metrics rows from `.fabric/metrics.jsonl`. Missing file
464
+ * returns []. Malformed rows are dropped silently (the sidecar is best-
465
+ * effort observability; a corrupt row never blocks a reader).
265
466
  *
266
- * - `ensureKnowledgeFresh`: detects drift, emits ledger events, invalidates cache.
267
- * Does NOT rewrite agents.meta.json. Optimised for hot-path consumers (MCP tools).
467
+ * Exposed for the NEW-34 `fab metrics` CLI dashboard + future doctor lints
468
+ * (e.g. cite-goodhart pattern replay) that need counter trends without
469
+ * walking events.jsonl.
470
+ */
471
+ declare function readMetrics(projectRoot: string): Promise<MetricsRow[]>;
472
+ type MetricsRow = {
473
+ timestamp: string;
474
+ window: string;
475
+ counters: Record<string, number>;
476
+ };
477
+ /**
478
+ * Canonical metric counter names used by the high-frequency emitters that
479
+ * left events.jsonl as part of the rc.37 Wave B clean-slate. Centralized
480
+ * here so the B5 hard-gate (`metric_event_in_jsonl`) can grep for these
481
+ * exact strings and fail when one accidentally re-appears in the audit
482
+ * ledger emit path.
483
+ */
484
+ declare const METRIC_COUNTER_NAMES: {
485
+ readonly knowledge_consumed: "knowledge_consumed";
486
+ readonly edit_intent_checked: "edit_intent_checked";
487
+ readonly knowledge_context_planned: "knowledge_context_planned";
488
+ readonly knowledge_sections_fetched: "knowledge_sections_fetched";
489
+ };
490
+ type MetricCounterName = (typeof METRIC_COUNTER_NAMES)[keyof typeof METRIC_COUNTER_NAMES];
491
+
492
+ /**
493
+ * Start the background rotation timer for a project root. Idempotent —
494
+ * calling twice on the same root replaces the prior interval. Returns a
495
+ * stop handle the caller can invoke at shutdown.
268
496
  *
269
- * - `reconcileKnowledge`: full scan + rewrites agents.meta.json (via knowledge-meta-builder)
270
- * + emits ledger events. Used by startup (TASK-022) and doctor repair (TASK-023).
497
+ * The tick fires fire-and-forget (no await on the setInterval callback) so
498
+ * the cadence stays accurate even when fs is slow. The first tick fires
499
+ * AFTER one full interval — startup-time rotation should run separately
500
+ * if desired (most callers don't, because rotation work blocks the
501
+ * connect path).
271
502
  */
503
+ declare function startRotationTick(projectRoot: string, options?: {
504
+ intervalMs?: number;
505
+ }): () => void;
506
+ /**
507
+ * Cancel the background rotation timer for a project root if one is
508
+ * running. Does NOT trigger a final rotation — callers that want a final
509
+ * rotation should call rotateEventLedgerIfNeeded(projectRoot) directly.
510
+ */
511
+ declare function stopRotationTick(projectRoot: string): void;
512
+
513
+ /**
514
+ * Shared constants used across the server package.
515
+ */
516
+ /** MCP resource URI for the project's bootstrap README (L0 rules) file. */
517
+ declare const AGENTS_MD_RESOURCE_URI = "fabric://bootstrap-readme";
518
+
272
519
  interface KnowledgeSyncOptions {
273
520
  mode?: "incremental" | "full";
274
521
  /** When true, invalid frontmatter throws RuleValidationError (default: false — collect as warning). */
275
522
  throwOnInvalidFrontmatter?: boolean;
523
+ /**
524
+ * v2.0.0-rc.29 TASK-005 (BUG-G1): when true, `ensureKnowledgeFresh`
525
+ * synchronously follows a drift detection with a `reconcileKnowledge`
526
+ * call to materialize the auto-heal (rewrite agents.meta.json + emit a
527
+ * paired `knowledge_meta_auto_healed` event). Default false preserves
528
+ * the rc.28 hot-path semantics where drift detection never blocks the
529
+ * MCP read on a meta rebuild. Opt-in is intended for callers that can
530
+ * tolerate ~tens-of-ms extra latency in exchange for the invariant
531
+ * "every knowledge_drift_detected has a paired heal event in the same
532
+ * tail window." Audit (BUG-G1) found 5/72 drifts healed on this repo
533
+ * (~7%) because the hot path emitted detect-only events.
534
+ */
535
+ autoHealOnDrift?: boolean;
276
536
  }
277
537
  interface StructuredWarning {
278
538
  code: string;
@@ -310,8 +570,20 @@ interface KnowledgeSyncReport {
310
570
  */
311
571
  declare function ensureKnowledgeFresh(projectRoot: string, opts?: KnowledgeSyncOptions): Promise<KnowledgeSyncReport>;
312
572
  interface ReconcileKnowledgeOptions {
313
- /** Identifies who triggered the reconcile; controls which summary ledger event is written. */
314
- trigger?: "startup" | "doctor" | "manual";
573
+ /**
574
+ * Identifies who triggered the reconcile; controls which summary ledger event is written.
575
+ *
576
+ * v2.0.0-rc.23 TASK-005 (a-B): `auto-heal-description` added so plan_context
577
+ * can drive a full reconcile when it detects nodes with `description === undefined`
578
+ * (legacy meta drift the revision-hash gate cannot detect).
579
+ *
580
+ * v2.0.0-rc.27 TASK-001 (§2.9 root): `post-approve` / `post-modify` added so
581
+ * `fab_review` approve/modify-layer-flip can drive an immediate meta rebuild
582
+ * — without this the new entry's `nodes[id]` stays empty until the next
583
+ * plan_context call's auto-heal, which leaves the entry undiscoverable in
584
+ * the description_index window between approve and the next hint call.
585
+ */
586
+ trigger?: "startup" | "doctor" | "manual" | "auto-heal-description" | "auto-heal-after-drift" | "post-approve" | "post-modify";
315
587
  }
316
588
  /**
317
589
  * Full scan + rewrites agents.meta.json with ground-truth disk state + emits
@@ -324,23 +596,6 @@ interface ReconcileKnowledgeOptions {
324
596
  */
325
597
  declare function reconcileKnowledge(projectRoot: string, opts?: ReconcileKnowledgeOptions): Promise<KnowledgeSyncReport>;
326
598
 
327
- declare class ServeLockHeldError extends IOFabricError {
328
- readonly code = "SERVE_LOCK_HELD";
329
- readonly httpStatus = 423;
330
- }
331
- interface LockState {
332
- pid: number;
333
- acquiredAt: number;
334
- host?: string;
335
- }
336
- interface AcquireOptions {
337
- force?: boolean;
338
- }
339
- declare function acquireLock(projectRoot: string, opts?: AcquireOptions): void;
340
- declare function releaseLock(projectRoot: string): void;
341
- declare function readLockState(projectRoot: string): LockState | null;
342
- declare function checkLockOrThrow(projectRoot: string, opts?: AcquireOptions): void;
343
-
344
599
  /**
345
600
  * Returns an info-level startup message when CLAUDE.md or AGENTS.md exist at
346
601
  * the project root, or null when neither is present.
@@ -375,11 +630,5 @@ interface ShutdownHandlerDeps {
375
630
  * `invoked` flag, so per-signal dedup is isolated.
376
631
  */
377
632
  declare function createShutdownHandler(deps: ShutdownHandlerDeps): () => void;
378
- declare function startHttpServer(options: {
379
- port: number;
380
- projectRoot: string;
381
- host?: string;
382
- authToken?: string;
383
- }): Promise<Server>;
384
633
 
385
- export { AGENTS_MD_RESOURCE_URI, type AcquireOptions, type DoctorApplyLintMutation, type DoctorApplyLintMutationKind, type DoctorApplyLintReport, type DoctorFixReport, type DoctorIssue, type DoctorReport, EVENT_LEDGER_PATH, type InFlightTracker, KnowledgeIdAllocator, type KnowledgeMetaBuildResult, type KnowledgeMetaBuildSource, type KnowledgeSyncLedgerEvent, type KnowledgeSyncOptions, type KnowledgeSyncReport, LEDGER_PATH, LEGACY_LEDGER_PATH, type LedgerEvent, type LockState, type PlanContextInput, type PlanContextResult, type ReconcileKnowledgeOptions, type RequirementProfile, type SelectionTokenState, ServeLockHeldError, type ShutdownHandlerDeps, type StructuredWarning, type WriteKnowledgeMetaOptions, acquireLock, appendEventLedgerEvent, buildKnowledgeMeta, checkLockOrThrow, computeKnowledgeBasedAgentsMeta, computeKnowledgeTestIndex, createFabricServer, createInFlightTracker, createShutdownHandler, deriveKnowledgeMetaLayer, deriveKnowledgeMetaTopologyType, ensureKnowledgeFresh, extractKnowledge, flushAndSyncEventLedger, formatPreexistingRootMessage, getEventLedgerPath, getLedgerPath, getLegacyLedgerPath, isSameKnowledgeTestIndex, planContext, readLockState, readSelectionToken, reconcileKnowledge, releaseLock, reviewKnowledge, runDoctorApplyLint, runDoctorFix, runDoctorReport, stableStringify, startHttpServer, startStdioServer, writeKnowledgeMeta };
634
+ export { AGENTS_MD_RESOURCE_URI, type ArchiveHistoryEntry, type ArchiveHistoryReport, type CiteCoverageReport, type DoctorApplyLintMutation, type DoctorApplyLintMutationKind, type DoctorApplyLintReport, type DoctorFixReport, type DoctorIssue, type DoctorReport, EVENT_LEDGER_PATH, type EnrichDescriptionsCandidate, type EnrichDescriptionsMode, type EnrichDescriptionsReport, type HistoryAllReport, type HistoryDayRow, type InFlightTracker, KnowledgeIdAllocator, type KnowledgeMetaBuildResult, type KnowledgeMetaBuildSource, type KnowledgeSyncLedgerEvent, type KnowledgeSyncOptions, type KnowledgeSyncReport, LEDGER_PATH, LEGACY_LEDGER_PATH, type LedgerEvent, METRICS_LEDGER_PATH, METRIC_COUNTER_NAMES, type MetricCounterName, type MetricsRow, type PlanContextInput, type PlanContextResult, type RecallInput, type RecallResult, type ReconcileKnowledgeOptions, type RequirementProfile, type SelectionTokenState, type ShutdownHandlerDeps, type StructuredWarning, type WriteKnowledgeMetaOptions, appendEventLedgerEvent, buildKnowledgeMeta, bumpCounter, computeKnowledgeBasedAgentsMeta, computeKnowledgeTestIndex, createFabricServer, createInFlightTracker, createShutdownHandler, deriveKnowledgeMetaLayer, deriveKnowledgeMetaTopologyType, drainCounters, enrichDescriptions, ensureKnowledgeFresh, extractKnowledge, flushAndSyncEventLedger, flushMetrics, formatPreexistingRootMessage, getEventLedgerPath, getLedgerPath, getLegacyLedgerPath, getMetricsLedgerPath, isSameKnowledgeTestIndex, loadKbIdTypeMap, planContext, readMetrics, readSelectionToken, recall, reconcileKnowledge, reviewKnowledge, runDoctorApplyLint, runDoctorArchiveHistory, runDoctorCiteCoverage, runDoctorFix, runDoctorHistoryAll, runDoctorReport, stableStringify, startMetricsFlush, startRotationTick, startStdioServer, stopMetricsFlush, stopRotationTick, writeKnowledgeMeta };