@karmaniverous/jeeves-watcher 0.20.0 → 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,13 +1,14 @@
1
1
  import { FastifyInstance } from 'fastify';
2
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, IssueRecord, InferenceRule, SchemaEntry, SourcesConfig, VectorStoreConfig, DocumentResult, DocumentRejectionError, DocumentFailure, VcsConfig, JeevesWatcherConfig, WatchConfig, LoggingConfig, VcsRetentionConfig } from '@karmaniverous/jeeves-watcher-core';
5
- export { ApiConfig, ConfigWatchConfig, EmbeddingConfig, InferenceRule, IssueRecord, JeevesWatcherConfig, JeevesWatcherConfigInput, LoggingConfig, VectorStoreConfig, WatchConfig, apiConfigSchema, configWatchConfigSchema, embeddingConfigSchema, inferenceRuleSchema, issueRecordSchema, jeevesWatcherConfigSchema, loggingConfigSchema, vectorStoreConfigSchema, watchConfigSchema } from '@karmaniverous/jeeves-watcher-core';
6
6
  import { ErrorObject } from 'ajv';
7
7
  import Handlebars from 'handlebars';
8
8
  import { z } from 'zod';
9
- import { JsonMapMap } from '@karmaniverous/jsonmap';
10
9
  import { Stats } from 'node:fs';
10
+ import ParcelWatcher from '@parcel/watcher';
11
+ import { JsonMapMap } from '@karmaniverous/jsonmap';
11
12
 
12
13
  /**
13
14
  * @module embedding/types
@@ -80,6 +81,38 @@ type ProviderFactory = (config: EmbeddingConfig, logger?: pino.Logger) => Embedd
80
81
  */
81
82
  declare function createEmbeddingProvider(config: EmbeddingConfig, logger?: pino.Logger, additionalProviders?: Map<string, ProviderFactory>): EmbeddingProvider;
82
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
+
83
116
  /**
84
117
  * @module enrichment/EnrichmentStore
85
118
  * SQLite-backed enrichment metadata store. Persists path- or source-reference-keyed metadata (references verbatim) at stateDir/enrichments.sqlite. Atomic writes, supports move.
@@ -602,6 +635,60 @@ declare class VirtualRuleStore {
602
635
  get size(): number;
603
636
  }
604
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
+
605
692
  /**
606
693
  * @module cache/ContentHashCache
607
694
  * In-memory cache mapping normalized file paths to content hashes.
@@ -860,6 +947,74 @@ declare class ValuesManager extends BinaryFileStore<ValuesIndex> {
860
947
  getForRule(ruleName: string): Record<string, unknown[]>;
861
948
  }
862
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
+
863
1018
  /**
864
1019
  * @module vectorStore/types
865
1020
  * Vector store interface and type definitions.
@@ -961,6 +1116,37 @@ interface VectorStore {
961
1116
  * @param keys - Payload keys to remove.
962
1117
  */
963
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[]>;
964
1150
  /**
965
1151
  * Get the payload of a point by ID.
966
1152
  *
@@ -1041,6 +1227,8 @@ interface VectorStore {
1041
1227
  * fresh client (see QdrantWriter).
1042
1228
  */
