@karmaniverous/jeeves-meta 0.4.1 → 0.5.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.js CHANGED
@@ -1,7 +1,11 @@
1
- import { readdirSync, unlinkSync, readFileSync, mkdirSync, writeFileSync, existsSync, statSync, copyFileSync, watchFile } from 'node:fs';
2
- import { join, dirname, relative } from 'node:path';
1
+ import fs, { readdirSync, unlinkSync, readFileSync, mkdirSync, writeFileSync, existsSync, statSync, copyFileSync, watchFile } from 'node:fs';
2
+ import path, { join, dirname, resolve, relative } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import 'node:fs/promises';
5
+ import process$1 from 'node:process';
3
6
  import { z } from 'zod';
4
7
  import { createHash, randomUUID } from 'node:crypto';
8
+ import { tmpdir } from 'node:os';
5
9
  import pino from 'pino';
6
10
  import { Cron } from 'croner';
7
11
  import Fastify from 'fastify';
@@ -102,6 +106,122 @@ function createSnapshot(metaPath, meta) {
102
106
  return archiveFile;
103
107
  }
104
108
 
109
+ const toPath = urlOrPath => urlOrPath instanceof URL ? fileURLToPath(urlOrPath) : urlOrPath;
110
+
111
+ function findUpSync(name, {
112
+ cwd = process$1.cwd(),
113
+ type = 'file',
114
+ stopAt,
115
+ } = {}) {
116
+ let directory = path.resolve(toPath(cwd) ?? '');
117
+ const {root} = path.parse(directory);
118
+ stopAt = path.resolve(directory, toPath(stopAt) ?? root);
119
+ const isAbsoluteName = path.isAbsolute(name);
120
+
121
+ while (directory) {
122
+ const filePath = isAbsoluteName ? name : path.join(directory, name);
123
+
124
+ try {
125
+ const stats = fs.statSync(filePath, {throwIfNoEntry: false});
126
+ if ((type === 'file' && stats?.isFile()) || (type === 'directory' && stats?.isDirectory())) {
127
+ return filePath;
128
+ }
129
+ } catch {}
130
+
131
+ if (directory === stopAt || directory === root) {
132
+ break;
133
+ }
134
+
135
+ directory = path.dirname(directory);
136
+ }
137
+ }
138
+
139
+ const isTypeOnlyPackageJsonData = packageData => {
140
+ if (!packageData || typeof packageData !== 'object' || Array.isArray(packageData)) {
141
+ return false;
142
+ }
143
+
144
+ const keys = Object.keys(packageData);
145
+ return keys.length === 1 && keys[0] === 'type' && typeof packageData.type === 'string';
146
+ };
147
+
148
+ const isTypeOnlyPackageJsonSync = filePath => {
149
+ let fileContents;
150
+
151
+ try {
152
+ fileContents = fs.readFileSync(filePath, 'utf8');
153
+ } catch {
154
+ return false;
155
+ }
156
+
157
+ try {
158
+ return isTypeOnlyPackageJsonData(JSON.parse(fileContents));
159
+ } catch {
160
+ return false;
161
+ }
162
+ };
163
+
164
+ const getNextSearchDirectory = filePath => {
165
+ const packageDirectoryPath = path.dirname(filePath);
166
+ const parentDirectoryPath = path.dirname(packageDirectoryPath);
167
+ return parentDirectoryPath === packageDirectoryPath ? undefined : parentDirectoryPath;
168
+ };
169
+
170
+ const findPackageDirectorySync = (directory, ignoreTypeOnlyPackageJson) => {
171
+ const filePath = findUpSync('package.json', {cwd: directory});
172
+ if (!filePath) {
173
+ return undefined;
174
+ }
175
+
176
+ const packageDirectoryPath = path.dirname(filePath);
177
+ if (!ignoreTypeOnlyPackageJson) {
178
+ return packageDirectoryPath;
179
+ }
180
+
181
+ if (!isTypeOnlyPackageJsonSync(filePath)) {
182
+ return packageDirectoryPath;
183
+ }
184
+
185
+ const nextDirectory = getNextSearchDirectory(filePath);
186
+ if (!nextDirectory) {
187
+ return undefined;
188
+ }
189
+
190
+ return findPackageDirectorySync(nextDirectory, ignoreTypeOnlyPackageJson);
191
+ };
192
+
193
+ function packageDirectorySync({cwd, ignoreTypeOnlyPackageJson} = {}) {
194
+ return findPackageDirectorySync(cwd ?? process$1.cwd(), ignoreTypeOnlyPackageJson);
195
+ }
196
+
197
+ /**
198
+ * Shared constants for the jeeves-meta service package.
199
+ *
200
+ * @module constants
201
+ */
202
+ /** Default HTTP port for the jeeves-meta service. */
203
+ const DEFAULT_PORT = 1938;
204
+ /** Default port as a string (for Commander CLI defaults). */
205
+ const DEFAULT_PORT_STR = String(DEFAULT_PORT);
206
+ /** Service name identifier. */
207
+ const SERVICE_NAME = 'jeeves-meta';
208
+ /** Service version, read from package.json at startup. */
209
+ const SERVICE_VERSION = (() => {
210
+ try {
211
+ const dir = dirname(fileURLToPath(import.meta.url));
212
+ const root = packageDirectorySync({ cwd: dir });
213
+ if (root) {
214
+ const pkg = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'));
215
+ if (pkg.version)
216
+ return pkg.version;
217
+ }
218
+ return 'unknown';
219
+ }
220
+ catch {
221
+ return 'unknown';
222
+ }
223
+ })();
224
+
105
225
  /**
106
226
  * Zod schema for jeeves-meta service configuration.
107
227
  *
@@ -161,6 +281,8 @@ const serviceConfigSchema = metaConfigSchema.extend({
161
281
  schedule: z.string().default('*/30 * * * *'),
162
282
  /** Optional channel identifier for reporting. */
163
283
  reportChannel: z.string().optional(),
284
+ /** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
285
+ watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
164
286
  /** Logging configuration. */
165
287
  logging: loggingSchema.default(() => loggingSchema.parse({})),
166
288
  });
@@ -274,114 +396,29 @@ function normalizePath(p) {
274
396
  }
275
397
 
