@karmaniverous/jeeves-meta 0.12.4 → 0.13.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,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { Command } from 'commander';
3
3
  import { JeevesComponentDescriptor } from '@karmaniverous/jeeves';
4
+ export { sleepAsync as sleep } from '@karmaniverous/jeeves';
4
5
  import pino, { Logger } from 'pino';
5
6
  import * as fastify from 'fastify';
6
7
  import { FastifyInstance, FastifyBaseLogger } from 'fastify';
@@ -236,7 +237,6 @@ declare const serviceConfigSchema: z.ZodObject<{
236
237
  metaProperty: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
237
238
  metaArchiveProperty: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
238
239
  port: z.ZodDefault<z.ZodNumber>;
239
- host: z.ZodDefault<z.ZodString>;
240
240
  schedule: z.ZodDefault<z.ZodString>;
241
241
  reportChannel: z.ZodOptional<z.ZodString>;
242
242
  serverBaseUrl: z.ZodOptional<z.ZodString>;
@@ -636,7 +636,7 @@ declare class Scheduler {
636
636
  * Shared between the descriptor's `onConfigApply` and the file-watcher
637
637
  * hot-reload in `bootstrap.ts`.
638
638
  */
639
- declare const RESTART_REQUIRED_FIELDS: readonly ["port", "host", "watcherUrl", "gatewayUrl", "gatewayApiKey", "defaultArchitect", "defaultCritic"];
639
+ declare const RESTART_REQUIRED_FIELDS: readonly ["port", "watcherUrl", "gatewayUrl", "gatewayApiKey", "defaultArchitect", "defaultCritic"];
640
640
 
641
641
  /**
642
642
  * Load and resolve jeeves-meta service config.
@@ -1368,6 +1368,8 @@ declare function selectCandidate(candidates: StalenessCandidate[]): StalenessCan
1368
1368
  * @returns True if any file in scope was modified after `_generatedAt`.
1369
1369
  */
1370
1370
  declare function isStale(scopePrefix: string, meta: MetaJson, watcher: WatcherClient): Promise<boolean>;
1371
+ /** Maximum staleness for never-synthesized metas (1 year in seconds). */
1372
+ declare const MAX_STALENESS_SECONDS: number;
1371
1373
  /**
1372
1374
  * Compute actual staleness in seconds (now minus _generatedAt).
1373
1375
  *
@@ -1453,9 +1455,6 @@ declare function registerRoutes(app: FastifyInstance, deps: RouteDeps): void;
1453
1455
  */
1454
1456
  declare function verifyRuleApplication(watcher: WatcherClient, logger: MinimalLogger): Promise<number>;
1455
1457
 
1456
- /** Sleep for a given number of milliseconds. */
1457
- declare function sleep(ms: number): Promise<void>;
1458
-
1459
1458
  /** Options for creating the server. */
1460
1459
  interface ServerOptions {
1461
1460
  /** Pino logger instance. */
@@ -1519,5 +1518,5 @@ declare function registerShutdownHandlers(deps: ShutdownDeps): void;
1519
1518
  */
1520
1519
  declare function startService(config: ServiceConfig, configPath?: string): Promise<void>;
1521
1520
 
1522
- export { DEFAULT_PORT, DEFAULT_PORT_STR, GatewayExecutor, HttpWatcherClient, ProgressReporter, RESTART_REQUIRED_FIELDS, 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, metaDescriptor, metaErrorSchema, metaJsonSchema, migrateConfigPath, normalizePath, orchestrate, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerCustomCliCommands, registerRoutes, registerShutdownHandlers, releaseLock, resolveConfigPath, resolveMetaDir, selectCandidate, serviceConfigSchema, sleep, startService, toMetaError, verifyRuleApplication };
1521
+ export { DEFAULT_PORT, DEFAULT_PORT_STR, GatewayExecutor, HttpWatcherClient, MAX_STALENESS_SECONDS, ProgressReporter, RESTART_REQUIRED_FIELDS, 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, metaDescriptor, metaErrorSchema, metaJsonSchema, migrateConfigPath, normalizePath, orchestrate, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerCustomCliCommands, registerRoutes, registerShutdownHandlers, releaseLock, resolveConfigPath, resolveMetaDir, selectCandidate, serviceConfigSchema, startService, toMetaError, verifyRuleApplication };
1523
1522
  export type { BuilderOutput, EnqueueResult, GatewayExecutorOptions, HttpWatcherClientOptions, InferenceRuleSpec, LockState, LoggerConfig, MergeOptions, MetaConfig, MetaContext, MetaEntry, MetaError, MetaExecutor, MetaJson, MetaListResult, MetaListSummary, MetaNode, MetaSpawnOptions, MetaSpawnResult, MinimalLogger, OrchestrateResult, OwnershipTree, ProgressCallback, ProgressEvent, ProgressPhase, ProgressReporterConfig, QueueItem, QueueState, RouteDeps, ServerOptions, ServiceConfig, ServiceStats, StalenessCandidate, WatcherClient, WatcherScanPoint, WatcherScanRequest, WatcherScanResult };
package/dist/index.js CHANGED
@@ -11,9 +11,9 @@ import require$$4 from 'util';
11
11
  import require$$5 from 'assert';
12
12
  import require$$2 from 'events';
13
13
  import vm from 'vm';
14
+ import { z } from 'zod';
14
15
  import * as commander from 'commander';
15
16
  import { tmpdir } from 'node:os';
16
- import { z } from 'zod';
17
17
  import 'node:child_process';
18
18
  import { randomUUID, createHash } from 'node:crypto';
19
19
  import pino from 'pino';
@@ -7243,6 +7243,32 @@ requireSemver();
7243
7243
 
7244
7244
  /** Core config file name. */
7245
7245
  const CONFIG_FILE = 'config.json';
7246
+
7247
+ /**
7248
+ * Default port assignments for Jeeves platform services.
7249
+ *
7250
+ * @remarks
7251
+ * Each port number is a historical reference:
7252
+ * - 1934: Wodehouse's *Thank You, Jeeves*; Popper's *Logic of Scientific Discovery*
7253
+ * - 1936: Turing's "On Computable Numbers"; Church's lambda calculus
7254
+ * - 1937: Turing's paper in *Proceedings of the London Mathematical Society*
7255
+ * - 1938: Wodehouse's *The Code of the Woosters*; Shannon's relay/switching paper
7256
+ */
7257
+ /** Default port for jeeves-server. */
7258
+ const SERVER_PORT = 1934;
7259
+ /** Default port for jeeves-watcher. */
7260
+ const WATCHER_PORT = 1936;
7261
+ /** Default port for jeeves-runner. */
7262
+ const RUNNER_PORT = 1937;
7263
+ /** Default port for jeeves-meta. */
7264
+ const META_PORT = 1938;
7265
+ /** Map of service names to their default ports. */
7266
+ const DEFAULT_PORTS = {
7267
+ server: SERVER_PORT,
7268
+ watcher: WATCHER_PORT,
7269
+ runner: RUNNER_PORT,
7270
+ meta: META_PORT,
7271
+ };
7246
7272
  /**
7247
7273
  * Get the core config directory path.
7248
7274
  *
@@ -7504,6 +7530,47 @@ function createStatusHandler(options) {
7504
7530
  };
7505
7531
  };
7506
7532
  }
7533
+ /** Core shared config section. */
7534
+ const workspaceCoreConfigSchema = z
7535
+ .object({
7536
+ /** Workspace root path. */
7537
+ workspace: z.string().optional().describe('Workspace root path'),
7538
+ /** Platform config root path. */
7539
+ configRoot: z.string().optional().describe('Platform config root path'),
7540
+ /** OpenClaw gateway URL. */
7541
+ gatewayUrl: z.string().optional().describe('OpenClaw gateway URL'),
7542
+ })
7543
+ .partial();
7544
+ /** Memory shared config section. */
7545
+ const workspaceMemoryConfigSchema = z
7546
+ .object({
7547
+ /** MEMORY.md character budget. */
7548
+ budget: z.number().int().positive().optional().describe('Memory budget'),
7549
+ /** Warning threshold as a fraction of budget. */
7550
+ warningThreshold: z
7551
+ .number()
7552
+ .min(0)
7553
+ .max(1)
7554
+ .optional()
7555
+ .describe('Memory warning threshold'),
7556
+ /** Staleness threshold in days. */
7557
+ staleDays: z
7558
+ .number()
7559
+ .int()
7560
+ .positive()
7561
+ .optional()
7562
+ .describe('Memory staleness threshold in days'),
7563
+ })
7564
+ .partial();
7565
+ /** Workspace config Zod schema. */
7566
+ z.object({
7567
+ /** JSON Schema pointer for IDE autocomplete. */
7568
+ $schema: z.string().optional().describe('JSON Schema pointer'),
7569
+ /** Core shared defaults. */
7570
+ core: workspaceCoreConfigSchema.optional(),
7571
+ /** Memory hygiene shared defaults. */
7572
+ memory: workspaceMemoryConfigSchema.optional(),
7573
+ });
7507
7574
 
7508
7575
  function getDefaultExportFromCjs (x) {
7509
7576
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
@@ -7821,6 +7888,38 @@ function loadConfig(configDir) {
7821
7888
  }
7822
7889
  }
7823
7890
 
7891
+ /**
7892
+ * Service URL resolution.
7893
+ *
7894
+ * @remarks
7895
+ * Resolves the URL for a named Jeeves service using the following
7896
+ * resolution order:
7897
+ * 1. Consumer's own component config
7898
+ * 2. Core config (`{configRoot}/jeeves-core/config.json`)
7899
+ * 3. Default port constants
7900
+ */
7901
+ /**
7902
+ * Resolve the URL for a named Jeeves service.
7903
+ *
7904
+ * @param serviceName - The service name (e.g., 'watcher', 'runner').
7905
+ * @param consumerName - Optional consumer component name for config override.
7906
+ * @returns The resolved service URL.
7907
+ * @throws Error if `init()` has not been called or the service is unknown.
7908
+ */
7909
+ function getServiceUrl(serviceName, consumerName) {
7910
+ // 2. Check core config
7911
+ const coreDir = getCoreConfigDir();
7912
+ const coreConfig = loadConfig(coreDir);
7913
+ const coreUrl = coreConfig?.services[serviceName]?.url;
7914
+ if (coreUrl)
7915
+ return coreUrl;
7916
+ // 3. Fall back to port constants
7917
+ const port = DEFAULT_PORTS[serviceName];
7918
+ {
7919
+ return `http://127.0.0.1:${String(port)}`;
7920
+ }
7921
+ }
7922
+
7824
7923
  /**
7825
7924
  * Resolve the bind address for a Jeeves service.
7826
7925
  *
@@ -7858,6 +7957,10 @@ function getBindAddress(componentName) {
7858
7957
  // Tier 4: Default
7859
7958
  return DEFAULT_BIND_ADDRESS;
7860
7959
  }
7960
+ /** Async sleep via setTimeout. */
7961
+ function sleepAsync(ms) {
7962
+ return new Promise((r) => setTimeout(r, ms));
7963
+ }
7861
7964
 
7862
7965
  /**
7863
7966
  * Custom CLI commands for the jeeves-meta service.
@@ -7869,7 +7972,11 @@ function getBindAddress(componentName) {
7869
7972
  */
7870
7973
  /** Build the full API URL for a given port string and path. */
7871
7974
  function apiUrl(port, apiPath) {
7872
- return `http://127.0.0.1:${port}${apiPath}`;
7975
+ const url = new URL(apiPath, getServiceUrl('meta'));
7976
+ if (port !== DEFAULT_PORT_STR) {
7977
+ url.port = port;
7978
+ }
7979
+ return url.toString();
7873
7980
  }
7874
7981
  /** Wrap an async CLI action with consistent error handling. */
7875
7982
  function withErrorHandling(fn, label) {
@@ -8024,7 +8131,6 @@ function registerCustomCliCommands(program) {
8024
8131
  */
8025
8132
  const RESTART_REQUIRED_FIELDS = [
8026
8133
  'port',
8027
- 'host',
8028
8134
  'watcherUrl',
8029
8135
  'gatewayUrl',
8030
8136
  'gatewayApiKey',
@@ -8136,8 +8242,6 @@ const autoSeedRuleSchema = z.object({
8136
8242
  const serviceConfigSchema = metaConfigSchema.extend({
8137
8243
  /** HTTP port for the service (default: 1938). */
8138
8244
  port: z.number().int().min(1).max(65535).default(1938),
8139
- /** Bind address for the HTTP server (default: 127.0.0.1). */
8140
- host: z.string().default('127.0.0.1'),
8141
8245
  /** Cron schedule for synthesis cycles (default: every 30 min). */
8142
8246
  schedule: z.string().default('*/30 * * * *'),
8143
8247
  /** Optional channel identifier for reporting. */
@@ -8523,6 +8627,174 @@ async function readMetaJson(metaPath) {
8523
8627
  return JSON.parse(raw);
8524
8628
  }
8525
8629
 
8630
+ /**
8631
+ * Escape special glob characters in a path so it can be used as a literal
8632
+ * prefix in glob patterns.
8633
+ *
8634
+ * Glob metacharacters `* ? [ ] { } ( ) !` are escaped with a backslash so
8635
+ * that paths containing parentheses (e.g. Slack channel IDs) or other
8636
+ * special characters are matched literally by the watcher's walk endpoint.
8637
+ *
8638
+ * @module escapeGlob
8639
+ */
8640
+ /**
8641
+ * Escape glob metacharacters in a string using character-class wrapping.
8642
+ *
8643
+ * Backslash escaping (`\(`) does not work reliably on Windows where `\` is
8644
+ * the path separator. Instead, each metacharacter is wrapped in a character
8645
+ * class (e.g. `(` → `[(]`) which is universally supported by glob libraries.
8646
+ *
8647
+ * Square brackets themselves are escaped as `[[]` and `[]]`.
8648
+ *
8649
+ * @param s - Raw path string.
8650
+ * @returns String with glob metacharacters wrapped in character classes.
8651
+ */
8652
+ function escapeGlob(s) {
8653
+ return s.replace(/[*?[\]{}()!]/g, (ch) => `[${ch}]`);
8654
+ }
8655
+
8656
+ /**
8657
+ * Filter file paths by modification time.
8658
+ *
8659
+ * Shared utility for staleness detection and delta file enumeration.
8660
+ * Uses `fs.statSync` for fast local mtime checks on known paths.
8661
+ *
8662
+ * @module mtimeFilter
8663
+ */
8664
+ /**
8665
+ * Check if any file in the list was modified after the given timestamp.
8666
+ *
8667
+ * Short-circuits on first match for efficiency (staleness checks).
8668
+ *
8669
+ * @param files - Array of file paths to check.
8670
+ * @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` match.
8671
+ * @returns True if any file was modified after the timestamp.
8672
+ */
8673
+ function hasModifiedAfter(files, afterMs) {
8674
+ for (const filePath of files) {
8675
+ try {
8676
+ if (statSync(filePath).mtimeMs > afterMs)
8677
+ return true;
8678
+ }
8679
+ catch {
8680
+ // Unreadable file — skip
8681
+ }
8682
+ }
8683
+ return false;
8684
+ }
8685
+ /**
8686
+ * Filter files to only those modified after the given timestamp.
8687
+ *
8688
+ * @param files - Array of file paths to filter.
8689
+ * @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` are included.
8690
+ * @returns Filtered array of file paths.
8691
+ */
8692
+ function filterModifiedAfter(files, afterMs) {
8693
+ return files.filter((filePath) => {
8694
+ try {
8695
+ return statSync(filePath).mtimeMs > afterMs;
8696
+ }
8697
+ catch {
8698
+ return false;
8699
+ }
8700
+ });
8701
+ }
8702
+
8703
+ /**
8704
+ * Staleness detection via watcher walk.
8705
+ *
8706
+ * A meta is stale when any watched file in its scope was modified after
8707
+ * `_generatedAt`.
8708
+ *
8709
+ * @module scheduling/staleness
8710
+ */
8711
+ /**
8712
+ * Check if a meta is stale.
8713
+ *
8714
+ * Uses watcher `/walk` to enumerate watched files under the scope prefix,
8715
+ * then applies a local mtime check (fast) to detect any modifications since
8716
+ * `_generatedAt`. Short-circuits on first match.
8717
+ *
8718
+ * @param scopePrefix - Path prefix for this meta's scope.
8719
+ * @param meta - Current meta.json content.
8720
+ * @param watcher - WatcherClient instance.
8721
+ * @returns True if any file in scope was modified after `_generatedAt`.
8722
+ */
8723
+ async function isStale(scopePrefix, meta, watcher) {
8724
+ if (!meta._generatedAt)
8725
+ return true; // Never synthesized = stale
8726
+ const files = await watcher.walk([`${escapeGlob(scopePrefix)}/**`]);
8727
+ // Exclude .meta/ subtree — synthesis outputs must not trigger staleness.
8728
+ // Handle both forward and back slashes for cross-platform compatibility.
8729
+ const metaSep = /[/\\]\.meta(?:[/\\]|$)/;
8730
+ const filtered = files.filter((f) => !metaSep.test(f));
8731
+ return hasModifiedAfter(filtered, new Date(meta._generatedAt).getTime());
8732
+ }
8733
+ /** Maximum staleness for never-synthesized metas (1 year in seconds). */
8734
+ const MAX_STALENESS_SECONDS = 365 * 86_400;
8735
+ /**
8736
+ * Compute actual staleness in seconds (now minus _generatedAt).
8737
+ *
8738
+ * Never-synthesized metas are capped at {@link MAX_STALENESS_SECONDS}
8739
+ * (1 year) so that depth weighting can differentiate them. Without
8740
+ * bounding, `Infinity * depthFactor` = `Infinity` for all depths.
8741
+ *
8742
+ * @param meta - Current meta.json content.
8743
+ * @returns Staleness in seconds, capped at 1 year for never-synthesized metas.
8744
+ */
8745
+ function actualStaleness(meta) {
8746
+ if (!meta._generatedAt)
8747
+ return MAX_STALENESS_SECONDS;
8748
+ const generatedMs = new Date(meta._generatedAt).getTime();
8749
+ return Math.min((Date.now() - generatedMs) / 1000, MAX_STALENESS_SECONDS);
8750
+ }
8751
+ /**
8752
+ * Check whether the architect step should be triggered.
8753
+ *
8754
+ * @param meta - Current meta.json.
8755
+ * @param structureChanged - Whether the structure hash changed.
8756
+ * @param steerChanged - Whether the steer directive changed.
8757
+ * @param architectEvery - Config: run architect every N cycles.
8758
+ * @returns True if the architect step should run.
8759
+ */
8760
+ function isArchitectTriggered(meta, structureChanged, steerChanged, architectEvery) {
8761
+ return (!meta._builder ||
8762
+ structureChanged ||
8763
+ steerChanged ||
8764
+ (meta._synthesisCount ?? 0) >= architectEvery);
8765
+ }
8766
+ /**
8767
+ * Detect whether the steer directive changed since the last archive.
8768
+ *
8769
+ * @param currentSteer - Current _steer value (or undefined).
8770
+ * @param archiveSteer - Archive _steer value (or undefined).
8771
+ * @param hasArchive - Whether an archive snapshot exists.
8772
+ * @returns True if steer changed.
8773
+ */
8774
+ function hasSteerChanged(currentSteer, archiveSteer, hasArchive) {
8775
+ if (!hasArchive)
8776
+ return Boolean(currentSteer);
8777
+ return currentSteer !== archiveSteer;
8778
+ }
8779
+ /**
8780
+ * Compute a normalized staleness score (0–1) for display purposes.
8781
+ *
8782
+ * Uses the same depth/emphasis weighting as candidate selection,
8783
+ * normalized to a 30-day window.
8784
+ *
8785
+ * @param stalenessSeconds - Raw staleness in seconds (null = never synthesized).
8786
+ * @param depth - Meta tree depth.
8787
+ * @param emphasis - Scheduling emphasis multiplier.
8788
+ * @param depthWeight - Depth weighting exponent from config.
8789
+ * @returns Normalized score between 0 and 1.
8790
+ */
8791
+ function computeStalenessScore(stalenessSeconds, depth, emphasis, depthWeight) {
8792
+ if (stalenessSeconds === null)
8793
+ return 1;
8794
+ const depthFactor = Math.pow(1 + depthWeight, depth);
8795
+ return Math.min(1, (stalenessSeconds * depthFactor * emphasis) / (30 * 86400));
8796
+ }
8797
+
8526
8798
  /**
8527
8799
  * Build the ownership tree from discovered .meta/ paths.
8528
8800
  *
@@ -8605,8 +8877,6 @@ function findNode(tree, targetPath) {
8605
8877
  *
8606
8878
  * @module discovery/listMetas
8607
8879
  */
8608
- /** Maximum staleness for never-synthesized metas (1 year in seconds). */
8609
- const MAX_STALENESS_SECONDS$1 = 365 * 86_400;
8610
8880
  /**
8611
8881
  * Discover, deduplicate, and enrich all metas.
8612
8882
  *
@@ -8642,7 +8912,7 @@ async function listMetas(config, watcher) {
8642
8912
  // Compute staleness
8643
8913
  let stalenessSeconds;
8644
8914
  if (neverSynth) {
8645
- stalenessSeconds = MAX_STALENESS_SECONDS$1;
8915
+ stalenessSeconds = MAX_STALENESS_SECONDS;
8646
8916
  }
8647
8917
  else {
8648
8918
  const genAt = new Date(meta._generatedAt).getTime();
@@ -8675,79 +8945,6 @@ async function listMetas(config, watcher) {
8675
8945
  };
8676
8946
  }
8677
8947
 
8678
- /**
8679
- * Escape special glob characters in a path so it can be used as a literal
8680
- * prefix in glob patterns.
8681
- *
8682
- * Glob metacharacters `* ? [ ] { } ( ) !` are escaped with a backslash so
8683
- * that paths containing parentheses (e.g. Slack channel IDs) or other
8684
- * special characters are matched literally by the watcher's walk endpoint.
8685
- *
8686
- * @module escapeGlob
8687
- */
8688
- /**
8689
- * Escape glob metacharacters in a string using character-class wrapping.
8690
- *
8691
- * Backslash escaping (`\(`) does not work reliably on Windows where `\` is
8692
- * the path separator. Instead, each metacharacter is wrapped in a character
8693
- * class (e.g. `(` → `[(]`) which is universally supported by glob libraries.
8694
- *
8695
- * Square brackets themselves are escaped as `[[]` and `[]]`.
8696
- *
8697
- * @param s - Raw path string.
8698
- * @returns String with glob metacharacters wrapped in character classes.
8699
- */
8700
- function escapeGlob(s) {
8701
- return s.replace(/[*?[\]{}()!]/g, (ch) => `[${ch}]`);
8702
- }
8703
-
8704
- /**
8705
- * Filter file paths by modification time.
8706
- *
8707
- * Shared utility for staleness detection and delta file enumeration.
8708
- * Uses `fs.statSync` for fast local mtime checks on known paths.
8709
- *
8710
- * @module mtimeFilter
8711
- */
8712
- /**
8713
- * Check if any file in the list was modified after the given timestamp.
8714
- *
8715
- * Short-circuits on first match for efficiency (staleness checks).
8716
- *
8717
- * @param files - Array of file paths to check.
8718
- * @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` match.
8719
- * @returns True if any file was modified after the timestamp.
8720
- */
8721
- function hasModifiedAfter(files, afterMs) {
8722
- for (const filePath of files) {
8723
- try {
8724
- if (statSync(filePath).mtimeMs > afterMs)
8725
- return true;
8726
- }
8727
- catch {
8728
- // Unreadable file — skip
8729
- }
8730
- }
8731
- return false;
8732
- }
8733
- /**
8734
- * Filter files to only those modified after the given timestamp.
8735
- *
8736
- * @param files - Array of file paths to filter.
8737
- * @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` are included.
8738
- * @returns Filtered array of file paths.
8739
- */
8740
- function filterModifiedAfter(files, afterMs) {
8741
- return files.filter((filePath) => {
8742
- try {
8743
- return statSync(filePath).mtimeMs > afterMs;
8744
- }
8745
- catch {
8746
- return false;
8747
- }
8748
- });
8749
- }
8750
-
8751
8948
  /**
8752
8949
  * Compute the file scope owned by a meta node.
8753
8950
  *
@@ -8824,11 +9021,6 @@ function getDeltaFiles(generatedAt, scopeFiles) {
8824
9021
  return filterModifiedAfter(scopeFiles, new Date(generatedAt).getTime());
8825
9022
  }
8826
9023
 
8827
- /** Sleep for a given number of milliseconds. */
8828
- function sleep(ms) {
8829
- return new Promise((resolve) => setTimeout(resolve, ms));
8830
- }
8831
-
8832
9024
  /**
8833
9025
  * Error thrown when a spawned subprocess is aborted via AbortController.
8834
9026
  *
@@ -8983,7 +9175,7 @@ class GatewayExecutor {
8983
9175
  JSON.stringify(spawnResult));
8984
9176
  }
8985
9177
  // Step 2: Poll for completion via sessions_history
8986
- await sleep(3000);
9178
+ await sleepAsync(3000);
8987
9179
  while (Date.now() < deadline) {
8988
9180
  // Check for abort before each poll iteration
8989
9181
  if (this.controller.signal.aborted) {
@@ -9047,7 +9239,7 @@ class GatewayExecutor {
9047
9239
  catch {
9048
9240
  // Transient poll failure — keep trying
9049
9241
  }
9050
- await sleep(this.pollIntervalMs);
9242
+ await sleepAsync(this.pollIntervalMs);
9051
9243
  }
9052
9244
  throw new SpawnTimeoutError('Synthesis subprocess timed out after ' + timeoutMs.toString() + 'ms', outputPath);
9053
9245
  }
@@ -9720,101 +9912,6 @@ function discoverStalestPath(candidates, depthWeight) {
9720
9912
  return winner?.node.metaPath ?? null;
9721
9913
  }
9722
9914
 
9723
- /**
9724
- * Staleness detection via watcher walk.
9725
- *
9726
- * A meta is stale when any watched file in its scope was modified after
9727
- * `_generatedAt`.
9728
- *
9729
- * @module scheduling/staleness
9730
- */
9731
- /**
9732
- * Check if a meta is stale.
9733
- *
9734
- * Uses watcher `/walk` to enumerate watched files under the scope prefix,
9735
- * then applies a local mtime check (fast) to detect any modifications since
9736
- * `_generatedAt`. Short-circuits on first match.
9737
- *
9738
- * @param scopePrefix - Path prefix for this meta's scope.
9739
- * @param meta - Current meta.json content.
9740
- * @param watcher - WatcherClient instance.
9741
- * @returns True if any file in scope was modified after `_generatedAt`.
9742
- */
9743
- async function isStale(scopePrefix, meta, watcher) {
9744
- if (!meta._generatedAt)
9745
- return true; // Never synthesized = stale
9746
- const files = await watcher.walk([`${escapeGlob(scopePrefix)}/**`]);
9747
- // Exclude .meta/ subtree — synthesis outputs must not trigger staleness.
9748
- // Handle both forward and back slashes for cross-platform compatibility.
9749
- const metaSep = /[/\\]\.meta(?:[/\\]|$)/;
9750
- const filtered = files.filter((f) => !metaSep.test(f));
9751
- return hasModifiedAfter(filtered, new Date(meta._generatedAt).getTime());
9752
- }
9753
- /** Maximum staleness for never-synthesized metas (1 year in seconds). */
9754
- const MAX_STALENESS_SECONDS = 365 * 86_400;
9755
- /**
9756
- * Compute actual staleness in seconds (now minus _generatedAt).
9757
- *
9758
- * Never-synthesized metas are capped at {@link MAX_STALENESS_SECONDS}
9759
- * (1 year) so that depth weighting can differentiate them. Without
9760
- * bounding, `Infinity * depthFactor` = `Infinity` for all depths.
9761
- *
9762
- * @param meta - Current meta.json content.
9763
- * @returns Staleness in seconds, capped at 1 year for never-synthesized metas.
9764
- */
9765
- function actualStaleness(meta) {
9766
- if (!meta._generatedAt)
9767
- return MAX_STALENESS_SECONDS;
9768
- const generatedMs = new Date(meta._generatedAt).getTime();
9769
- return Math.min((Date.now() - generatedMs) / 1000, MAX_STALENESS_SECONDS);
9770
- }
9771
- /**
9772
- * Check whether the architect step should be triggered.
9773
- *
9774
- * @param meta - Current meta.json.
9775
- * @param structureChanged - Whether the structure hash changed.
9776
- * @param steerChanged - Whether the steer directive changed.
9777
- * @param architectEvery - Config: run architect every N cycles.
9778
- * @returns True if the architect step should run.
9779
- */
9780
- function isArchitectTriggered(meta, structureChanged, steerChanged, architectEvery) {
9781
- return (!meta._builder ||
9782
- structureChanged ||
9783
- steerChanged ||
9784
- (meta._synthesisCount ?? 0) >= architectEvery);
9785
- }
9786
- /**
9787
- * Detect whether the steer directive changed since the last archive.
9788
- *
9789
- * @param currentSteer - Current _steer value (or undefined).
9790
- * @param archiveSteer - Archive _steer value (or undefined).
9791
- * @param hasArchive - Whether an archive snapshot exists.
9792
- * @returns True if steer changed.
9793
- */
9794
- function hasSteerChanged(currentSteer, archiveSteer, hasArchive) {
9795
- if (!hasArchive)
9796
- return Boolean(currentSteer);
9797
- return currentSteer !== archiveSteer;
9798
- }
9799
- /**
9800
- * Compute a normalized staleness score (0–1) for display purposes.
9801
- *
9802
- * Uses the same depth/emphasis weighting as candidate selection,
9803
- * normalized to a 30-day window.
9804
- *
9805
- * @param stalenessSeconds - Raw staleness in seconds (null = never synthesized).
9806
- * @param depth - Meta tree depth.
9807
- * @param emphasis - Scheduling emphasis multiplier.
9808
- * @param depthWeight - Depth weighting exponent from config.
9809
- * @returns Normalized score between 0 and 1.
9810
- */
9811
- function computeStalenessScore(stalenessSeconds, depth, emphasis, depthWeight) {
9812
- if (stalenessSeconds === null)
9813
- return 1;
9814
- const depthFactor = Math.pow(1 + depthWeight, depth);
9815
- return Math.min(1, (stalenessSeconds * depthFactor * emphasis) / (30 * 86400));
9816
- }
9817
-
9818
9915
  /**
9819
9916
  * Shared error utilities.
9820
9917
  *
@@ -10875,7 +10972,10 @@ function buildMetaRules(config) {
10875
10972
  properties: {
10876
10973
  file: {
10877
10974
  properties: {
10878
- path: { type: 'string', glob: '**/jeeves-meta.config.json' },
10975
+ path: {
10976
+ type: 'string',
10977
+ glob: '**/jeeves-meta{.config.json,/config.json}',
10978
+ },
10879
10979
  },
10880
10980
  },
10881
10981
  },
@@ -12008,7 +12108,7 @@ class HttpWatcherClient {
12008
12108
  }
12009
12109
  // Exponential backoff
12010
12110
  const delayMs = this.backoffBaseMs * Math.pow(this.backoffFactor, attempt);
12011
- await sleep(delayMs);
12111
+ await sleepAsync(delayMs);
12012
12112
  }
12013
12113
  // Unreachable, but TypeScript needs it
12014
12114
  throw new Error('Retry exhausted');
@@ -12264,4 +12364,4 @@ const metaDescriptor = jeevesComponentDescriptorSchema.parse({
12264
12364
  customCliCommands: registerCustomCliCommands,
12265
12365
  });
12266
12366
 
12267
- export { DEFAULT_PORT, DEFAULT_PORT_STR, GatewayExecutor, HttpWatcherClient, ProgressReporter, RESTART_REQUIRED_FIELDS, 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, metaDescriptor, metaErrorSchema, metaJsonSchema, migrateConfigPath, normalizePath, orchestrate, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerCustomCliCommands, registerRoutes, registerShutdownHandlers, releaseLock, resolveConfigPath, resolveMetaDir, selectCandidate, serviceConfigSchema, sleep, startService, toMetaError, verifyRuleApplication };
12367
+ export { DEFAULT_PORT, DEFAULT_PORT_STR, GatewayExecutor, HttpWatcherClient, MAX_STALENESS_SECONDS, ProgressReporter, RESTART_REQUIRED_FIELDS, 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, metaDescriptor, metaErrorSchema, metaJsonSchema, migrateConfigPath, normalizePath, orchestrate, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerCustomCliCommands, registerRoutes, registerShutdownHandlers, releaseLock, resolveConfigPath, resolveMetaDir, selectCandidate, serviceConfigSchema, sleepAsync as sleep, startService, toMetaError, verifyRuleApplication };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karmaniverous/jeeves-meta",
3
- "version": "0.12.4",
3
+ "version": "0.13.0",
4
4
  "author": "Jason Williscroft",
5
5
  "description": "Fastify HTTP service for the Jeeves Meta synthesis engine",
6
6
  "license": "BSD-3-Clause",
@@ -38,10 +38,10 @@
38
38
  "typescript"
39
39
  ],
40
40
  "engines": {
41
- "node": ">=20"
41
+ "node": ">=22"
42
42
  },
43
43
  "dependencies": {
44
- "@karmaniverous/jeeves": "^0.4.6",
44
+ "@karmaniverous/jeeves": "^0.5.1",
45
45
  "commander": "^14",
46
46
  "croner": "^10",
47
47
  "fastify": "^5.8",