@karmaniverous/jeeves-watcher 0.19.1 → 0.21.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,24 +1,59 @@
1
- import { JeevesComponentDescriptor } from '@karmaniverous/jeeves';
2
1
  import { FastifyInstance } from 'fastify';
2
+ import { JeevesComponentDescriptor } from '@karmaniverous/jeeves';
3
+ import { EmbeddingConfig, IssueRecord, InferenceRule, SchemaEntry, SourcesConfig, VectorStoreConfig, DocumentResult, DocumentRejectionError, DocumentFailure, VcsRetentionConfig, VcsConfig, JeevesWatcherConfig, VcsHealth, InitialScanStatus, RestartCause, ConfigWatchReindexScope, ChangeSourceHealth, WatchConfig, ReindexStatus, IndexCheckHealth, LoggingConfig } from '@karmaniverous/jeeves-watcher-core';
4
+ export { ApiConfig, ConfigWatchConfig, EmbeddingConfig, InferenceRule, InitialScanStatus, IssueRecord, JeevesWatcherConfig, JeevesWatcherConfigInput, LoggingConfig, ReindexStatus, VectorStoreConfig, WatchConfig, apiConfigSchema, configWatchConfigSchema, embeddingConfigSchema, inferenceRuleSchema, issueRecordSchema, jeevesWatcherConfigSchema, loggingConfigSchema, vectorStoreConfigSchema, watchConfigSchema } from '@karmaniverous/jeeves-watcher-core';
3
5
  import pino from 'pino';
4
- import { EmbeddingConfig, InferenceRule, SchemaEntry, VectorStoreConfig, VcsConfig, JeevesWatcherConfig, WatchConfig, LoggingConfig, VcsRetentionConfig } from '@karmaniverous/jeeves-watcher-core';
5
- export { ApiConfig, ConfigWatchConfig, EmbeddingConfig, InferenceRule, JeevesWatcherConfig, JeevesWatcherConfigInput, LoggingConfig, VectorStoreConfig, WatchConfig, apiConfigSchema, configWatchConfigSchema, embeddingConfigSchema, inferenceRuleSchema, jeevesWatcherConfigSchema, loggingConfigSchema, vectorStoreConfigSchema, watchConfigSchema } from '@karmaniverous/jeeves-watcher-core';
6
- import { z } from 'zod';
6
+ import { ErrorObject } from 'ajv';
7
7
  import Handlebars from 'handlebars';
8
- import { JsonMapMap } from '@karmaniverous/jsonmap';
8
+ import { z } from 'zod';
9
9
  import { Stats } from 'node:fs';
10
+ import ParcelWatcher from '@parcel/watcher';
11
+ import { JsonMapMap } from '@karmaniverous/jsonmap';
10
12
 
11
13
  /**
12
14
  * @module embedding/types
13
15
  * Embedding provider type definitions.
14
16
  */
15
17
 
18
+ /**
19
+ * Per-call budget for the shared rate limiter. Providers that do not
20
+ * rate-limit ignore the budget; providers that retry internally await
21
+ * `beforeRetry` before each retry.
22
+ *
23
+ * The budget bounds the limiter, not the provider's own network retries:
24
+ * each of the `attempts` provider calls may itself retry a transient
25
+ * (non-429) failure up to `NETWORK_RETRY_POLICY.attempts` times in all, with
26
+ * backoff sleeps, so one `embed()` can issue up to `attempts` x 5 HTTP
27
+ * requests.
28
+ */
29
+ interface EmbedOptions {
30
+ /**
31
+ * Maximum provider calls for this call: a 429 ends a provider call and
32
+ * costs one attempt. Transient network retries inside a provider call do
33
+ * not count against it (see the interface note).
34
+ */
35
+ attempts?: number;
36
+ /**
37
+ * Maximum total time to wait for the limiter (token or `Retry-After`
38
+ * pause), in ms. When a wait would exceed it, the call fails fast. The
39
+ * provider's own network-retry backoff sleeps are not counted.
40
+ */
41
+ maxWaitMs?: number;
42
+ /**
43
+ * Awaited by a provider before each retry it makes internally (any HTTP
44
+ * request after the first of an `embed()` call), so every request draws
45
+ * from the shared limiter. Supplied by the rate-limited wrapper; it may
46
+ * reject (e.g. `EmbeddingUnavailableError`), which the provider must
47
+ * propagate without retrying.
48
+ */
49
+ beforeRetry?: () => Promise<void>;
50
+ }
16
51
  /**
17
52
  * An embedding provider that converts text into vector representations.
18
53
  */
19
54
  interface EmbeddingProvider {
20
- /** Generate embedding vectors for the given texts. */
21
- embed(texts: string[]): Promise<number[][]>;
55
+ /** Generate embedding vectors for the given texts (one provider request). */
56
+ embed(texts: string[], options?: EmbedOptions): Promise<number[][]>;
22
57
  /** The dimensionality of the embedding vectors. */
23
58
  dimensions: number;
24
59
  }
@@ -46,9 +81,41 @@ type ProviderFactory = (config: EmbeddingConfig, logger?: pino.Logger) => Embedd
46
81
  */
47
82
  declare function createEmbeddingProvider(config: EmbeddingConfig, logger?: pino.Logger, additionalProviders?: Map<string, ProviderFactory>): EmbeddingProvider;
48
83
 
84
+ /**
85
+ * @module embedding/EmbeddingHealth
86
+ * Embedding health (D86, D88, D89): wraps the shared rate-limited provider and tracks ok/failing (3+ failures since the last success and 60 s without one), with transition logs and alerts. Health shows codes, never provider text.
87
+ *
88
+ * A rate-limit wait that later succeeds is not a failure; any error a call
89
+ * throws (including a fail-fast `EmbeddingUnavailableError`) is. The first
90
+ * success returns the state to `ok`.
91
+ */
92
+
93
+ /** Embedding health state. */
94
+ type EmbeddingHealthState = 'ok' | 'failing';
95
+ /** The most recent embedding error: when, and a machine-readable code. */
96
+ interface EmbeddingErrorSummary {
97
+ /** ISO timestamp. */
98
+ at: string;
99
+ /** `rate_limited`, `timeout`, `http_<status>`, `network` or `error`. */
100
+ code: string;
101
+ }
102
+ /** `GET /status` `health.embedding`. */
103
+ interface EmbeddingHealthSnapshot {
104
+ /** `ok` or `failing`. */
105
+ state: EmbeddingHealthState;
106
+ /** Last successful call (ISO), `null` before the first. */
107
+ lastSuccessAt: string | null;
108
+ /** Most recent error, `null` if none yet. */
109
+ lastError: EmbeddingErrorSummary | null;
110
+ /** When the state turned `failing` (ISO), `null` while `ok`. */
111
+ failingSince: string | null;
112
+ /** End of the stored provider back-off (ISO), `null` if none. */
113
+ backoffUntil: string | null;
114
+ }
115
+
49
116
  /**
50
117
  * @module enrichment/EnrichmentStore
51
- * SQLite-backed enrichment metadata store. Persists path-keyed metadata at stateDir/enrichments.sqlite. Atomic writes, supports move.
118
+ * SQLite-backed enrichment metadata store. Persists path- or source-reference-keyed metadata (references verbatim) at stateDir/enrichments.sqlite. Atomic writes, supports move.
52
119
  */
53
120
 
54
121
  /**
@@ -188,95 +255,44 @@ declare abstract class JsonFileStore<T> {
188
255
 
189
256
  /**
190
257
  * @module issues/types
191
- * Zod schemas and TypeScript types for the issues tracking system. Defines IssueRecord and IssuesFile structures.
258
+ * Issue record schema and types for the issues tracking system. IssueRecord is defined in core (GET /issues response); IssuesFile is the on-disk shape.
192
259
  */
193
260
 
194
- /** Schema for a single issue record tracking a processing failure. */
195
- declare const issueRecordSchema: z.ZodObject<{
196
- type: z.ZodUnion<readonly [z.ZodLiteral<"type_collision">, z.ZodLiteral<"interpolation_error">]>;
197
- property: z.ZodOptional<z.ZodString>;
198
- rules: z.ZodOptional<z.ZodArray<z.ZodString>>;
199
- rule: z.ZodOptional<z.ZodString>;
200
- types: z.ZodOptional<z.ZodArray<z.ZodString>>;
201
- message: z.ZodString;
202
- timestamp: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
203
- }, z.core.$strip>;
204
- /** A single issue record tracking a processing failure for a file. */
205
- type IssueRecord = z.infer<typeof issueRecordSchema>;
206
261
  /** Issues file: array of issue records per file path. */
207
262
  type IssuesFile = Partial<Record<string, IssueRecord[]>>;
208
263
 
209
264
  /**
210
265
  * @module issues/IssuesManager
211
- * Manages persistent issue tracking for file processing failures. Read-modify-write with in-memory cache.
266
+ * Manages persistent issue tracking for processing failures, keyed by document key (file path or source reference). Read-modify-write with in-memory cache.
212
267
  */
213
268
 
214
269
  /**
215
- * Manages a persistent issues.json file tracking processing failures per file.
270
+ * Manages a persistent issues.json file tracking processing failures per
271
+ * document key (file path or source reference).
216
272
  */
217
273
  declare class IssuesManager extends JsonFileStore<IssuesFile> {
218
274
  constructor(stateDir: string, logger: pino.Logger);
219
275
  protected createEmpty(): IssuesFile;
220
- /** Record or update an issue for a file path. */
221
- record(filePath: string, type: IssueRecord['type'], message: string, options?: {
222
- property?: string;
223
- rules?: string[];
224
- types?: string[];
225
- }): void;
226
- /** Clear an issue for a file path (called on successful processing). */
227
- clear(filePath: string): void;
276
+ /**
277
+ * Record an issue for a document key (file path or source reference).
278
+ *
279
+ * @param key - Document key (file path or source reference).
280
+ * @param type - Canonical issue code.
281
+ * @param message - Error message.
282
+ * @param options - Optional operation, property, rule(s), types and
283
+ * structured `details` (stored as JSON, e.g. AJV errors).
284
+ */
285
+ record(key: string, type: IssueRecord['type'], message: string, options?: Pick<IssueRecord, 'operation' | 'property' | 'rules' | 'rule' | 'types' | 'details'>): void;
286
+ /** Clear the issues of a document key (called on successful processing). */
287
+ clear(key: string): void;
228
288
  /** Wipe all issues (called on full reindex start). */
229
289
  clearAll(): void;
290
+ /** Get the current issues of one document key, if any. */
291
+ get(key: string): IssueRecord[] | undefined;
230
292
  /** Get all current issues. */
231
293
  getAll(): IssuesFile;
232
294
  }
233
295
 
234
- /**
235
- * @module cache/ContentHashCache
236
- * In-memory cache mapping normalized file paths to content hashes.
237
- * Supports reverse lookup by hash for move correlation.
238
- */
239
- /**
240
- * In-memory content hash cache for move detection.
241
- *
242
- * Maps normalized file paths to SHA-256 content hashes.
243
- * Supports reverse lookup (hash → paths) for correlating
244
- * unlink+add events as file moves.
245
- */
246
- declare class ContentHashCache {
247
- private readonly pathToHash;
248
- private readonly hashToPaths;
249
- /**
250
- * Store or update the content hash for a file path.
251
- *
252
- * @param filePath - The file path (will be normalized).
253
- * @param hash - The SHA-256 content hash.
254
- */
255
- set(filePath: string, hash: string): void;
256
- /**
257
- * Get the content hash for a file path.
258
- *
259
- * @param filePath - The file path (will be normalized).
260
- * @returns The content hash, or undefined if not cached.
261
- */
262
- get(filePath: string): string | undefined;
263
- /**
264
- * Remove a file path from the cache.
265
- *
266
- * @param filePath - The file path (will be normalized).
267
- */
268
- delete(filePath: string): void;
269
- /**
270
- * Reverse lookup: get all file paths with a given content hash.
271
- *
272
- * @param hash - The content hash to look up.
273
- * @returns Array of normalized file paths with that hash.
274
- */
275
- getByHash(hash: string): string[];
276
- /** Number of cached entries. */
277
- get size(): number;
278
- }
279
-
280
296
  /**
281
297
  * @module templates/engine
282
298
  * Handlebars template compilation, caching, and resolution (file path vs named ref vs inline).
@@ -391,6 +407,29 @@ declare function buildTemplateEngine(rules: InferenceRule[], namedTemplates?: Re
391
407
  */
392
408
  declare function registerBuiltinHelpers(hbs: typeof Handlebars): void;
393
409
 
410
+ /**
411
+ * @module rules/sourceAttributes
412
+ * Zod schema and builder for source-document rule attributes (`source` identity block + `json` record; no `file` key). Pure, no I/O.
413
+ */
414
+
415
+ /**
416
+ * Attributes a source document is matched and resolved against. There is no
417
+ * `file` key, so file rules (`file.path` globs) never match a source
418
+ * document, and a source rule (`required: ["source"]`) never matches a file.
419
+ */
420
+ declare const sourceAttributesSchema: z.ZodObject<{
421
+ source: z.ZodObject<{
422
+ ref: z.ZodString;
423
+ name: z.ZodString;
424
+ id: z.ZodString;
425
+ rule: z.ZodString;
426
+ retrievedAt: z.ZodString;
427
+ }, z.core.$strip>;
428
+ json: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
429
+ }, z.core.$strip>;
430
+ /** Attributes of a source document. */
431
+ type SourceAttributes = z.infer<typeof sourceAttributesSchema>;
432
+
394
433
  /**
395
434
  * @module rules/attributes
396
435
  * Builds file attribute objects for rule matching. Pure function: derives attributes from path, stats, and extracted data.
@@ -420,6 +459,8 @@ interface FileAttributes {
420
459
  /** Parsed JSON content, if any. */
421
460
  json?: Record<string, unknown>;
422
461
  }
462
+ /** Attributes rules are matched and resolved against: a file or a source document. */
463
+ type RuleAttributes = FileAttributes | SourceAttributes;
423
464
  /**
424
465
  * Build {@link FileAttributes} from a file path and stat info.
425
466
  *
@@ -442,8 +483,13 @@ declare function buildAttributes(filePath: string, stats: Stats, extractedFrontm
442
483
  interface CompiledRule {
443
484
  /** The original rule definition. */
444
485
  rule: InferenceRule;
445
- /** The compiled AJV validate function. */
446
- validate: (data: unknown) => boolean;
486
+ /**
487
+ * The compiled AJV validate function. After a failed call, `errors` holds
488
+ * the AJV errors (used for `match_failed` issues).
489
+ */
490
+ validate: ((data: unknown) => boolean) & {
491
+ errors?: ErrorObject[] | null;
492
+ };
447
493
  }
448
494
  /**
449
495
  * Compile an array of inference rules into executable validators.
@@ -454,9 +500,31 @@ interface CompiledRule {
454
500
  */
455
501
  declare function compileRules(rules: InferenceRule[]): CompiledRule[];
456
502
 
503
+ /**
504
+ * @module rules/ruleErrors
505
+ * Interpolation errors collected while applying rules (schema `set` resolution, `render`, `template`): Zod schema, type and one-line formatting. Pure.
506
+ */
507
+
508
+ /**
509
+ * One interpolation failure of a matched rule. The rule's output for that
510
+ * stage is missing (schema failure: the rule's metadata, map and render are
511
+ * skipped; render/template failure: the raw content is used instead).
512
+ */
513
+ declare const ruleApplicationErrorSchema: z.ZodObject<{
514
+ rule: z.ZodString;
515
+ stage: z.ZodEnum<{
516
+ schema: "schema";
517
+ render: "render";
518
+ template: "template";
519
+ }>;
520
+ message: z.ZodString;
521
+ }, z.core.$strip>;
522
+ /** One interpolation failure of a matched rule. */
523
+ type RuleApplicationError = z.infer<typeof ruleApplicationErrorSchema>;
524
+
457
525
  /**
458
526
  * @module rules/apply
459
- * Applies compiled inference rules to file attributes, producing merged metadata via template resolution and JsonMap transforms.
527
+ * Applies compiled inference rules to file or source attributes, producing merged metadata via schema resolution, JsonMap transforms and render/template output; collects interpolation errors.
460
528
  */
461
529
 
462
530
  /**
@@ -478,6 +546,11 @@ interface ApplyRulesResult {
478
546
  matchedRules: string[];
479
547
  /** The renderAs value from the last matching rule that declares it, or null. */
480
548
  renderAs: string | null;
549
+ /**
550
+ * Interpolation failures (schema `set` resolution, `render`, `template`)
551
+ * of matched rules, in order. Each was also logged as a warning.
552
+ */
553
+ errors: RuleApplicationError[];
481
554
  }
482
555
  /**
483
556
  * Optional parameters for applyRules beyond the required compiledRules and attributes.
@@ -503,14 +576,16 @@ interface ApplyRulesOptions {
503
576
  *
504
577
  * Rules are evaluated in order; later rules override earlier ones.
505
578
  * If a rule has a `map`, the JsonMap transformation is applied after `set` resolution,
506
- * and map output overrides set output on conflict.
579
+ * and map output overrides set output on conflict. A failing schema, render
580
+ * or template is logged and reported in `errors`; the rule's output for that
581
+ * stage is skipped (a schema failure skips the rest of the rule).
507
582
  *
508
583
  * @param compiledRules - The compiled rules to evaluate.
509
- * @param attributes - The file attributes to match against.
584
+ * @param attributes - The file or source attributes to match against.
510
585
  * @param options - Optional configuration for rule application.
511
- * @returns The merged metadata and optional rendered content.
586
+ * @returns The merged metadata, optional rendered content and errors.
512
587
  */
513
- declare function applyRules(compiledRules: CompiledRule[], attributes: FileAttributes, options?: ApplyRulesOptions): Promise<ApplyRulesResult>;
588
+ declare function applyRules(compiledRules: CompiledRule[], attributes: RuleAttributes, options?: ApplyRulesOptions): Promise<ApplyRulesResult>;
514
589
 