1043
1229
  declare class VectorStoreClient implements VectorStore {
1230
+ /** Facet reads behind `countFacets` (spec Design 12). */
1231
+ readonly facets: FacetStore;
1044
1232
  private readonly client;
1045
1233
  private readonly writer;
1046
1234
  private readonly clientConfig;
@@ -1048,6 +1236,7 @@ declare class VectorStoreClient implements VectorStore {
1048
1236
  private readonly dims;
1049
1237
  private readonly pinoLogger?;
1050
1238
  private readonly payloadIndexes;
1239
+ private readonly systemIndexes;
1051
1240
  /**
1052
1241
  * Create a new VectorStoreClient.
1053
1242
  *
@@ -1066,18 +1255,26 @@ declare class VectorStoreClient implements VectorStore {
1066
1255
  * (see QdrantWriter).
1067
1256
  */
1068
1257
  private createClient;
1069
- /**
1070
- * Count points matching a filter.
1071
- *
1072
- * @param filter - Optional Qdrant filter.
1073
- * @returns The number of matching points.
1074
- */
1258
+ /** {@inheritDoc VectorStore.count} */
1075
1259
  count(filter?: Record<string, unknown>): Promise<number>;
1076
1260
  /**
1077
1261
  * Ensure the collection exists (Cosine, configured dimensions) and create
1078
- * any missing payload indexes (configured plus `source`/`source_rule`).
1262
+ * any missing payload indexes (configured plus `source`/`source_rule`),
1263
+ * then start the background system-index builds (D74, D154; not awaited).
1079
1264
  */
1080
1265
  ensureCollection(): Promise<void>;
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[]>;
1081
1278
  /** {@inheritDoc VectorStore.upsert} */
1082
1279
  upsert(points: VectorPoint[]): Promise<void>;
1083
1280
  /** {@inheritDoc VectorStore.delete} */
@@ -1086,77 +1283,39 @@ declare class VectorStoreClient implements VectorStore {
1086
1283
  setPayload(ids: string[], payload: Record<string, unknown>): Promise<void>;
1087
1284
  /** {@inheritDoc VectorStore.deletePayload} */
1088
1285
  deletePayload(ids: string[], keys: string[]): Promise<void>;
1089
- /**
1090
- * Get the payload of a point by ID.
1091
- *
1092
- * @param id - The point ID.
1093
- * @returns The payload, or `null` if the point doesn't exist.
1094
- * @throws When the read fails (never reported as a missing point).
1095
- */
1286
+ /** {@inheritDoc VectorStore.getPayload} */
1096
1287
  getPayload(id: string): Promise<Record<string, unknown> | null>;
1097
- /**
1098
- * Get collection info including point count, dimensions, and payload field schema.
1099
- *
1100
- * When Qdrant has payload indexes, uses `payload_schema` directly. Otherwise
1101
- * samples points to discover fields and infer types.
1102
- */
1288
+ /** {@inheritDoc VectorStore.getCollectionInfo} */
1103
1289
  getCollectionInfo(): Promise<CollectionInfo>;
1104
- /**
1105
- * Search for similar vectors.
1106
- *
1107
- * @param vector - The query vector.
1108
- * @param limit - Maximum results to return.
1109
- * @param filter - Optional Qdrant filter.
1110
- * @returns An array of search results.
1111
- */
1290
+ /** {@inheritDoc VectorStore.search} */
1112
1291
  search(vector: number[], limit: number, filter?: Record<string, unknown>, offset?: number): Promise<SearchResult[]>;
1113
- /**
1114
- * Create a full-text payload index on the specified field.
1115
- * Idempotent — skips if the field already has a text index.
1116
- *
1117
- * @param fieldName - The payload field to index.
1118
- */
1292
+ /** {@inheritDoc VectorStore.ensureTextIndex} */
1119
1293
  ensureTextIndex(fieldName: string): Promise<void>;
1120
- /**
1121
- * Hybrid search combining dense vector and full-text match with RRF fusion.
1122
- *
1123
- * @param vector - The query vector.
1124
- * @param queryText - The raw query text for full-text matching.
1125
- * @param limit - Maximum results to return.
1126
- * @param textWeight - Weight for text results in RRF (0–1).
1127
- * @param filter - Optional Qdrant filter.
1128
- * @param offset - Optional result offset for pagination.
1129
- * @returns An array of search results.
1130
- */
1294
+ /** {@inheritDoc VectorStore.hybridSearch} */
1131
1295
  hybridSearch(vector: number[], queryText: string, limit: number, textWeight: number, filter?: Record<string, unknown>, offset?: number): Promise<SearchResult[]>;
1132
- /**
1133
- * Retrieve points with their vectors by ID.
1134
- *
1135
- * @param ids - The point IDs to retrieve.
1136
- * @returns Points with vectors and payloads; missing IDs are omitted.
1137
- * @throws When the read fails (never reported as missing points).
1138
- */
1296
+ /** {@inheritDoc VectorStore.getPointsWithVectors} */
1139
1297
  getPointsWithVectors(ids: string[]): Promise<VectorPoint[]>;
1140
- /**
1141
- * Scroll one page of points matching a filter.
1142
- *
1143
- * @param filter - Optional Qdrant filter.
1144
- * @param limit - Page size.
1145
- * @param offset - Cursor offset from previous page.
1146
- * @param fields - Optional field projection.
1147
- * @returns Page of points and next cursor.
1148
- */
1298
+ /** {@inheritDoc VectorStore.scrollPage} */
1149
1299
  scrollPage(filter?: Record<string, unknown>, limit?: number, offset?: string | number, fields?: string[]): Promise<ScrollPageResult>;
1150
- /**
1151
- * Scroll through all points matching a filter.
1152
- *
1153
- * @param filter - Optional Qdrant filter.
1154
- * @param limit - Page size for scrolling.
1155
- * @yields Scrolled points.
1156
- */
1300
+ /** {@inheritDoc VectorStore.scroll} */
1157
1301
  scroll(filter?: Record<string, unknown>, limit?: number): AsyncGenerator<ScrolledPoint>;
1158
1302
  }
1159
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
+
1160
1319
  /**
1161
1320
  * @module processor/ProcessorConfig
1162
1321
  * Configuration interface for DocumentProcessor, extracted for single-responsibility.
@@ -1211,6 +1370,10 @@ interface DocumentProcessorDeps {
1211
1370
  contentHashCache?: ContentHashCache;
1212
1371
  /** Optional source callback client; without it, source documents fail `source_unavailable`. */
1213
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;
1214
1377
  }
1215
1378
 
1216
1379
  /**
@@ -1236,7 +1399,7 @@ interface RenderResult {
1236
1399
 
1237
1400
  /**
1238
1401
  * @module processor/documentPipeline
1239
- * The shared indexing pipeline for resolved documents (file or source): onResolved hook, empty check, hash compare, chunk, embed, upsert, orphan cleanup; plus point deletion.
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.
1240
1403
  */
1241
1404
 
1242
1405
  /**
@@ -1282,8 +1445,18 @@ interface SourceOperationsInterface {
1282
1445
  compiledRule(name: string): CompiledRule | undefined;
1283
1446
  /** Retrieve and render a document without embedding or writing anything. */
1284
1447
  render(ref: string, rule: string): Promise<SourceRenderOutcome>;
1285
- /** Merge enrichment metadata into an indexed document's points; `null` if not indexed. */
1286
- updateMetadata(ref: string, metadata: Record<string, unknown>): Promise<Record<string, unknown> | null>;
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>;
1287
1460
  }
1288
1461
  /** A processor exposing source operations (`DocumentProcessor` implements it). */
