@karmaniverous/jeeves-meta 0.20.0-0 → 0.20.0-2

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/README.md CHANGED
@@ -56,7 +56,7 @@ jeeves-meta service install --config /path/to/jeeves-meta/config.json
56
56
 
57
57
  | Method | Path | Description |
58
58
  | --- | --- | --- |
59
- | GET | `/status` | Service health, queue state, dependency checks, phase-state summary |
59
+ | GET | `/status` | Service health, queue state, dependency checks, phase-state summary. Meta counts come from the last meta list (at most 60 s old, refreshed in the background), so the check never waits on a corpus walk. |
60
60
  | GET | `/metas` | List metas with summary stats and per-meta projection. Response includes `phaseState` and `owedPhase` per meta. |
61
61
  | GET | `/metas/:path` | Full detail for a single meta, with optional archive history. Response includes `phaseState`, `owedPhase` and `crossRefs`. |
62
62
  | GET | `/preview` | Dry-run: preview inputs for next synthesis |
@@ -71,7 +71,7 @@ jeeves-meta service install --config /path/to/jeeves-meta/config.json
71
71
  | PATCH | `/metas/:path` | Update user-settable reserved properties (`_steer`, `_emphasis`, `_depth`, `_crossRefs`, `_disabled`) |
72
72
  | GET | `/openapi.json` | The OpenAPI 3.1 document of every route above |
73
73
 
74
- Every route is defined in `META_CATALOG` (`@karmaniverous/jeeves-meta-core`) and validated against it: a malformed request is answered 400 `FST_ERR_VALIDATION`. Every route except `/config`, `/config/apply` and `/openapi.json` answers 503 `META_NOT_READY` while the service starts. Use [`@karmaniverous/jeeves-meta-sdk`](../sdk/README.md) for a typed client.
74
+ Every route is defined in `META_CATALOG` (`@karmaniverous/jeeves-meta-core`) and validated against it: a malformed request is answered 400 `FST_ERR_VALIDATION`. While the service starts, `GET /status` answers 200 with `serviceState: "starting"` (no meta counts until the first meta list loads), `/config`, `/config/apply` and `/openapi.json` are served, and every other route answers 503 `META_NOT_READY`. Use [`@karmaniverous/jeeves-meta-sdk`](../sdk/README.md) for a typed client.
75
75
 
76
76
  ### Errors
77
77
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { sleepAsync, computeInsiderKey, fetchWithTimeout, DEFAULT_PORTS, createConfigQueryHandler, createStatusHandler, getBindAddress, getServiceUrl, jeevesComponentDescriptorSchema, createConfigApplyHandler, createServiceCli } from '@karmaniverous/jeeves';
2
+ import { sleepAsync, computeInsiderKey, fetchWithTimeout, DEFAULT_PORTS, createConfigQueryHandler, createStatusHandler, getBindAddress, rejectWindowsDrivePath, init, WORKSPACE_CONFIG_DEFAULTS, resolveConfigValue, loadWorkspaceConfig, getServiceUrl, jeevesComponentDescriptorSchema, createConfigApplyHandler, createServiceCli } from '@karmaniverous/jeeves';
3
3
  import { normalizePath, metaConfigSchema, META_ERROR_STATUS, updateBodySchema, META_COMPONENT, META_CATALOG, META_OPENAPI_DOCUMENT } from '@karmaniverous/jeeves-meta-core';
4
4
  import fs, { unlinkSync, writeFileSync, existsSync, readFileSync, statSync, mkdirSync, readdirSync, watchFile } from 'node:fs';
5
5
  import path, { join, dirname, relative, posix, sep, resolve } from 'node:path';
@@ -657,7 +657,8 @@ function getDeltaFiles(generatedAt, scopeFiles) {
657
657
  }
658
658
 
659
659
  /**
660
- * In-memory cache for listMetas results with TTL and concurrent refresh guard.
660
+ * In-memory cache for listMetas results with TTL, concurrent refresh guard
661
+ * and a non-blocking read (`peek`) for health checks.
661
662
  *
662
663
  * @module cache
663
664
  */
@@ -677,6 +678,22 @@ class MetaCache {
677
678
  }
678
679
  return this.refresh(config, watcher);
679
680
  }