515
590
  /**
516
591
  * @module rules/virtualRules
@@ -560,6 +635,242 @@ declare class VirtualRuleStore {
560
635
  get size(): number;
561
636
  }
562
637
 
638
+ /**
639
+ * @module processor/DocumentLocks
640
+ * One in-process FIFO lock per document key (D98): every process, delete, move, too_large and metadata change of a document runs under it. API routes wait at most 10 s, then 503 (D118). Idle locks are dropped.
641
+ *
642
+ * Keys: the normalised file path (the same normalisation as point IDs) or
643
+ * the source reference. A move takes both keys in sorted order, so two moves
644
+ * cannot deadlock.
645
+ */
646
+
647
+ /** Options of one locked operation. */
648
+ interface DocumentLockOptions {
649
+ /** Longest wait for the lock (omit: unbounded, as for background work). */
650
+ lockWaitMs?: number;
651
+ }
652
+ /**
653
+ * The lock key of a file.
654
+ *
655
+ * @param filePath - The file path, as given.
656
+ * @returns The normalised path (lower-case, forward slashes).
657
+ */
658
+ declare function fileLockKey(filePath: string): string;
659
+ /** The per-document locks of one processor. */
660
+ declare class DocumentLocks {
661
+ private readonly logger;
662
+ private readonly locks;
663
+ /** @param logger - Logger for long-hold warnings. */
664
+ constructor(logger: pino.Logger);
665
+ /** Number of keys currently locked or waited for. */
666
+ get size(): number;
667
+ /**
668
+ * Run `work` under the lock of `key`.
669
+ *
670
+ * @param key - Document key ({@link fileLockKey} or a source reference).
671
+ * @param operation - Operation name (warnings, timeout error).
672
+ * @param work - The work (must not take the same key again).
673
+ * @param options - Bounded wait.
674
+ * @returns What `work` returns.
675
+ * @throws DocumentLockTimeoutError when a bounded wait runs out.
676
+ */
677
+ run<T>(key: string, operation: string, work: () => Promise<T>, options?: DocumentLockOptions): Promise<T>;
678
+ /**
679
+ * Run `work` holding the locks of every key, taken one at a time in sorted
680
+ * order (duplicates once).
681
+ *
682
+ * @param keys - Document keys.
683
+ * @param operation - Operation name.
684
+ * @param work - The work.
685
+ * @param options - Bounded wait (per lock).
686
+ * @returns What `work` returns.
687
+ */
688
+ runAll<T>(keys: string[], operation: string, work: () => Promise<T>, options?: DocumentLockOptions): Promise<T>;
689
+ private lockFor;
690
+ }
691
+
692
+ /**
693
+ * @module cache/ContentHashCache
694
+ * In-memory cache mapping normalized file paths to content hashes.
695
+ * Supports reverse lookup by hash for move correlation.
696
+ */
697
+ /**
698
+ * In-memory content hash cache for move detection.
699
+ *
700
+ * Maps normalized file paths to SHA-256 content hashes.
701
+ * Supports reverse lookup (hash → paths) for correlating
702
+ * unlink+add events as file moves.
703
+ */
704
+ declare class ContentHashCache {
705
+ private readonly pathToHash;
706
+ private readonly hashToPaths;
707
+ /**
708
+ * Store or update the content hash for a file path.
709
+ *
710
+ * @param filePath - The file path (will be normalized).
711
+ * @param hash - The SHA-256 content hash.
712
+ */
713
+ set(filePath: string, hash: string): void;
714
+ /**
715
+ * Get the content hash for a file path.
716
+ *
717
+ * @param filePath - The file path (will be normalized).
718
+ * @returns The content hash, or undefined if not cached.
719
+ */
720
+ get(filePath: string): string | undefined;
721
+ /**
722
+ * Remove a file path from the cache.
723
+ *
724
+ * @param filePath - The file path (will be normalized).
725
+ */
726
+ delete(filePath: string): void;
727
+ /**
728
+ * Reverse lookup: get all file paths with a given content hash.
729
+ *
730
+ * @param hash - The content hash to look up.
731
+ * @returns Array of normalized file paths with that hash.
732
+ */
733
+ getByHash(hash: string): string[];
734
+ /** Number of cached entries. */
735
+ get size(): number;
736
+ }
737
+
738
+ /**
739
+ * @module sources/types
740
+ * Zod schemas and derived types for source callback outcomes (document / gone / error) and reference rejections. Pure declarations; no I/O.
741
+ */
742
+
743
+ /**
744
+ * Error codes a source callback can produce: the subset of the canonical
745
+ * issue codes that belong to retrieval.
746
+ */
747
+ declare const sourceErrorCodeSchema: z.ZodEnum<{
748
+ source_unavailable: "source_unavailable";
749
+ unsupported_content_type: "unsupported_content_type";
750
+ }>;
751
+ /** A source callback error code. */
752
+ type SourceErrorCode = z.infer<typeof sourceErrorCodeSchema>;
753
+ /** Outcome of one source callback (discriminated on `kind`). */
754
+ declare const sourceRetrievalResultSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
755
+ kind: z.ZodLiteral<"document">;
756
+ body: z.ZodRecord<z.ZodString, z.ZodUnknown>;
757
+ }, z.core.$strip>, z.ZodObject<{
758
+ kind: z.ZodLiteral<"gone">;
759
+ }, z.core.$strip>, z.ZodObject<{
760
+ kind: z.ZodLiteral<"error">;
761
+ code: z.ZodEnum<{
762
+ source_unavailable: "source_unavailable";
763
+ unsupported_content_type: "unsupported_content_type";
764
+ }>;
765
+ message: z.ZodString;
766
+ }, z.core.$strip>], "kind">;
767
+ /** Outcome of one source callback. */
768
+ type SourceRetrievalResult = z.infer<typeof sourceRetrievalResultSchema>;
769
+ /**
770
+ * Why a reference was rejected before any request was made:
771
+ * `invalid_reference` (not `<name>://<id>`), `unknown_source` (name not in
772
+ * config `sources`), `invalid_id` (empty, `.` or `..` segment, or the built
773
+ * URL would leave the configured origin). The reference-level subset of
774
+ * core `documentRejectionCodeSchema`.
775
+ */
776
+ declare const sourceRejectionCodeSchema: z.ZodEnum<{
777
+ invalid_reference: "invalid_reference";
778
+ unknown_source: "unknown_source";
779
+ invalid_id: "invalid_id";
780
+ }>;
781
+ /** A reference rejection code. */
782
+ type SourceRejectionCode = z.infer<typeof sourceRejectionCodeSchema>;
783
+ /** A parsed `<name>://<id>` reference. */
784
+ declare const parsedSourceReferenceSchema: z.ZodObject<{
785
+ ref: z.ZodString;
786
+ name: z.ZodString;
787
+ id: z.ZodString;
788
+ }, z.core.$strip>;
789
+ /** A parsed `<name>://<id>` reference. */
790
+ type ParsedSourceReference = z.infer<typeof parsedSourceReferenceSchema>;
791
+ /** A reference resolved against config: parsed parts plus the URL to request. */
792
+ declare const resolvedSourceReferenceSchema: z.ZodObject<{
793
+ ref: z.ZodString;
794
+ name: z.ZodString;
795
+ id: z.ZodString;
796
+ url: z.ZodString;
797
+ }, z.core.$strip>;
798
+ /** A reference resolved against config. */
799
+ type ResolvedSourceReference = z.infer<typeof resolvedSourceReferenceSchema>;
800
+ /**
801
+ * Thrown (or rejected) when a reference cannot be retrieved for a reason
802
+ * known before any request is made. No request is ever sent for a rejected
803
+ * reference.
804
+ */
805
+ declare class SourceReferenceError extends Error {
806
+ /** Rejection code. */
807
+ readonly code: SourceRejectionCode;
808
+ /**
809
+ * @param code - Rejection code.
810
+ * @param message - Human-readable reason (never contains header values).
811
+ */
812
+ constructor(code: SourceRejectionCode, message: string);
813
+ }
814
+
815
+ /**
816
+ * @module sources/SourceClient
817
+ * Registry of configured sources: resolves `<name>://<id>` to a config-built URL and retrieves it with per-source concurrency caps. Network I/O via requestSource.
818
+ */
819
+
820
+ /**
821
+ * Retrieves source documents by callback. Injected into later pipeline
822
+ * stages (resolver, notify queue, reindex) so tests can substitute it.
823
+ */
824
+ interface SourceRetriever {
825
+ /**
826
+ * Validate a reference and build its URL without making a request.
827
+ *
828
+ * @throws {@link SourceReferenceError}
829
+ */
830
+ resolve(reference: string): ResolvedSourceReference;
831
+ /**
832
+ * Retrieve a document. Rejects with {@link SourceReferenceError} (and
833
+ * sends nothing) for an invalid reference; otherwise never rejects.
834
+ */
835
+ retrieve(reference: string): Promise<SourceRetrievalResult>;
836
+ }
837
+ /**
838
+ * The source callback client. Constructed once from the parsed `sources`
839
+ * config (defaults applied, `${ENV}` already substituted at load). Each
840
+ * source gets its own concurrency semaphore; the `timeoutMs` clock starts
841
+ * when a request leaves the queue, not while it waits for a slot. Callbacks
842
+ * do not draw from the embedding rate limiter.
843
+ */
844
+ declare class SourceClient implements SourceRetriever {
845
+ private readonly entries;
846
+ /**
847
+ * @param sources - The parsed `sources` config section.
848
+ * @throws Error when a `url` template is not an absolute http(s) URL or
849
+ * places `{id}` in its origin (scheme, host or port).
850
+ */
851
+ constructor(sources?: SourcesConfig);
852
+ /** {@inheritDoc SourceRetriever.resolve} */
853
+ resolve(reference: string): ResolvedSourceReference;
854
+ /** {@inheritDoc SourceRetriever.retrieve} */
855
+ retrieve(reference: string): Promise<SourceRetrievalResult>;
856
+ private resolveEntry;
857
+ }
858
+
859
+ /**
860
+ * @module sources/sourceUrl
861
+ * Pure helpers: parse `<name>://<id>` references, encode ids segment-by-segment, and build callback URLs from a configured template. No I/O.
862
+ */
863
+
864
+ /**
865
+ * Parse a source reference.
866
+ *
867
+ * @param reference - A `<name>://<id>` string.
868
+ * @returns The parsed reference.
869
+ * @throws {@link SourceReferenceError} `invalid_reference` when the string
870
+ * does not match the core reference schema.
871
+ */
872
+ declare function parseSourceReference(reference: string): ParsedSourceReference;
873
+
563
874
  /**
564
875
  * @module util/BinaryFileStore
565
876
  * Binary-backed read/modify/write store with in-memory caching and debounced flush.
@@ -619,7 +930,7 @@ declare abstract class BinaryFileStore<T> {
619
930
  /** Per-rule distinct values: rule name → field name → sorted unique values. */
620
931
  type ValuesIndex = Record<string, Record<string, unknown[]>>;
621
932
  /**
622
- * Manages a persistent values.json file tracking distinct metadata values per rule.
933
+ * Manages a persistent values.v8 file tracking distinct metadata values per rule.
623
934
  */