1289
1462
  interface SourceOperationsProvider {
@@ -1306,10 +1479,16 @@ interface DocumentProcessorInterface {
1306
1479
  processFile(filePath: string): Promise<void>;
1307
1480
  /** Delete a file's points/metadata from the system. */
1308
1481
  deleteFile(filePath: string): Promise<void>;
1309
- /** Process a metadata sidecar update for a file (payload update only). */
1310
- processMetadataUpdate(filePath: string, metadata: Record<string, unknown>): Promise<Record<string, unknown> | null>;
1311
- /** Process a rules update for a file (rebuild merged metadata, payload update only). */
1312
- 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>;
1313
1492
  /** Render a file through the rule engine without embedding. */
1314
1493
  renderFile(filePath: string): Promise<RenderResult>;
1315
1494
  /** Move a file's vector points and metadata from old path to new path (no re-embed). */
@@ -1337,23 +1516,19 @@ type WatcherProcessor = DocumentProcessorInterface & SourceOperationsProvider;
1337
1516
  * Handles extracting text, computing embeddings, and syncing with the vector store.
1338
1517
  */
1339
1518
  declare class DocumentProcessor implements WatcherProcessor {
1519
+ private readonly deps;
1340
1520
  private config;
1341
- private readonly embeddingProvider;
1342
- private readonly vectorStore;
1343
1521
  private compiledRules;
1344
- private readonly logger;
1345
1522
  private templateEngine?;
1346
- private readonly enrichmentStore?;
1347
- private readonly issuesManager?;
1348
- private readonly valuesManager?;
1349
- private readonly contentHashCache?;
1350
- private readonly sourceRetriever?;
1523
+ private readonly locks;
1351
1524
  /**
1352
1525
  * Create a new DocumentProcessor.
1353
1526
  *
1354
1527
  * @param deps - The processor dependencies.
1355
1528
  */
1356
1529
  constructor(deps: DocumentProcessorDeps);
1530
+ /** The processing ceilings now (read per document, D44). */
1531
+ private limits;
1357
1532
  /** Rule context reflecting the current rules, templates and config. */
1358
1533
  private ruleContext;
1359
1534
  /** Shared pipeline dependencies. */
@@ -1389,18 +1564,22 @@ declare class DocumentProcessor implements WatcherProcessor {
1389
1564
  *
1390
1565
  * @param filePath - The file whose metadata to update.
1391
1566
  * @param metadata - The new metadata to merge.
1567
+ * @param options - Bounded lock wait (API routes, D118).
1392
1568
  * @returns The merged payload, or `null` if the file is not indexed.
1569
+ * @throws DocumentLockTimeoutError when a bounded lock wait runs out.
1393
1570
  */
1394
- 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>;
1395
1572
  /**
1396
1573
  * Re-apply inference rules to a file without re-embedding (see
1397
1574
  * `reapplyFileRules`: system fields and the stored `file_path` are
1398
1575
  * re-asserted; interpolation failures are recorded as an issue).
1399
1576
  *
1400
1577
  * @param filePath - The file to update.
1578
+ * @param options - Bounded lock wait (API routes, D118).
1401
1579
  * @returns The merged metadata, or `null` if the file is not indexed.
1580
+ * @throws DocumentLockTimeoutError when a bounded lock wait runs out.
1402
1581
  */
1403
- processRulesUpdate(filePath: string): Promise<Record<string, unknown> | null>;
1582
+ processRulesUpdate(filePath: string, options?: DocumentLockOptions): Promise<Record<string, unknown> | null>;
1404
1583
  /**
1405
1584
  * Render a file through the rule engine without embedding.
1406
1585
  * Returns rendered content, renderAs, matched rules, and metadata.
@@ -1715,6 +1894,166 @@ declare class CommitMessageGenerator {
1715
1894
  generate(files: string[], diffSummary: string): Promise<string | null>;
1716
1895
  }
1717
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
+
1718
2057
  /**
1719
2058
  * @module vcs/types
1720
2059
  * Shared types for the VCS subsystem.
@@ -1733,7 +2072,7 @@ interface PushError {
1733
2072
 
1734
2073
  /**
1735
2074
  * @module vcs/VcsManager
1736
- * 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.
1737
2076
  */
1738
2077
 
1739
2078
  /** Per-root circuit breaker state, exposed via /vcs/status. */
@@ -1741,75 +2080,56 @@ interface VcsBreakerState extends CircuitBreakerSnapshot {
1741
2080
  /** Files queued or in flight, not yet committed. */
1742
2081
  pendingCount: number;
1743
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
+ }
1744
2090
  /**
1745
- * Per-root VCS manager for git-backed content versioning.
1746
- *
1747
- * Each VCS-enabled watch root gets its own VcsManager instance, which owns
1748
- * a throttled commit pipeline: file changes are batched, staged, committed
1749
- * (with optional AI-generated messages), and optionally pushed to a remote.
1750
- *
1751
- * Concurrency: only one commit is in-flight at a time per root. Index.lock
1752
- * contention is handled with exponential backoff retries (D10). Failed
1753
- * batches are always retained; a circuit breaker spaces out retries of a
1754
- * 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).
1755
2093
  */
1756
2094
  declare class VcsManager {
1757
2095
  readonly config: VcsConfig;
1758
2096
  readonly rootPath: string;
1759
2097
  readonly remoteUrl: string | undefined;
1760
- private readonly accessToken;
2098
+ /** Serialises this root's git work (D66, D81). */
2099
+ readonly lock: RootGitLock;
1761
2100
  private readonly logger;
1762
- private readonly commitMessageBuilder;
2101
+ private readonly committer;
1763
2102
  private readonly breaker;
1764
- private readonly pending;
2103
+ private readonly queue;
1765
2104
  private readonly pendingReversions;
1766
- private readonly _pushErrors;
1767
2105
  private readonly squashManager;
1768
- private throttleTimer;
1769
- private breakerRetryTimer;
1770
- private commitInFlight;
1771
- private inFlightFileCount;
2106
+ private sweepTimer;
1772
2107
  private started;
1773
- private paused;
1774
2108
  private isBaseline;
1775
- private _lastPushTime;
1776
- 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);
1777
2110
  get lastPushTime(): string | null;
1778
2111
  get pushErrors(): readonly PushError[];
1779
2112
  /** Current circuit breaker state and backlog, for /vcs/status. */
1780
2113
  get breakerState(): VcsBreakerState;
2114
+ /** First breaker trip since the last success, for `health.vcs`. */
2115
+ get trippedSince(): string | null;
1781
2116
  /**
1782
- * Begin accepting file changes. Sets the manager to started state and
1783
- * starts the squash manager if configured.
1784
- *
1785
- * Performs startup orphan detection/recovery before starting the squash
1786
- * manager, ensuring the repo is on the configured branch, and reports
1787
- * (never clears) an abandoned cherry-pick, rebase, or merge.
1788
- *
1789
- * Baseline mode (commit prefix `"baseline:"`) remains active until
1790
- * {@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}.
1791
2120
  */
1792
2121
  start(): Promise<void>;
1793
- /**
1794
- * Signal that the initial filesystem scan is complete.
1795
- * Clears the baseline flag so subsequent commits use normal "watcher: batch" messages.
1796
- */
2122
+ /** Initial scan done: later commits use `watcher: batch` messages. */
1797
2123
  endBaseline(): void;
1798
2124
  /**
1799
- * Add a file to the pending set and start the throttle timer.
1800
- * If the pending set reaches maxBatchSize, commit a batch immediately.
1801
- *
1802
- * 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
1803
2127
  * time-based, so a busy root cannot hammer a deterministic failure.
1804
2128
  *
1805
2129
  * @param filePath - Absolute path of the changed file.
1806
2130
  */
1807
2131
  fileChanged(filePath: string): void;
1808
- /**
1809
- * Record reversion metadata so the next commit uses a revert-prefixed message.
1810
- *
1811
- * @param reversion - The reversion metadata to record.
1812
- */
2132
+ /** Record a reversion so the next commit uses a revert-prefixed message. */
1813
2133
  addPendingReversion(reversion: PendingReversion): void;
1814
2134
  /**
1815
2135
  * Queue a deleted file. Staging records the deletion if git tracks it.
@@ -1817,66 +2137,31 @@ declare class VcsManager {
1817
2137
  * @param filePath - Absolute path of the deleted file.
1818
2138
  */
1819
2139
  handleUnlink(filePath: string): void;
1820
- /**
1821
- * Pause the commit pipeline. Drains pending commits via flush(), then
1822
- * sets a flag that makes commitBatch queue instead of execute.
1823
- * Used by SquashManager to coordinate squash operations.
1824
- */
1825
- pause(): Promise<void>;
1826
- /**
1827
- * Resume the commit pipeline after a pause.
1828
- * Re-enables normal commit operations and starts a throttle timer
1829
- * if there are pending files that accumulated during the pause.
1830
- */
1831
- resume(): void;
1832
- /**
1833
- * Commit all pending files now, in batches of at most maxBatchSize.
1834
- */
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. */
1835
2143
  flush(): Promise<void>;
1836
- /**
1837
- * Stop accepting file changes, flush pending, and clean up timers.
1838
- */
2144
+ /** Stop accepting file changes, flush pending, and clean up timers. */
1839
2145
  stop(): Promise<void>;
2146
+ /** Run a squash now (as the cron does); undefined when retention is off. */
2147
+ runSquash(): ReturnType<SquashManager['runSquash']> | undefined;
1840
2148
  /**
1841
2149
  * Add tracked-but-deleted paths (`git ls-files --deleted`) in this
1842
2150
  * manager's watch scope to pending, so the next commit (typically the
1843
- * baseline) records deletions that happened while nothing was watching.
1844
- * Called by the coordinator before flush/endBaseline on startup. See #249.
2151
+ * baseline) records deletions made while nothing was watching (#249).
1845
2152
  *
1846
2153
  * @param isInScope - Returns true if the absolute path routes to this
1847
2154
  * manager (the coordinator's routing and watch-scope logic).
1848
2155
  */
1849
2156
  reconcileDeletions(isInScope: (absolutePath: string) => boolean): Promise<void>;
1850
- /** Take up to maxBatchSize files from the pending set. */
1851
- private takeBatch;
1852
- /** Chain a batch behind the in-flight commit, tracking it as backlog. */
1853
- private enqueueBatch;
1854
- /** Put files back in the pending set. Pending is a set, so this cannot grow it past the distinct paths seen. */
1855
- private retain;
1856
- private clearThrottle;
1857
2157
  /**
1858
- * Start the throttle timer to fire flush() after commitThrottleMs.
1859
- * If a timer is already running, do not reset it — this is throttle, not debounce.
1860
- */
1861
- private resetThrottle;
1862
- private clearBreakerRetry;
1863
- /**
1864
- * Ensure a timer will flush pending files when the breaker's cooldown
1865
- * elapses, without needing a new file-change event.
1866
- */
1867
- private scheduleBreakerRetry;
1868
- /**
1869
- * Commit a batch with index.lock retry, stale lock detection, pause
1870
- * support, and the circuit breaker. A failed batch is always retained.
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.
2161
+ *
2162
+ * @returns true on success (including "nothing staged").
1871
2163
  */
1872
2164
  private commitBatch;
1873
- /**
1874
- * One commit attempt: stage, skip if nothing is staged, commit, push.
1875
- *
1876
- * @returns true if a commit was created; false for a no-op.
1877
- */
1878
- private tryCommit;
1879
- private onCommitSuccess;
1880
2165
  private onCommitFailure;
1881
2166
  }
1882
2167
 
@@ -1894,10 +2179,15 @@ declare class VcsCoordinator {
1894
2179
  private readonly roots;
1895
2180
  private readonly logger;
1896
2181
  private readonly isInWatchScope;
1897
- constructor(config: JeevesWatcherConfig, logger: pino.Logger);
1898
2182
  /**
1899
- * Start all VcsManager instances.
1900
- * 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).
1901
2191
  */
1902
2192
  start(): Promise<void>;
1903
2193
  /**
@@ -1920,6 +2210,12 @@ declare class VcsCoordinator {
1920
2210
  * Flush all managers and clean up.
1921
2211
  */
1922
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;
1923
2219
  /**
1924
2220
  * Get the sorted root paths (longest-first).
1925
2221
  */
@@ -1942,48 +2238,6 @@ declare class VcsCoordinator {
1942
2238
  findManagerForPath(normalizedPath: string): VcsManager | undefined;
1943
2239
  }
1944
2240
 
1945
- /**
1946
- * @module api/InitialScanTracker
1947
- * Tracks initial filesystem scan state for status reporting.
1948
- */
1949
- /** Initial scan status snapshot for API consumers. */
1950
- interface InitialScanStatus {
1951
- /** Whether the initial scan is currently in progress. */
1952
- active: boolean;
1953
- /** Number of files matched by watch globs. */
1954
- filesMatched?: number;
1955
- /** Number of files enqueued for processing so far. */
1956
- filesEnqueued?: number;
1957
- /** ISO 8601 timestamp when the scan started. */
1958
- startedAt?: string;
1959
- /** ISO 8601 timestamp when the scan completed. */
1960
- completedAt?: string;
1961
- /** Total scan duration in milliseconds. */
1962
- durationMs?: number;
1963
- }
1964
- /**
1965
- * Tracks the state of the initial filesystem scan after service start.
1966
- */
1967
- declare class InitialScanTracker {
1968
- private _active;
1969
- private _filesMatched;
1970
- private _filesEnqueued;
1971
- private _startedAt?;
1972
- private _completedAt?;
1973
- private _durationMs?;
1974
- private _started;
1975
- /** Mark the initial scan as started. */
1976
- start(): void;
1977
- /** Set the total number of matched files. */
1978
- setMatched(count: number): void;
1979
- /** Increment the processed file count. */
1980
- incrementEnqueued(): void;
1981
- /** Mark the scan as complete. */
1982
- complete(): void;
1983
- /** Get current scan status. */
1984
- getStatus(): InitialScanStatus;
1985
- }
1986
-
1987
2241
  /**
1988
2242
  * @module health
1989
2243
  * Tracks consecutive system-level failures and applies exponential backoff.
@@ -2039,12 +2293,265 @@ declare class SystemHealth {
2039
2293
  * @param signal - Optional abort signal.
2040
2294
  */
2041
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>;
2042
2305
  /** Current consecutive failure count. */
2043
2306
  get failures(): number;
2044
2307
  }
2045
2308
 
2046
2309
  /**
2047
- * Options for {@link FileSystemWatcher} beyond basic config.
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
+
2359
+ /**
2360
+ * @module api/InitialScanTracker
2361
+ * Tracks initial filesystem scan state for status reporting.
2362
+ */
2363
+
2364
+ /**
2365
+ * Tracks the state of the initial filesystem scan after service start.
2366
+ */
2367
+ declare class InitialScanTracker {
2368
+ private _active;
2369
+ private _filesMatched;
2370
+ private _filesEnqueued;
2371
+ private _startedAt?;
2372
+ private _completedAt?;
2373
+ private _durationMs?;
2374
+ private _started;
2375
+ /** Mark the initial scan as started. */
2376
+ start(): void;
2377
+ /** Set the total number of matched files. */
2378
+ setMatched(count: number): void;
2379
+ /** Increment the processed file count. */
2380
+ incrementEnqueued(): void;
2381
+ /** Mark the scan as complete. */
2382
+ complete(): void;
2383
+ /** Get current scan status. */
2384
+ getStatus(): InitialScanStatus;
2385
+ }
2386
+
2387
+ /**
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.
2390
+ */
2391
+
2392
+ /** VCS change callback. */
2393
+ type VcsFileChange = (filePath: string, event: 'add' | 'change' | 'unlink') => void;
2394
+
2395
+ /**
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.
2398
+ */
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;
2414
+ }
2415
+ /**
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).
2420
+ */
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;
2431
+ /**
2432
+ * Remove a file.
2433
+ *
2434
+ * @returns True when it was present.
2435
+ */
2436
+ delete(path: string): boolean;
2437
+ /** The entry for a path, if present. */
2438
+ get(path: string): InventoryEntry | undefined;
2439
+ /**
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).
2442
+ */
2443
+ isDirectory(path: string): boolean;
2444
+ /** Every file path, as the source reported it (`/walk`, D83). */
2445
+ paths(): string[];
2446
+ /**
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).
2521
+ *
2522
+ * @param count - Failures to add (default 1).
2523
+ */
2524
+ recordRenewFailure(count?: number): void;
2525
+ /** A subscription renewal finished (Linux parcel, D129, D157). */
2526
+ recordRenew(durationMs: number): void;
2527
+ /**
2528
+ * The previous process exited for a restart (D196): its cause and when.
2529
+ *
2530
+ * @param restart - The record it left.
2531
+ */
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;
2546
+ }
2547
+
2548
+ /**
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.
2048
2555
  */
2049
2556
  interface FileSystemWatcherOptions {
2050
2557
  /** Maximum consecutive system-level failures before fatal error. */
@@ -2060,27 +2567,75 @@ interface FileSystemWatcherOptions {
2060
2567
  /** Optional content hash cache for move detection. */
2061
2568
  contentHashCache?: ContentHashCache;
2062
2569
  /** Optional callback for VCS file change notifications. */
2063
- onVcsFileChange?: (filePath: string, event: 'add' | 'change' | 'unlink') => void;
2064
- /** Optional callback invoked when the initial filesystem scan completes. */
2065
- 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;
2066
2616
  }
2617
+
2067
2618
  /**
2068
- * 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.
2069
2621
  */
2070
- 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 {
2071
2629
  private readonly config;
2072
2630
  private readonly queue;
2073
2631
  private readonly processor;
2074
2632
  private readonly logger;
2633
+ private readonly options;
2075
2634
  private readonly health;
2076
- private readonly gitignoreFilter?;
2077
- private readonly initialScanTracker?;
2078
- private readonly contentHashCache?;
2079
- private readonly onVcsFileChange?;
2080
- private readonly onInitialScanComplete?;
2081
- private moveCorrelator?;
2082
- private globMatches;
2083
- private watcher;
2635
+ private running;
2636
+ /** A stopped source left its watches live (D196). */
2637
+ private leftWatches;
2638
+ private initialScanComplete;
2084
2639
  /**
2085
2640
  * Create a new FileSystemWatcher.
2086
2641
  *
@@ -2092,36 +2647,64 @@ declare class FileSystemWatcher {
2092
2647
  */
2093
2648
  constructor(config: WatchConfig, queue: EventQueue, processor: DocumentProcessorInterface, logger: pino.Logger, options?: FileSystemWatcherOptions);
2094
2649
  /**
2095
- * Start watching the filesystem and processing events.
2096
- */
2097
- start(): void;
2098
- /**
2099
- * 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).
2100
2652
  *
2101
- * Uses chokidar's in-memory `getWatched()` state — no filesystem I/O.
2102
- * Returns an empty array if the watcher hasn't been started.
2653
+ * @throws ChangeSourceLoadError when parcel is selected and cannot load.
2103
2654
  */
2104
- getWatchedFiles(): string[];
2105
- /**
2106
- * 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.
2107
2685
  */
2686
+ getWatchedFiles(): string[];
2687
+ /** Whether the initial filesystem scan is complete. */
2108
2688
  get isReady(): boolean;
2109
2689
  /**
2110
- * 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).
2111
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. */
2112
2700
  stop(): Promise<void>;
2113
- /**
2114
- * Get the system health tracker.
2115
- */
2701
+ /** Get the system health tracker. */
2116
2702
  get systemHealth(): SystemHealth;
2117
2703
  /**
2118
- * Check if a path is gitignored and should be skipped.
2119
- */
2120
- private isGitignored;
2121
- /**
2122
- * 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).
2123
2706
  */
2124
- private handleGitignoreChange;
2707
+ refreshReconcileSchedule(): void;
2125
2708
  /**
2126
2709
  * Wrap a processing operation with health tracking.
2127
2710
  * On success, resets the failure counter.
@@ -2134,19 +2717,7 @@ declare class FileSystemWatcher {
2134
2717
  * @module api/ReindexTracker
2135
2718
  * Tracks reindex operation state for status reporting. Single instance shared across handlers.
2136
2719
  */
2137
- /** Reindex status snapshot for API consumers. */
2138
- interface ReindexStatus {
2139
- /** Whether a reindex operation is currently in progress. */
2140
- active: boolean;
2141
- /** The active reindex scope (when {@link active} is true). */
2142
- scope?: string;
2143
- /** ISO 8601 timestamp when the current reindex started (when {@link active} is true). */
2144
- startedAt?: string;
2145
- /** Number of files processed so far (when {@link active} is true). */
2146
- filesProcessed?: number;
2147
- /** Total number of files to process (when {@link active} is true). */
2148
- totalFiles?: number;
2149
- }
2720
+
2150
2721
  /**
2151
2722
  * Tracks the state of reindex operations.
2152
2723
  */
@@ -2209,33 +2780,56 @@ interface ApiServerOptions {
2209
2780
  configPath: string;
2210
2781
  /** Helper introspection for merged document. */
2211
2782
  helperIntrospection?: AllHelpersIntrospection;
2212
- /** Virtual rule store for externally registered inference rules. */
2213
- virtualRuleStore?: VirtualRuleStore;
2783
+ /** Virtual rule store for externally registered inference rules (the
2784
+ * `/rules/*` routes). */
2785
+ virtualRuleStore: VirtualRuleStore;
2214
2786
  /** Gitignore filter for reindex path validation. */
2215
2787
  gitignoreFilter?: GitignoreFilter;
2216
2788
  /** Service version string for /status endpoint. */
2217
2789
  version?: string;
2218
2790
  /** Initial scan tracker for /status visibility. */
2219
2791
  initialScanTracker?: InitialScanTracker;
2220
- /** Filesystem watcher instance for /walk endpoint (in-memory file list). */
2792
+ /** Filesystem watcher instance for /walk endpoint (its inventory, D83). */
2221
2793
  fileSystemWatcher?: FileSystemWatcher;
2222
2794
  /** Getter for live filesystem watcher access after hot-reload rebuilds. */
2223
2795
  getFileSystemWatcher?: () => FileSystemWatcher | undefined;
2224
2796
  /** Optional enrichment store for persisted enrichment metadata. */
2225
2797
  enrichmentStore?: EnrichmentStoreInterface;
2226
- /** 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`). */
2227
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
+ };
2228
2818
  }
2229
2819
 
2230
2820
  /**
2231
2821
  * @module api
2232
- * Fastify API server factory. Registers all route handlers (status, query, documents, config, rules, VCS) and returns an unstarted server instance.
2822
+ * Fastify API server factory. Registers every `WATCHER_CATALOG` route (plus `GET /openapi.json`) from the catalog, and returns an unstarted server instance.
2233
2823
  */
2234
2824
 
2235
2825
  /**
2236
2826
  * Create the Fastify API server with all routes registered.
2237
2827
  *
2238
- * 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`).
2239
2833
  *
2240
2834
  * @param options - The server options.
2241
2835
  * @returns A configured Fastify instance.
@@ -2279,17 +2873,37 @@ interface LoadConfigOptions {
2279
2873
  declare function loadConfig(configPath?: string, options?: LoadConfigOptions): Promise<JeevesWatcherConfig>;
2280
2874
 
2281
2875
  /**
2282
- * @module logger
2283
- * Creates pino logger instances. I/O: optionally writes logs to file via pino/file transport. Defaults to stdout at info level.
2284
- */
2285
-
2286
- /**
2287
- * 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.
2288
2878
  *
2289
2879
  * @param config - Optional logging configuration.
2290
2880
  * @returns A configured pino logger.
2291
2881
  */
2292
- 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
+ }
2293
2907
 
2294
2908
  /**
2295
2909
  * @module app/factories
@@ -2323,6 +2937,13 @@ interface JeevesWatcherFactories {
2323
2937
  createFileSystemWatcher: (config: JeevesWatcherConfig['watch'], queue: EventQueue, processor: DocumentProcessorInterface, logger: pino.Logger, options?: FileSystemWatcherOptions) => FileSystemWatcher;
2324
2938
  /** Create the HTTP API server. */
2325
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;
2326
2947
  }