276
398
  /**
277
- * Paginated scan helper for exhaustive scope enumeration.
399
+ * Discover .meta/ directories via watcher `/walk` endpoint.
278
400
  *
279
- * @module paginatedScan
280
- */
281
- /**
282
- * Perform a paginated scan that follows cursor tokens until exhausted.
283
- *
284
- * @param watcher - WatcherClient instance.
285
- * @param params - Base scan parameters (cursor is managed internally).
286
- * @returns All matching files across all pages.
287
- */
288
- async function paginatedScan(watcher, params, logger) {
289
- const allFiles = [];
290
- let cursor;
291
- let pageCount = 0;
292
- const start = Date.now();
293
- do {
294
- const pageStart = Date.now();
295
- const result = await watcher.scan({ ...params, cursor });
296
- allFiles.push(...result.files);
297
- pageCount++;
298
- logger?.debug({
299
- page: pageCount,
300
- files: result.files.length,
301
- pageMs: Date.now() - pageStart,
302
- hasNext: Boolean(result.next),
303
- }, 'paginatedScan page');
304
- cursor = result.next;
305
- } while (cursor);
306
- logger?.debug({
307
- pages: pageCount,
308
- totalFiles: allFiles.length,
309
- totalMs: Date.now() - start,
310
- }, 'paginatedScan complete');
311
- return allFiles;
312
- }
313
-
314
- /**
315
- * Discover .meta/ directories via watcher scan.
316
- *
317
- * Replaces filesystem-based globMetas() with a watcher query
318
- * that returns indexed .meta/meta.json points, filtered by domain.
401
+ * Uses filesystem enumeration through the watcher (not Qdrant) to find
402
+ * all `.meta/meta.json` files and returns deduplicated meta directory paths.
319
403
  *
320
404
  * @module discovery/discoverMetas
321
405
  */
322
406
  /**
323
- * Build a single Qdrant filter clause from a key-value pair.
407
+ * Discover all .meta/ directories via watcher walk.
324
408
  *
325
- * Arrays use `match.value` on the first element (Qdrant array membership).
326
- * Scalars (string, number, boolean) use `match.value` directly.
327
- * Objects and other non-filterable types are skipped with a warning.
328
- */
329
- function buildMatchClause(key, value) {
330
- if (Array.isArray(value)) {
331
- if (value.length === 0)
332
- return null;
333
- return { key, match: { value: value[0] } };
334
- }
335
- if (typeof value === 'string' ||
336
- typeof value === 'number' ||
337
- typeof value === 'boolean') {
338
- return { key, match: { value } };
339
- }
340
- // Non-filterable value (object, null, etc.) — valid for tagging but
341
- // cannot be expressed as a Qdrant match clause.
342
- return null;
343
- }
344
- /**
345
- * Build a Qdrant filter from config metaProperty.
346
- *
347
- * Iterates all key-value pairs in `metaProperty` (a generic record)
348
- * to construct `must` clauses. Always appends `file_path: meta.json`
349
- * for deduplication.
409
+ * Uses the watcher's `/walk` endpoint to find all `.meta/meta.json` files
410
+ * and returns deduplicated meta directory paths.
350
411
  *
351
- * @param config - Meta config with metaProperty.
352
- * @returns Qdrant filter object for scanning live metas.
353
- */
354
- function buildMetaFilter(config) {
355
- const must = [];
356
- for (const [key, value] of Object.entries(config.metaProperty)) {
357
- const clause = buildMatchClause(key, value);
358
- if (clause)
359
- must.push(clause);
360
- }
361
- must.push({
362
- key: 'file_path',
363
- match: { text: '.meta/meta.json' },
364
- });
365
- return { must };
366
- }
367
- /**
368
- * Discover all .meta/ directories via watcher scan.
369
- *
370
- * Queries the watcher for indexed .meta/meta.json points using the
371
- * configured domain filter. Returns deduplicated meta directory paths.
372
- *
373
- * @param config - Meta config (for domain filter).
374
- * @param watcher - WatcherClient for scan queries.
412
+ * @param watcher - WatcherClient for walk queries.
375
413
  * @returns Array of normalized .meta/ directory paths.
376
414
  */