681
+ /**
682
+ * The cached result without waiting on a stale one: when it has expired,
683
+ * return it anyway and refresh in the background. Only the first load,
684
+ * with nothing cached yet, is awaited. For callers that must answer fast
685
+ * (`GET /status`), where a 60 s-old list is good enough.
686
+ */
687
+ async peek(config, watcher) {
688
+ if (!this.result)
689
+ return this.refresh(config, watcher);
690
+ if (Date.now() - this.updatedAt >= TTL_MS) {
691
+ // A failed background refresh keeps the old result; the next caller
692
+ // retries it.
693
+ this.refresh(config, watcher).catch(() => undefined);
694
+ }
695
+ return this.result;
696
+ }
680
697
  /** Force-expire the cache so next get() triggers a refresh. */
681
698
  invalidate() {
682
699
  this.updatedAt = 0;
@@ -31854,6 +31871,8 @@ async function checkWatcher(url) {
31854
31871
  function deriveServiceState(deps) {
31855
31872
  if (deps.shuttingDown)
31856
31873
  return 'stopping';
31874
+ if (!deps.ready)
31875
+ return 'starting';
31857
31876
  if (deps.queue.currentPhase)
31858
31877
  return 'synthesizing';
31859
31878
  if (deps.queue.depth > 0)
@@ -31888,41 +31907,47 @@ function createStatusHandlers(deps) {
31888
31907
  };
31889
31908
  let nextPhase = null;
31890
31909
  let metaCounts = null;
31891
- try {
31892
- const metaResult = await cache.get(config, watcher);
31893
- // Count raw phase states (before retry) for display
31894
- for (const entry of metaResult.entries) {
31895
- const ps = derivePhaseState(entry.meta);
31896
- for (const phase of ['architect', 'builder', 'critic']) {
31897
- phaseStateSummary[phase][ps[phase]]++;
31910
+ // While starting, the first meta list is still loading: report the
31911
+ // state, dependencies and stats without it (Decision 199).
31912
+ if (deps.ready) {
31913
+ try {
31914
+ // Never wait on a refresh of the whole corpus: a health check answers
31915
+ // from the last list, refreshed in the background.
31916
+ const metaResult = await cache.peek(config, watcher);
31917
+ // Count raw phase states (before retry) for display
31918
+ for (const entry of metaResult.entries) {
31919
+ const ps = derivePhaseState(entry.meta);
31920
+ for (const phase of ['architect', 'builder', 'critic']) {
31921
+ phaseStateSummary[phase][ps[phase]]++;
31922
+ }
31898
31923
  }
31899
- }
31900
- // Build candidates (with auto-retry) for scheduling
31901
- const candidates = buildPhaseCandidates(metaResult.entries, config.architectEvery);
31902
- // Find next phase candidate
31903
- const winner = selectPhaseCandidate(candidates, config.depthWeight);
31904
- if (winner) {
31905
- nextPhase = {
31906
- path: winner.node.metaPath,
31907
- phase: winner.owedPhase,
31908
- band: winner.band,
31909
- staleness: finiteOrNull(winner.effectiveStaleness),
31924
+ // Build candidates (with auto-retry) for scheduling
31925
+ const candidates = buildPhaseCandidates(metaResult.entries, config.architectEvery);
31926
+ // Find next phase candidate
31927
+ const winner = selectPhaseCandidate(candidates, config.depthWeight);
31928
+ if (winner) {
31929
+ nextPhase = {
31930
+ path: winner.node.metaPath,
31931
+ phase: winner.owedPhase,
31932
+ band: winner.band,
31933
+ staleness: finiteOrNull(winner.effectiveStaleness),
31934
+ };
31935
+ }
31936
+ // Meta counts summary
31937
+ const summary = computeSummary(metaResult.entries, config.depthWeight);
31938
+ metaCounts = {
31939
+ total: summary.total,
31940
+ enabled: summary.total - summary.disabled,
31941
+ disabled: summary.disabled,
31942
+ neverSynthesized: summary.neverSynthesized,
31943
+ stale: summary.stale,
31944
+ errors: summary.errors,
31945
+ locked: summary.locked,
31910
31946
  };
31911
31947
  }
31912
- // Meta counts summary
31913
- const summary = computeSummary(metaResult.entries, config.depthWeight);
31914
- metaCounts = {
31915
- total: summary.total,
31916
- enabled: summary.total - summary.disabled,
31917
- disabled: summary.disabled,
31918
- neverSynthesized: summary.neverSynthesized,
31919
- stale: summary.stale,
31920
- errors: summary.errors,
31921
- locked: summary.locked,
31922
- };
31923
- }
31924
- catch {
31925
- // Watcher unreachable — phase summary unavailable
31948
+ catch {
31949
+ // Watcher unreachable — phase summary unavailable
31950
+ }
31926
31951
  }
31927
31952
  return {
31928
31953
  serviceState: deriveServiceState(deps),
@@ -32169,7 +32194,12 @@ function registerRoutes(app, deps) {
32169
32194
  * @module server
32170
32195
  */
32171
32196
  /** Routes served while the service is starting (matched route URLs). */
32172
- const UNGATED_ROUTES = new Set(['/config', '/config/apply', '/openapi.json']);
32197
+ const UNGATED_ROUTES = new Set([
32198
+ '/status',
32199
+ '/config',
32200
+ '/config/apply',
32201
+ '/openapi.json',
32202
+ ]);
32173
32203
  /**
32174
32204
  * Create and configure the Fastify server.
32175
32205
  *
@@ -32400,9 +32430,10 @@ async function startService(config, configPath, configApply) {
32400
32430
  queue.onEnqueue(() => {
32401
32431
  void queue.processQueue(synthesizeFn);
32402
32432
  });
32403
- // Startup: clean stale locks (gap #16)
32433
+ // Startup: clean stale locks (gap #16). Through the cache, so the first
32434
+ // GET /status after start-up answers without a second walk.
32404
32435
  try {
32405
- const metaResult = await listMetas(config, watcher);
32436
+ const metaResult = await cache.get(config, watcher);
32406
32437
  const metaPaths = metaResult.entries.map((e) => e.node.metaPath);
32407
32438
  cleanupStaleLocks(metaPaths, logger);
32408
32439
  }
@@ -32742,11 +32773,61 @@ function createMetaClient(options) {
32742
32773
  return Object.fromEntries(methods);
32743
32774
  }
32744
32775
 
32776
+ /**
32777
+ * Workspace and config root for the custom CLI commands, so they can ask
32778
+ * jeeves where meta runs (`getServiceUrl('meta')`, which reads the core
32779
+ * config under the config root).
32780
+ *
32781
+ * Each root follows jeeves' shared precedence: the flag, then
32782
+ * `JEEVES_WORKSPACE` / `JEEVES_CONFIG_ROOT`, then `core.*` in the
32783
+ * workspace's `jeeves.config.json`, then the defaults (`.`, `./config`).
32784
+ * jeeves does not export its own resolver, so this composes the exported
32785
+ * `loadWorkspaceConfig()` and `resolveConfigValue()` as jeeves-runner does;
32786
+ * karmaniverous/jeeves#169 moves this into jeeves.
32787
+ *
32788
+ * @module cliRoots
32789
+ */
32790
+ /**
32791
+ * Resolve the workspace and config root by flag, env,
32792
+ * `jeeves.config.json`, then default.
32793
+ *
32794
+ * @param opts - The parsed flags.
32795
+ * @param env - Environment to read (default `process.env`).
32796
+ * @returns Both roots, as given (not yet made absolute).
32797
+ */
32798
+ function resolveRoots(opts, env = process.env) {
32799
+ const defaults = WORKSPACE_CONFIG_DEFAULTS.core;
32800
+ const workspaceSeed = resolveConfigValue(opts.workspace, env['JEEVES_WORKSPACE'], undefined, defaults.workspace).value;
32801
+ const file = loadWorkspaceConfig(workspaceSeed);
32802
+ return {
32803
+ workspace: resolveConfigValue(opts.workspace, env['JEEVES_WORKSPACE'], file?.core?.workspace, defaults.workspace).value,
32804
+ configRoot: resolveConfigValue(opts.configRoot, env['JEEVES_CONFIG_ROOT'], file?.core?.configRoot, defaults.configRoot).value,
32805
+ };
32806
+ }
32807
+ /**
32808
+ * Resolve both roots and initialise jeeves with them.
32809
+ *
32810
+ * @param opts - The parsed flags.
32811
+ * @param env - Environment to read (default `process.env`).
32812
+ */
32813
+ function initRoots(opts, env = process.env) {
32814
+ const roots = resolveRoots(opts, env);
32815
+ // Check the raw values: on Linux, resolve('j:/x') hides the drive letter.
32816
+ rejectWindowsDrivePath('configRoot', roots.configRoot);
32817
+ rejectWindowsDrivePath('workspacePath', roots.workspace);
32818
+ init({
32819
+ workspacePath: resolve(roots.workspace),
32820
+ configRoot: resolve(roots.configRoot),
32821
+ });
32822
+ }
32823
+
32745
32824
  /**
32746
32825
  * Custom CLI commands for the jeeves-meta service, built on the SDK
32747
32826
  * (`@karmaniverous/jeeves-meta-sdk`, Decision 187). Each command calls one
32748
32827
  * catalog entry and prints the response as JSON; a failure prints one
32749
- * error line to stderr and sets exit code 1.
32828
+ * error line to stderr and sets exit code 1. The service URL comes from
32829
+ * jeeves (`getServiceUrl('meta')`) after the workspace and config root are
32830
+ * resolved (`cliRoots`); `--port` overrides its port.
32750
32831
  *
32751
32832
  * Registered via `descriptor.customCliCommands` and added to the
32752
32833
  * standard service CLI produced by `createServiceCli`.
@@ -32754,15 +32835,17 @@ function createMetaClient(options) {
32754
32835
  * @module customCliCommands
32755
32836
  */
32756
32837
  /**
32757
- * The service's base URL on the given port.
32838
+ * The service's base URL: jeeves' URL for meta, with `--port` when it is
32839
+ * not the default.
32758
32840
  *
32759
- * @param port - Port string from `--port`.
32841
+ * @param opts - The command's options.
32760
32842
  * @returns The base URL.
32761
32843
  */
32762
- function serviceBaseUrl(port) {
32844
+ function serviceBaseUrl(opts) {
32845
+ initRoots(opts);
32763
32846
  const url = new URL(getServiceUrl('meta'));
32764
- if (port !== DEFAULT_PORT_STR)
32765
- url.port = port;
32847
+ if (opts.port !== DEFAULT_PORT_STR)
32848
+ url.port = opts.port;
32766
32849
  return url.origin;
32767
32850
  }
32768
32851
  /**
@@ -32781,12 +32864,12 @@ function errorLine(error) {
32781
32864
  * Run one command: call the service, print the result as JSON, or print
32782
32865
  * the error line and set exit code 1.
32783
32866
  *
32784
- * @param port - Port string from `--port`.
32867
+ * @param opts - The command's options.
32785
32868
  * @param call - The SDK call.
32786
32869
  */
32787
- async function runCommand(port, call) {
32870
+ async function runCommand(opts, call) {
32788
32871
  try {
32789
- const data = await call(createMetaClient({ baseUrl: serviceBaseUrl(port) }));
32872
+ const data = await call(createMetaClient({ baseUrl: serviceBaseUrl(opts) }));
32790
32873
  console.log(JSON.stringify(data, null, 2));
32791
32874
  }
32792
32875
  catch (error) {
@@ -32794,27 +32877,38 @@ async function runCommand(port, call) {
32794
32877
  process.exitCode = 1;
32795
32878
  }
32796
32879
  }
32880
+ /**
32881
+ * Add the options every service command takes: `--port`, and the roots
32882
+ * jeeves resolves the service URL from (no defaults, so the environment and
32883
+ * `jeeves.config.json` apply when a flag is absent).
32884
+ *
32885
+ * @param cmd - The command.
32886
+ * @returns It.
32887
+ */
32888
+ function withServiceOptions(cmd) {
32889
+ return cmd
32890
+ .option('-p, --port <port>', 'Service port', DEFAULT_PORT_STR)
32891
+ .option('-w, --workspace <path>', 'Workspace root path')
32892
+ .option('--config-root <path>', 'Platform config root path');
32893
+ }
32797
32894
  /**
32798
32895
  * Register all custom meta commands on the parent program.
32799
32896
  *
32800
32897
  * @param program - The service CLI.
32801
32898
  */
32802
32899
  function registerCustomCliCommands(program) {
32803
- const command = (spec, description) => program
32804
- .command(spec)
32805
- .description(description)
32806
- .option('-p, --port <port>', 'Service port', DEFAULT_PORT_STR);
32807
- command('list', 'List all discovered meta entities').action((opts) => runCommand(opts.port, (meta) => meta.listMetas()));
32808
- command('detail <path>', 'Show full detail for a single meta entity').action((path, opts) => runCommand(opts.port, (meta) => meta.metaDetail({ path })));
32900
+ const command = (spec, description) => withServiceOptions(program.command(spec).description(description));
32901
+ command('list', 'List all discovered meta entities').action((opts) => runCommand(opts, (meta) => meta.listMetas()));
32902
+ command('detail <path>', 'Show full detail for a single meta entity').action((path, opts) => runCommand(opts, (meta) => meta.metaDetail({ path })));
32809
32903
  command('preview', 'Dry-run: preview inputs for next synthesis cycle')
32810
32904
  .option('--path <path>', 'Specific meta path to preview')
32811
- .action((opts) => runCommand(opts.port, (meta) => meta.preview(opts.path ? { path: opts.path } : {})));
32905
+ .action((opts) => runCommand(opts, (meta) => meta.preview(opts.path ? { path: opts.path } : {})));
32812
32906
  command('synthesize', 'Trigger synthesis (enqueues work)')
32813
32907
  .option('--path <path>', 'Specific meta path to synthesize')
32814
- .action((opts) => runCommand(opts.port, (meta) => meta.synthesize(opts.path ? { path: opts.path } : {})));
32815
- command('seed <path>', 'Create .meta/ directory + meta.json for a path').action((path, opts) => runCommand(opts.port, (meta) => meta.seed({ path })));
32816
- command('unlock <path>', 'Remove .lock file from a meta entity').action((path, opts) => runCommand(opts.port, (meta) => meta.unlock({ path })));
32817
- command('abort', 'Abort the current synthesis in progress').action((opts) => runCommand(opts.port, (meta) => meta.abort()));
32908
+ .action((opts) => runCommand(opts, (meta) => meta.synthesize(opts.path ? { path: opts.path } : {})));
32909
+ command('seed <path>', 'Create .meta/ directory + meta.json for a path').action((path, opts) => runCommand(opts, (meta) => meta.seed({ path })));
32910
+ command('unlock <path>', 'Remove .lock file from a meta entity').action((path, opts) => runCommand(opts, (meta) => meta.unlock({ path })));
32911
+ command('abort', 'Abort the current synthesis in progress').action((opts) => runCommand(opts, (meta) => meta.abort()));
32818
32912
  command('prune', 'Prune old archive snapshots beyond maxArchive').action(() => {
32819
32913
  // The service has no prune route yet (jeeves-meta#69); 0.19.0 called a
32820
32914
  // missing route and failed with HTTP 404.
@@ -32824,16 +32918,9 @@ function registerCustomCliCommands(program) {
32824
32918
  const queue = program
32825
32919
  .command('queue')
32826
32920
  .description('Queue management commands');
32827
- queue
32828
- .command('list')
32829
- .description('Show current queue state')
32830
- .option('-p, --port <port>', 'Service port', DEFAULT_PORT_STR)
32831
- .action((opts) => runCommand(opts.port, (meta) => meta.queue()));
32832
- queue
32833
- .command('clear')
32834
- .description('Remove all pending items from the queue')
32835
- .option('-p, --port <port>', 'Service port', DEFAULT_PORT_STR)
32836
- .action((opts) => runCommand(opts.port, (meta) => meta.queueClear()));
32921
+ const queueCommand = (name, description) => withServiceOptions(queue.command(name).description(description));
32922
+ queueCommand('list', 'Show current queue state').action((opts) => runCommand(opts, (meta) => meta.queue()));
32923
+ queueCommand('clear', 'Remove all pending items from the queue').action((opts) => runCommand(opts, (meta) => meta.queueClear()));
32837
32924
  }
32838
32925
 
32839
32926
  /**
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@karmaniverous/jeeves": "0.11.0",
11
- "@karmaniverous/jeeves-meta-core": "^0.4.0-0",
11
+ "@karmaniverous/jeeves-meta-core": "^0.4.0-1",
12
12
  "@karmaniverous/jeeves-meta-sdk": "^0.0.0 || ^0.1.0-0",
13
13
  "@karmaniverous/jeeves-watcher-sdk": "^0.4.1",
14
14
  "commander": "^15",
@@ -112,5 +112,5 @@
112
112
  "wire:record": "vitest run --config vitest.wire-record.config.ts"
113
113
  },
114
114
  "type": "module",
115
- "version": "0.20.0-0"
115
+ "version": "0.20.0-2"
116
116
  }