2327
2948
 
2328
2949
  /**
@@ -2369,9 +2990,7 @@ declare function startFromConfig(configPath?: string, descriptor?: JeevesCompone
2369
2990
  * Main application orchestrator. Wires components, manages lifecycle (start/stop/reload).
2370
2991
  */
2371
2992
 
2372
- /**
2373
- * Main application class that wires together all components.
2374
- */
2993
+ /** Main application class that wires together all components. */
2375
2994
  declare class JeevesWatcher {
2376
2995
  private config;
2377
2996
  private readonly configPath?;
@@ -2385,34 +3004,33 @@ declare class JeevesWatcher {
2385
3004
  private server;
2386
3005
  private processor;
2387
3006
  private configWatcher;
2388
- private issuesManager;
2389
- private valuesManager;
3007
+ private stores;
2390
3008
  private helperIntrospection;
2391
3009
  private readonly virtualRuleStore;
2392
3010
  private vectorStore;
2393
3011
  private embeddingProvider;
3012
+ private embeddingHealth;
2394
3013
  private gitignoreFilter;
2395
- private enrichmentStore;
2396
- private contentHashCache;
2397
3014
  private vcsCoordinator;
2398
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;
2399
3021
  private readonly version;
2400
3022
  /** Create a new JeevesWatcher instance. */
2401
3023
  constructor(config: JeevesWatcherConfig, configPath?: string, descriptor?: JeevesComponentDescriptor, factories?: Partial<JeevesWatcherFactories>, runtimeOptions?: JeevesWatcherRuntimeOptions);
2402
3024
  /** Start the watcher, API server, and all components. */
2403
3025
  start(): Promise<void>;
2404
- /** Set up logger, VCS, embedding provider, and vector store. */
2405
- private initInfrastructure;
2406
3026
  /** Build the processing pipeline: rules, templates, processor, queue, VCS. */
2407
3027
  private initPipeline;
2408
3028
  /** Wire up watcher and VCS coordinator, then start all services. */
2409
3029
  private startServicesAndWatcher;
2410
- /**
2411
- * Gracefully stop all components: the API server closes first (no new
2412
- * notifications), then producers stop and queues drain (see
2413
- * shutdownComponents).
2414
- */
3030
+ /** Gracefully stop: API server first (no new notifications), then producers, then queues drain (shutdownComponents). */
2415
3031
  stop(): Promise<void>;
3032
+ /** Wiring every watcher the app builds shares, across rebuilds. */
3033
+ private watcherWiring;
2416
3034
  private apiServerOptions;
2417
3035
  private reloadConfig;
2418
3036
  }
@@ -2487,112 +3105,5 @@ interface ResolvedWatchRoot {
2487
3105
  relativePath: string;
2488
3106
  }
2489
3107
 
2490
- /**
2491
- * @module vcs/SquashManager
2492
- * Handles scheduled squash retention for git-backed VCS roots.
2493
- */
2494
-
2495
- /** Result of a squash operation. */
2496
- interface SquashResult {
2497
- squashed: boolean;
2498
- commitsRemoved?: number;
2499
- commitsRetained?: number;
2500
- error?: string;
2501
- }
2502
- /** Commit metadata for retention calculation. */
2503
- interface CommitInfo {
2504
- hash: string;
2505
- date: Date;
2506
- }
2507
- /** Options for SquashManager construction beyond the required params. */
2508
- interface SquashManagerOptions {
2509
- /** Git branch name for squash operations. Default: "master". */
2510
- branch?: string;
2511
- /** Called before squash to drain and pause the commit pipeline. */
2512
- pauseCommits?: () => Promise<void>;
2513
- /** Called after squash to resume the commit pipeline. */
2514
- resumeCommits?: () => void;
2515
- /** Remote URL for force-push after squash. */
2516
- remoteUrl?: string;
2517
- /** Access token for HTTPS push authentication. */
2518
- accessToken?: string;
2519
- }
2520
- /**
2521
- * Manages scheduled squash retention for a single VCS root.
2522
- *
2523
- * Runs on a cron schedule (checked every 60s). When triggered, squashes
2524
- * commits older than the retention boundary (the tighter of maxAgeDays
2525
- * and maxVersions) into a single "historical baseline" orphan commit,
2526
- * then cherry-picks retained commits on top. Force-pushes to remote
2527
- * if configured (D13).
2528
- */
2529
- declare class SquashManager {
2530
- private readonly rootPath;
2531
- private readonly retention;
2532
- private readonly branch;
2533
- private readonly pauseCommits;
2534
- private readonly resumeCommits;
2535
- private readonly remoteUrl;
2536
- private readonly accessToken;
2537
- private readonly logger;
2538
- private intervalTimer;
2539
- private running;
2540
- constructor(rootPath: string, retention: VcsRetentionConfig, logger: pino.Logger, options?: SquashManagerOptions);
2541
- /**
2542
- * Start the cron-based squash scheduler.
2543
- * Checks every 60 seconds if the cron expression matches.
2544
- */
2545
- start(): void;
2546
- /**
2547
- * Stop the cron scheduler.
2548
- */
2549
- stop(): void;
2550
- /**
2551
- * Run the squash operation. Exposed publicly for testing.
2552
- *
2553
- * Coordinates with VcsManager by calling pause/resume callbacks
2554
- * to drain and suspend the commit pipeline during the squash.
2555
- */
2556
- runSquash(): Promise<SquashResult>;
2557
- /**
2558
- * Refuse to start a multi-step history rewrite while another git
2559
- * operation holds the index or an abandoned cherry-pick, rebase, or merge
2560
- * is present (#249: report only, never auto-clear).
2561
- *
2562
- * @returns The refusal reason, or undefined if the squash may proceed.
2563
- */
2564
- private checkRepoPreconditions;
2565
- /**
2566
- * Get all commits in chronological order (oldest first).
2567
- */
2568
- getCommitLog(): Promise<CommitInfo[]>;
2569
- /**
2570
- * Calculate the retention boundary index.
2571
- * Returns the index of the oldest commit to KEEP.
2572
- * All commits before this index will be squashed into a baseline.
2573
- *
2574
- * Strategy: find the tighter constraint between age and count.
2575
- * - Age: keep commits newer than maxAgeDays
2576
- * - Count: keep the last maxVersions commits
2577
- * The boundary is the one that retains FEWER old commits (tighter).
2578
- */
2579
- calculateRetentionBoundary(commits: CommitInfo[]): number;
2580
- /**
2581
- * Perform the squash: create orphan with baseline + cherry-pick retained commits.
2582
- * Uses the configured branch name instead of dynamic detection (Bug 1).
2583
- */
2584
- private performSquash;
2585
- /**
2586
- * Best-effort cleanup after a failed squash. Aborts any half-finished
2587
- * cherry-pick first, so a failure never leaves a sequencer behind (#249),
2588
- * then returns to the configured branch and removes a leftover lock.
2589
- */
2590
- private cleanupFailedSquash;
2591
- /**
2592
- * Force push to remote after squash (history rewrite requires force).
2593
- */
2594
- private forcePushIfConfigured;
2595
- }
2596
-
2597
- export { CommitMessageGenerator, ContentHashCache, DEFAULT_SOURCE_NOTIFY_CONCURRENCY, DocumentProcessor, EnrichmentStore, EventQueue, FileSystemWatcher, GitignoreFilter, InitialScanTracker, IssuesManager, JeevesWatcher, ReindexTracker, SourceClient, SourceNotifyQueue, SourceNotifyQueueStoppedError, SourceReferenceError, SquashManager, SystemHealth, TemplateEngine, ValuesManager, VcsCoordinator, VcsManager, VectorStoreClient, VirtualRuleStore, applyRules, buildAttributes, buildTemplateEngine, compileRules, contentHash, createApiServer, createEmbeddingProvider, createHandlebarsInstance, createLogger, extractText, loadConfig, loadCustomHelpers, mergeEnrichment, parseSourceReference, parsedSourceReferenceSchema, pointId, registerBuiltinHelpers, resolveTemplateSource, resolvedSourceReferenceSchema, sourceAttributesSchema, sourceErrorCodeSchema, sourceNotifyQueueOptionsSchema, sourceRejectionCodeSchema, sourceRetrievalResultSchema, startFromConfig, watcherDescriptor, withoutSourceSupport };
2598
- export type { AllHelpersIntrospection, ApiServerOptions, ApplyRulesOptions, ApplyRulesResult, CircuitBreakerSnapshot, CollectionInfo, CommitInfo, CompiledRule, CompiledTemplate, ConfigWarning, DocumentProcessorDeps, DocumentProcessorInterface, EmbedOptions, EmbeddingProvider, EnrichmentStoreInterface, EventQueueOptions, ExtractedText, Extractor, FileAttributes, FileSystemWatcherOptions, HelperModuleIntrospection, IndexOptions, InitialScanStatus, IssuesFile, JeevesWatcherFactories, JeevesWatcherRuntimeOptions, LoadConfigOptions, ParsedSourceReference, PausableQueue, PayloadFieldSchema, PendingReversion, ProcessFn, ProcessNowOptions, ProcessorConfig, ProviderFactory, PushError, ReindexStatus, RenderResult, ResolvedSourceReference, ResolvedWatchRoot, RuleAttributes, RuleLogger, ScrollPageResult, ScrolledPoint, SearchResult, SourceAttributes, SourceErrorCode, SourceNotifyQueueOptions, SourceOperationsInterface, SourceOperationsProvider, SourceProcess, SourceQueueIssues, SourceRejectionCode, SourceRenderOutcome, SourceRetrievalResult, SourceRetriever, SquashManagerOptions, SquashResult, SystemHealthOptions, ValuesIndex, VcsBreakerState, VectorPoint, VectorStore, WatchEvent, WatcherProcessor };
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 };