624
935
  declare class ValuesManager extends BinaryFileStore<ValuesIndex> {
625
936
  constructor(stateDir: string, logger: pino.Logger);
@@ -636,6 +947,74 @@ declare class ValuesManager extends BinaryFileStore<ValuesIndex> {
636
947
  getForRule(ruleName: string): Record<string, unknown[]>;
637
948
  }
638
949
 
950
+ /**
951
+ * @module vectorStore/systemIndexes
952
+ * System payload indexes (D74, D154, D155): created at startup by background wait=true calls (startup never waits), reported as building, ready, conflict or failed; plus keyword-index readiness for by-filter writes. Qdrant I/O.
953
+ *
954
+ * `chunk_index` (integer) lets `countFacets` count documents, not chunks;
955
+ * `file_path` (keyword) serves metadata writes by filter (D139);
956
+ * `matched_rules` (keyword) serves rule-scoped counts (D155). A field
957
+ * already indexed with another type is left alone (`conflict`): writes keep
958
+ * using ID addressing, and `countFacets` answers 503 naming the index.
959
+ */
960
+
961
+ /** State of one system index (`GET /status` `health.collection.systemIndexes`). */
962
+ type SystemIndexState = 'building' | 'ready' | 'conflict' | 'failed';
963
+
964
+ /**
965
+ * @module vectorStore/facets
966
+ * Qdrant reads behind `countFacets` (spec Design 12): approximate facet counts (D194), one-point ordered scrolls for a field's range, and the collection's payload index types. Every count and scroll passes Qdrant's `timeout` (whole seconds) so an abandoned call also stops in Qdrant. Qdrant I/O.
967
+ */
968
+
969
+ /** A facet value as Qdrant reports it. */
970
+ type FacetValue = string | number | boolean;
971
+ /** One counted value. */
972
+ interface FacetHit {
973
+ /** The payload value. */
974
+ value: FacetValue;
975
+ /** Points holding it. */
976
+ count: number;
977
+ }
978
+ /** Which end of a range an ordered scroll reads. */
979
+ type RangeEnd = 'min' | 'max';
980
+ /** A range bound: a number, a stored date string, or null when nothing matched. */
981
+ type RangeBound = number | string | null;
982
+ /** One facet count or range read: field, filter and Qdrant timeout. */
983
+ interface FacetRead {
984
+ /** Payload field. */
985
+ field: string;
986
+ /** Qdrant filter (already scoped by the caller). */
987
+ filter: Record<string, unknown>;
988
+ /** Qdrant `timeout` query parameter, whole seconds. */
989
+ timeoutS: number;
990
+ }
991
+ /** Facet reads used by `countFacets` (`VectorStoreClient.facets`). */
992
+ interface FacetStore {
993
+ /**
994
+ * Approximate value counts of a field, highest first (D194).
995
+ *
996
+ * @param read - Field, filter and timeout.
997
+ * @param limit - Most values returned.
998
+ * @returns The counted values.
999
+ */
1000
+ facetCounts(read: FacetRead, limit: number): Promise<FacetHit[]>;
1001
+ /**
1002
+ * One end of a field's range over the filter.
1003
+ *
1004
+ * @param read - Field, filter and timeout.
1005
+ * @param end - `min` or `max`.
1006
+ * @returns The bound, or null when no point holds the field.
1007
+ */
1008
+ rangeBound(read: FacetRead, end: RangeEnd): Promise<RangeBound>;
1009
+ /** @returns Payload index type by field (`keyword`, `integer`, ...). */
1010
+ payloadIndexTypes(): Promise<Record<string, string>>;
1011
+ /**
1012
+ * @param field - A system index field (`chunk_index`, `matched_rules`).
1013
+ * @returns Its state, or undefined before startup.
1014
+ */
1015
+ systemIndexState(field: string): SystemIndexState | undefined;
1016
+ }
1017
+
639
1018
  /**
640
1019
  * @module vectorStore/types
641
1020
  * Vector store interface and type definitions.
@@ -730,11 +1109,50 @@ interface VectorStore {
730
1109
  * @param payload - Payload fields to set.
731
1110
  */
732
1111
  setPayload(ids: string[], payload: Record<string, unknown>): Promise<void>;
1112
+ /**
1113
+ * Remove payload keys from the specified point IDs (other keys kept).
1114
+ *
1115
+ * @param ids - Point IDs to update.
1116
+ * @param keys - Payload keys to remove.
1117
+ */
1118
+ deletePayload(ids: string[], keys: string[]): Promise<void>;
1119
+ /**
1120
+ * Whether `field` has a ready `keyword` payload index, so a by-filter
1121
+ * write on it is cheap (D154). A system index counts once its background
1122
+ * build returned; any other field when it was keyword-indexed at startup.
1123
+ *
1124
+ * @param field - Payload field.
1125
+ * @returns True when by-filter writes on `field` may be used.
1126
+ */
1127
+ keywordIndexReady(field: string): boolean;
1128
+ /**
1129
+ * Set payload fields on every point matching a filter (D139).
1130
+ *
1131
+ * @param filter - Qdrant filter selecting one document's points.
1132
+ * @param payload - Payload fields to set.
1133
+ */
1134
+ setPayloadWhere(filter: Record<string, unknown>, payload: Record<string, unknown>): Promise<void>;
1135
+ /**
1136
+ * Remove payload keys from every point matching a filter (D139).
1137
+ *
1138
+ * @param filter - Qdrant filter selecting one document's points.
1139
+ * @param keys - Payload keys to remove.
1140
+ */
1141
+ deletePayloadWhere(filter: Record<string, unknown>, keys: string[]): Promise<void>;
1142
+ /**
1143
+ * The IDs among `ids` that exist, in the given order (no payload, no
1144
+ * vectors; needs no index, D154).
1145
+ *
1146
+ * @param ids - Candidate point IDs.
1147
+ * @returns The existing IDs.
1148
+ */
1149
+ existingIds(ids: string[]): Promise<string[]>;
733
1150
  /**
734
1151
  * Get the payload of a point by ID.
735
1152
  *
736
1153
  * @param id - The point ID.
737
1154
  * @returns The payload, or `null` if the point doesn't exist.
1155
+ * @throws When the read fails (never reported as a missing point).
738
1156
  */
739
1157
  getPayload(id: string): Promise<Record<string, unknown> | null>;
740
1158
  /**
@@ -796,19 +1214,29 @@ interface VectorStore {
796
1214
  getPointsWithVectors(ids: string[]): Promise<VectorPoint[]>;
797
1215
  }
798
1216
 
1217
+ /**
1218
+ * @module vectorStore
1219
+ * Qdrant-backed {@link VectorStore}: collection setup, retried writes (see `writes`), payload reads, search, hybrid search and scrolling. Qdrant I/O.
1220
+ */
1221
+
799
1222
  /**
800
1223
  * Client wrapper for Qdrant vector store operations.
801
1224
  *
802
1225
  * Implements the {@link VectorStore} interface for dependency inversion.
1226
+ * Writes (upsert, delete, set/delete payload) are retried, each retry on a
1227
+ * fresh client (see QdrantWriter).
803
1228
  */
804
1229
  declare class VectorStoreClient implements VectorStore {
1230
+ /** Facet reads behind `countFacets` (spec Design 12). */
1231
+ readonly facets: FacetStore;
805
1232
  private readonly client;
806
- private writeClient;
1233
+ private readonly writer;
807
1234
  private readonly clientConfig;
808
1235
  private readonly collectionName;
809
1236
  private readonly dims;
810
- private readonly log;
811
1237
  private readonly pinoLogger?;
1238
+ private readonly payloadIndexes;
1239
+ private readonly systemIndexes;
812
1240
  /**
813
1241
  * Create a new VectorStoreClient.
814
1242
  *
@@ -823,123 +1251,71 @@ declare class VectorStoreClient implements VectorStore {
823
1251
  * Used to avoid stale HTTP keep-alive connections. The Qdrant JS client's
824
1252
  * internal undici Agent uses keepAliveTimeout: 10s, which causes ECONNRESET
825
1253
  * when connections sit idle during slow embedding calls (Gemini p99 ~8s).
826
- * Creating a fresh client for write operations ensures clean TCP connections.
1254
+ * Writes therefore run on their own client, replaced on every retry
1255
+ * (see QdrantWriter).
827
1256
  */
828
1257
  private createClient;
829
- /**
830
- * Count points matching a filter.
831
- *
832
- * @param filter - Optional Qdrant filter.
833
- * @returns The number of matching points.
834
- */
1258
+ /** {@inheritDoc VectorStore.count} */
835
1259
  count(filter?: Record<string, unknown>): Promise<number>;
836
1260
  /**
837
- * Ensure the collection exists with correct dimensions and Cosine distance.
1261
+ * Ensure the collection exists (Cosine, configured dimensions) and create
1262
+ * any missing payload indexes (configured plus `source`/`source_rule`),
1263
+ * then start the background system-index builds (D74, D154; not awaited).
838
1264
  */
839
1265
  ensureCollection(): Promise<void>;
840
- /**
841
- * Retry a Qdrant operation with standardized config and logging.
842
- *
843
- * @param operation - Operation name for logging (e.g., 'upsert', 'delete').
844
- * @param fn - Async function to retry.
845
- */
846
- private retryOperation;
847
- private getWriteClient;
848
- /**
849
- * Upsert points into the collection.
850
- *
851
- * Uses the shared client. On retry (after ECONNRESET from stale connections),
852
- * creates a fresh client to recover.
853
- *
854
- * @param points - The points to upsert.
855
- */
1266
+ /** @returns Each system index's state (`health.collection.systemIndexes`). */
1267
+ systemIndexStates(): Record<string, SystemIndexState>;
1268
+ /** @returns A promise settling once the background index builds end. */
1269
+ whenSystemIndexesSettled(): Promise<void>;
1270
+ /** {@inheritDoc VectorStore.keywordIndexReady} */
1271
+ keywordIndexReady(field: string): boolean;
1272
+ /** {@inheritDoc VectorStore.setPayloadWhere} */
1273
+ setPayloadWhere(filter: Record<string, unknown>, payload: Record<string, unknown>): Promise<void>;
1274
+ /** {@inheritDoc VectorStore.deletePayloadWhere} */
1275
+ deletePayloadWhere(filter: Record<string, unknown>, keys: string[]): Promise<void>;
1276
+ /** {@inheritDoc VectorStore.existingIds} */
1277
+ existingIds(ids: string[]): Promise<string[]>;
1278
+ /** {@inheritDoc VectorStore.upsert} */
856
1279
  upsert(points: VectorPoint[]): Promise<void>;
857
- /**
858
- * Delete points by their IDs.
859
- *
860
- * Uses the shared client. On retry, creates a fresh client to recover.
861
- *
862
- * @param ids - The point IDs to delete.
863
- */
1280
+ /** {@inheritDoc VectorStore.delete} */
864
1281
  delete(ids: string[]): Promise<void>;
865
- /**
866
- * Set payload fields for the specified point IDs.
867
- *
868
- * This merges the given payload object into each point's existing payload.
869
- *
870
- * @param ids - Point IDs to update.
871
- * @param payload - Payload fields to set.
872
- */
1282
+ /** {@inheritDoc VectorStore.setPayload} */
873
1283
  setPayload(ids: string[], payload: Record<string, unknown>): Promise<void>;
874
- /**
875
- * Get the payload of a point by ID.
876
- *
877
- * @param id - The point ID.
878
- * @returns The payload, or `null` if the point doesn't exist.
879
- */
1284
+ /** {@inheritDoc VectorStore.deletePayload} */
1285
+ deletePayload(ids: string[], keys: string[]): Promise<void>;
1286
+ /** {@inheritDoc VectorStore.getPayload} */
880
1287
  getPayload(id: string): Promise<Record<string, unknown> | null>;
881
- /**
882
- * Get collection info including point count, dimensions, and payload field schema.
883
- *
884
- * When Qdrant has payload indexes, uses `payload_schema` directly. Otherwise
885
- * samples points to discover fields and infer types.
886
- */
1288
+ /** {@inheritDoc VectorStore.getCollectionInfo} */
887
1289
  getCollectionInfo(): Promise<CollectionInfo>;
888
- /**
889
- * Search for similar vectors.
890
- *
891
- * @param vector - The query vector.
892
- * @param limit - Maximum results to return.
893
- * @param filter - Optional Qdrant filter.
894
- * @returns An array of search results.
895
- */
1290
+ /** {@inheritDoc VectorStore.search} */
896
1291
  search(vector: number[], limit: number, filter?: Record<string, unknown>, offset?: number): Promise<SearchResult[]>;
897
- /**
898
- * Create a full-text payload index on the specified field.
899
- * Idempotent — skips if the field already has a text index.
900
- *
901
- * @param fieldName - The payload field to index.
902
- */
1292
+ /** {@inheritDoc VectorStore.ensureTextIndex} */
903
1293
  ensureTextIndex(fieldName: string): Promise<void>;
904
- /**
905
- * Hybrid search combining dense vector and full-text match with RRF fusion.
906
- *
907
- * @param vector - The query vector.
908
- * @param queryText - The raw query text for full-text matching.
909
- * @param limit - Maximum results to return.
910
- * @param textWeight - Weight for text results in RRF (0–1).
911
- * @param filter - Optional Qdrant filter.
912
- * @param offset - Optional result offset for pagination.
913
- * @returns An array of search results.
914
- */
1294
+ /** {@inheritDoc VectorStore.hybridSearch} */
915
1295
  hybridSearch(vector: number[], queryText: string, limit: number, textWeight: number, filter?: Record<string, unknown>, offset?: number): Promise<SearchResult[]>;
916
- /**
917
- * Retrieve points with their vectors by ID.
918
- *
919
- * @param ids - The point IDs to retrieve.
920
- * @returns Points with vectors and payloads; missing IDs are omitted.
921
- */
1296
+ /** {@inheritDoc VectorStore.getPointsWithVectors} */
922
1297
  getPointsWithVectors(ids: string[]): Promise<VectorPoint[]>;
923
- /**
924
- * Scroll one page of points matching a filter.
925
- *
926
- * @param filter - Optional Qdrant filter.
927
- * @param limit - Page size.
928
- * @param offset - Cursor offset from previous page.
929
- * @param fields - Optional field projection.
930
- * @returns Page of points and next cursor.
931
- */
1298
+ /** {@inheritDoc VectorStore.scrollPage} */
932
1299
  scrollPage(filter?: Record<string, unknown>, limit?: number, offset?: string | number, fields?: string[]): Promise<ScrollPageResult>;
933
- /**
934
- * Scroll through all points matching a filter.
935
- *
936
- * @param filter - Optional Qdrant filter.
937
- * @param limit - Page size for scrolling.
938
- * @yields Scrolled points.
939
- */
1300
+ /** {@inheritDoc VectorStore.scroll} */
940
1301
  scroll(filter?: Record<string, unknown>, limit?: number): AsyncGenerator<ScrolledPoint>;
941
1302
  }
942
1303
 
1304
+ /**
1305
+ * @module processor/processingLimits
1306
+ * Resolve the `processing` ceilings (D44, D64) from the live config to bytes and a chunk count; read per document, so a reload applies to the next one. Pure.
1307
+ */
1308
+
1309
+ /** The resolved ceilings. */
1310
+ interface ProcessingLimits {
1311
+ /** Largest file processed (bytes, checked with `stat` before extraction). */
1312
+ maxFileBytes: number;
1313
+ /** Most chunks one document may split into. */
1314
+ maxChunksPerFile: number;
1315
+ /** Largest extracted text (UTF-8 bytes). */
1316
+ maxTextBytes: number;
1317
+ }
1318
+
943
1319
  /**
944
1320
  * @module processor/ProcessorConfig
945
1321
  * Configuration interface for DocumentProcessor, extracted for single-responsibility.
@@ -963,10 +1339,48 @@ interface ProcessorConfig {
963
1339
  globalSchemas?: Record<string, SchemaEntry>;
964
1340
  }
965
1341
 
1342
+ /**
1343
+ * @module processor/DocumentProcessorDeps
1344
+ * Constructor dependencies for DocumentProcessor (config, embedding provider, vector store, rules, optional stores), extracted for single-responsibility.
1345
+ */
1346
+
1347
+ /**
1348
+ * Dependencies injected into DocumentProcessor.
1349
+ */
1350
+ interface DocumentProcessorDeps {
1351
+ /** Processor configuration (chunk sizes, directories, maps). */
1352
+ config: ProcessorConfig;
1353
+ /** Provider for generating text embeddings. */
1354
+ embeddingProvider: EmbeddingProvider;
1355
+ /** Vector store for persistence. */
1356
+ vectorStore: VectorStore;
1357
+ /** Pre-compiled inference rules for metadata extraction. */
1358
+ compiledRules: CompiledRule[];
1359
+ /** Pino logger instance. */
1360
+ logger: pino.Logger;
1361
+ /** Optional Handlebars template engine for content templates. */
1362
+ templateEngine?: TemplateEngine;
1363
+ /** Optional enrichment store for persisted enrichment metadata. */
1364
+ enrichmentStore?: EnrichmentStoreInterface;
1365
+ /** Optional issues manager; processing failures are recorded as typed issues (#255). */
1366
+ issuesManager?: IssuesManager;
1367
+ /** Optional values manager for tracking rule-extracted values. */
1368
+ valuesManager?: ValuesManager;
1369
+ /** Optional content hash cache for move detection. */
1370
+ contentHashCache?: ContentHashCache;
1371
+ /** Optional source callback client; without it, source documents fail `source_unavailable`. */
1372
+ sourceRetriever?: SourceRetriever;
1373
+ /** Per-document locks (D98); default: the processor's own. */
1374
+ documentLocks?: DocumentLocks;
1375
+ /** The processing ceilings, read per document (D44); default: every default. */
1376
+ getProcessingLimits?: () => ProcessingLimits;
1377
+ }
1378
+
966
1379
  /**
967
1380
  * @module processor/renderResult
968
- * Type definition for the render endpoint result.
1381
+ * The render endpoint result and its one constructor from a resolved document (file or source). Pure, no I/O.
969
1382
  */
1383
+
970
1384
  /**
971
1385
  * Result returned by {@link DocumentProcessorInterface.renderFile}.
972
1386
  */
@@ -983,6 +1397,73 @@ interface RenderResult {
983
1397
  transformed: boolean;
984
1398
  }
985
1399
 
1400
+ /**
1401
+ * @module processor/documentPipeline
1402
+ * The shared indexing pipeline for resolved documents (file or source): onResolved hook, empty check, text and chunk ceilings (too_large), hash compare, streamed embed and upsert; plus point deletion.
1403
+ */
1404
+
1405
+ /**
1406
+ * Options for indexing one document (see
1407
+ * {@link SourceOperationsInterface.process}).
1408
+ */
1409
+ interface IndexOptions {
1410
+ /**
1411
+ * Re-embed and rewrite even when the content hash is unchanged (source
1412
+ * documents on a `full` reindex).
1413
+ */
1414
+ force?: boolean;
1415
+ }
1416
+
1417
+ /**
1418
+ * @module processor/SourceOperations
1419
+ * Source-document operations for the API: notify validation (reference, configured source, rule), stored designated rule lookup, uncached render, metadata enrichment and processing. Qdrant and callback I/O.
1420
+ */
1421
+
1422
+ /** Outcome of rendering a source document (`POST /render { source }`). */
1423
+ type SourceRenderOutcome = {
1424
+ kind: 'rendered';
1425
+ result: RenderResult;
1426
+ } | {
1427
+ kind: 'gone';
1428
+ } | {
1429
+ kind: 'failed';
1430
+ error: DocumentFailure;
1431
+ };
1432
+ /** Source-document operations used by the API. */
1433
+ interface SourceOperationsInterface {
1434
+ /** Process a reference with its designated rule (see `processSourceDocument`); `force` re-embeds unchanged content. */
1435
+ process(ref: string, rule: string, options?: IndexOptions): Promise<DocumentResult>;
1436
+ /** Synchronously validate a notification; `undefined` when acceptable. */
1437
+ validate(ref: string, rule: string): DocumentRejectionError | undefined;
1438
+ /** Designated rule stored on the document's chunk-0 point, or `null` if not indexed. */
1439
+ storedRule(ref: string): Promise<string | null>;
1440
+ /**
1441
+ * The compiled rule a designated rule name resolves to in the merged rule
1442
+ * set (config rules, then virtual rules: first of that name), or
1443
+ * `undefined` when no such rule exists.
1444
+ */
1445
+ compiledRule(name: string): CompiledRule | undefined;
1446
+ /** Retrieve and render a document without embedding or writing anything. */
1447
+ render(ref: string, rule: string): Promise<SourceRenderOutcome>;
1448
+ /**
1449
+ * Merge enrichment metadata into an indexed document's points; `null` if
1450
+ * not indexed. `options.lockWaitMs` bounds the document-lock wait (D118).
1451
+ */
1452
+ updateMetadata(ref: string, metadata: Record<string, unknown>, options?: DocumentLockOptions): Promise<Record<string, unknown> | null>;
1453
+ /**
1454
+ * Run `work` under the processor's document lock of `ref` (D98), e.g. a
1455
+ * whole-document delete. `ref` is any document key: a source reference,
1456
+ * or `fileLockKey(path)` for a file (`POST /rebuild-metadata`).
1457
+ * `options.lockWaitMs` bounds the wait (D118).
1458
+ */
1459
+ withLock<T>(ref: string, operation: string, work: () => Promise<T>, options?: DocumentLockOptions): Promise<T>;
1460
+ }
1461
+ /** A processor exposing source operations (`DocumentProcessor` implements it). */
1462
+ interface SourceOperationsProvider {
1463
+ /** Source operations bound to the processor's current rules and stores. */
1464
+ sourceOperations(): SourceOperationsInterface;
1465
+ }
1466
+
986
1467
  /**
987
1468
  * @module processor/types
988
1469
  * Document processor interface definitions.
@@ -998,10 +1479,16 @@ interface DocumentProcessorInterface {
998
1479
  processFile(filePath: string): Promise<void>;
999
1480
  /** Delete a file's points/metadata from the system. */
1000
1481
  deleteFile(filePath: string): Promise<void>;
1001
- /** Process a metadata sidecar update for a file (payload update only). */
1002
- processMetadataUpdate(filePath: string, metadata: Record<string, unknown>): Promise<Record<string, unknown> | null>;
1003
- /** Process a rules update for a file (rebuild merged metadata, payload update only). */
1004
- processRulesUpdate(filePath: string): Promise<Record<string, unknown> | null>;
1482
+ /**
1483
+ * Process a metadata sidecar update for a file (payload update only).
1484
+ * `options.lockWaitMs` bounds the document-lock wait (API routes, D118).
1485
+ */
1486
+ processMetadataUpdate(filePath: string, metadata: Record<string, unknown>, options?: DocumentLockOptions): Promise<Record<string, unknown> | null>;
1487
+ /**
1488
+ * Process a rules update for a file (rebuild merged metadata, payload
1489
+ * update only). `options.lockWaitMs` bounds the document-lock wait.
1490
+ */
1491
+ processRulesUpdate(filePath: string, options?: DocumentLockOptions): Promise<Record<string, unknown> | null>;
1005
1492
  /** Render a file through the rule engine without embedding. */
1006
1493
  renderFile(filePath: string): Promise<RenderResult>;
1007
1494
  /** Move a file's vector points and metadata from old path to new path (no re-embed). */
@@ -1009,76 +1496,61 @@ interface DocumentProcessorInterface {
1009
1496
  /** Update compiled inference rules and associated engines. */
1010
1497
  updateRules(compiledRules: CompiledRule[], templateEngine?: TemplateEngine, customMapLib?: Record<string, (...args: unknown[]) => unknown>): void;
1011
1498
  }
1012
-
1013
1499
  /**
1014
- * @module processor
1015
- *
1016
- * Core document processing pipeline. Handles extracting text, computing embeddings, syncing with vector store.
1500
+ * The processor the service runs on: file operations plus source operations.
1501
+ * `createDocumentProcessor` returns it; the API server and the notify queue
1502
+ * depend on it (`DocumentProcessor` implements it). Adapt a custom file-only
1503
+ * processor with `withoutSourceSupport`, whose source operations fail.
1017
1504
  */
1505
+ type WatcherProcessor = DocumentProcessorInterface & SourceOperationsProvider;
1018
1506
 
1019
1507
  /**
1020
- * Core document processing pipeline.
1508
+ * @module processor
1021
1509
  *
1022
- * Handles extracting text, computing embeddings, and syncing with the vector store.
1510
+ * Core document processing pipeline. Resolves files and source documents, computes embeddings, syncs with vector store.
1023
1511
  */
1024
- interface DocumentProcessorDeps {
1025
- /** Processor configuration (chunk sizes, directories, maps). */
1026
- config: ProcessorConfig;
1027
- /** Provider for generating text embeddings. */
1028
- embeddingProvider: EmbeddingProvider;
1029
- /** Vector store for persistence. */
1030
- vectorStore: VectorStore;
1031
- /** Pre-compiled inference rules for metadata extraction. */
1032
- compiledRules: CompiledRule[];
1033
- /** Pino logger instance. */
1034
- logger: pino.Logger;
1035
- /** Optional Handlebars template engine for content templates. */
1036
- templateEngine?: TemplateEngine;
1037
- /** Optional enrichment store for persisted enrichment metadata. */
1038
- enrichmentStore?: EnrichmentStoreInterface;
1039
- /** Optional issues manager for tracking processing errors. */
1040
- issuesManager?: IssuesManager;
1041
- /** Optional values manager for tracking rule-extracted values. */
1042
- valuesManager?: ValuesManager;
1043
- /** Optional content hash cache for move detection. */
1044
- contentHashCache?: ContentHashCache;
1045
- }
1512
+
1046
1513
  /**
1047
1514
  * Core document processing pipeline.
1048
1515
  *
1049
1516
  * Handles extracting text, computing embeddings, and syncing with the vector store.
1050
1517
  */
1051
- declare class DocumentProcessor implements DocumentProcessorInterface {
1518
+ declare class DocumentProcessor implements WatcherProcessor {
1519
+ private readonly deps;
1052
1520
  private config;
1053
- private readonly embeddingProvider;
1054
- private readonly vectorStore;
1055
1521
  private compiledRules;
1056
- private readonly logger;
1057
1522
  private templateEngine?;
1058
- private readonly enrichmentStore?;
1059
- private readonly issuesManager?;
1060
- private readonly valuesManager?;
1061
- private readonly contentHashCache?;
1523
+ private readonly locks;
1062
1524
  /**
1063
1525
  * Create a new DocumentProcessor.
1064
1526
  *
1065
1527
  * @param deps - The processor dependencies.
1066
1528
  */
1067
- constructor({ config, embeddingProvider, vectorStore, compiledRules, logger, templateEngine, enrichmentStore, issuesManager, valuesManager, contentHashCache, }: DocumentProcessorDeps);
1068
- /**
1069
- * Build merged metadata for a file and add matched_rules.
1070
- */
1071
- private buildMetadataWithRules;
1529
+ constructor(deps: DocumentProcessorDeps);
1530
+ /** The processing ceilings now (read per document, D44). */
1531
+ private limits;
1532
+ /** Rule context reflecting the current rules, templates and config. */
1533
+ private ruleContext;
1534
+ /** Shared pipeline dependencies. */
1535
+ private pipelineContext;
1536
+ /** File resolver bound to the current rule context. */
1537
+ private fileResolver;
1538
+ /** Dependencies of the payload-only file operations. */
1539
+ private fileOperations;
1072
1540
  /**
1073
- * Execute an async operation with standardized file error handling.
1541
+ * Run a file operation with standard failure handling: log, and record a
1542
+ * typed issue when `context.issueType` (the code for untagged failures)
1543
+ * is given; return `fallback` on failure.
1074
1544
  */
1075
- private withFileErrorHandling;
1545
+ private withFailureHandling;
1076
1546
  /**
1077
1547
  * Process a file through the full pipeline: extract, hash, chunk, embed, upsert.
1078
1548
  *
1079
1549
  * @param filePath - The file to process.
1080
1550
  */
1081
1551
  processFile(filePath: string): Promise<void>;
1552
+ /** {@inheritDoc SourceOperationsProvider.sourceOperations} */
1553
+ sourceOperations(): SourceOperationsInterface;
1082
1554
  /**
1083
1555
  * Delete all chunks for a file from the vector store and remove metadata.
1084
1556
  *
@@ -1086,22 +1558,28 @@ declare class DocumentProcessor implements DocumentProcessorInterface {
1086
1558
  */
1087
1559
  deleteFile(filePath: string): Promise<void>;
1088
1560
  /**
1089
- * Process a metadata update: merge into enrichment store, update Qdrant payloads (no re-embed).
1561
+ * Process a metadata update: merge into enrichment store, update Qdrant
1562
+ * payloads (no re-embed). The stored `file_path` (casing included) and
1563
+ * `matched_rules` are kept.
1090
1564
  *
1091
1565
  * @param filePath - The file whose metadata to update.
1092
1566
  * @param metadata - The new metadata to merge.
1567
+ * @param options - Bounded lock wait (API routes, D118).
1093
1568
  * @returns The merged payload, or `null` if the file is not indexed.
1569
+ * @throws DocumentLockTimeoutError when a bounded lock wait runs out.
1094
1570
  */
1095
- processMetadataUpdate(filePath: string, metadata: Record<string, unknown>): Promise<Record<string, unknown> | null>;
1571
+ processMetadataUpdate(filePath: string, metadata: Record<string, unknown>, options?: DocumentLockOptions): Promise<Record<string, unknown> | null>;
1096
1572
  /**
1097
- * Re-apply inference rules to a file without re-embedding.
1098
- * Reads file attributes, applies current rules, merges with enrichment metadata,
1099
- * and updates Qdrant payloads.
1573
+ * Re-apply inference rules to a file without re-embedding (see
1574
+ * `reapplyFileRules`: system fields and the stored `file_path` are
1575
+ * re-asserted; interpolation failures are recorded as an issue).
1100
1576
  *
1101
1577
  * @param filePath - The file to update.
1578
+ * @param options - Bounded lock wait (API routes, D118).
1102
1579
  * @returns The merged metadata, or `null` if the file is not indexed.
1580
+ * @throws DocumentLockTimeoutError when a bounded lock wait runs out.
1103
1581
  */
1104
- processRulesUpdate(filePath: string): Promise<Record<string, unknown> | null>;
1582
+ processRulesUpdate(filePath: string, options?: DocumentLockOptions): Promise<Record<string, unknown> | null>;
1105
1583
  /**
1106
1584
  * Render a file through the rule engine without embedding.
1107
1585
  * Returns rendered content, renderAs, matched rules, and metadata.
@@ -1128,10 +1606,29 @@ declare class DocumentProcessor implements DocumentProcessorInterface {
1128
1606
  updateRules(compiledRules: CompiledRule[], templateEngine?: TemplateEngine, customMapLib?: Record<string, (...args: unknown[]) => unknown>): void;
1129
1607
  }
1130
1608
 
1609
+ /**
1610
+ * @module queue/pausable
1611
+ * The pause/resume/quiesce surface shared by the file event queue and the source notify queue, and a combinator that drives several queues as one (used by prune). No I/O.
1612
+ */
1613
+ /**
1614
+ * A queue that can stop starting new work, resume, and report when its
1615
+ * in-flight work has finished. Prune uses this to quiesce every writer
1616
+ * before it computes and executes its plan.
1617
+ */
1618
+ interface PausableQueue {
1619
+ /** Stop starting new work; enqueues are still accepted. */
1620
+ pause(): void;
1621
+ /** Resume starting work. */
1622
+ resume(): void;
1623
+ /** Resolve once no work is in flight (queued work may remain). */
1624
+ waitForIdleWorkers(): Promise<void>;
1625
+ }
1626
+
1131
1627
  /**
1132
1628
  * @module queue
1133
- * Debounced, rate-limited, concurrent event queue for file watchers. Manages priority queuing and async callbacks. No direct I/O; orchestrates processing.
1629
+ * Debounced, concurrent event queue for file watchers. Manages priority queuing and async callbacks. Embedding request rate is limited by the shared embedding provider wrapper, not here. No direct I/O.
1134
1630
  */
1631
+
1135
1632
  /**
1136
1633
  * A watch event.
1137
1634
  */
@@ -1157,27 +1654,22 @@ interface EventQueueOptions {
1157
1654
  debounceMs: number;
1158
1655
  /** Maximum number of concurrent processors. */
1159
1656
  concurrency: number;
1160
- /** Optional max processed events per minute (token bucket). */
1161
- rateLimitPerMinute?: number;
1162
1657
  }
1163
1658
  /**
1164
- * A debounced, rate-limited, concurrent event queue.
1659
+ * A debounced, concurrent event queue.
1165
1660
  */
1166
1661
  declare class EventQueue {
1167
1662
  private readonly debounceMs;
1168
1663
  private readonly concurrency;
1169
- private readonly rateLimitPerMinute?;
1170
1664
  private started;
1171
1665
  private paused;
1666
+ private stopped;
1172
1667
  private active;
1173
1668
  private readonly debounceTimers;
1174
1669
  private readonly latestByKey;
1175
1670
  private readonly normalQueue;
1176
1671
  private readonly lowQueue;
1177
- private tokens;
1178
- private lastRefillMs;
1179
- private drainWaiters;
1180
- private idleWorkerWaiters;
1672
+ private readonly signals;
1181
1673
  /**
1182
1674
  * Create an event queue.
1183
1675
  *
@@ -1185,16 +1677,25 @@ declare class EventQueue {
1185
1677
  */
1186
1678
  constructor(options: EventQueueOptions);
1187
1679
  /**
1188
- * Enqueue an event, debounced per path+priority.
1680
+ * Enqueue an event, debounced per path+priority. Ignored once stopped.
1189
1681
  *
1190
1682
  * @param event - The watch event.
1191
1683
  * @param fn - The processing function to invoke when dequeued.
1192
1684
  */
1193
1685
  enqueue(event: WatchEvent, fn: ProcessFn): void;
1194
1686
  /**
1195
- * Start processing events.
1687
+ * Start processing events (events enqueued earlier are held until now).
1688
+ */
1689
+ start(): void;
1690
+ /**
1691
+ * Alias of {@link EventQueue.start}, kept for existing callers.
1196
1692
  */
1197
1693
  process(): void;
1694
+ /**
1695
+ * Stop: cancel pending debounces, drop queued events, ignore new ones, and
1696
+ * resolve once in-flight work finishes. Drain waiters resolve then too.
1697
+ */
1698
+ stop(): Promise<void>;
1198
1699
  /**
1199
1700
  * Pause processing events. Events can still be enqueued but will not be processed.
1200
1701
  */
@@ -1218,13 +1719,136 @@ declare class EventQueue {
1218
1719
  */
1219
1720
  waitForIdleWorkers(): Promise<void>;
1220
1721
  private push;
1221
- private refillTokens;
1222
- private takeToken;
1223
1722
  private nextItem;
1224
1723
  private pump;
1225
1724
  private isIdle;
1226
- private maybeResolveDrain;
1227
- private maybeResolveIdleWorkers;
1725
+ private settle;
1726
+ }
1727
+
1728
+ /**
1729
+ * @module queue/sourceNotifyTypes
1730
+ * Options, errors and job bookkeeping types of the source notify queue (validated options schema, stopped error, coalesced jobs and their waiters). No I/O.
1731
+ */
1732
+
1733
+ /**
1734
+ * Default number of references processed at once: the core
1735
+ * `embedding.concurrency` default, which the service passes in.
1736
+ */
1737
+ declare const DEFAULT_SOURCE_NOTIFY_CONCURRENCY: number;
1738
+ /** Options for the source notify queue. */
1739
+ declare const sourceNotifyQueueOptionsSchema: z.ZodObject<{
1740
+ concurrency: z.ZodDefault<z.ZodNumber>;
1741
+ }, z.core.$strip>;
1742
+ /** Options for the source notify queue (input form; defaults applied). */
1743
+ type SourceNotifyQueueOptions = z.input<typeof sourceNotifyQueueOptionsSchema>;
1744
+ /** Issues-store operations the queue needs. */
1745
+ type SourceQueueIssues = Pick<IssuesManager, 'record' | 'clear'>;
1746
+ /** Per-run options of `SourceNotifyQueue.processNow`. */
1747
+ interface ProcessNowOptions {
1748
+ /**
1749
+ * Re-embed even when content is unchanged (reindex scope `full`). Merged
1750
+ * as OR into a run it joins.
1751
+ */
1752
+ force?: boolean;
1753
+ }
1754
+ /** Rejection for `SourceNotifyQueue.processNow` after `stop()`. */
1755
+ declare class SourceNotifyQueueStoppedError extends Error {
1756
+ /** Create the error. */
1757
+ constructor();
1758
+ }
1759
+
1760
+ /**
1761
+ * @module queue/SourceNotifyQueue
1762
+ * In-memory notify queue for source documents, keyed by reference: pending notifications merge, in-flight ones mark the reference dirty for exactly one re-run. Runs each reference through the source operations; records an issue when a run rejects unexpectedly.
1763
+ */
1764
+
1765
+ /** What the queue runs: one reference with its designated rule. */
1766
+ type SourceProcess = Pick<SourceOperationsInterface, 'process'>;
1767
+ /**
1768
+ * Coalescing queue for source-document notifications (Decision 12).
1769
+ *
1770
+ * - A notification for a **pending** reference is merged into it (the latest
1771
+ * rule wins; the reference keeps its place in line).
1772
+ * - One for an **in-flight** reference marks it dirty: when the current run
1773
+ * ends the reference is queued once more (at the back), however many
1774
+ * notifications arrived meanwhile.
1775
+ * - Every run calls `process`, which retrieves current state, so a
1776
+ * burst on one reference costs at most two retrievals and the final state
1777
+ * wins. Runs never overlap for the same reference.
1778
+ *
1779
+ * References are compared exactly (case-sensitive), like source point IDs.
1780
+ * The queue holds work until {@link start}; {@link pause} (prune) stops new
1781
+ * runs without rejecting enqueues. Failures never escape: `process`
1782
+ * records issues itself, and an unexpected rejection becomes a `failed`
1783
+ * result and an `extraction_error` issue (with the designated rule).
1784
+ *
1785
+ * Reindex runs go through {@link processNow} as well, so a notification and
1786
+ * a reindex of one reference never run in parallel and the latest state wins.
1787
+ */
1788
+ declare class SourceNotifyQueue implements PausableQueue {
1789
+ private readonly processor;
1790
+ private readonly logger?;
1791
+ private readonly issues?;
1792
+ private readonly concurrency;
1793
+ private started;
1794
+ private paused;
1795
+ private stopped;
1796
+ private readonly pending;
1797
+ private readonly inFlight;
1798
+ private readonly signals;
1799
+ /**
1800
+ * @param processor - Processes one reference with its designated rule
1801
+ * (source operations).
1802
+ * @param options - Queue options (validated).
1803
+ * @param logger - Logger for unexpected processing rejections.
1804
+ * @param issues - Issues store for unexpected processing rejections.
1805
+ */
1806
+ constructor(processor: SourceProcess, options?: SourceNotifyQueueOptions, logger?: pino.Logger | undefined, issues?: SourceQueueIssues | undefined);
1807
+ /**
1808
+ * Notify that a source document changed (fire and forget).
1809
+ *
1810
+ * @param ref - The exact `<name>://<id>` reference.
1811
+ * @param rule - The designated rule name.
1812
+ * @returns `false` when the queue is stopped and the notification was dropped.
1813
+ */
1814
+ enqueue(ref: string, rule: string): boolean;
1815
+ /**
1816
+ * Process a reference and resolve with the result of a run that started
1817
+ * after this call, so the result reflects the source's state at or after
1818
+ * the call (the `?wait=true` path). Coalesces with other notifications:
1819
+ * if the reference is pending, this joins that run; if it is in flight,
1820
+ * this waits for the current run and then the one dirty re-run.
1821
+ *
1822
+ * @param ref - The exact `<name>://<id>` reference.
1823
+ * @param rule - The designated rule name.
1824
+ * @param options - `force` re-embeds unchanged content.
1825
+ * @returns The per-document result.
1826
+ * @throws {@link SourceNotifyQueueStoppedError} (as a rejection) when the
1827
+ * queue is stopped before the run starts.
1828
+ */
1829
+ processNow(ref: string, rule: string, options?: ProcessNowOptions): Promise<DocumentResult>;
1830
+ /** Start processing (work enqueued earlier is held until now). */
1831
+ start(): void;
1832
+ /** {@inheritDoc PausableQueue.pause} */
1833
+ pause(): void;
1834
+ /** {@inheritDoc PausableQueue.resume} */
1835
+ resume(): void;
1836
+ /** {@inheritDoc PausableQueue.waitForIdleWorkers} */
1837
+ waitForIdleWorkers(): Promise<void>;
1838
+ /** Resolve once nothing is pending, owed (dirty) or in flight. */
1839
+ drain(): Promise<void>;
1840
+ /**
1841
+ * Stop: drop pending and owed runs (their `processNow` callers reject with
1842
+ * {@link SourceNotifyQueueStoppedError}), refuse new work, and resolve once
1843
+ * in-flight runs finish.
1844
+ */
1845
+ stop(): Promise<void>;
1846
+ private schedule;
1847
+ private pump;
1848
+ private run;
1849
+ private execute;
1850
+ private isIdle;
1851
+ private settle;
1228
1852
  }
1229
1853
 
1230
1854
  /**
@@ -1270,6 +1894,166 @@ declare class CommitMessageGenerator {
1270
1894
  generate(files: string[], diffSummary: string): Promise<string | null>;
1271
1895
  }
1272
1896
 
1897
+ /**
1898
+ * @module vcs/RootGitLock
1899
+ * Per-root, in-process FIFO async mutex serialising the watcher's own git work on a root: batch commits (with push), squash, untracking and temp-file sweeps (D66, D81). Never re-entered. Logs long waits and holds; an API route's wait is bounded (D102).
1900
+ *
1901
+ * It cannot exclude other processes (people and agents run git in the same
1902
+ * repositories), so everything done under it must also be safe without it.
1903
+ */
1904
+
1905
+ /** FIFO mutex for one VCS root. */
1906
+ declare class RootGitLock {
1907
+ private readonly lock;
1908
+ /**
1909
+ * @param root - Repository root (named in warnings).
1910
+ * @param logger - Logger for long-wait and long-hold warnings.
1911
+ */
1912
+ constructor(root: string, logger: pino.Logger);
1913
+ /** The operation holding the lock, if any. */
1914
+ get heldBy(): string | undefined;
1915
+ /**
1916
+ * Run `work` while holding the lock. Callers queue in arrival order; the
1917
+ * lock is released when `work` settles, whatever its outcome.
1918
+ *
1919
+ * @param operation - Operation name, for warnings and {@link heldBy}.
1920
+ * @param work - The work to run under the lock (must not take it again).
1921
+ * @returns What `work` returns.
1922
+ */
1923
+ run<T>(operation: string, work: () => Promise<T>): Promise<T>;
1924
+ /**
1925
+ * {@link run}, giving up when the lock is not acquired within `waitMs`
1926
+ * (D102). A caller that gives up never runs `work`; its place in the
1927
+ * queue passes straight on, so later callers keep their order.
1928
+ *
1929
+ * @param operation - Operation name, for warnings and {@link heldBy}.
1930
+ * @param waitMs - Longest wait for the lock.
1931
+ * @param work - The work to run under the lock (must not take it again).
1932
+ * @returns What `work` returns.
1933
+ * @throws LockWaitTimeoutError when the wait runs out.
1934
+ */
1935
+ runWithin<T>(operation: string, waitMs: number, work: () => Promise<T>): Promise<T>;
1936
+ }
1937
+
1938
+ /**
1939
+ * @module vcs/squashPlumbing
1940
+ * Plumbing-only history rewrite for squash retention (D148, D166): list the first-parent history, rebuild it with `commit-tree` from a parentless baseline, keeping each retained commit's tree, raw message, identities and raw dates. Never touches the working tree or the index.
1941
+ */
1942
+ /** Commit metadata for retention calculation. */
1943
+ interface CommitInfo {
1944
+ /** Full hash. */
1945
+ hash: string;
1946
+ /** Author date. */
1947
+ date: Date;
1948
+ }
1949
+
1950
+ /**
1951
+ * @module vcs/SquashManager
1952
+ * Scheduled squash retention for one VCS root: under RootGitLock, flush the pending batch, then rewrite the branch in plumbing and move it with a compare-and-swap `update-ref` (D101, D148, D166). Force-pushes when a remote is set.
1953
+ *
1954
+ * No checkout, reset or cherry-pick runs, so the working tree and the index
1955
+ * are never touched: a file untracked by `/vcs/exclude` or the size cap
1956
+ * keeps its newest content on disk, and no clean tree is needed. Squash
1957
+ * still refuses while a merge, rebase, cherry-pick or revert is in
1958
+ * progress, or when HEAD is not on the configured branch (D20).
1959
+ */
1960
+
1961
+ /** Result of a squash operation. */
1962
+ interface SquashResult {
1963
+ squashed: boolean;
1964
+ commitsRemoved?: number;
1965
+ commitsRetained?: number;
1966
+ error?: string;
1967
+ }
1968
+ /** Test hooks around the reference update (not public API). @internal */
1969
+ interface SquashHooks {
1970
+ /** Receives the rebuilt tip; returns the tip to check and publish. */
1971
+ rebuiltTip?: (tip: string) => Promise<string>;
1972
+ /** Runs after the tree check, just before `update-ref`. */
1973
+ beforeUpdateRef?: () => Promise<void>;
1974
+ }
1975
+ /** Options for SquashManager construction beyond the required params. */
1976
+ interface SquashManagerOptions {
1977
+ /** Git branch name for squash operations. Default: "master". */
1978
+ branch?: string;
1979
+ /** The root's lock; squash holds it for its whole run (D81). */
1980
+ lock: RootGitLock;
1981
+ /**
1982
+ * Commit pending changes inside the hold through the non-locking commit
1983
+ * path (D101). Resolves false when that commit failed (squash refuses).
1984
+ */
1985
+ flushInHold?: () => Promise<boolean>;
1986
+ /** Remote URL for force-push after squash. */
1987
+ remoteUrl?: string;
1988
+ /** Access token for HTTPS push authentication. */
1989
+ accessToken?: string;
1990
+ /** Test hooks (not public API). @internal */
1991
+ hooks?: SquashHooks;
1992
+ }
1993
+ /**
1994
+ * Manages scheduled squash retention for a single VCS root.
1995
+ *
1996
+ * Runs on a cron schedule (checked every 60s). When triggered, squashes
1997
+ * commits older than the retention boundary (the tighter of maxAgeDays
1998
+ * and maxVersions) into a single "historical baseline" commit and
1999
+ * recreates the retained commits on top of it.
2000
+ */
2001
+ declare class SquashManager {
2002
+ private readonly rootPath;
2003
+ private readonly retention;
2004
+ private readonly logger;
2005
+ private readonly options;
2006
+ private readonly branch;
2007
+ private intervalTimer;
2008
+ private running;
2009
+ constructor(rootPath: string, retention: VcsRetentionConfig, logger: pino.Logger, options: SquashManagerOptions);
2010
+ /**
2011
+ * Start the cron-based squash scheduler.
2012
+ * Checks every 60 seconds if the cron expression matches.
2013
+ */
2014
+ start(): void;
2015
+ /**
2016
+ * Stop the cron scheduler.
2017
+ */
2018
+ stop(): void;
2019
+ /**
2020
+ * Run the squash under the root's lock: flush, guard, rewrite, publish,
2021
+ * force-push. Exposed publicly for testing.
2022
+ */
2023
+ runSquash(): Promise<SquashResult>;
2024
+ /**
2025
+ * Refuse while an abandoned cherry-pick, rebase or merge is present, or
2026
+ * when HEAD is not on the configured branch (#249, D20: report only).
2027
+ *
2028
+ * @returns The refusal reason, or undefined if the squash may proceed.
2029
+ */
2030
+ private checkRepoPreconditions;
2031
+ /** Rewrite and publish the branch (inside the hold). */
2032
+ private squashBranch;
2033
+ /** Log a finished squash, force push if configured, and report it. */
2034
+ private completed;
2035
+ /**
2036
+ * Get all first-parent commits of HEAD in chronological order (oldest
2037
+ * first), or none when HEAD is unborn.
2038
+ */
2039
+ getCommitLog(): Promise<CommitInfo[]>;
2040
+ /**
2041
+ * Calculate the retention boundary index.
2042
+ * Returns the index of the oldest commit to KEEP.
2043
+ * All commits before this index will be squashed into a baseline.
2044
+ *
2045
+ * Strategy: find the tighter constraint between age and count.
2046
+ * - Age: keep commits newer than maxAgeDays
2047
+ * - Count: keep the last maxVersions commits
2048
+ * The boundary is the one that retains FEWER old commits (tighter).
2049
+ */
2050
+ calculateRetentionBoundary(commits: CommitInfo[]): number;
2051
+ /**
2052
+ * Force push to remote after squash (history rewrite requires force).
2053
+ */
2054
+ private forcePushIfConfigured;
2055
+ }
2056
+
1273
2057
  /**
1274
2058
  * @module vcs/types
1275
2059
  * Shared types for the VCS subsystem.
@@ -1288,7 +2072,7 @@ interface PushError {
1288
2072
 
1289
2073
  /**
1290
2074
  * @module vcs/VcsManager
1291
- * Per-root VCS manager for git-backed content versioning.
2075
+ * Per-root VCS manager for git-backed content versioning: a throttled batch queue, a circuit breaker, the root's RootGitLock, squash scheduling and startup maintenance. Commits run through {@link BatchCommitter} under the lock.
1292
2076
  */
1293
2077
 
1294
2078
  /** Per-root circuit breaker state, exposed via /vcs/status. */
@@ -1296,75 +2080,56 @@ interface VcsBreakerState extends CircuitBreakerSnapshot {
1296
2080
  /** Files queued or in flight, not yet committed. */
1297
2081
  pendingCount: number;
1298
2082
  }
2083
+ /** Optional VcsManager collaborators. */
2084
+ interface VcsManagerOptions {
2085
+ /** `vcs.maxFileBytes` in bytes, read at each staging (D67). */
2086
+ getMaxFileBytes?: () => number;
2087
+ /** Path-list timeout override (tests only). */
2088
+ timeoutFor?: (paths: number) => number;
2089
+ }
1299
2090
  /**
1300
- * Per-root VCS manager for git-backed content versioning.
1301
- *
1302
- * Each VCS-enabled watch root gets its own VcsManager instance, which owns
1303
- * a throttled commit pipeline: file changes are batched, staged, committed
1304
- * (with optional AI-generated messages), and optionally pushed to a remote.
1305
- *
1306
- * Concurrency: only one commit is in-flight at a time per root. Index.lock
1307
- * contention is handled with exponential backoff retries (D10). Failed
1308
- * batches are always retained; a circuit breaker spaces out retries of a
1309
- * persistently failing root (#249).
2091
+ * Per-root VCS manager: batches file changes and commits them under the
2092
+ * root's {@link RootGitLock}; failed batches are always retained (#249).
1310
2093
  */
1311
2094
  declare class VcsManager {
1312
2095
  readonly config: VcsConfig;
1313
2096
  readonly rootPath: string;
1314
2097
  readonly remoteUrl: string | undefined;
1315
- private readonly accessToken;
2098
+ /** Serialises this root's git work (D66, D81). */
2099
+ readonly lock: RootGitLock;
1316
2100
  private readonly logger;
1317
- private readonly commitMessageBuilder;
2101
+ private readonly committer;
1318
2102
  private readonly breaker;
1319
- private readonly pending;
2103
+ private readonly queue;
1320
2104
  private readonly pendingReversions;
1321
- private readonly _pushErrors;
1322
2105
  private readonly squashManager;
1323
- private throttleTimer;
1324
- private breakerRetryTimer;
1325
- private commitInFlight;
1326
- private inFlightFileCount;
2106
+ private sweepTimer;
1327
2107
  private started;
1328
- private paused;
1329
2108
  private isBaseline;
1330
- private _lastPushTime;
1331
- constructor(rootPath: string, config: VcsConfig, logger: pino.Logger, commitMessageGenerator?: CommitMessageGenerator, remoteUrl?: string, accessToken?: string);
2109
+ constructor(rootPath: string, config: VcsConfig, logger: pino.Logger, commitMessageGenerator?: CommitMessageGenerator, remoteUrl?: string, accessToken?: string, options?: VcsManagerOptions);
1332
2110
  get lastPushTime(): string | null;
1333
2111
  get pushErrors(): readonly PushError[];
1334
2112
  /** Current circuit breaker state and backlog, for /vcs/status. */
1335
2113
  get breakerState(): VcsBreakerState;
2114
+ /** First breaker trip since the last success, for `health.vcs`. */
2115
+ get trippedSince(): string | null;
1336
2116
  /**
1337
- * Begin accepting file changes. Sets the manager to started state and
1338
- * starts the squash manager if configured.
1339
- *
1340
- * Performs startup orphan detection/recovery before starting the squash
1341
- * manager, ensuring the repo is on the configured branch, and reports
1342
- * (never clears) an abandoned cherry-pick, rebase, or merge.
1343
- *
1344
- * Baseline mode (commit prefix `"baseline:"`) remains active until
1345
- * {@link endBaseline} is called by the coordinator after the initial scan.
2117
+ * Begin accepting file changes after startup maintenance under the lock
2118
+ * (never a checkout, D167); start the hourly sweep and squash. Baseline
2119
+ * mode lasts until {@link endBaseline}.
1346
2120
  */
1347
2121
  start(): Promise<void>;
1348
- /**
1349
- * Signal that the initial filesystem scan is complete.
1350
- * Clears the baseline flag so subsequent commits use normal "watcher: batch" messages.
1351
- */
2122
+ /** Initial scan done: later commits use `watcher: batch` messages. */
1352
2123
  endBaseline(): void;
1353
2124
  /**
1354
- * Add a file to the pending set and start the throttle timer.
1355
- * If the pending set reaches maxBatchSize, commit a batch immediately.
1356
- *
1357
- * The circuit breaker is intentionally not reset here: recovery is
2125
+ * Add a file to the pending set and start the throttle timer; a full set
2126
+ * commits at once. The breaker is intentionally not reset here: recovery is
1358
2127
  * time-based, so a busy root cannot hammer a deterministic failure.
1359
2128
  *
1360
2129
  * @param filePath - Absolute path of the changed file.
1361
2130
  */
1362
2131
  fileChanged(filePath: string): void;
1363
- /**
1364
- * Record reversion metadata so the next commit uses a revert-prefixed message.
1365
- *
1366
- * @param reversion - The reversion metadata to record.
1367
- */
2132
+ /** Record a reversion so the next commit uses a revert-prefixed message. */
1368
2133
  addPendingReversion(reversion: PendingReversion): void;
1369
2134
  /**
1370
2135
  * Queue a deleted file. Staging records the deletion if git tracks it.
@@ -1372,66 +2137,31 @@ declare class VcsManager {
1372
2137
  * @param filePath - Absolute path of the deleted file.
1373
2138
  */
1374
2139
  handleUnlink(filePath: string): void;
1375
- /**
1376
- * Pause the commit pipeline. Drains pending commits via flush(), then
1377
- * sets a flag that makes commitBatch queue instead of execute.
1378
- * Used by SquashManager to coordinate squash operations.
1379
- */
1380
- pause(): Promise<void>;
1381
- /**
1382
- * Resume the commit pipeline after a pause.
1383
- * Re-enables normal commit operations and starts a throttle timer
1384
- * if there are pending files that accumulated during the pause.
1385
- */
1386
- resume(): void;
1387
- /**
1388
- * Commit all pending files now, in batches of at most maxBatchSize.
1389
- */
2140
+ /** Push now; the caller holds {@link lock} (`/vcs/exclude`, D71). */
2141
+ pushInHold(): Promise<void>;
2142
+ /** Commit all pending files now, in batches of at most maxBatchSize. */
1390
2143
  flush(): Promise<void>;
1391
- /**
1392
- * Stop accepting file changes, flush pending, and clean up timers.
1393
- */
2144
+ /** Stop accepting file changes, flush pending, and clean up timers. */
1394
2145
  stop(): Promise<void>;
2146
+ /** Run a squash now (as the cron does); undefined when retention is off. */
2147
+ runSquash(): ReturnType<SquashManager['runSquash']> | undefined;
1395
2148
  /**
1396
2149
  * Add tracked-but-deleted paths (`git ls-files --deleted`) in this
1397
2150
  * manager's watch scope to pending, so the next commit (typically the
1398
- * baseline) records deletions that happened while nothing was watching.
1399
- * Called by the coordinator before flush/endBaseline on startup. See #249.
2151
+ * baseline) records deletions made while nothing was watching (#249).
1400
2152
  *
1401
2153
  * @param isInScope - Returns true if the absolute path routes to this
1402
2154
  * manager (the coordinator's routing and watch-scope logic).
1403
2155
  */
1404
2156
  reconcileDeletions(isInScope: (absolutePath: string) => boolean): Promise<void>;
1405
- /** Take up to maxBatchSize files from the pending set. */
1406
- private takeBatch;
1407
- /** Chain a batch behind the in-flight commit, tracking it as backlog. */
1408
- private enqueueBatch;
1409
- /** Put files back in the pending set. Pending is a set, so this cannot grow it past the distinct paths seen. */
1410
- private retain;
1411
- private clearThrottle;
1412
2157
  /**
1413
- * Start the throttle timer to fire flush() after commitThrottleMs.
1414
- * If a timer is already running, do not reset it — this is throttle, not debounce.
1415
- */
1416
- private resetThrottle;
1417
- private clearBreakerRetry;
1418
- /**
1419
- * Ensure a timer will flush pending files when the breaker's cooldown
1420
- * elapses, without needing a new file-change event.
1421
- */
1422
- private scheduleBreakerRetry;
1423
- /**
1424
- * Commit a batch with index.lock retry, stale lock detection, pause
1425
- * support, and the circuit breaker. A failed batch is always retained.
1426
- */
1427
- private commitBatch;
1428
- /**
1429
- * One commit attempt: stage, skip if nothing is staged, commit, push.
2158
+ * Commit a batch (the caller holds the lock) with index.lock retry,
2159
+ * stale lock removal and the circuit breaker. A failed batch is always
2160
+ * retained.
1430
2161
  *
1431
- * @returns true if a commit was created; false for a no-op.
2162
+ * @returns true on success (including "nothing staged").
1432
2163
  */
1433
- private tryCommit;
1434
- private onCommitSuccess;
2164
+ private commitBatch;
1435
2165
  private onCommitFailure;
1436
2166
  }
1437
2167
 
@@ -1449,10 +2179,15 @@ declare class VcsCoordinator {
1449
2179
  private readonly roots;
1450
2180
  private readonly logger;
1451
2181
  private readonly isInWatchScope;
1452
- constructor(config: JeevesWatcherConfig, logger: pino.Logger);
1453
2182
  /**
1454
- * Start all VcsManager instances.
1455
- * Runs startup orphan detection/recovery for each root.
2183
+ * @param config - Config at construction (roots, remotes, retention).
2184
+ * @param logger - Logger.
2185
+ * @param getConfig - Live config, for settings read at each staging
2186
+ * (`vcs.maxFileBytes`, D67). Default: the construction config.
2187
+ */
2188
+ constructor(config: JeevesWatcherConfig, logger: pino.Logger, getConfig?: () => JeevesWatcherConfig);
2189
+ /**
2190
+ * Start all VcsManager instances (each runs its startup maintenance).
1456
2191
  */
1457
2192
  start(): Promise<void>;
1458
2193
  /**
@@ -1475,6 +2210,12 @@ declare class VcsCoordinator {
1475
2210
  * Flush all managers and clean up.
1476
2211
  */
1477
2212
  stop(): Promise<void>;
2213
+ /**
2214
+ * VCS state for `GET /status` `health.vcs` (D86).
2215
+ *
2216
+ * @returns Root count, total backlog, tripped roots and the oldest trip.
2217
+ */
2218
+ getHealth(): VcsHealth;
1478
2219
  /**
1479
2220
  * Get the sorted root paths (longest-first).
1480
2221
  */
@@ -1497,25 +2238,129 @@ declare class VcsCoordinator {
1497
2238
  findManagerForPath(normalizedPath: string): VcsManager | undefined;
1498
2239
  }
1499
2240
 
2241
+ /**
2242
+ * @module health
2243
+ * Tracks consecutive system-level failures and applies exponential backoff.
2244
+ * Triggers fatal error callback when maxRetries is exceeded.
2245
+ */
2246
+
2247
+ /**
2248
+ * Options for {@link SystemHealth}.
2249
+ */
2250
+ interface SystemHealthOptions {
2251
+ /** Maximum consecutive failures before fatal. Default: Infinity. */
2252
+ maxRetries?: number;
2253
+ /** Maximum backoff delay in milliseconds. Default: 60000. */
2254
+ maxBackoffMs?: number;
2255
+ /** Base delay in milliseconds for exponential backoff. Default: 1000. */
2256
+ baseDelayMs?: number;
2257
+ /** Called when maxRetries is exceeded. If not set, throws. */
2258
+ onFatalError?: (error: unknown) => void;
2259
+ /** Logger instance. */
2260
+ logger: pino.Logger;
2261
+ }
2262
+ /**
2263
+ * Tracks system health via consecutive failure count and exponential backoff.
2264
+ */
2265
+ declare class SystemHealth {
2266
+ private consecutiveFailures;
2267
+ private readonly maxRetries;
2268
+ private readonly maxBackoffMs;
2269
+ private readonly baseDelayMs;
2270
+ private readonly onFatalError?;
2271
+ private readonly logger;
2272
+ constructor(options: SystemHealthOptions);
2273
+ /**
2274
+ * Record a successful system operation. Resets the failure counter.
2275
+ */
2276
+ recordSuccess(): void;
2277
+ /**
2278
+ * Record a system-level failure. If maxRetries is exceeded, triggers fatal error.
2279
+ *
2280
+ * @param error - The error that occurred.
2281
+ * @returns Whether the watcher should continue (false = fatal).
2282
+ */
2283
+ recordFailure(error: unknown): boolean;
2284
+ /**
2285
+ * Compute the current backoff delay based on consecutive failures.
2286
+ *
2287
+ * @returns Delay in milliseconds.
2288
+ */
2289
+ get currentBackoffMs(): number;
2290
+ /**
2291
+ * Sleep for the current backoff duration.
2292
+ *
2293
+ * @param signal - Optional abort signal.
2294
+ */
2295
+ backoff(signal?: AbortSignal): Promise<void>;
2296
+ /**
2297
+ * Run one operation with health tracking: back off, run it and record
2298
+ * the success, or record its failure.
2299
+ *
2300
+ * @param fn - The operation.
2301
+ * @returns False when the failure was fatal (the caller stops).
2302
+ * @throws When retries are exhausted and no fatal-error callback is set.
2303
+ */
2304
+ run(fn: () => Promise<void>): Promise<boolean>;
2305
+ /** Current consecutive failure count. */
2306
+ get failures(): number;
2307
+ }
2308
+
2309
+ /**
2310
+ * @module watcher/changeSource/types
2311
+ * The change-source seam (spec Design 9, D120, D149): a `ChangeSource` supplies raw events and root signals to a `ChangeSink`; `FileSystemWatcher` implements the sink and owns every decision. Types only.
2312
+ */
2313
+
2314
+ /** A classified event, as chokidar reports it. */
2315
+ type ClassifiedEventType = 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir';
2316
+ /**
2317
+ * One unclassified event of a parcel batch (D149): an absolute path and a
2318
+ * type that is a hint only (`lstat` decides).
2319
+ */
2320
+ interface RawChangeEvent {
2321
+ /** Absolute path. */
2322
+ path: string;
2323
+ /** Parcel's event type. */
2324
+ type: 'create' | 'update' | 'delete';
2325
+ }
2326
+ /**
2327
+ * Where a change source delivers events and signals. Implemented by
2328
+ * `FileSystemWatcher`, which owns classification, stability waits, pairing,
2329
+ * renewals and every reconcile trigger (D120).
2330
+ */
2331
+ interface ChangeSink {
2332
+ /** A classified event (chokidar). */
2333
+ event: (type: ClassifiedEventType, path: string, stats?: Stats) => void;
2334
+ /** One whole parcel batch for a root, sorted by path (D149). */
2335
+ rawEvents: (root: string, events: RawChangeEvent[]) => void;
2336
+ /** The source finished its initial set for `root`. */
2337
+ initialScanDone: (root: string) => void;
2338
+ /** The source re-created `root`'s subscription after an error (D131). */
2339
+ resubscribed: (root: string) => void;
2340
+ /**
2341
+ * `root` became unreachable (D109), or is reachable but cannot be
2342
+ * watched (`reachable`, D156: it is still reconciled); `code` is the
2343
+ * error code.
2344
+ */
2345
+ rootUnavailable: (root: string, code: string, options?: RootUnavailableOptions) => void;
2346
+ /** `root` is reachable again (D109). */
2347
+ rootAvailable: (root: string) => void;
2348
+ }
2349
+ /** Details of {@link ChangeSink.rootUnavailable}. */
2350
+ interface RootUnavailableOptions {
2351
+ /**
2352
+ * The root is still on disk but unwatched (a non-transient subscribe
2353
+ * failure, D156): it counts in `unavailableRoots` and is still
2354
+ * reconciled.
2355
+ */
2356
+ reachable?: boolean;
2357
+ }
2358
+
1500
2359
  /**
1501
2360
  * @module api/InitialScanTracker
1502
2361
  * Tracks initial filesystem scan state for status reporting.
1503
2362
  */
1504
- /** Initial scan status snapshot for API consumers. */
1505
- interface InitialScanStatus {
1506
- /** Whether the initial scan is currently in progress. */
1507
- active: boolean;
1508
- /** Number of files matched by watch globs. */
1509
- filesMatched?: number;
1510
- /** Number of files enqueued for processing so far. */
1511
- filesEnqueued?: number;
1512
- /** ISO 8601 timestamp when the scan started. */
1513
- startedAt?: string;
1514
- /** ISO 8601 timestamp when the scan completed. */
1515
- completedAt?: string;
1516
- /** Total scan duration in milliseconds. */
1517
- durationMs?: number;
1518
- }
2363
+
1519
2364
  /**
1520
2365
  * Tracks the state of the initial filesystem scan after service start.
1521
2366
  */
@@ -1540,66 +2385,173 @@ declare class InitialScanTracker {
1540
2385
  }
1541
2386
 
1542
2387
  /**
1543
- * @module health
1544
- * Tracks consecutive system-level failures and applies exponential backoff.
1545
- * Triggers fatal error callback when maxRetries is exceeded.
2388
+ * @module watcher/fileEvents
2389
+ * What a filtered file event does: the VCS hook, move correlation and the processing queue. I/O: enqueues processor work.
1546
2390
  */
1547
2391
 
2392
+ /** VCS change callback. */
2393
+ type VcsFileChange = (filePath: string, event: 'add' | 'change' | 'unlink') => void;
2394
+
1548
2395
  /**
1549
- * Options for {@link SystemHealth}.
2396
+ * @module watcher/inventory/Inventory
2397
+ * `FileSystemWatcher`'s inventory (D99): the files that passed its filters, keyed by `normalizeDocumentKey`, with the child-set directory index (D127, D150). Pure.
1550
2398
  */
1551
- interface SystemHealthOptions {
1552
- /** Maximum consecutive failures before fatal. Default: Infinity. */
1553
- maxRetries?: number;
1554
- /** Maximum backoff delay in milliseconds. Default: 60000. */
1555
- maxBackoffMs?: number;
1556
- /** Base delay in milliseconds for exponential backoff. Default: 1000. */
1557
- baseDelayMs?: number;
1558
- /** Called when maxRetries is exceeded. If not set, throws. */
1559
- onFatalError?: (error: unknown) => void;
1560
- /** Logger instance. */
1561
- logger: pino.Logger;
2399
+ /** File size and modification time, as a walk or `stat` reports them. */
2400
+ interface FileStats {
2401
+ /** Size in bytes. */
2402
+ size: number;
2403
+ /** Modification time (ms since epoch). */
2404
+ mtimeMs: number;
2405
+ }
2406
+ /** One inventory entry. */
2407
+ interface InventoryEntry {
2408
+ /** The path as the source reported it. */
2409
+ path: string;
2410
+ /** Size, when the inventory tracks stats (parcel source). */
2411
+ size?: number;
2412
+ /** Modification time, when the inventory tracks stats. */
2413
+ mtimeMs?: number;
1562
2414
  }
1563
2415
  /**
1564
- * Tracks system health via consecutive failure count and exponential backoff.
2416
+ * The files the change source last saw that pass `FileSystemWatcher`'s
2417
+ * filters. Stats are stored only when `trackStats` is set (the parcel
2418
+ * source, whose initial set is the walk); under chokidar entries hold paths
2419
+ * only (spec Design 9).
1565
2420
  */
1566
- declare class SystemHealth {
1567
- private consecutiveFailures;
1568
- private readonly maxRetries;
1569
- private readonly maxBackoffMs;
1570
- private readonly baseDelayMs;
1571
- private readonly onFatalError?;
1572
- private readonly logger;
1573
- constructor(options: SystemHealthOptions);
2421
+ declare class Inventory {
2422
+ private readonly trackStats;
2423
+ private readonly entries;
2424
+ private readonly index;
2425
+ /** @param trackStats - Store size and mtime with each entry. */
2426
+ constructor(trackStats: boolean);
2427
+ /** Number of files. */
2428
+ get size(): number;
2429
+ /** Add or update a file. */
2430
+ set(path: string, stats?: FileStats): void;
1574
2431
  /**
1575
- * Record a successful system operation. Resets the failure counter.
2432
+ * Remove a file.
2433
+ *
2434
+ * @returns True when it was present.
1576
2435
  */
1577
- recordSuccess(): void;
2436
+ delete(path: string): boolean;
2437
+ /** The entry for a path, if present. */
2438
+ get(path: string): InventoryEntry | undefined;
1578
2439
  /**
1579
- * Record a system-level failure. If maxRetries is exceeded, triggers fatal error.
1580
- *
1581
- * @param error - The error that occurred.
1582
- * @returns Whether the watcher should continue (false = fatal).
2440
+ * Whether a path is a directory the inventory holds files under (D127:
2441
+ * an absent path is classified as a directory delete by this).
1583
2442
  */
1584
- recordFailure(error: unknown): boolean;
2443
+ isDirectory(path: string): boolean;
2444
+ /** Every file path, as the source reported it (`/walk`, D83). */
2445
+ paths(): string[];
1585
2446
  /**
1586
- * Compute the current backoff delay based on consecutive failures.
2447
+ * The entries beneath a directory, at any depth, keyed by document key;
2448
+ * O(subtree size), never a scan of the inventory.
2449
+ */
2450
+ entriesUnder(dir: string): Map<string, InventoryEntry>;
2451
+ }
2452
+
2453
+ /**
2454
+ * @module watcher/changeSource/parcelModule
2455
+ * Loading `@parcel/watcher` only when it is selected (D125): a cached dynamic import whose failure becomes a refusal naming the chokidar switch, the native-load error test, and the explicit backend per platform (D124). I/O: the import.
2456
+ */
2457
+
2458
+ /** The parts of `@parcel/watcher` the parcel source uses. */
2459
+ type ParcelWatcherModule = Pick<typeof ParcelWatcher, 'subscribe'>;
2460
+ /** Imports the module (injectable for tests). */
2461
+ type ParcelImporter = () => Promise<ParcelWatcherModule>;
2462
+
2463
+ /**
2464
+ * @module watcher/changeSource/restart
2465
+ * The process-restart seam of D196: on Windows the parcel source never unsubscribes (`@parcel/watcher` 2.6.0 crashes the process natively there), so a watcher rebuild or a subscription recovery exits the process for the service manager to restart it. The app implements the graceful restarter; `exitingRestarter` is the fallback for a watcher built without the app. I/O: `process.exit` in that fallback only.
2466
+ */
2467
+
2468
+ /** One request to exit for a restart. */
2469
+ interface RestartRequest {
2470
+ /** The cause (a code, no path: it reaches health, D89). */
2471
+ cause: RestartCause;
2472
+ /** The root concerned, for the log line only. */
2473
+ root?: string;
2474
+ /** The error behind it, for the log line only. */
2475
+ err?: unknown;
2476
+ /**
2477
+ * A config reload's reindex scope (`configWatch.reindex`), run by the
2478
+ * restarted process instead of this one.
2479
+ */
2480
+ reindex?: ConfigWatchReindexScope;
2481
+ }
2482
+ /**
2483
+ * Exits the process for a service-manager restart (D196). `restart`
2484
+ * returns at once: the exit follows a graceful stop, so callers inside the
2485
+ * change source's queue never wait on themselves. Only the first request
2486
+ * of a process counts. Injected, so tests never call `process.exit`.
2487
+ */
2488
+ interface ProcessRestarter {
2489
+ /** Log the cause, stop gracefully, then exit with code 75 (`EX_TEMPFAIL`). */
2490
+ restart: (request: RestartRequest) => void;
2491
+ }
2492
+
2493
+ /**
2494
+ * @module watcher/ChangeSourceHealthTracker
2495
+ * `health.changeSource` (D133, D157): counters since start, owned by the app so they survive watcher rebuilds, plus the cause of the exit for a restart this process started after (D196). Pure.
2496
+ */
2497
+
2498
+ /**
2499
+ * Counts and timestamps behind `health.changeSource`. Never holds a path in
2500
+ * its snapshot (D89).
2501
+ */
2502
+ declare class ChangeSourceHealthTracker {
2503
+ private source;
2504
+ private resubscribes;
2505
+ private renewFailures;
2506
+ private reconcileEvents;
2507
+ private reconcileErrors;
2508
+ private lastReconcileAt;
2509
+ private lastReconcileDurationMs;
2510
+ private lastRenewDurationMs;
2511
+ private lastRestart;
2512
+ private readonly unavailable;
2513
+ private pendingStability;
2514
+ /** Record the running source and its pending-stability reader. */
2515
+ attach(source: ChangeSourceHealth['source'], pending: () => number): void;
2516
+ /** A subscription was re-created after an error (D131). */
2517
+ recordResubscribe(): void;
2518
+ /**
2519
+ * A renewal forced a source rebuild (D165), or rejected inside one
2520
+ * (D176).
1587
2521
  *
1588
- * @returns Delay in milliseconds.
2522
+ * @param count - Failures to add (default 1).
1589
2523
  */
1590
- get currentBackoffMs(): number;
2524
+ recordRenewFailure(count?: number): void;
2525
+ /** A subscription renewal finished (Linux parcel, D129, D157). */
2526
+ recordRenew(durationMs: number): void;
1591
2527
  /**
1592
- * Sleep for the current backoff duration.
2528
+ * The previous process exited for a restart (D196): its cause and when.
1593
2529
  *
1594
- * @param signal - Optional abort signal.
2530
+ * @param restart - The record it left.
1595
2531
  */
1596
- backoff(signal?: AbortSignal): Promise<void>;
1597
- /** Current consecutive failure count. */
1598
- get failures(): number;
2532
+ recordRestart(restart: {
2533
+ cause: RestartCause;
2534
+ at: string;
2535
+ }): void;
2536
+ /** Mark a root unavailable (D109) or available again. */
2537
+ setUnavailable(root: string, unavailable: boolean): void;
2538
+ /** Synthetic events a reconcile emitted (D90). */
2539
+ recordReconcileEvents(count: number): void;
2540
+ /** A reconcile finished (walk and compare). */
2541
+ recordReconcile(durationMs: number, at: Date): void;
2542
+ /** A reconcile errored and emitted nothing (D90). */
2543
+ recordReconcileError(): void;
2544
+ /** The `health.changeSource` body. */
2545
+ snapshot(): ChangeSourceHealth;
1599
2546
  }
1600
2547
 
1601
2548
  /**
1602
- * Options for {@link FileSystemWatcher} beyond basic config.
2549
+ * @module watcher/running
2550
+ * What one `FileSystemWatcher.start` builds: the change source (parcel or chokidar, D125), filters, inventory, filtered handlers, queue actions and, for parcel, the batch pipeline, wired together. Pure apart from constructing the source (not started here).
2551
+ */
2552
+
2553
+ /**
2554
+ * Options for `FileSystemWatcher` beyond basic config.
1603
2555
  */
1604
2556
  interface FileSystemWatcherOptions {
1605
2557
  /** Maximum consecutive system-level failures before fatal error. */
@@ -1615,27 +2567,75 @@ interface FileSystemWatcherOptions {
1615
2567
  /** Optional content hash cache for move detection. */
1616
2568
  contentHashCache?: ContentHashCache;
1617
2569
  /** Optional callback for VCS file change notifications. */
1618
- onVcsFileChange?: (filePath: string, event: 'add' | 'change' | 'unlink') => void;
1619
- /** Optional callback invoked when the initial filesystem scan completes. */
1620
- onInitialScanComplete?: () => void | Promise<void>;
2570
+ onVcsFileChange?: VcsFileChange;
2571
+ /**
2572
+ * Optional callback invoked when the initial filesystem scan completes,
2573
+ * with the index check's view of the watcher that completed it (D197).
2574
+ */
2575
+ onInitialScanComplete?: (view: IndexCheckView) => void | Promise<void>;
2576
+ /** `health.changeSource` counters (D133), owned by the app across rebuilds. */
2577
+ changeSourceHealth?: ChangeSourceHealthTracker;
2578
+ /** Live `watch.reconcileIntervalMs` (D111), read at each scheduling. */
2579
+ getReconcileIntervalMs?: () => number | undefined;
2580
+ /** Imports `@parcel/watcher` (tests; default the real module, D125). */
2581
+ importParcel?: ParcelImporter;
2582
+ /** The service state directory (the parcel rebuild sentinel, D165). */
2583
+ stateDir?: string;
2584
+ /**
2585
+ * Exits for a service-manager restart where parcel never unsubscribes
2586
+ * (Windows, D196); default an immediate exit.
2587
+ */
2588
+ restarter?: ProcessRestarter;
2589
+ }
2590
+
2591
+ /**
2592
+ * @module watcher/indexCheck/view
2593
+ * What the index check at start (D197) reads from a running `FileSystemWatcher`: its roots, why a root must be skipped (unavailable D109, unwatched D156, start walk failed) and whether the inventory holds a path. Pure.
2594
+ */
2595
+
2596
+ /** Why a root is not checked (besides being gone at check time). */
2597
+ type IndexCheckSkipReason = 'unavailable' | 'unwatched' | 'walkFailed';
2598
+ /** The running watcher as the index check sees it. */
2599
+ interface IndexCheckView {
2600
+ /** The resolved watch roots, nested ones collapsed (D195 case). */
2601
+ roots: readonly string[];
2602
+ /**
2603
+ * Why the root must be skipped, or undefined when it can be checked.
2604
+ *
2605
+ * @param root - One of {@link IndexCheckView.roots}.
2606
+ */
2607
+ skipReason: (root: string) => IndexCheckSkipReason | undefined;
2608
+ /**
2609
+ * Whether the inventory holds the file (its point-ID key form).
2610
+ *
2611
+ * @param path - A file path.
2612
+ */
2613
+ has: (path: string) => boolean;
2614
+ /** Whether this running state is still the watcher's (no stop or rebuild). */
2615
+ live: () => boolean;
1621
2616
  }
2617
+
1622
2618
  /**
1623
- * Filesystem watcher that maps chokidar events to the processing queue.
2619
+ * @module watcher
2620
+ * Filesystem watcher: drives a change source (parcel or chokidar, D125) through the `ChangeSink` seam, filters its events, keeps the inventory and enqueues processing. I/O: watches files, enqueues processor work.
1624
2621
  */
1625
- declare class FileSystemWatcher {
2622
+
2623
+ /**
2624
+ * Filesystem watcher. A change source only reports events (D120); this
2625
+ * class filters them, keeps the inventory (D99) and maps them to the
2626
+ * processing queue.
2627
+ */
2628
+ declare class FileSystemWatcher implements ChangeSink {
1626
2629
  private readonly config;
1627
2630
  private readonly queue;
1628
2631
  private readonly processor;
1629
2632
  private readonly logger;
2633
+ private readonly options;
1630
2634
  private readonly health;
1631
- private readonly gitignoreFilter?;
1632
- private readonly initialScanTracker?;
1633
- private readonly contentHashCache?;
1634
- private readonly onVcsFileChange?;
1635
- private readonly onInitialScanComplete?;
1636
- private moveCorrelator?;
1637
- private globMatches;
1638
- private watcher;
2635
+ private running;
2636
+ /** A stopped source left its watches live (D196). */
2637
+ private leftWatches;
2638
+ private initialScanComplete;
1639
2639
  /**
1640
2640
  * Create a new FileSystemWatcher.
1641
2641
  *
@@ -1647,36 +2647,64 @@ declare class FileSystemWatcher {
1647
2647
  */
1648
2648
  constructor(config: WatchConfig, queue: EventQueue, processor: DocumentProcessorInterface, logger: pino.Logger, options?: FileSystemWatcherOptions);
1649
2649
  /**
1650
- * Start watching the filesystem and processing events.
1651
- */
1652
- start(): void;
1653
- /**
1654
- * Get the list of all currently watched file paths (absolute).
2650
+ * Start watching the filesystem and processing events. With parcel the
2651
+ * roots are subscribed first, then walked in the background (D58).
1655
2652
  *
1656
- * Uses chokidar's in-memory `getWatched()` state — no filesystem I/O.
1657
- * Returns an empty array if the watcher hasn't been started.
2653
+ * @throws ChangeSourceLoadError when parcel is selected and cannot load.
1658
2654
  */
1659
- getWatchedFiles(): string[];
1660
- /**
1661
- * Whether the initial filesystem scan is complete.
2655
+ start(): Promise<void>;
2656
+ /** A classified event from the change source (chokidar), with the stats it read, if any. */
2657
+ event(type: ClassifiedEventType, path: string, stats?: Stats): void;
2658
+ /**
2659
+ * The directory-create handler (D91, D120): reconcile a directory created
2660
+ * after the initial scan, so files written into it before its watch was
2661
+ * set up are not missed. The reconciler drops it before the scan ends.
2662
+ */
2663
+ private directoryCreated;
2664
+ /** Start a reconcile of `dir` (every trigger is owned here, D120). */
2665
+ private reconcile;
2666
+ /**
2667
+ * A raw parcel batch (D149): into the FIFO, classified from the disk and
2668
+ * applied in order.
2669
+ */
2670
+ rawEvents(root: string, events: RawChangeEvent[]): void;
2671
+ /** The source finished its initial set for a root; the scan completes when every root has. */
2672
+ initialScanDone(sourceRoot: string): void;
2673
+ /** The source re-created a root's subscription after an error (D131). */
2674
+ resubscribed(sourceRoot: string): void;
2675
+ /** A root is unreachable (D109), or reachable but unwatched (D156). */
2676
+ rootUnavailable(sourceRoot: string, code: string, options?: RootUnavailableOptions): void;
2677
+ /** A root is reachable again (D109). */
2678
+ rootAvailable(sourceRoot: string): void;
2679
+ /** A root signal's context (the canonical root), or undefined when not running. */
2680
+ private signal;
2681
+ /**
2682
+ * The inventory's file paths (absolute, as the source reported them):
2683
+ * matched files only, never directories or filtered files (D83). No
2684
+ * filesystem I/O; empty before start.
1662
2685
  */
2686
+ getWatchedFiles(): string[];
2687
+ /** Whether the initial filesystem scan is complete. */
1663
2688
  get isReady(): boolean;
1664
2689
  /**
1665
- * Stop the filesystem watcher.
2690
+ * Whether a rebuild must exit for a restart (D196): the running source,
2691
+ * or one this watcher stopped (its watches stay live), cannot release
2692
+ * its watches in-process (parcel on Windows).
1666
2693
  */
2694
+ get restartsToRebuild(): boolean;
2695
+ /** The running watcher's inventory (D99), undefined before start. */
2696
+ get inventory(): Inventory | undefined;
2697
+ /** What the index check at start reads (D197); undefined before start. */
2698
+ get indexCheckView(): IndexCheckView | undefined;
2699
+ /** Stop the filesystem watcher. */
1667
2700
  stop(): Promise<void>;
1668
- /**
1669
- * Get the system health tracker.
1670
- */
2701
+ /** Get the system health tracker. */
1671
2702
  get systemHealth(): SystemHealth;
1672
2703
  /**
1673
- * Check if a path is gitignored and should be skipped.
1674
- */
1675
- private isGitignored;
1676
- /**
1677
- * If the changed file is a `.gitignore`, invalidate the filter cache.
2704
+ * Re-arm the periodic reconcile timers with the live interval: a reload
2705
+ * that changed only `watch.reconcileIntervalMs` needs no rebuild (D111).
1678
2706
  */
1679
- private handleGitignoreChange;
2707
+ refreshReconcileSchedule(): void;
1680
2708
  /**
1681
2709
  * Wrap a processing operation with health tracking.
1682
2710
  * On success, resets the failure counter.
@@ -1689,19 +2717,7 @@ declare class FileSystemWatcher {
1689
2717
  * @module api/ReindexTracker
1690
2718
  * Tracks reindex operation state for status reporting. Single instance shared across handlers.
1691
2719
  */
1692
- /** Reindex status snapshot for API consumers. */
1693
- interface ReindexStatus {
1694
- /** Whether a reindex operation is currently in progress. */
1695
- active: boolean;
1696
- /** The active reindex scope (when {@link active} is true). */
1697
- scope?: string;
1698
- /** ISO 8601 timestamp when the current reindex started (when {@link active} is true). */
1699
- startedAt?: string;
1700
- /** Number of files processed so far (when {@link active} is true). */
1701
- filesProcessed?: number;
1702
- /** Total number of files to process (when {@link active} is true). */
1703
- totalFiles?: number;
1704
- }
2720
+
1705
2721
  /**
1706
2722
  * Tracks the state of reindex operations.
1707
2723
  */
@@ -1724,24 +2740,30 @@ declare class ReindexTracker {
1724
2740
  }
1725
2741
 
1726
2742
  /**
1727
- * @module api
1728
- * Fastify API server factory. Registers all route handlers and returns an unstarted server instance.
2743
+ * @module api/apiServerOptions
2744
+ * Options accepted by createApiServer: processor, stores, queues, config access, trackers and optional collaborators (virtual rules, VCS, source queue). Types only.
1729
2745
  */
1730
2746
 
1731
2747
  /**
1732
- * Options for {@link createApiServer}.
2748
+ * Options for `createApiServer`.
1733
2749
  */
1734
2750
  interface ApiServerOptions {
2751
+ /**
2752
+ * Queues paused while a prune deletes points. The service passes
2753
+ * `combinePausableQueues(fileQueue, sourceQueue)` (`app/index.ts`), so a
2754
+ * prune quiesces both the file event queue and the source notify queue.
2755
+ */
2756
+ queue: PausableQueue;
2757
+ /** Source notify queue (`POST /documents`); absent in file-only setups. */
2758
+ sourceQueue?: SourceNotifyQueue;
1735
2759
  /** The component descriptor (used for config-apply handler). */
1736
2760
  descriptor: JeevesComponentDescriptor;
1737
- /** The document processor. */
1738
- processor: DocumentProcessorInterface;
2761
+ /** The document processor: file and source operations. */
2762
+ processor: WatcherProcessor;
1739
2763
  /** The vector store client. */
1740
2764
  vectorStore: VectorStoreClient;
1741
2765
  /** The embedding provider. */
1742
2766
  embeddingProvider: EmbeddingProvider;
1743
- /** The event queue. */
1744
- queue: EventQueue;
1745
2767
  /** The application configuration (used as initial/fallback value). */
1746
2768
  config: JeevesWatcherConfig;
1747
2769
  /** Config getter for live config access after hot-reload. */
@@ -1758,27 +2780,56 @@ interface ApiServerOptions {
1758
2780
  configPath: string;
1759
2781
  /** Helper introspection for merged document. */
1760
2782
  helperIntrospection?: AllHelpersIntrospection;
1761
- /** Virtual rule store for externally registered inference rules. */
1762
- virtualRuleStore?: VirtualRuleStore;
2783
+ /** Virtual rule store for externally registered inference rules (the
2784
+ * `/rules/*` routes). */
2785
+ virtualRuleStore: VirtualRuleStore;
1763
2786
  /** Gitignore filter for reindex path validation. */
1764
2787
  gitignoreFilter?: GitignoreFilter;
1765
2788
  /** Service version string for /status endpoint. */
1766
2789
  version?: string;
1767
2790
  /** Initial scan tracker for /status visibility. */
1768
2791
  initialScanTracker?: InitialScanTracker;
1769
- /** Filesystem watcher instance for /walk endpoint (in-memory file list). */
2792
+ /** Filesystem watcher instance for /walk endpoint (its inventory, D83). */
1770
2793
  fileSystemWatcher?: FileSystemWatcher;
1771
2794
  /** Getter for live filesystem watcher access after hot-reload rebuilds. */
1772
2795
  getFileSystemWatcher?: () => FileSystemWatcher | undefined;
1773
2796
  /** Optional enrichment store for persisted enrichment metadata. */
1774
2797
  enrichmentStore?: EnrichmentStoreInterface;
1775
- /** VCS coordinator for git-backed versioning API routes. */
2798
+ /** VCS coordinator for the git-backed versioning (`/vcs/*`) routes; without
2799
+ * it they answer as unregistered (`api/featureGates`). */
1776
2800
  vcsCoordinator?: VcsCoordinator;
2801
+ /**
2802
+ * Longest document-lock wait of `POST /metadata`, `POST /rules/reapply`
2803
+ * and `POST /documents/delete` (default 10 s, D118; tests only).
2804
+ */
2805
+ documentLockWaitMs?: number;
2806
+ /** Embedding health (`GET /status` `health.embedding`, D88). */
2807
+ embeddingHealth?: {
2808
+ snapshot: () => EmbeddingHealthSnapshot;
2809
+ };
2810
+ /** Change-source health (`GET /status` `health.changeSource`, D133). */
2811
+ changeSourceHealth?: {
2812
+ snapshot: () => ChangeSourceHealth;
2813
+ };
2814
+ /** Index check at start (`health.indexCheck`, D197). */
2815
+ indexCheckHealth?: {
2816
+ snapshot: () => IndexCheckHealth;
2817
+ };
1777
2818
  }
2819
+
2820
+ /**
2821
+ * @module api
2822
+ * Fastify API server factory. Registers every `WATCHER_CATALOG` route (plus `GET /openapi.json`) from the catalog, and returns an unstarted server instance.
2823
+ */
2824
+
1778
2825
  /**
1779
2826
  * Create the Fastify API server with all routes registered.
1780
2827
  *
1781
- * The returned instance is not yet listening — call `server.listen()` to start.
2828
+ * The returned instance is not yet listening; call `server.listen()` to
2829
+ * start. The catalog routes are registered in a child plugin, so a
2830
+ * registration error surfaces from `ready()` or `listen()` (spec #281 D29).
2831
+ * The `/vcs/*` routes answer as unregistered unless a `vcsCoordinator` is
2832
+ * given (`api/featureGates`).
1782
2833
  *
1783
2834
  * @param options - The server options.
1784
2835
  * @returns A configured Fastify instance.
@@ -1786,29 +2837,90 @@ interface ApiServerOptions {
1786
2837
  declare function createApiServer(options: ApiServerOptions): FastifyInstance;
1787
2838
 
1788
2839
  /**
1789
- * @module logger
1790
- * Creates pino logger instances. I/O: optionally writes logs to file via pino/file transport. Defaults to stdout at info level.
2840
+ * @module config/warnings
2841
+ * The non-fatal config warning shape shared by config loading (unknown source keys, reserved rule field names) and `POST /config/validate`. Types only.
2842
+ */
2843
+ /** A non-fatal config validation warning. */
2844
+ interface ConfigWarning {
2845
+ /** Config location the warning refers to. */
2846
+ path: string;
2847
+ /** Human-readable warning message. */
2848
+ message: string;
2849
+ }
2850
+
2851
+ /**
2852
+ * @module config/loadConfig
2853
+ * Loads the watcher config via cosmiconfig: schema validation, rule/map file resolution, defaults, env substitution and secret checks; reports non-fatal warnings (unknown source keys, reserved rule field names) through onWarning. Reads files.
2854
+ */
2855
+
2856
+ /** Options of {@link loadConfig}. */
2857
+ interface LoadConfigOptions {
2858
+ /**
2859
+ * Receives non-fatal warnings once the config has loaded successfully:
2860
+ * unknown keys under `sources.<name>` (which validation drops), then rule
2861
+ * schema properties named like reserved system fields.
2862
+ */
2863
+ onWarning?: (warning: ConfigWarning) => void;
2864
+ }
2865
+ /**
2866
+ * Load the jeeves-watcher configuration.
2867
+ *
2868
+ * @param configPath - Optional explicit path to a config file.
2869
+ * @param options - Optional warning sink.
2870
+ * @returns The loaded configuration.
2871
+ * @throws If no configuration is found or validation fails.
1791
2872
  */
2873
+ declare function loadConfig(configPath?: string, options?: LoadConfigOptions): Promise<JeevesWatcherConfig>;
1792
2874
 
1793
2875
  /**
1794
- * Create a pino logger instance.
2876
+ * Create a pino logger. With `logging.file`, the file is open (or the
2877
+ * call rejects) before the logger is returned, so no early line is lost.
1795
2878
  *
1796
2879
  * @param config - Optional logging configuration.
1797
2880
  * @returns A configured pino logger.
1798
2881
  */
1799
- declare function createLogger(config?: LoggingConfig): pino.Logger;
2882
+ declare function createLogger(config?: LoggingConfig): Promise<pino.Logger>;
2883
+
2884
+ /**
2885
+ * @module app/processRestarter
2886
+ * The app's exit for a service-manager restart (D196): log the cause, leave a restart record in the state directory, stop gracefully (with a hard deadline), then exit with `RESTART_EXIT_CODE`; at the next start, read the record, report it in `health.changeSource` (the app runs any deferred config reindex) and clear it once the start succeeded. I/O: the record file, a timer, `process.exit`.
2887
+ */
2888
+
2889
+ /** What `createProcessRestarter` needs from the app. */
2890
+ interface ProcessRestarterDeps {
2891
+ /** The state directory (the record goes there). */
2892
+ stateDir: string;
2893
+ /** The app's logger, once it exists. */
2894
+ logger: () => pino.Logger | undefined;
2895
+ /** The app's graceful stop. */
2896
+ stop: () => Promise<void>;
2897
+ /**
2898
+ * The longest the graceful stop may take before the process exits
2899
+ * anyway: some stop steps (a hung `lstat` or git call) have no timeout.
2900
+ */
2901
+ deadlineMs: number;
2902
+ /** `process.exit` (tests). */
2903
+ exit?: (code: number) => void;
2904
+ /** The clock (tests). */
2905
+ now?: () => Date;
2906
+ }
1800
2907
 
1801
2908
  /**
1802
2909
  * @module app/factories
1803
2910
  * Component factory interfaces and defaults for {@link JeevesWatcher}. Override in tests to inject mocks.
1804
2911
  */
1805
2912
 
2913
+ /** Runtime options for {@link JeevesWatcher} that aren't serializable in config. */
2914
+ interface JeevesWatcherRuntimeOptions {
2915
+ /** Callback invoked on unrecoverable system error. If not set, throws. */
2916
+ onFatalError?: (error: unknown) => void;
2917
+ }
1806
2918
  /**
1807
2919
  * Component factories for {@link JeevesWatcher}. Override in tests to inject mocks.
1808
2920
  */
1809
2921
  interface JeevesWatcherFactories {
1810
2922
  /** Load and validate a {@link JeevesWatcherConfig} from disk. */
1811
- loadConfig: (configPath?: string) => Promise<JeevesWatcherConfig>;
2923
+ loadConfig: (configPath?: string, options?: LoadConfigOptions) => Promise<JeevesWatcherConfig>;
1812
2924
  /** Create a pino logger instance. */
1813
2925
  createLogger: typeof createLogger;
1814
2926
  /** Create an embedding provider from config. */
@@ -1818,15 +2930,43 @@ interface JeevesWatcherFactories {
1818
2930
  /** Compile inference rules from config. */
1819
2931
  compileRules: typeof compileRules;
1820
2932
  /** Create a document processor for file ingestion. */
1821
- createDocumentProcessor: (deps: ConstructorParameters<typeof DocumentProcessor>[0]) => DocumentProcessorInterface;
2933
+ createDocumentProcessor: (deps: ConstructorParameters<typeof DocumentProcessor>[0]) => WatcherProcessor;
1822
2934
  /** Create an event queue for batching file-system events. */
1823
2935
  createEventQueue: (options: ConstructorParameters<typeof EventQueue>[0]) => EventQueue;
1824
2936
  /** Create a file-system watcher for the configured watch paths. */
1825
2937
  createFileSystemWatcher: (config: JeevesWatcherConfig['watch'], queue: EventQueue, processor: DocumentProcessorInterface, logger: pino.Logger, options?: FileSystemWatcherOptions) => FileSystemWatcher;
1826
2938
  /** Create the HTTP API server. */
1827
2939
  createApiServer: typeof createApiServer;
2940
+ /** Import `@parcel/watcher` when it is the selected source (D125). */
2941
+ importParcel: ParcelImporter;
2942
+ /**
2943
+ * The exit for a service-manager restart (D196). Tests replace it, so
2944
+ * they never call `process.exit`.
2945
+ */
2946
+ createProcessRestarter: (deps: ProcessRestarterDeps) => ProcessRestarter;
1828
2947
  }
1829
2948
 
2949
+ /**
2950
+ * @module app/sources
2951
+ * Startup wiring for source documents: builds the source callback client from `sources` config (read at startup only), the notify queue over the processor's source operations, and the adapter giving a file-only processor failing source operations. No I/O.
2952
+ */
2953
+
2954
+ /**
2955
+ * Adapt a custom file-only processor to the {@link WatcherProcessor} the
2956
+ * service runs on. File operations are forwarded unchanged. Its source
2957
+ * operations fail: every notify-queue run returns a `failed` result with
2958
+ * `source_unavailable` (the run's issue, as for a failing callback), and
2959
+ * every other source operation throws the API's `503 not_available`, so
2960
+ * `POST /documents`, `POST /metadata { source }`, `POST /render { source }`
2961
+ * and `POST /reindex { source }` answer as before source support existed.
2962
+ * Use it in a custom `createDocumentProcessor` factory; never wrap a
2963
+ * processor that has source operations of its own (they would be hidden).
2964
+ *
2965
+ * @param processor - The file-only processor.
2966
+ * @returns The processor with failing source operations.
2967
+ */
2968
+ declare function withoutSourceSupport(processor: DocumentProcessorInterface): WatcherProcessor;
2969
+
1830
2970
  /**
1831
2971
  * @module app/startFromConfig
1832
2972
  * Convenience entry point: loads config from disk and starts a {@link JeevesWatcher}.
@@ -1850,14 +2990,7 @@ declare function startFromConfig(configPath?: string, descriptor?: JeevesCompone
1850
2990
  * Main application orchestrator. Wires components, manages lifecycle (start/stop/reload).
1851
2991
  */
1852
2992
 
1853
- /** Runtime options for {@link JeevesWatcher} that aren't serializable in config. */
1854
- interface JeevesWatcherRuntimeOptions {
1855
- /** Callback invoked on unrecoverable system error. If not set, throws. */
1856
- onFatalError?: (error: unknown) => void;
1857
- }
1858
- /**
1859
- * Main application class that wires together all components.
1860
- */
2993
+ /** Main application class that wires together all components. */
1861
2994
  declare class JeevesWatcher {
1862
2995
  private config;
1863
2996
  private readonly configPath?;
@@ -1867,52 +3000,41 @@ declare class JeevesWatcher {
1867
3000
  private logger;
1868
3001
  private watcher;
1869
3002
  private queue;
3003
+ private sourceQueue;
1870
3004
  private server;
1871
3005
  private processor;
1872
3006
  private configWatcher;
1873
- private issuesManager;
1874
- private valuesManager;
3007
+ private stores;
1875
3008
  private helperIntrospection;
1876
- private virtualRuleStore;
3009
+ private readonly virtualRuleStore;
1877
3010
  private vectorStore;
1878
3011
  private embeddingProvider;
3012
+ private embeddingHealth;
1879
3013
  private gitignoreFilter;
1880
- private enrichmentStore;
1881
- private contentHashCache;
1882
3014
  private vcsCoordinator;
1883
3015
  private readonly initialScanTracker;
3016
+ private readonly changeSourceHealth;
3017
+ private readonly indexCheck;
3018
+ private readonly restarter;
3019
+ /** The state directory (default `ROOT_DEFAULTS.stateDir`). */
3020
+ private readonly stateDir;
1884
3021
  private readonly version;
1885
3022
  /** Create a new JeevesWatcher instance. */
1886
3023
  constructor(config: JeevesWatcherConfig, configPath?: string, descriptor?: JeevesComponentDescriptor, factories?: Partial<JeevesWatcherFactories>, runtimeOptions?: JeevesWatcherRuntimeOptions);
1887
- /**
1888
- * Start the watcher, API server, and all components.
1889
- */
3024
+ /** Start the watcher, API server, and all components. */
1890
3025
  start(): Promise<void>;
1891
- /** Set up logger, VCS, embedding provider, and vector store. */
1892
- private initInfrastructure;
1893
3026
  /** Build the processing pipeline: rules, templates, processor, queue, VCS. */
1894
3027
  private initPipeline;
1895
3028
  /** Wire up watcher and VCS coordinator, then start all services. */
1896
3029
  private startServicesAndWatcher;
1897
- /**
1898
- * Gracefully stop all components.
1899
- */
3030
+ /** Gracefully stop: API server first (no new notifications), then producers, then queues drain (shutdownComponents). */
1900
3031
  stop(): Promise<void>;
1901
- private startApiServer;
1902
- private startConfigWatch;
1903
- private stopConfigWatch;
3032
+ /** Wiring every watcher the app builds shares, across rebuilds. */
3033
+ private watcherWiring;
3034
+ private apiServerOptions;
1904
3035
  private reloadConfig;
1905
3036
  }
1906
3037
 
1907
- /**
1908
- * Load the jeeves-watcher configuration.
1909
- *
1910
- * @param configPath - Optional explicit path to a config file.
1911
- * @returns The loaded configuration.
1912
- * @throws If no configuration is found or validation fails.
1913
- */
1914
- declare function loadConfig(configPath?: string): Promise<JeevesWatcherConfig>;
1915
-
1916
3038
  /**
1917
3039
  * @module descriptor
1918
3040
  * Jeeves Component Descriptor for the watcher service. Single source of truth
@@ -1983,112 +3105,5 @@ interface ResolvedWatchRoot {
1983
3105
  relativePath: string;
1984
3106
  }
1985
3107
 
1986
- /**
1987
- * @module vcs/SquashManager
1988
- * Handles scheduled squash retention for git-backed VCS roots.
1989
- */
1990
-
1991
- /** Result of a squash operation. */
1992
- interface SquashResult {
1993
- squashed: boolean;
1994
- commitsRemoved?: number;
1995
- commitsRetained?: number;
1996
- error?: string;
1997
- }
1998
- /** Commit metadata for retention calculation. */
1999
- interface CommitInfo {
2000
- hash: string;
2001
- date: Date;
2002
- }
2003
- /** Options for SquashManager construction beyond the required params. */
2004
- interface SquashManagerOptions {
2005
- /** Git branch name for squash operations. Default: "master". */
2006
- branch?: string;
2007
- /** Called before squash to drain and pause the commit pipeline. */
2008
- pauseCommits?: () => Promise<void>;
2009
- /** Called after squash to resume the commit pipeline. */
2010
- resumeCommits?: () => void;
2011
- /** Remote URL for force-push after squash. */
2012
- remoteUrl?: string;
2013
- /** Access token for HTTPS push authentication. */
2014
- accessToken?: string;
2015
- }
2016
- /**
2017
- * Manages scheduled squash retention for a single VCS root.
2018
- *
2019
- * Runs on a cron schedule (checked every 60s). When triggered, squashes
2020
- * commits older than the retention boundary (the tighter of maxAgeDays
2021
- * and maxVersions) into a single "historical baseline" orphan commit,
2022
- * then cherry-picks retained commits on top. Force-pushes to remote
2023
- * if configured (D13).
2024
- */
2025
- declare class SquashManager {
2026
- private readonly rootPath;
2027
- private readonly retention;
2028
- private readonly branch;
2029
- private readonly pauseCommits;
2030
- private readonly resumeCommits;
2031
- private readonly remoteUrl;
2032
- private readonly accessToken;
2033
- private readonly logger;
2034
- private intervalTimer;
2035
- private running;
2036
- constructor(rootPath: string, retention: VcsRetentionConfig, logger: pino.Logger, options?: SquashManagerOptions);
2037
- /**
2038
- * Start the cron-based squash scheduler.
2039
- * Checks every 60 seconds if the cron expression matches.
2040
- */
2041
- start(): void;
2042
- /**
2043
- * Stop the cron scheduler.
2044
- */
2045
- stop(): void;
2046
- /**
2047
- * Run the squash operation. Exposed publicly for testing.
2048
- *
2049
- * Coordinates with VcsManager by calling pause/resume callbacks
2050
- * to drain and suspend the commit pipeline during the squash.
2051
- */
2052
- runSquash(): Promise<SquashResult>;
2053
- /**
2054
- * Refuse to start a multi-step history rewrite while another git
2055
- * operation holds the index or an abandoned cherry-pick, rebase, or merge
2056
- * is present (#249: report only, never auto-clear).
2057
- *
2058
- * @returns The refusal reason, or undefined if the squash may proceed.
2059
- */
2060
- private checkRepoPreconditions;
2061
- /**
2062
- * Get all commits in chronological order (oldest first).
2063
- */
2064
- getCommitLog(): Promise<CommitInfo[]>;
2065
- /**
2066
- * Calculate the retention boundary index.
2067
- * Returns the index of the oldest commit to KEEP.
2068
- * All commits before this index will be squashed into a baseline.
2069
- *
2070
- * Strategy: find the tighter constraint between age and count.
2071
- * - Age: keep commits newer than maxAgeDays
2072
- * - Count: keep the last maxVersions commits
2073
- * The boundary is the one that retains FEWER old commits (tighter).
2074
- */
2075
- calculateRetentionBoundary(commits: CommitInfo[]): number;
2076
- /**
2077
- * Perform the squash: create orphan with baseline + cherry-pick retained commits.
2078
- * Uses the configured branch name instead of dynamic detection (Bug 1).
2079
- */
2080
- private performSquash;
2081
- /**
2082
- * Best-effort cleanup after a failed squash. Aborts any half-finished
2083
- * cherry-pick first, so a failure never leaves a sequencer behind (#249),
2084
- * then returns to the configured branch and removes a leftover lock.
2085
- */
2086
- private cleanupFailedSquash;
2087
- /**
2088
- * Force push to remote after squash (history rewrite requires force).
2089
- */
2090
- private forcePushIfConfigured;
2091
- }
2092
-
2093
- export { CommitMessageGenerator, ContentHashCache, DocumentProcessor, EnrichmentStore, EventQueue, FileSystemWatcher, GitignoreFilter, InitialScanTracker, IssuesManager, JeevesWatcher, ReindexTracker, SquashManager, SystemHealth, TemplateEngine, ValuesManager, VcsCoordinator, VcsManager, VectorStoreClient, VirtualRuleStore, applyRules, buildAttributes, buildTemplateEngine, compileRules, contentHash, createApiServer, createEmbeddingProvider, createHandlebarsInstance, createLogger, extractText, issueRecordSchema, loadConfig, loadCustomHelpers, mergeEnrichment, pointId, registerBuiltinHelpers, resolveTemplateSource, startFromConfig, watcherDescriptor };
2094
- export type { AllHelpersIntrospection, ApiServerOptions, ApplyRulesOptions, ApplyRulesResult, CollectionInfo, CommitInfo, CompiledRule, CompiledTemplate, DocumentProcessorDeps, DocumentProcessorInterface, EmbeddingProvider, EnrichmentStoreInterface, EventQueueOptions, ExtractedText, Extractor, FileAttributes, FileSystemWatcherOptions, HelperModuleIntrospection, InitialScanStatus, IssueRecord, IssuesFile, JeevesWatcherFactories, JeevesWatcherRuntimeOptions, PayloadFieldSchema, PendingReversion, ProcessFn, ProcessorConfig, ProviderFactory, PushError, ReindexStatus, RenderResult, ResolvedWatchRoot, RuleLogger, ScrollPageResult, ScrolledPoint, SearchResult, SquashManagerOptions, SquashResult, SystemHealthOptions, ValuesIndex, VectorPoint, VectorStore, WatchEvent };
3108
+ export { ChangeSourceHealthTracker, CommitMessageGenerator, ContentHashCache, DEFAULT_SOURCE_NOTIFY_CONCURRENCY, DocumentLocks, DocumentProcessor, EnrichmentStore, EventQueue, FileSystemWatcher, GitignoreFilter, InitialScanTracker, Inventory, IssuesManager, JeevesWatcher, ReindexTracker, RootGitLock, SourceClient, SourceNotifyQueue, SourceNotifyQueueStoppedError, SourceReferenceError, SquashManager, SystemHealth, TemplateEngine, ValuesManager, VcsCoordinator, VcsManager, VectorStoreClient, VirtualRuleStore, applyRules, buildAttributes, buildTemplateEngine, compileRules, contentHash, createApiServer, createEmbeddingProvider, createHandlebarsInstance, createLogger, extractText, fileLockKey, loadConfig, loadCustomHelpers, mergeEnrichment, parseSourceReference, parsedSourceReferenceSchema, pointId, registerBuiltinHelpers, resolveTemplateSource, resolvedSourceReferenceSchema, sourceAttributesSchema, sourceErrorCodeSchema, sourceNotifyQueueOptionsSchema, sourceRejectionCodeSchema, sourceRetrievalResultSchema, startFromConfig, watcherDescriptor, withoutSourceSupport };
3109
+ export type { AllHelpersIntrospection, ApiServerOptions, ApplyRulesOptions, ApplyRulesResult, ChangeSink, CircuitBreakerSnapshot, ClassifiedEventType, CollectionInfo, CommitInfo, CompiledRule, CompiledTemplate, ConfigWarning, DocumentLockOptions, DocumentProcessorDeps, DocumentProcessorInterface, EmbedOptions, EmbeddingErrorSummary, EmbeddingHealthSnapshot, EmbeddingHealthState, EmbeddingProvider, EnrichmentStoreInterface, EventQueueOptions, ExtractedText, Extractor, FacetHit, FacetRead, FacetStore, FacetValue, FileAttributes, FileStats, FileSystemWatcherOptions, HelperModuleIntrospection, IndexCheckSkipReason, IndexCheckView, IndexOptions, InventoryEntry, IssuesFile, JeevesWatcherFactories, JeevesWatcherRuntimeOptions, LoadConfigOptions, ParcelImporter, ParcelWatcherModule, ParsedSourceReference, PausableQueue, PayloadFieldSchema, PendingReversion, ProcessFn, ProcessNowOptions, ProcessRestarter, ProcessRestarterDeps, ProcessingLimits, ProcessorConfig, ProviderFactory, PushError, RangeBound, RangeEnd, RawChangeEvent, RenderResult, ResolvedSourceReference, ResolvedWatchRoot, RestartRequest, RootUnavailableOptions, RuleAttributes, RuleLogger, ScrollPageResult, ScrolledPoint, SearchResult, SourceAttributes, SourceErrorCode, SourceNotifyQueueOptions, SourceOperationsInterface, SourceOperationsProvider, SourceProcess, SourceQueueIssues, SourceRejectionCode, SourceRenderOutcome, SourceRetrievalResult, SourceRetriever, SquashManagerOptions, SquashResult, SystemHealthOptions, SystemIndexState, ValuesIndex, VcsBreakerState, VcsFileChange, VcsManagerOptions, VectorPoint, VectorStore, WatchEvent, WatcherProcessor };