377
- async function discoverMetas(config, watcher, logger) {
378
- const filter = buildMetaFilter(config);
379
- const scanFiles = await paginatedScan(watcher, { filter, fields: ['file_path'] }, logger);
415
+ async function discoverMetas(watcher) {
416
+ const allPaths = await watcher.walk(['**/.meta/meta.json']);
380
417
  // Deduplicate by .meta/ directory path (handles multi-chunk files)
381
418
  const seen = new Set();
382
419
  const metaPaths = [];
383
- for (const sf of scanFiles) {
384
- const fp = normalizePath(sf.file_path);
420
+ for (const filePath of allPaths) {
421
+ const fp = normalizePath(filePath);
385
422
  // Derive .meta/ directory from file_path (strip /meta.json)
386
423
  const metaPath = fp.replace(/\/meta\.json$/, '');
387
424
  if (seen.has(metaPath))
@@ -522,6 +559,25 @@ function cleanupStaleLocks(metaPaths, logger) {
522
559
  }
523
560
  }
524
561
 
562
+ /**
563
+ * Read and parse a meta.json file from a `.meta/` directory.
564
+ *
565
+ * Shared utility to eliminate repeated `JSON.parse(readFileSync(...))` across
566
+ * discovery, orchestration, and route handlers.
567
+ *
568
+ * @module readMetaJson
569
+ */
570
+ /**
571
+ * Read and parse a meta.json file from a `.meta/` directory path.
572
+ *
573
+ * @param metaPath - Path to the `.meta/` directory.
574
+ * @returns Parsed meta.json content.
575
+ * @throws If the file doesn't exist or contains invalid JSON.
576
+ */
577
+ function readMetaJson(metaPath) {
578
+ return JSON.parse(readFileSync(join(metaPath, 'meta.json'), 'utf8'));
579
+ }
580
+
525
581
  /**
526
582
  * Build the ownership tree from discovered .meta/ paths.
527
583
  *
@@ -617,9 +673,9 @@ const MAX_STALENESS_SECONDS$1 = 365 * 86_400;
617
673
  * @param watcher - Watcher HTTP client for discovery.
618
674
  * @returns Enriched meta list with summary statistics and ownership tree.
619
675
  */
620
- async function listMetas(config, watcher, logger) {
621
- // Step 1: Discover deduplicated meta paths via watcher scan
622
- const metaPaths = await discoverMetas(config, watcher, logger);
676
+ async function listMetas(config, watcher) {
677
+ // Step 1: Discover deduplicated meta paths via watcher walk
678
+ const metaPaths = await discoverMetas(watcher);
623
679
  // Step 2: Build ownership tree
624
680
  const tree = buildOwnershipTree(metaPaths);
625
681
  // Step 3: Read and enrich each meta from disk
@@ -638,7 +694,7 @@ async function listMetas(config, watcher, logger) {
638
694
  for (const node of tree.nodes.values()) {
639
695
  let meta;
640
696
  try {
641
- meta = JSON.parse(readFileSync(join(node.metaPath, 'meta.json'), 'utf8'));
697
+ meta = readMetaJson(node.metaPath);
642
698
  }
643
699
  catch {
644
700
  // Skip unreadable metas
@@ -726,67 +782,50 @@ async function listMetas(config, watcher, logger) {
726
782
  }
727
783
 
728
784
  /**
729
- * Recursive filesystem walker for file enumeration.
785
+ * Filter file paths by modification time.
730
786
  *
731
- * Replaces paginated watcher scans for scope/delta/staleness checks.
732
- * Returns normalized forward-slash paths.
787
+ * Shared utility for staleness detection and delta file enumeration.
788
+ * Uses `fs.statSync` for fast local mtime checks on known paths.
733
789
  *
734
- * @module walkFiles
790
+ * @module mtimeFilter
735
791
  */
736
- /** Default directory names to always skip. */
737
- const DEFAULT_SKIP = new Set([
738
- 'node_modules',
739
- '.git',
740
- '.rollup.cache',
741
- 'dist',
742
- 'Thumbs.db',
743
- ]);
744
792
  /**
745
- * Recursively walk a directory and return all file paths.
793
+ * Check if any file in the list was modified after the given timestamp.
746
794
  *
747
- * @param root - Root directory to walk.
748
- * @param options - Walk options.
749
- * @returns Array of normalized file paths.
795
+ * Short-circuits on first match for efficiency (staleness checks).
796
+ *
797
+ * @param files - Array of file paths to check.
798
+ * @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` match.
799
+ * @returns True if any file was modified after the timestamp.
750
800
  */
751
- function walkFiles(root, options) {
752
- const exclude = new Set([...DEFAULT_SKIP, ...(options?.exclude ?? [])]);
753
- const modifiedAfter = options?.modifiedAfter;
754
- const maxDepth = options?.maxDepth ?? 50;
755
- const results = [];
756
- function walk(dir, depth) {
757
- if (depth > maxDepth)
758
- return;
759
- let entries;
801
+ function hasModifiedAfter(files, afterMs) {
802
+ for (const filePath of files) {
760
803
  try {
761
- entries = readdirSync(dir, { withFileTypes: true });
804
+ if (statSync(filePath).mtimeMs > afterMs)
805
+ return true;
762
806
  }
763
807
  catch {
764
- return; // Permission errors, missing dirs — skip
765
- }
766
- for (const entry of entries) {
767
- if (exclude.has(entry.name))
768
- continue;
769
- const fullPath = join(dir, entry.name);
770
- if (entry.isDirectory()) {
771
- walk(fullPath, depth + 1);
772
- }
773
- else if (entry.isFile()) {
774
- if (modifiedAfter !== undefined) {
775
- try {
776
- const stat = statSync(fullPath);
777
- if (Math.floor(stat.mtimeMs / 1000) <= modifiedAfter)
778
- continue;
779
- }
780
- catch {
781
- continue;
782
- }
783
- }
784
- results.push(normalizePath(fullPath));
785
- }
808
+ // Unreadable file — skip
786
809
  }
787
810
  }
788
- walk(root, 0);
789
- return results;
811
+ return false;
812
+ }
813
+ /**
814
+ * Filter files to only those modified after the given timestamp.
815
+ *
816
+ * @param files - Array of file paths to filter.
817
+ * @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` are included.
818
+ * @returns Filtered array of file paths.
819
+ */
820
+ function filterModifiedAfter(files, afterMs) {
821
+ return files.filter((filePath) => {
822
+ try {
823
+ return statSync(filePath).mtimeMs > afterMs;
824
+ }
825
+ catch {
826
+ return false;
827
+ }
828
+ });
790
829
  }
791
830
 
792
831
  /**
@@ -796,7 +835,7 @@ function walkFiles(root, options) {
796
835
  * - Its own .meta/ subtree (outputs, not inputs)
797
836
  * - Child meta ownerPath subtrees (except their .meta/meta.json for rollups)
798
837
  *
799
- * Uses filesystem walks instead of watcher scans for performance.
838
+ * All filesystem enumeration delegated to the watcher's `/walk` endpoint.
800
839
  *
801
840
  * @module discovery/scope
802
841
  */
@@ -813,7 +852,7 @@ function getScopePrefix(node) {
813
852
  * - The node's own .meta/ subtree (synthesis outputs are not scope inputs)
814
853
  * - Child meta ownerPath subtrees (except child .meta/meta.json for rollups)
815
854
  *
816
- * walkFiles already returns normalized forward-slash paths.
855
+ * Watcher walk returns normalized forward-slash paths.
817
856
  */
818
857
  function filterInScope(node, files) {
819
858
  const prefix = node.ownerPath + '/';
@@ -838,10 +877,10 @@ function filterInScope(node, files) {
838
877
  });
839
878
  }
840
879
  /**
841
- * Get all files in scope for a meta node via filesystem walk.
880
+ * Get all files in scope for a meta node via watcher walk.
842
881
  */
843
- function getScopeFiles(node) {
844
- const allFiles = walkFiles(node.ownerPath);
882
+ async function getScopeFiles(node, watcher) {
883
+ const allFiles = await watcher.walk([`${node.ownerPath}/**`]);
845
884
  return {
846
885
  scopeFiles: filterInScope(node, allFiles),
847
886
  allFiles,
@@ -851,13 +890,12 @@ function getScopeFiles(node) {
851
890
  * Get files modified since a given timestamp within a meta node's scope.
852
891
  *
853
892
  * If no generatedAt is provided (first run), returns all scope files.
893
+ * Reuses scope files from getScopeFiles() and filters locally by mtime.
854
894
  */
855
- function getDeltaFiles(node, generatedAt, scopeFiles) {
895
+ function getDeltaFiles(generatedAt, scopeFiles) {
856
896
  if (!generatedAt)
857
897
  return scopeFiles;
858
- const modifiedAfter = Math.floor(new Date(generatedAt).getTime() / 1000);
859
- const deltaFiles = walkFiles(node.ownerPath, { modifiedAfter });
860
- return filterInScope(node, deltaFiles);
898
+ return filterModifiedAfter(scopeFiles, new Date(generatedAt).getTime());
861
899
  }
862
900
 
863
901
  /**
@@ -954,7 +992,7 @@ class GatewayExecutor {
954
992
  this.gatewayUrl = (options.gatewayUrl ?? 'http://127.0.0.1:18789').replace(/\/+$/, '');
955
993
  this.apiKey = options.apiKey;
956
994
  this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
957
- this.workspaceDir = options.workspaceDir ?? 'J:\\jeeves\\jeeves-meta';
995
+ this.workspaceDir = options.workspaceDir ?? join(tmpdir(), 'jeeves-meta');
958
996
  }
959
997
  /** Invoke a gateway tool via the /tools/invoke HTTP endpoint. */
960
998
  async invoke(tool, args) {
@@ -1158,10 +1196,10 @@ function condenseScopeFiles(files, maxIndividual = 30) {
1158
1196
  * @param watcher - WatcherClient for scope enumeration.
1159
1197
  * @returns The computed context package.
1160
1198
  */
1161
- function buildContextPackage(node, meta) {
1162
- // Scope and delta files via watcher scan
1163
- const { scopeFiles } = getScopeFiles(node);
1164
- const deltaFiles = getDeltaFiles(node, meta._generatedAt, scopeFiles);
1199
+ async function buildContextPackage(node, meta, watcher) {
1200
+ // Scope and delta files via watcher walk
1201
+ const { scopeFiles } = await getScopeFiles(node, watcher);
1202
+ const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
1165
1203
  // Child meta outputs
1166
1204
  const childMetas = {};
1167
1205
  for (const child of node.children) {
@@ -1485,6 +1523,67 @@ function mergeAndWrite(options) {
1485
1523
  return result.data;
1486
1524
  }
1487
1525
 
1526
+ /**
1527
+ * Build a minimal MetaNode from a known meta path using watcher walk.
1528
+ *
1529
+ * Used for targeted synthesis (when a specific path is requested) to avoid
1530
+ * the full discovery + ownership tree build. Discovers only immediate child
1531
+ * `.meta/` directories.
1532
+ *
1533
+ * @module discovery/buildMinimalNode
1534
+ */
1535
+ /**
1536
+ * Build a minimal MetaNode for a known meta path.
1537
+ *
1538
+ * Walks the owner directory for child `.meta/meta.json` files and constructs
1539
+ * a shallow ownership tree (self + direct children only).
1540
+ *
1541
+ * @param metaPath - Absolute path to the `.meta/` directory.
1542
+ * @param watcher - WatcherClient for filesystem enumeration.
1543
+ * @returns MetaNode with direct children wired.
1544
+ */
1545
+ async function buildMinimalNode(metaPath, watcher) {
1546
+ const normalized = normalizePath(metaPath);
1547
+ const ownerPath = normalizePath(dirname(metaPath));
1548
+ // Find child metas using watcher walk.
1549
+ // We include only *direct* children (nearest descendants in the ownership tree)
1550
+ // to match the ownership semantics used elsewhere.
1551
+ const rawMetaJsonPaths = await watcher.walk([
1552
+ `${ownerPath}/**/.meta/meta.json`,
1553
+ ]);
1554
+ const candidateMetaPaths = [
1555
+ ...new Set(rawMetaJsonPaths.map((p) => normalizePath(dirname(p)))),
1556
+ ].filter((p) => p !== normalized);
1557
+ const candidates = candidateMetaPaths
1558
+ .map((mp) => ({ metaPath: mp, ownerPath: normalizePath(dirname(mp)) }))
1559
+ .sort((a, b) => a.ownerPath.length - b.ownerPath.length);
1560
+ const directChildren = [];
1561
+ for (const c of candidates) {
1562
+ const nestedUnderExisting = directChildren.some((d) => c.ownerPath === d.ownerPath ||
1563
+ c.ownerPath.startsWith(d.ownerPath + '/'));
1564
+ if (!nestedUnderExisting)
1565
+ directChildren.push(c);
1566
+ }
1567
+ const children = directChildren.map((c) => ({
1568
+ metaPath: c.metaPath,
1569
+ ownerPath: c.ownerPath,
1570
+ treeDepth: 1,
1571
+ children: [],
1572
+ parent: null,
1573
+ }));
1574
+ const node = {
1575
+ metaPath: normalized,
1576
+ ownerPath,
1577
+ treeDepth: 0,
1578
+ children,
1579
+ parent: null,
1580
+ };
1581
+ for (const child of children) {
1582
+ child.parent = node;
1583
+ }
1584
+ return node;
1585
+ }
1586
+
1488
1587
  /**
1489
1588
  * Weighted staleness formula for candidate selection.
1490
1589
  *
@@ -1564,29 +1663,30 @@ function discoverStalestPath(candidates, depthWeight) {
1564
1663
  }
1565
1664
 
1566
1665
  /**
1567
- * Staleness detection via watcher scan.
1666
+ * Staleness detection via watcher walk.
1568
1667
  *
1569
- * A meta is stale when any file in its scope was modified after _generatedAt.
1668
+ * A meta is stale when any watched file in its scope was modified after
1669
+ * `_generatedAt`.
1570
1670
  *
1571
1671
  * @module scheduling/staleness
1572
1672
  */
1573
1673
  /**
1574
- * Check if a meta is stale by querying the watcher for modified files.
1674
+ * Check if a meta is stale.
1675
+ *
1676
+ * Uses watcher `/walk` to enumerate watched files under the scope prefix,
1677
+ * then applies a local mtime check (fast) to detect any modifications since
1678
+ * `_generatedAt`. Short-circuits on first match.
1575
1679
  *
1576
1680
  * @param scopePrefix - Path prefix for this meta's scope.
1577
1681
  * @param meta - Current meta.json content.
1578
1682
  * @param watcher - WatcherClient instance.
1579
- * @returns True if any file in scope was modified after _generatedAt.
1683
+ * @returns True if any file in scope was modified after `_generatedAt`.
1580
1684
  */
1581
- function isStale(scopePrefix, meta) {
1685
+ async function isStale(scopePrefix, meta, watcher) {
1582
1686
  if (!meta._generatedAt)
1583
1687
  return true; // Never synthesized = stale
1584
- const generatedAtUnix = Math.floor(new Date(meta._generatedAt).getTime() / 1000);
1585
- const modified = walkFiles(scopePrefix, {
1586
- modifiedAfter: generatedAtUnix,
1587
- maxDepth: 1,
1588
- });
1589
- return modified.length > 0;
1688
+ const files = await watcher.walk([`${scopePrefix}/**`]);
1689
+ return hasModifiedAfter(files, new Date(meta._generatedAt).getTime());
1590
1690
  }
1591
1691
  /** Maximum staleness for never-synthesized metas (1 year in seconds). */
1592
1692
  const MAX_STALENESS_SECONDS = 365 * 86_400;
@@ -1796,72 +1896,13 @@ function finalizeCycle(opts) {
1796
1896
  * @param watcher - Watcher HTTP client.
1797
1897
  * @returns Result indicating whether synthesis occurred.
1798
1898
  */
1799
- /**
1800
- * Build a minimal MetaNode from the filesystem for a known meta path.
1801
- * Discovers immediate child .meta/ dirs without a full watcher scan.
1802
- */
1803
- function buildMinimalNode(metaPath) {
1804
- const normalized = normalizePath(metaPath);
1805
- const ownerPath = normalizePath(dirname(metaPath));
1806
- // Find child .meta/ directories by scanning the owner directory
1807
- const children = [];
1808
- function findChildMetas(dir, depth) {
1809
- if (depth > 10)
1810
- return; // Safety limit
1811
- try {
1812
- const entries = readdirSync(dir, { withFileTypes: true });
1813
- for (const entry of entries) {
1814
- if (!entry.isDirectory())
1815
- continue;
1816
- const fullPath = normalizePath(join(dir, entry.name));
1817
- if (entry.name === '.meta' && fullPath !== normalized) {
1818
- // Found a child .meta — check it has meta.json
1819
- if (existsSync(join(fullPath, 'meta.json'))) {
1820
- children.push({
1821
- metaPath: fullPath,
1822
- ownerPath: normalizePath(dirname(fullPath)),
1823
- treeDepth: 1, // Relative to target
1824
- children: [],
1825
- parent: null, // Set below
1826
- });
1827
- }
1828
- // Don't recurse into .meta dirs
1829
- return;
1830
- }
1831
- if (entry.name === 'node_modules' ||
1832
- entry.name === '.git' ||
1833
- entry.name === 'archive')
1834
- continue;
1835
- findChildMetas(fullPath, depth + 1);
1836
- }
1837
- }
1838
- catch {
1839
- // Permission errors, etc — skip
1840
- }
1841
- }
1842
- findChildMetas(ownerPath, 0);
1843
- const node = {
1844
- metaPath: normalized,
1845
- ownerPath,
1846
- treeDepth: 0,
1847
- children,
1848
- parent: null,
1849
- };
1850
- // Wire parent references
1851
- for (const child of children) {
1852
- child.parent = node;
1853
- }
1854
- return node;
1855
- }
1856
1899
  /** Run the architect/builder/critic pipeline on a single node. */
1857
1900
  async function synthesizeNode(node, currentMeta, config, executor, watcher, onProgress) {
1858
- const architectPrompt = currentMeta._architect ?? config.defaultArchitect;
1859
- const criticPrompt = currentMeta._critic ?? config.defaultCritic;
1860
1901
  // Step 5-6: Steer change detection
1861
1902
  const latestArchive = readLatestArchive(node.metaPath);
1862
1903
  const steerChanged = hasSteerChanged(currentMeta._steer, latestArchive?._steer, Boolean(latestArchive));
1863
1904
  // Step 7: Compute context (includes scope files and delta files)
1864
- const ctx = buildContextPackage(node, currentMeta);
1905
+ const ctx = await buildContextPackage(node, currentMeta, watcher);
1865
1906
  // Step 5 (deferred): Structure hash from context scope files
1866
1907
  const newStructureHash = computeStructureHash(ctx.scopeFiles);
1867
1908
  const structureChanged = newStructureHash !== currentMeta._structureHash;
@@ -1905,9 +1946,9 @@ async function synthesizeNode(node, currentMeta, config, executor, watcher, onPr
1905
1946
  metaPath: node.metaPath,
1906
1947
  current: currentMeta,
1907
1948
  config,
1908
- architect: architectPrompt,
1949
+ architect: currentMeta._architect ?? '',
1909
1950
  builder: '',
1910
- critic: criticPrompt,
1951
+ critic: currentMeta._critic ?? '',
1911
1952
  builderOutput: null,
1912
1953
  feedback: null,
1913
1954
  structureHash: newStructureHash,
@@ -1991,9 +2032,9 @@ async function synthesizeNode(node, currentMeta, config, executor, watcher, onPr
1991
2032
  metaPath: node.metaPath,
1992
2033
  current: currentMeta,
1993
2034
  config,
1994
- architect: architectPrompt,
2035
+ architect: currentMeta._architect ?? '',
1995
2036
  builder: builderBrief,
1996
- critic: criticPrompt,
2037
+ critic: currentMeta._critic ?? '',
1997
2038
  builderOutput,
1998
2039
  feedback,
1999
2040
  structureHash: newStructureHash,
@@ -2017,11 +2058,11 @@ async function orchestrateOnce(config, executor, watcher, targetPath, onProgress
2017
2058
  const targetMetaJson = join(normalizedTarget, 'meta.json');
2018
2059
  if (!existsSync(targetMetaJson))
2019
2060
  return { synthesized: false };
2020
- const node = buildMinimalNode(normalizedTarget);
2061
+ const node = await buildMinimalNode(normalizedTarget, watcher);
2021
2062
  if (!acquireLock(node.metaPath))
2022
2063
  return { synthesized: false };
2023
2064
  try {
2024
- const currentMeta = JSON.parse(readFileSync(targetMetaJson, 'utf8'));
2065
+ const currentMeta = readMetaJson(normalizedTarget);
2025
2066
  return await synthesizeNode(node, currentMeta, config, executor, watcher, onProgress);
2026
2067
  }
2027
2068
  finally {
@@ -2029,18 +2070,17 @@ async function orchestrateOnce(config, executor, watcher, targetPath, onProgress
2029
2070
  }
2030
2071
  }
2031
2072
  // Full discovery path (scheduler-driven, no specific target)
2032
- // Step 1: Discover via watcher scan
2073
+ // Step 1: Discover via watcher walk
2033
2074
  const discoveryStart = Date.now();
2034
- const metaPaths = await discoverMetas(config, watcher, logger);
2075
+ const metaPaths = await discoverMetas(watcher);
2035
2076
  logger?.debug({ paths: metaPaths.length, durationMs: Date.now() - discoveryStart }, 'discovery complete');
2036
2077
  if (metaPaths.length === 0)
2037
2078
  return { synthesized: false };
2038
2079
  // Read meta.json for each discovered meta
2039
2080
  const metas = new Map();
2040
2081
  for (const mp of metaPaths) {
2041
- const metaFilePath = join(mp, 'meta.json');
2042
2082
  try {
2043
- metas.set(normalizePath(mp), JSON.parse(readFileSync(metaFilePath, 'utf8')));
2083
+ metas.set(normalizePath(mp), readMetaJson(mp));
2044
2084
  }
2045
2085
  catch {
2046
2086
  // Skip metas with unreadable meta.json
@@ -2073,13 +2113,12 @@ async function orchestrateOnce(config, executor, watcher, targetPath, onProgress
2073
2113
  for (const candidate of ranked) {
2074
2114
  if (!acquireLock(candidate.node.metaPath))
2075
2115
  continue;
2076
- const verifiedStale = isStale(getScopePrefix(candidate.node), candidate.meta);
2116
+ const verifiedStale = await isStale(getScopePrefix(candidate.node), candidate.meta, watcher);
2077
2117
  if (!verifiedStale && candidate.meta._generatedAt) {
2078
2118
  // Bump _generatedAt so it doesn't win next cycle
2079
- const metaFilePath = join(candidate.node.metaPath, 'meta.json');
2080
- const freshMeta = JSON.parse(readFileSync(metaFilePath, 'utf8'));
2119
+ const freshMeta = readMetaJson(candidate.node.metaPath);
2081
2120
  freshMeta._generatedAt = new Date().toISOString();
2082
- writeFileSync(metaFilePath, JSON.stringify(freshMeta, null, 2));
2121
+ writeFileSync(join(candidate.node.metaPath, 'meta.json'), JSON.stringify(freshMeta, null, 2));
2083
2122
  releaseLock(candidate.node.metaPath);
2084
2123
  if (config.skipUnchanged)
2085
2124
  continue;
@@ -2092,7 +2131,7 @@ async function orchestrateOnce(config, executor, watcher, targetPath, onProgress
2092
2131
  return { synthesized: false };
2093
2132
  const node = winner.node;
2094
2133
  try {
2095
- const currentMeta = JSON.parse(readFileSync(join(node.metaPath, 'meta.json'), 'utf8'));
2134
+ const currentMeta = readMetaJson(node.metaPath);
2096
2135
  return await synthesizeNode(node, currentMeta, config, executor, watcher, onProgress);
2097
2136
  }
2098
2137
  finally {
@@ -2336,7 +2375,7 @@ class Scheduler {
2336
2375
  */
2337
2376
  async discoverStalest() {
2338
2377
  try {
2339
- const result = await listMetas(this.config, this.watcher, this.logger);
2378
+ const result = await listMetas(this.config, this.watcher);
2340
2379
  const stale = result.entries
2341
2380
  .filter((e) => e.stalenessSeconds > 0)
2342
2381
  .map((e) => ({
@@ -2622,7 +2661,7 @@ function registerMetasRoutes(app, deps) {
2622
2661
  app.get('/metas', async (request) => {
2623
2662
  const query = metasQuerySchema.parse(request.query);
2624
2663
  const { config, watcher } = deps;
2625
- const result = await listMetas(config, watcher, request.log);
2664
+ const result = await listMetas(config, watcher);
2626
2665
  let entries = result.entries;
2627
2666
  // Apply filters
2628
2667
  if (query.pathPrefix) {
@@ -2685,7 +2724,7 @@ function registerMetasRoutes(app, deps) {
2685
2724
  const query = metaDetailQuerySchema.parse(request.query);
2686
2725
  const { config, watcher } = deps;
2687
2726
  const targetPath = normalizePath(decodeURIComponent(request.params.path));
2688
- const result = await listMetas(config, watcher, request.log);
2727
+ const result = await listMetas(config, watcher);
2689
2728
  const targetNode = findNode(result.tree, targetPath);
2690
2729
  if (!targetNode) {
2691
2730
  return reply.status(404).send({
@@ -2718,7 +2757,7 @@ function registerMetasRoutes(app, deps) {
2718
2757
  return r;
2719
2758
  };
2720
2759
  // Compute scope
2721
- const { scopeFiles, allFiles } = getScopeFiles(targetNode);
2760
+ const { scopeFiles, allFiles } = await getScopeFiles(targetNode, watcher);
2722
2761
  // Compute staleness
2723
2762
  const metaTyped = meta;
2724
2763
  const staleSeconds = metaTyped._generatedAt
@@ -2765,7 +2804,7 @@ function registerPreviewRoute(app, deps) {
2765
2804
  const query = request.query;
2766
2805
  let result;
2767
2806
  try {
2768
- result = await listMetas(config, watcher, request.log);
2807
+ result = await listMetas(config, watcher);
2769
2808
  }
2770
2809
  catch {
2771
2810
  return reply.status(503).send({
@@ -2799,16 +2838,16 @@ function registerPreviewRoute(app, deps) {
2799
2838
  }
2800
2839
  targetNode = findNode(result.tree, stalestPath);
2801
2840
  }
2802
- const meta = JSON.parse(readFileSync(join(targetNode.metaPath, 'meta.json'), 'utf8'));
2841
+ const meta = readMetaJson(targetNode.metaPath);
2803
2842
  // Scope files
2804
- const { scopeFiles } = getScopeFiles(targetNode);
2843
+ const { scopeFiles } = await getScopeFiles(targetNode, watcher);
2805
2844
  const structureHash = computeStructureHash(scopeFiles);
2806
2845
  const structureChanged = structureHash !== meta._structureHash;
2807
2846
  const latestArchive = readLatestArchive(targetNode.metaPath);
2808
2847
  const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
2809
2848
  const architectTriggered = isArchitectTriggered(meta, structureChanged, steerChanged, config.architectEvery);
2810
2849
  // Delta files
2811
- const deltaFiles = getDeltaFiles(targetNode, meta._generatedAt, scopeFiles);
2850
+ const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
2812
2851
  // EMA token estimates
2813
2852
  const estimatedTokens = {
2814
2853
  architect: meta._architectTokensAvg ?? meta._architectTokens ?? 0,
@@ -2923,11 +2962,11 @@ function registerStatusRoute(app, deps) {
2923
2962
  else {
2924
2963
  status = 'idle';
2925
2964
  }
2926
- // Metas summary is expensive (paginated watcher scan + disk reads).
2965
+ // Metas summary is expensive (watcher walk + disk reads).
2927
2966
  // Use GET /metas for full inventory; status is a lightweight health check.
2928
2967
  return {
2929
- service: 'jeeves-meta',
2930
- version: '0.4.0',
2968
+ service: SERVICE_NAME,
2969
+ version: SERVICE_VERSION,
2931
2970
  uptime: process.uptime(),
2932
2971
  status,
2933
2972
  currentTarget: queue.current?.path ?? null,
@@ -2944,7 +2983,10 @@ function registerStatusRoute(app, deps) {
2944
2983
  nextAt: scheduler?.nextRunAt?.toISOString() ?? null,
2945
2984
  },
2946
2985
  dependencies: {
2947
- watcher: watcherHealth,
2986
+ watcher: {
2987
+ ...watcherHealth,
2988
+ rulesRegistered: deps.registrar?.isRegistered ?? false,
2989
+ },
2948
2990
  gateway: gatewayHealth,
2949
2991
  },
2950
2992
  };
@@ -2972,7 +3014,7 @@ function registerSynthesizeRoute(app, deps) {
2972
3014
  // Discover stalest candidate
2973
3015
  let result;
2974
3016
  try {
2975
- result = await listMetas(config, watcher, request.log);
3017
+ result = await listMetas(config, watcher);
2976
3018
  }
2977
3019
  catch {
2978
3020
  return reply.status(503).send({
@@ -3149,7 +3191,15 @@ function buildMetaRules(config) {
3149
3191
  },
3150
3192
  ],
3151
3193
  render: {
3152
- frontmatter: ['meta_id', 'generated_at', '*', '!has_error'],
3194
+ frontmatter: [
3195
+ 'meta_id',
3196
+ 'generated_at',
3197
+ '*',
3198
+ '!_*',
3199
+ '!json',
3200
+ '!file',
3201
+ '!has_error',
3202
+ ],
3153
3203
  body: [{ path: 'json._content', heading: 1, label: 'Synthesis' }],
3154
3204
  },
3155
3205
  renderAs: 'md',
@@ -3341,6 +3391,8 @@ function registerShutdownHandlers(deps) {
3341
3391
  if (deps.routeDeps) {
3342
3392
  deps.routeDeps.shuttingDown = true;
3343
3393
  }
3394
+ // 0. Run optional cleanup
3395
+ deps.onShutdown?.();
3344
3396
  // 1. Stop scheduler
3345
3397
  if (deps.scheduler) {
3346
3398
  deps.scheduler.stop();
@@ -3374,7 +3426,7 @@ function registerShutdownHandlers(deps) {
3374
3426
  /**
3375
3427
  * HTTP implementation of the WatcherClient interface.
3376
3428
  *
3377
- * Talks to jeeves-watcher's POST /scan and POST /rules endpoints
3429
+ * Talks to jeeves-watcher's POST /walk and POST /rules/register endpoints
3378
3430
  * with retry and exponential backoff.
3379
3431
  *
3380
3432
  * @module watcher-client/HttpWatcherClient
@@ -3428,61 +3480,80 @@ class HttpWatcherClient {
3428
3480
  // Unreachable, but TypeScript needs it
3429
3481
  throw new Error('Retry exhausted');
3430
3482
  }
3431
- async scan(params) {
3432
- // Build Qdrant filter: merge explicit filter with pathPrefix/modifiedAfter
3433
- const mustClauses = [];
3434
- // Carry over any existing 'must' clauses from the provided filter
3435
- if (params.filter) {
3436
- const existing = params.filter.must;
3437
- if (Array.isArray(existing)) {
3438
- mustClauses.push(...existing);
3439
- }
3440
- }
3441
- // Translate pathPrefix into a Qdrant text match on file_path
3442
- if (params.pathPrefix !== undefined) {
3443
- mustClauses.push({
3444
- key: 'file_path',
3445
- match: { text: params.pathPrefix },
3446
- });
3483
+ async registerRules(source, rules) {
3484
+ await this.post('/rules/register', { source, rules });
3485
+ }
3486
+ async walk(globs) {
3487
+ const raw = (await this.post('/walk', { globs }));
3488
+ return (raw.paths ?? []);
3489
+ }
3490
+ }
3491
+
3492
+ /**
3493
+ * Periodic watcher health check for rule registration resilience.
3494
+ *
3495
+ * Pings watcher `/status` on a configurable interval, detects restarts
3496
+ * (uptime decrease), and re-registers virtual rules automatically.
3497
+ * Independent of the synthesis scheduler.
3498
+ *
3499
+ * @module rules/healthCheck
3500
+ */
3501
+ /**
3502
+ * Manages the periodic watcher health check loop.
3503
+ *
3504
+ * Starts a `setInterval` that pings the watcher and delegates
3505
+ * restart detection to `RuleRegistrar.checkAndReregister()`.
3506
+ */
3507
+ class WatcherHealthCheck {
3508
+ watcherUrl;
3509
+ intervalMs;
3510
+ registrar;
3511
+ logger;
3512
+ handle = null;
3513
+ constructor(opts) {
3514
+ this.watcherUrl = opts.watcherUrl.replace(/\/+$/, '');
3515
+ this.intervalMs = opts.intervalMs;
3516
+ this.registrar = opts.registrar;
3517
+ this.logger = opts.logger;
3518
+ }
3519
+ /** Start the periodic health check. No-op if intervalMs is 0. */
3520
+ start() {
3521
+ if (this.intervalMs <= 0) {
3522
+ this.logger.info('Watcher health check disabled (interval = 0)');
3523
+ return;
3447
3524
  }
3448
- // Translate modifiedAfter into a Qdrant range filter on modified_at
3449
- if (params.modifiedAfter !== undefined) {
3450
- mustClauses.push({
3451
- key: 'modified_at',
3452
- range: { gt: params.modifiedAfter },
3453
- });
3525
+ this.handle = setInterval(() => {
3526
+ void this.check();
3527
+ }, this.intervalMs);
3528
+ // Don't prevent process exit
3529
+ if (typeof this.handle === 'object' && 'unref' in this.handle) {
3530
+ this.handle.unref();
3454
3531
  }
3455
- const filter = { must: mustClauses };
3456
- const body = { filter };
3457
- if (params.fields !== undefined) {
3458
- body.fields = params.fields;
3532
+ this.logger.info({ intervalMs: this.intervalMs }, 'Watcher health check started');
3533
+ }
3534
+ /** Stop the periodic health check. */
3535
+ stop() {
3536
+ if (this.handle) {
3537
+ clearInterval(this.handle);
3538
+ this.handle = null;
3459
3539
  }
3460
- if (params.limit !== undefined) {
3461
- body.limit = params.limit;
3540
+ }
3541
+ /** Single health check iteration. */
3542
+ async check() {
3543
+ try {
3544
+ const res = await fetch(this.watcherUrl + '/status', {
3545
+ signal: AbortSignal.timeout(5000),
3546
+ });
3547
+ if (!res.ok) {
3548
+ this.logger.warn({ status: res.status }, 'Watcher health check: non-OK response');
3549
+ return;
3550
+ }
3551
+ const data = (await res.json());
3552
+ await this.registrar.checkAndReregister(data.uptime);
3462
3553
  }
3463
- if (params.cursor !== undefined) {
3464
- body.cursor = params.cursor;
3554
+ catch (err) {
3555
+ this.logger.debug({ err }, 'Watcher health check: unreachable (expected during startup)');
3465
3556
  }
3466
- const raw = (await this.post('/scan', body));
3467
- // jeeves-watcher returns { points, cursor }; map to ScanResponse.
3468
- const points = (raw.points ?? raw.files ?? []);
3469
- const next = (raw.cursor ?? raw.next);
3470
- const files = points.map((p) => {
3471
- const payload = (p.payload ?? p);
3472
- return {
3473
- file_path: (payload.file_path ?? payload.path ?? ''),
3474
- modified_at: (payload.modified_at ?? payload.mtime ?? 0),
3475
- content_hash: (payload.content_hash ?? ''),
3476
- ...payload,
3477
- };
3478
- });
3479
- return { files, next: next ?? undefined };
3480
- }
3481
- async registerRules(source, rules) {
3482
- await this.post('/rules/register', { source, rules });
3483
- }
3484
- async unregisterRules(source) {
3485
- await this.post('/rules/unregister', { source });
3486
3557
  }
3487
3558
  }
3488
3559
 
@@ -3622,7 +3693,16 @@ async function startService(config, configPath) {
3622
3693
  // Rule registration (fire-and-forget with retries)
3623
3694
  const registrar = new RuleRegistrar(config, logger, watcher);
3624
3695
  scheduler.setRegistrar(registrar);
3696
+ routeDeps.registrar = registrar;
3625
3697
  void registrar.register();
3698
+ // Periodic watcher health check (independent of scheduler)
3699
+ const healthCheck = new WatcherHealthCheck({
3700
+ watcherUrl: config.watcherUrl,
3701
+ intervalMs: config.watcherHealthIntervalMs,
3702
+ registrar,
3703
+ logger,
3704
+ });
3705
+ healthCheck.start();
3626
3706
  // Config hot-reload (gap #12)
3627
3707
  if (configPath) {
3628
3708
  watchFile(configPath, { interval: 5000 }, () => {
@@ -3656,8 +3736,11 @@ async function startService(config, configPath) {
3656
3736
  queue,
3657
3737
  logger,
3658
3738
  routeDeps,
3739
+ onShutdown: () => {
3740
+ healthCheck.stop();
3741
+ },
3659
3742
  });
3660
3743
  logger.info('Service fully initialized');
3661
3744
  }
3662
3745
 
3663
- export { GatewayExecutor, HttpWatcherClient, ProgressReporter, RuleRegistrar, Scheduler, SynthesisQueue, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildMetaFilter, buildOwnershipTree, cleanupStaleLocks, computeEffectiveStaleness, computeEma, computeStructureHash, createLogger, createServer, createSnapshot, discoverMetas, filterInScope, findNode, formatProgressEvent, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, listMetas, loadServiceConfig, mergeAndWrite, metaConfigSchema, metaErrorSchema, metaJsonSchema, normalizePath, orchestrate, paginatedScan, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerRoutes, registerShutdownHandlers, releaseLock, resolveConfigPath, resolveMetaDir, selectCandidate, serviceConfigSchema, sleep, startService, toMetaError, walkFiles };
3746
+ export { DEFAULT_PORT, DEFAULT_PORT_STR, GatewayExecutor, HttpWatcherClient, ProgressReporter, RuleRegistrar, SERVICE_NAME, SERVICE_VERSION, Scheduler, SynthesisQueue, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildOwnershipTree, cleanupStaleLocks, computeEffectiveStaleness, computeEma, computeStructureHash, createLogger, createServer, createSnapshot, discoverMetas, filterInScope, findNode, formatProgressEvent, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, listMetas, loadServiceConfig, mergeAndWrite, metaConfigSchema, metaErrorSchema, metaJsonSchema, normalizePath, orchestrate, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerRoutes, registerShutdownHandlers, releaseLock, resolveConfigPath, resolveMetaDir, selectCandidate, serviceConfigSchema, sleep, startService, toMetaError };